From 6ea692402b6b439e87390e1e9004683d7991232b Mon Sep 17 00:00:00 2001 From: Gabryel Reyes <66941456+gabryelreyes@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:02:33 +0200 Subject: [PATCH] Adapter (#3) * Added adapter for handling of search results * Adapter comes from an interface * Extracted processing into separate functions * Fixed branching * check for missing installations * added comment to the adapter usage * Tools are dependencies * Testing with https * Fixed toml * Updated documentation * remove unused file; check if Adapter class exists * fixed pylint error --------- Co-authored-by: jkerpe --- .gitignore | 4 +- README.md | 53 +++++- examples/README.md | 8 +- examples/adapter/adapter.py | 129 ++++++++++++++ pyproject.toml | 5 +- src/pyMetricCli/__main__.py | 240 +++++++++++++++++++++++++-- src/pyMetricCli/adapter_interface.py | 85 ++++++++++ src/pyMetricCli/jira.py | 138 +++++++++++++++ src/pyMetricCli/polarion.py | 146 ++++++++++++++++ src/pyMetricCli/ret.py | 7 + src/pyMetricCli/superset.py | 138 +++++++++++++++ 11 files changed, 932 insertions(+), 21 deletions(-) create mode 100644 examples/adapter/adapter.py create mode 100644 src/pyMetricCli/adapter_interface.py create mode 100644 src/pyMetricCli/jira.py create mode 100644 src/pyMetricCli/polarion.py create mode 100644 src/pyMetricCli/superset.py diff --git a/.gitignore b/.gitignore index a68c88a..17b6ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,6 @@ cython_debug/ #.idea/ # Development Scripts -dev/ \ No newline at end of file +dev/ + +temp/ \ No newline at end of file diff --git a/README.md b/README.md index 15c2b5e..8029457 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ pyMetricCli is a collection of scripts and API implementations for generating an - [Installation](#installation) - [Overview](#overview) - [Usage](#usage) -- [Commands](#commands) - [Examples](#examples) - [Used Libraries](#used-libraries) - [Issues, Ideas And Bugs](#issues-ideas-and-bugs) @@ -16,29 +15,69 @@ pyMetricCli is a collection of scripts and API implementations for generating an ## Installation -WIP +```cmd +git clone https://github.com/NewTec-GmbH/pyMetricCli.git +cd pyMetricCli +pip install . +``` + +This will also install the latest version of [pyJiraCli](https://github.com/NewTec-GmbH/pyJiraCli), [pyPolarionCli](https://github.com/NewTec-GmbH/pyPolarionCli), and [pySupersetCli](https://github.com/NewTec-GmbH/pySupersetCli) in your Python environment. ## Overview -WIP +pyMetricCli requires an adapter file to be supplied by the user. This shall contain the credentials and result-handling logic for the seach results. Please **DO NOT** commit this file into any public repository as your credentials might be exposed. + +An example adapter can be found [here](examples/adapter/adapter.py). ## Usage -WIP +Show help information: + +```cmd +pyJiraCli --help +``` + +Usage: + +```cmd +usage: pyMetricCli [-h] -a [--version] [-v] + +Collection of scripts and API implementations for generating and playing with metrics. + +options: + -h, --help show this help message and exit + --version show program's version number and exit + -v, --verbose Print full command details before executing the command. Enables logs of type INFO and WARNING. + +required arguments: + -a , --adapter_file + Adapter file to be used. +``` + +Example: + +```cmd +pyJiraCli --verbose --adapter_file "examples\adapter\adapter.py" +``` + +### Adapter -## Commands +The adapter file must contain the `Adapter` Class derived from the `AdapterInterface`, including all the methods and members defined in the interface. -WIP +The `***_config` dictionaries must be filled with the user credentials for each service. The `output` dictionary defines the columns of the table that will be sent to Superset, and this cannot be changed after the first time the script is ran. If you are receiving an Error 422 from Superset, a change in this dictionary may be the reason and you should contact your administrator so resolve the issue. ## Examples -Check out the all the [Examples](./examples). +Check out the [Examples](./examples) in the corresponding folder. ## Used Libraries Used 3rd party libraries which are not part of the standard Python package: - [toml](https://github.com/uiri/toml) - Parsing [TOML](https://en.wikipedia.org/wiki/TOML) - MIT License +- [pyJiraCli](https://github.com/NewTec-GmbH/pyJiraCli) - Interfacing with Jira - BSD-3 License +- [pyPolarionCli](https://github.com/NewTec-GmbH/pyPolarionCli) - Interfacing with Polarion - BSD-3 License +- [pySupersetCli](https://github.com/NewTec-GmbH/pySupersetCli) - Interfacing with Superset - BSD-3 License ## Issues, Ideas And Bugs diff --git a/examples/README.md b/examples/README.md index 1b976a9..7e783ab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1 +1,7 @@ -# Examples \ No newline at end of file +# Examples + +## Adapter + +Each project must provide an `Adapter` class with the methods `handle_jira` and `handle_polarion` to pack the data from the search results into the output dictionary. +Additionally, the dictionaries `output`, `jira_config`, `polarion_config` and `superset_config` must also be supplied. +The declaration, arguments and name of the methods must remain the same as in `examples\adapter\adapter.py`, otherwise the program will not work correctly. diff --git a/examples/adapter/adapter.py b/examples/adapter/adapter.py new file mode 100644 index 0000000..0557fea --- /dev/null +++ b/examples/adapter/adapter.py @@ -0,0 +1,129 @@ + +"""Project-Specific Adapter Module""" + +# BSD 3-Clause License +# +# Copyright (c) 2024, NewTec GmbH +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICU5LAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +################################################################################ +# Imports +################################################################################ + +import logging +from pyMetricCli.adapter_interface import AdapterInterface + +################################################################################ +# Variables +################################################################################ + +LOG = logging.getLogger(__name__) + +################################################################################ +# Classes +################################################################################ + + +class Adapter(AdapterInterface): + """ + Adapter class for handling different search results. + """ + + # Define the output dictionary + # Must include all possible values. + # Please make sure that the keys of the output dictionary are unique, regardless of their case. + # In this example, Polarion Status has 2 possible values: "open" and "closed". + output: dict = { + "status_open": 0, + "status_closed": 0 + } + + # Set "filter": "", if you don't want to search in Jira. + jira_config = { + "server": "https://jira.example.com", + "token": "", + "filter": "", + "max": "0", # 0 gets all issues that match the filter. + "fields": [], + "full": False + } + + # Set "query": "", if you don't want to search in Polarion. + polarion_config = { + "username": "", + "password": "", + "server": "http://polarion.example.com/polarion", + "project": "", + "query": "HAS_VALUE:status", # Query to get all work items with a status + "fields": ["status"] # Fields to include in the query + } + + superset_config = { + "server": "http://superset.example.com", + "user": "", + "password": "", + "database": 0, # Primary key of the database + "table": "", + "basic_auth": False, + "no_ssl": False + } + + def handle_jira(self, search_results: dict) -> bool: + """ + Handles the JIRA search results. + + Args: + search_results: The search results from the JIRA API. + + Returns: + bool: True if the search results were handled successfully, False otherwise. + """ + LOG.info("Handling JIRA search results...") + LOG.info(search_results) + return True + + def handle_polarion(self, search_results: dict) -> bool: + """ + Handles the Polarion search results. + + Args: + search_results: The search results from the Polarion API. + + Returns: + bool: True if the search results were handled successfully, False otherwise. + """ + LOG.info("Handling Polarion search results...") + LOG.info(search_results) + return True + +################################################################################ +# Functions +################################################################################ + +################################################################################ +# Main +################################################################################ diff --git a/pyproject.toml b/pyproject.toml index 7983601..40aa221 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,10 @@ classifiers = [ ] dependencies = [ - "toml>=0.10.2" + "toml>=0.10.2", + "pyJiraCli@git+https://github.com/NewTec-GmbH/pyJiraCli", + "pyPolarionCli@git+https://github.com/NewTec-GmbH/pyPolarionCli", + "pySupersetCli@git+https://github.com/NewTec-GmbH/pySupersetCli" ] [project.optional-dependencies] diff --git a/src/pyMetricCli/__main__.py b/src/pyMetricCli/__main__.py index 009ebbc..fb816bc 100644 --- a/src/pyMetricCli/__main__.py +++ b/src/pyMetricCli/__main__.py @@ -36,9 +36,18 @@ import sys import logging import argparse +import os.path +import importlib.util +import json +import datetime +import shutil from pyMetricCli.version import __version__, __author__, __email__, __repository__, __license__ from pyMetricCli.ret import Ret +from pyMetricCli.jira import Jira +from pyMetricCli.polarion import Polarion +from pyMetricCli.superset import Superset +from pyMetricCli.adapter_interface import AdapterInterface ################################################################################ # Variables @@ -52,6 +61,10 @@ PROG_GITHUB = f"Find the project on GitHub: {__repository__}" PROG_EPILOG = f"{PROG_COPYRIGHT} - {PROG_GITHUB}" +_TEMP_DIR_NAME = "temp" +_TEMP_FILE_NAME = "superset_input.json" +_TEMP_FILE_PATH = os.path.join(_TEMP_DIR_NAME, _TEMP_FILE_NAME) + ################################################################################ # Classes ################################################################################ @@ -78,12 +91,12 @@ def add_parser() -> argparse.ArgumentParser: required_arguments = parser.add_argument_group('required arguments') - required_arguments.add_argument('-c', - '--config_file', + required_arguments.add_argument('-a', + '--adapter_file', type=str, - metavar='', + metavar='', required=True, - help="Configuration file to be used.") + help="Adapter file to be used.") parser.add_argument("--version", action="version", @@ -98,6 +111,174 @@ def add_parser() -> argparse.ArgumentParser: return parser +def _import_adapter(adapter_path: str) -> AdapterInterface: + """ + Import the adapter module from the given path. + + Args: + adapter_path (str): The path to the adapter module. + + Returns: + AdapterInterface: Instance of an adapter class inherited from AdapterInterface. + """ + adapter_name = "adapter" + adapter_instance = None + + if not os.path.isfile(adapter_path): + LOG.error("The adapter file does not exist.") + else: + module_spec = importlib.util.spec_from_file_location(adapter_name, + adapter_path) + adapter = importlib.util.module_from_spec(module_spec) + sys.modules[adapter_name] = adapter + module_spec.loader.exec_module(adapter) + adapter_instance = adapter.Adapter() + + # Check all required attributes and methods of the adapter class. + # Must be done as Python does not enforce interfaces. + if not isinstance(adapter_instance, AdapterInterface): + LOG.error("The adapter class must inherit from AdapterInterface.") + adapter_instance = None + elif not hasattr(adapter_instance, "output"): + LOG.error("The adapter class must have an 'output' attribute.") + adapter_instance = None + elif not hasattr(adapter_instance, "jira_config"): + LOG.error("The adapter class must have a 'jira_config' attribute.") + adapter_instance = None + elif not hasattr(adapter_instance, "polarion_config"): + LOG.error("The adapter class must have a 'polarion_config' attribute.") + adapter_instance = None + elif not hasattr(adapter_instance, "superset_config"): + LOG.error("The adapter class must have a 'superset_config' attribute.") + adapter_instance = None + elif not hasattr(adapter_instance, "handle_jira"): + LOG.error("The adapter class must have a 'handle_jira' method.") + adapter_instance = None + elif not hasattr(adapter_instance, "handle_polarion"): + LOG.error("The adapter class must have a 'handle_polarion' method.") + adapter_instance = None + else: + LOG.info("Adapter class successfully imported.") + + # Check if the values of the output dictionary in the adapter class are unique. + output_list = list(adapter_instance.output.keys()) + output_list_lowercase = [status.lower() for status in output_list] + + number_unique_values = len(set(output_list_lowercase)) + if number_unique_values != len(output_list): + LOG.error( + "The keys in the output dictionary in the adapter class must be unique.") + adapter_instance = None + + return adapter_instance + + +def _process_jira(adapter: AdapterInterface) -> Ret: + """ + Process the Jira query and search results. + + Returns: + Ret: The return status. + """ + ret_status = Ret.OK + + if adapter.jira_config.get("filter", "") != "": + # Overwrite the output directory with the temp directory. + adapter.jira_config["file"] = os.path.join( + _TEMP_DIR_NAME, "jira_search_results.json") + + LOG.info("Searching in Jira: %s", + adapter.jira_config["filter"]) + + jira_instance = Jira(adapter.jira_config) + if jira_instance.is_installed is False: + LOG.error("pyJiraCli is not installed!") + ret_status = Ret.ERROR_NOT_INSTALLED_JIRA + else: + jira_results = jira_instance.search() + if adapter.handle_jira(jira_results) is False: + ret_status = Ret.ERROR_ADAPTER_HANDLER_JIRA + + return ret_status + + +def _process_polarion(adapter: AdapterInterface) -> Ret: + """ + Process the Polarion query and search results. + + Returns: + Ret: The return status. + """ + ret_status = Ret.OK + + if adapter.polarion_config.get("query", "") != "": + # Overwrite the output directory with the temp directory. + adapter.polarion_config["output"] = _TEMP_DIR_NAME + + LOG.info("Searching in Polarion: %s", + adapter.polarion_config["query"]) + + polarion_instance = Polarion(adapter.polarion_config) + if polarion_instance.is_installed is False: + LOG.error("pyPolarionCli is not installed!") + ret_status = Ret.ERROR_NOT_INSTALLED_POLARION + else: + polarion_results = polarion_instance.search() + if adapter.handle_polarion(polarion_results) is False: + ret_status = Ret.ERROR_ADAPTER_HANDLER_POLARION + + return ret_status + + +def _save_temp_file(output: dict) -> Ret: + """ + Save the output dictionary to a temporary file. + + Args: + output (dict): The output dictionary. + + Returns: + Ret: The return status. + """ + ret_status = Ret.OK + + try: + # Write to the file. + with open(_TEMP_FILE_PATH, "w", encoding="UTF-8") as file: + json.dump(output, file, indent=2) + except Exception as e: # pylint: disable=broad-except + LOG.error("An error occurred writing the temporary file: %s", e) + ret_status = Ret.ERROR + + return ret_status + + +def _process_superset(adapter: AdapterInterface) -> Ret: + """ + Process the Superset file upload. + + Returns: + Ret: The return status. + """ + ret_status = Ret.OK + + # Send the temporary file to the metric server using Superset. + superset_instance = Superset(adapter.superset_config) + if superset_instance.is_installed is False: + LOG.error("pySupersetCli is not installed!") + ret_status = Ret.ERROR_NOT_INSTALLED_SUPERSET + else: + ret = superset_instance.upload(_TEMP_FILE_PATH) + + if 0 != ret: + ret_status = Ret.ERROR_SUPERSET_UPLOAD + LOG.error("Error while uploading to Superset!") + else: + LOG.info("Successfully uploaded to Superset!") + + return ret_status + + def main() -> Ret: """ The program entry point function. @@ -117,6 +298,8 @@ def main() -> Ret: ret_status = Ret.ERROR_ARGPARSE parser.print_help() else: + adapter: AdapterInterface = None + # If the verbose flag is set, change the default logging level. if args.verbose: logging.basicConfig(level=logging.INFO) @@ -124,13 +307,48 @@ def main() -> Ret: for arg in vars(args): LOG.info("* %s = %s", arg, vars(args)[arg]) - try: - if args.config_file.endswith(".json") is False: - raise ValueError( - "Invalid config_file format. Please provide a JSON file.") - except Exception as e: # pylint: disable=broad-except - LOG.error("An error occurred: %s", e) - ret_status = Ret.ERROR + # Check if temp directory exists, if not create it. + os.makedirs(_TEMP_DIR_NAME, exist_ok=True) + + # Check if the adapter is a Python file. + if args.adapter_file.endswith(".py") is False: + ret_status = Ret.ERROR_INVALID_ARGUMENT + LOG.error("The adapter must be a Python file.") + else: + adapter = _import_adapter(args.adapter_file) + + if adapter is None: + LOG.error("The adapter module could not be imported.") + ret_status = Ret.ERROR + else: + if Ret.OK != _process_jira(adapter=adapter): + LOG.error("Error while processing Jira.") + ret_status = Ret.ERROR_ADAPTER_HANDLER_JIRA + + elif Ret.OK != _process_polarion(adapter=adapter): + LOG.error("Error while processing Polarion.") + ret_status = Ret.ERROR_ADAPTER_HANDLER_POLARION + else: + # Save the output dictionary to a temporary file. + LOG.info("Saving output to a temporary file...") + + # Get the output from the adapter. + processed_output = adapter.output + + # Ensure the output always contains a date. + processed_output["date"] = datetime.datetime.now( + ).isoformat() + + # Save the output to a temporary file. + if Ret.OK != _save_temp_file(output=processed_output): + ret_status = Ret.ERROR + LOG.error("Error while saving the temporary file.") + elif Ret.OK != _process_superset(adapter=adapter): + ret_status = Ret.ERROR_SUPERSET_UPLOAD + LOG.error("Error while processing Superset.") + + # Clean up the temporary directory if exists. + shutil.rmtree(_TEMP_DIR_NAME, ignore_errors=True) return ret_status diff --git a/src/pyMetricCli/adapter_interface.py b/src/pyMetricCli/adapter_interface.py new file mode 100644 index 0000000..ad96c07 --- /dev/null +++ b/src/pyMetricCli/adapter_interface.py @@ -0,0 +1,85 @@ +"""Adapter interface module.""" + +# BSD 3-Clause License +# +# Copyright (c) 2024, NewTec GmbH +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICU5LAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +################################################################################ +# Imports +################################################################################ + +################################################################################ +# Variables +################################################################################ + +################################################################################ +# Classes +################################################################################ + + +class AdapterInterface(): + """ + Adapter interface class for handling different search results. + """ + + output: dict + jira_config: dict + polarion_config: dict + superset_config: dict + + def handle_jira(self, _search_results: dict) -> bool: + """ + Handles the JIRA search results. + + Args: + search_results: The search results from the JIRA API. + + Returns: + bool: True if the search results were handled successfully, False otherwise. + """ + return False + + def handle_polarion(self, _search_results: dict) -> bool: + """ + Handles the Polarion search results. + + Args: + search_results: The search results from the Polarion API. + + Returns: + bool: True if the search results were handled successfully, False otherwise. + """ + return False + +################################################################################ +# Functions +################################################################################ + +################################################################################ +# Main +################################################################################ diff --git a/src/pyMetricCli/jira.py b/src/pyMetricCli/jira.py new file mode 100644 index 0000000..29ac60f --- /dev/null +++ b/src/pyMetricCli/jira.py @@ -0,0 +1,138 @@ +""" +Wrapper for the pyJiraCli Tool. +""" + +# BSD 3-Clause License +# +# Copyright (c) 2024, NewTec GmbH +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICU5LAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +################################################################################ +# Imports +################################################################################ + +import subprocess +import json +import logging + +################################################################################ +# Variables +################################################################################ + +LOG: logging.Logger = logging.getLogger(__name__) + +################################################################################ +# Classes +################################################################################ + + +class Jira: # pylint: disable=too-few-public-methods + """ + Wrapper for the pyJiraCli Tool. + """ + + def __init__(self, jira_config: dict) -> None: + self.config = jira_config + self.is_installed = self.__check_if_is_installed() + + def __run_pyjiracli(self, arguments) -> subprocess.CompletedProcess: + """ + Wrapper to run pyJiraCli command line. + + Args: + arguments (list): List of arguments to pass to pyJiraCli. + + Returns: + subprocess.CompletedProcess[bytes]: The result of the command. + Includes return code, stdout and stderr. + """ + # pylint: disable=duplicate-code + args = ["pyJiraCli"] # The executable to run. + args.extend(arguments) # Add the arguments to the command. + return subprocess.run(args, + capture_output=True, + check=False, + shell=False) + + def __check_if_is_installed(self) -> bool: + """ + Checks if the pyJiraCli Tool is installed. + + Returns: + bool: True if pyJiraCli is installed, False otherwise. + """ + is_installed = True + try: + ret = self.__run_pyjiracli(["--help"]) + ret.check_returncode() + except subprocess.CalledProcessError: + print("pyJiraCli is not installed!") + is_installed = False + return is_installed + + def search(self) -> dict: + """ + Search in Jira using the search command of pyJiraCli. + + Returns: + dict: Search results. + """ + output = {} + command_list: list = ["--server", self.config["server"], + "--token", self.config["token"], + "search", + self.config["filter"], + "--file", self.config["file"], + "--max", self.config["max"]] + ret = self.__run_pyjiracli(command_list) + + for field in self.config["fields"]: + command_list.append("--field") + command_list.append(field) + + if 0 != ret.returncode: + print("Error while running pyJiraCli!") + print(ret.stderr) + else: + + try: + with open(self.config["file"], "r", encoding="utf-8") as file: + output = json.load(file) + except Exception as e: # pylint: disable=broad-except + LOG.error( + "An error occurred loading the Jira results from file: %s", e) + + return output + + +################################################################################ +# Functions +################################################################################ + +################################################################################ +# Main +################################################################################ diff --git a/src/pyMetricCli/polarion.py b/src/pyMetricCli/polarion.py new file mode 100644 index 0000000..f2447fa --- /dev/null +++ b/src/pyMetricCli/polarion.py @@ -0,0 +1,146 @@ +""" +Wrapper for the pyPolarionCli Tool. +""" + +# BSD 3-Clause License +# +# Copyright (c) 2024, NewTec GmbH +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICU5LAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +################################################################################ +# Imports +################################################################################ + +import os +import subprocess +import json +import logging + +################################################################################ +# Variables +################################################################################ + +LOG: logging.Logger = logging.getLogger(__name__) + +################################################################################ +# Classes +################################################################################ + + +class Polarion: # pylint: disable=too-few-public-methods + """ + Wrapper for the pyPolarionCli Tool. + """ + + def __init__(self, polarion_config: dict) -> None: + self.config = polarion_config + self.is_installed = self.__check_if_is_installed() + + def __run_pypolarioncli(self, arguments) -> subprocess.CompletedProcess: + """ + Wrapper to run pyPolarionCli command line. + + Args: + arguments (list): List of arguments to pass to pyPolarionCli. + + Returns: + subprocess.CompletedProcess[bytes]: The result of the command. + Includes return code, stdout and stderr. + """ + args = ["pyPolarionCli"] # The executable to run. + args.extend(arguments) # Add the arguments to the command. + + return subprocess.run(args, + capture_output=True, + check=False, + shell=False) + + def __check_if_is_installed(self) -> bool: + """ + Checks if the pyPolarionCli Tool is installed. + + Returns: + bool: True if pyPolarionCli is installed, False otherwise. + """ + # pylint: disable=duplicate-code + is_installed = True + try: + ret = self.__run_pypolarioncli(["--help"]) + ret.check_returncode() + except subprocess.CalledProcessError: + print("pyPolarionCli is not installed!") + is_installed = False + return is_installed + + def search(self) -> dict: + """ + Search in Polarion using the search command of pyPolarionCli. + + Returns: + dict: Search results. + """ + output = {} + + output_file_name = os.path.join(self.config['output'], + f"{self.config['project']}_search_results.json") + + command_list: list = ["--user", self.config["username"], + "--password", self.config["password"], + "--server", self.config["server"], + "search", + "--project", self.config["project"], + "--output", self.config["output"], + "--query", self.config["query"]] + + for field in self.config["fields"]: + command_list.append("--field") + command_list.append(field) + + ret = self.__run_pypolarioncli(command_list) + + if 0 != ret.returncode: + print("Error while running pyPolarionCli!") + print(ret.stderr) + else: + + try: + with open(output_file_name, "r", encoding="utf-8") as file: + output = json.load(file) + except Exception as e: # pylint: disable=broad-except + LOG.error( + "An error occurred loading the Polarion results from file: %s", e) + + return output + + +################################################################################ +# Functions +################################################################################ + +################################################################################ +# Main +################################################################################ diff --git a/src/pyMetricCli/ret.py b/src/pyMetricCli/ret.py index 1f5b7b0..4420158 100644 --- a/src/pyMetricCli/ret.py +++ b/src/pyMetricCli/ret.py @@ -50,6 +50,13 @@ class Ret(IntEnum): OK = 0 ERROR = 1 ERROR_ARGPARSE = 2 # Must be 2 to match the argparse error code. + ERROR_INVALID_ARGUMENT = 3 + ERROR_ADAPTER_HANDLER_JIRA = 4 + ERROR_ADAPTER_HANDLER_POLARION = 5 + ERROR_SUPERSET_UPLOAD = 6 + ERROR_NOT_INSTALLED_JIRA = 7 + ERROR_NOT_INSTALLED_POLARION = 8 + ERROR_NOT_INSTALLED_SUPERSET = 9 ################################################################################ # Functions diff --git a/src/pyMetricCli/superset.py b/src/pyMetricCli/superset.py new file mode 100644 index 0000000..690b86a --- /dev/null +++ b/src/pyMetricCli/superset.py @@ -0,0 +1,138 @@ +""" +Wrapper for the pySupersetCli Tool. +""" + +# BSD 3-Clause License +# +# Copyright (c) 2024, NewTec GmbH +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICU5LAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +################################################################################ +# Imports +################################################################################ + +import subprocess +import logging + +################################################################################ +# Variables +################################################################################ + +LOG: logging.Logger = logging.getLogger(__name__) + +################################################################################ +# Classes +################################################################################ + + +class Superset: # pylint: disable=too-few-public-methods + """ + Wrapper for the pySupersetCli Tool. + """ + + def __init__(self, superset_config: dict) -> None: + self.config = superset_config + self.is_installed = self.__check_if_is_installed() + + def __run_pysupersetcli(self, arguments) -> subprocess.CompletedProcess: + """ + Wrapper to run pySupersetCli command line. + + Args: + arguments (list): List of arguments to pass to pySupersetCli. + + Returns: + subprocess.CompletedProcess[bytes]: The result of the command. + Includes return code, stdout and stderr. + """ + # pylint: disable=duplicate-code + args = ["pySupersetCli"] # The executable to run. + args.extend(arguments) # Add the arguments to the command. + return subprocess.run(args, + capture_output=True, + check=False, + shell=False) + + def __check_if_is_installed(self) -> bool: + """ + Checks if the pySupersetCli Tool is installed. + + Returns: + bool: True if pySupersetCli is installed, False otherwise. + """ + is_installed = True + try: + ret = self.__run_pysupersetcli(["--help"]) + ret.check_returncode() + except subprocess.CalledProcessError: + print("pySupersetCli is not installed!") + is_installed = False + return is_installed + + def upload(self, input_file) -> int: + """ + Upload to Superset using the upload command of pySupersetCli. + + Args: + input_file (str): Path to JSON file which shall be uploaded. + + Returns: + int: Return Code of the command. + """ + command_list: list = ["--verbose", + "--server", self.config["server"], + "--user", self.config["user"], + "--password", self.config["password"]] + + if self.config.get("basic_auth") is True: + command_list.append("--basic_auth") + + if self.config.get("no_ssl") is True: + command_list.append("--no_ssl") + + LOG.info("Uploading file: %s", input_file) + + command_list.extend(["upload", + "--database", self.config["database"], + "--table", self.config["table"], + "--file", input_file + ]) + ret = self.__run_pysupersetcli(command_list) + + if 0 != ret.returncode: + LOG.error("Error while uploading: %s", ret.stderr) + + return ret.returncode + + +################################################################################ +# Functions +################################################################################ + +################################################################################ +# Main +################################################################################