diff --git a/apps/jira_utils/README.md b/apps/jira_utils/README.md new file mode 100644 index 0000000..b1c4b10 --- /dev/null +++ b/apps/jira_utils/README.md @@ -0,0 +1,60 @@ +# pyutils-jira +Helper to identify wrong jira card references in a repository files. e.g. closed tickets or tickets with not relevant target versions in a test repository branch would be identified. +It looks for following patterns in files of a pytest repository: +```text + - jira_id=ABC-12345 # When jira id is present in a function call + - /browse/ABC-12345 # when jira is in a link in comments + - pytest.mark.jira_utils(ABC-12345) # when jira is in a marker +``` + +## Usage + +```bash +pyutils-jira +pyutils-jira --help +``` + +## Config file +A config file with the jira connection parameters like url, token, resolved_statuses, skip_project_ids, target_versions should be passed to command line option `--cfg-file` + +### Example: + +```yaml +pyutils-jira: + url: + token: mytoken + resolved_statuses: + - verified + - release pending + - closed + skip_project_ids: + - ABC + - DEF + target_versions: + - 1.0.0 + - 2.0.1 + issue_pattern: "([A-Z]+-[0-9]+)" + version_string_not_targeted_jiras: "vfuture" +``` +This would skip version checks on any jira ids associated with project ABC and DEF +This would also check if the current repository branch is pointing to any jira card that is not targeted for 1.0.0 or 2.0.1 version + +To run from CLI with `--target-versions` + +```bash +pyutils-jira --target-versions '1.0.0,2.0.1' +``` +#### Note: +To mark to skip a jira from these checks, one can add `` as a comment to the same line containing the specific jira + +Example: +```text + #https://issues.redhat.com/browse/OSD-5716 + ``` +If resolved_statuses is not provided in config file, by default the following status would be considered as resolved statuses: + +```text +["verified", "release pending", "closed", "resolved"] +``` + +Default value for `version_string_not_targeted_jiras` is "vfuture" diff --git a/apps/jira_utils/__init__.py b/apps/jira_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/jira_utils/jira_information.py b/apps/jira_utils/jira_information.py new file mode 100644 index 0000000..5f13280 --- /dev/null +++ b/apps/jira_utils/jira_information.py @@ -0,0 +1,264 @@ +import logging +import re +import sys +import os +import concurrent.futures +from functools import lru_cache + +import click +from jira import JIRA, JIRAError, Issue + +from simple_logger.logger import get_logger + +from apps.utils import ListParamType +from typing import Dict, List, Set, Tuple, Any + +from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed +from apps.utils import all_python_files, get_util_config + +LOGGER = get_logger(name=__name__) + + +@retry(retry=retry_if_exception_type(JIRAError), stop=stop_after_attempt(3), wait=wait_fixed(2)) +@lru_cache +def get_issue( + jira: JIRA, + jira_id: str, +) -> Issue: + LOGGER.debug(f"Retry staistics for {jira_id}: {get_issue.statistics}") + return jira.issue(id=jira_id, fields="status, issuetype, fixVersions") + + +def get_jira_ids_from_file_content(file_content: str, issue_pattern: str, jira_url: str) -> Set[str]: + """ + Try to find all Jira tickets in a given file content. + + Looking for the following patterns: + - jira_id=ABC-12345 # When jira id is present in a function call + - /browse/ABC-12345 # when jira is in a link in comments + - pytest.mark.jira_utils(ABC-12345) # when jira is in a marker + + Args: + file_content (str): The content of a given file. + issue_pattern (str): regex pattern for jira ids + + Returns: + set: A set of jira tickets. + """ + _pytest_jira_marker_bugs = re.findall(rf"pytest.mark.jira.*?{issue_pattern}.*", file_content, re.DOTALL) + _jira_id_arguments = re.findall(rf"jira_id\s*=[\s*\"\']*{issue_pattern}.*", file_content) + _jira_url_jiras = re.findall( + rf"{jira_url}/browse/{issue_pattern}", + file_content, + ) + + return set(_pytest_jira_marker_bugs + _jira_id_arguments + _jira_url_jiras) + + +def get_jiras_from_python_files(issue_pattern: str, jira_url: str) -> Dict[str, Set[str]]: + """ + Get all python files from the current directory and get list of jira ids from each of them + + Args: + issue_pattern (str): regex pattern for jira ids + jira_url (str): jira url that could be used to look for possible presence of jira references in a file + + Returns: + Dict: A dict of filenames and associated jira tickets. + + Note: any line containing would be not be checked for presence of a jira id + """ + jira_found: Dict[str, Set[str]] = {} + for filename in all_python_files(): + file_content = [] + with open(filename) as fd: + file_content = fd.readlines() + # if appears in a line, exclude that line from jira check + if unique_jiras := get_jira_ids_from_file_content( + file_content="\n".join([line for line in file_content if "" not in line]), + issue_pattern=issue_pattern, + jira_url=jira_url, + ): + jira_found[filename] = unique_jiras + + if jira_found: + _jira_found = "\n\t".join([f"{key}: {val}" for key, val in jira_found.items()]) + LOGGER.debug(f"Following jiras are found: \n\t{_jira_found}") + + return jira_found + + +def get_jira_information( + jira_object: JIRA, + jira_id: str, + skip_project_ids: List[str], + resolved_status: List[str], + jira_target_versions: List[str], + target_version_str: str, + file_name: str, +) -> Tuple[str, str]: + jira_error_string = "" + try: + # check resolved status: + jira_issue_metadata = get_issue(jira=jira_object, jira_id=jira_id).fields + current_jira_status = jira_issue_metadata.status.name.lower() + LOGGER.debug(f"Jira: {jira_id}, status: {current_jira_status}") + if current_jira_status in resolved_status: + jira_error_string += f"{jira_id} current status: {current_jira_status} is resolved." + + # validate correct target version if provided: + if jira_target_versions: + if skip_project_ids and jira_id.startswith(tuple(skip_project_ids)): + return file_name, jira_error_string + + fix_version = ( + re.search(r"([\d.]+)", jira_issue_metadata.fixVersions[0].name) + if (jira_issue_metadata.fixVersions) + else None + ) + current_target_version = fix_version.group(1) if fix_version else target_version_str + if not any([current_target_version == version for version in jira_target_versions]): + jira_error_string += ( + f"{jira_id} target version: {current_target_version}, does not match expected " + f"version {jira_target_versions}." + ) + except JIRAError as exp: + jira_error_string += f"{jira_id} JiraError status code: {exp.status_code}, details: {exp.text}]." + + return file_name, jira_error_string + + +def process_jira_command_line_config_file( + config_file_path: str, + url: str, + token: str, + issue_pattern: str, + resolved_statuses: List[str], + version_string_not_targeted_jiras: str, + target_versions: List[str], + skip_projects: List[str], +) -> Dict[str, Any]: + # Process all the arguments passed from command line or config file or environment variable + config_dict = get_util_config(util_name="pyutils-jira", config_file_path=config_file_path) + url = url or config_dict.get("url", "") + token = token or config_dict.get("token", "") + if not (url and token): + LOGGER.error("Jira url and token are required.") + sys.exit(1) + + return { + "url": url, + "token": token, + "issue_pattern": issue_pattern or config_dict.get("issue_pattern", ""), + "resolved_status": resolved_statuses or config_dict.get("resolved_statuses", []), + "not_targeted_version_str": config_dict.get( + "version_string_not_targeted_jiras", version_string_not_targeted_jiras + ), + "target_versions": target_versions or config_dict.get("target_versions", []), + "skip_project_ids": skip_projects or config_dict.get("skip_project_ids", []), + } + + +@click.command() +@click.option( + "--config-file-path", + help="Provide absolute path to the jira_utils config file.", + type=click.Path(exists=True), +) +@click.option( + "--target-versions", + help="Provide comma separated list of Jira target version, for version validation against a repo branch.", + type=ListParamType(), +) +@click.option( + "--skip-projects", + help="Provide comma separated list of Jira Project keys, against which version check should be skipped.", + type=ListParamType(), +) +@click.option("--url", help="Provide the Jira server URL", type=click.STRING, default=os.getenv("JIRA_SERVER_URL")) +@click.option("--token", help="Provide the Jira token.", type=click.STRING, default=os.getenv("JIRA_TOKEN")) +@click.option( + "--issue-pattern", + help="Provide the regex for Jira ids", + type=click.STRING, + show_default=True, + default="([A-Z]+-[0-9]+)", +) +@click.option( + "--resolved-statuses", + help="Comma separated list of Jira resolved statuses", + type=ListParamType(), + show_default=True, + default="verified, release pending, closed, resolved", +) +@click.option( + "--version-string-not-targeted-jiras", + help="Provide possible version strings for not yet targeted jiras", + type=click.STRING, + show_default=True, + default="vfuture", +) +@click.option("--verbose", default=False, is_flag=True) +def get_jira_mismatch( + config_file_path: str, + target_versions: List[str], + url: str, + token: str, + skip_projects: List[str], + resolved_statuses: List[str], + issue_pattern: str, + version_string_not_targeted_jiras: str, + verbose: bool, +) -> None: + LOGGER.setLevel(logging.DEBUG if verbose else logging.INFO) + if not (config_file_path or (token and url)): + LOGGER.error("Config file or token and url are required.") + sys.exit(1) + + # Process all the arguments passed from command line or config file or environment variable + jira_config_dict = process_jira_command_line_config_file( + config_file_path=config_file_path, + url=url, + token=token, + resolved_statuses=resolved_statuses, + issue_pattern=issue_pattern, + skip_projects=skip_projects, + version_string_not_targeted_jiras=version_string_not_targeted_jiras, + target_versions=target_versions, + ) + + jira_obj = JIRA(token_auth=jira_config_dict["token"], options={"server": jira_config_dict["url"]}) + jira_error: Dict[str, str] = {} + + if jira_id_dict := get_jiras_from_python_files( + issue_pattern=jira_config_dict["issue_pattern"], jira_url=jira_config_dict["url"] + ): + with concurrent.futures.ThreadPoolExecutor() as executor: + for file_name, ids in jira_id_dict.items(): + for jira_id in ids: + future_to_jiras = { + executor.submit( + get_jira_information, + jira_object=jira_obj, + jira_id=jira_id, + skip_project_ids=jira_config_dict["skip_project_ids"], + resolved_status=jira_config_dict["resolved_status"], + jira_target_versions=jira_config_dict["target_versions"], + target_version_str=jira_config_dict["not_targeted_version_str"], + file_name=file_name, + ) + } + + for future in concurrent.futures.as_completed(future_to_jiras): + file_name, jira_error_string = future.result() + if jira_error_string: + jira_error[file_name] = jira_error_string + + if jira_error: + _jira_error = "\n\t".join([f"{key}: {val}" for key, val in jira_error.items()]) + LOGGER.error(f"Following Jira ids failed jira version/statuscheck: \n\t{_jira_error}\n") + sys.exit(1) + + +if __name__ == "__main__": + get_jira_mismatch() diff --git a/apps/utils.py b/apps/utils.py index 3b1add6..7a4f925 100644 --- a/apps/utils.py +++ b/apps/utils.py @@ -5,13 +5,13 @@ from simple_logger.logger import get_logger import json import click -from typing import Any, Dict, Iterable +from typing import Any, Dict, Iterable, Optional LOGGER = get_logger(name=__name__) -def get_util_config(util_name: str, config_file_path: str) -> Dict[str, Any]: - if os.path.exists(config_file_path): +def get_util_config(util_name: str, config_file_path: Optional[str] = None) -> Dict[str, Any]: + if config_file_path and os.path.exists(config_file_path): with open(config_file_path) as _file: return yaml.safe_load(_file).get(util_name, {}) return {} @@ -80,7 +80,6 @@ def all_python_files() -> Iterable[str]: for root, _, files in os.walk(os.path.abspath(os.curdir)): if [_dir for _dir in exclude_dirs if _dir in root]: continue - for filename in files: if filename.endswith(".py"): yield os.path.join(root, filename) diff --git a/config.example.yaml b/config.example.yaml index 8decab9..f9972cf 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -9,3 +9,17 @@ pyutils-polarion-verify-tc-requirements: pyutils-polarion-set-automated: project_id: "ABCDEF" branch: "origin/main" +pyutils-jira: + url: + token: + resolved_statuses: + - verified + - release pending + - closed + issue_pattern: "([A-Z]+-[0-9]+)" + skip_project_ids: + - ABC + - DEF + jira_target_versions: + - 1.0.0 + version_string_not_targeted_jiras: "not-targeted" diff --git a/poetry.lock b/poetry.lock index 530f8cb..735a7f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "appnope" @@ -428,6 +428,17 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -552,6 +563,33 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +[[package]] +name = "jira" +version = "3.6.0" +description = "Python library for interacting with JIRA via REST APIs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jira-3.6.0-py3-none-any.whl", hash = "sha256:08b28388ee498542ebb6b05db87e6c46c37535c268717ccc23c84b377ea309fb"}, + {file = "jira-3.6.0.tar.gz", hash = "sha256:4c67497fe8dc2f60f1c4f7b33479f059c928bec3db9dcb5cd7b6a09b6ecc0942"}, +] + +[package.dependencies] +defusedxml = "*" +packaging = "*" +Pillow = ">=2.1.0" +requests = ">=2.10.0" +requests-oauthlib = ">=1.1.0" +requests-toolbelt = "*" +typing-extensions = ">=3.7.4.2" + +[package.extras] +async = ["requests-futures (>=0.9.7)"] +cli = ["ipython (>=4.0.0)", "keyring"] +docs = ["furo", "sphinx (>=5.0.0)", "sphinx-copybutton"] +opt = ["PyJWT", "filemagic (>=1.6)", "requests-jwt", "requests-kerberos"] +test = ["MarkupSafe (>=0.23)", "PyYAML (>=5.1)", "docutils (>=0.12)", "flaky", "oauthlib", "parameterized (>=0.8.1)", "pytest (>=6.0.0)", "pytest-cache", "pytest-cov", "pytest-instafail", "pytest-sugar", "pytest-timeout (>=1.3.1)", "pytest-xdist (>=2.2)", "requests-mock", "requires.io", "tenacity", "wheel (>=0.24.0)", "yanc (>=0.3.3)"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -615,6 +653,22 @@ files = [ [package.extras] nicer-shell = ["ipython"] +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "packaging" version = "23.2" @@ -698,6 +752,91 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +[[package]] +name = "pillow" +version = "10.2.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "pluggy" version = "1.5.0" @@ -1004,6 +1143,38 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "rich" version = "13.7.1" @@ -1064,6 +1235,21 @@ files = [ {file = "suds-1.1.2.tar.gz", hash = "sha256:1d5cfa74117193b244a4233f246c483d9f41198b448c5f14a8bad11c4f649f2b"}, ] +[[package]] +name = "tenacity" +version = "9.0.0" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + [[package]] name = "tomli" version = "2.0.1" @@ -1132,4 +1318,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "c6b3a15077b3ca292e7a2102ac79c547af8cb6c24b680cc7f6a0c11c97401b54" +content-hash = "d666cbb73412e8a79de21a69f4d749362a65456b24554d5f5fdea9b3e5d0e756" diff --git a/pyproject.toml b/pyproject.toml index 1d896b2..d23e949 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ omit = ["tests/*"] [tool.coverage.report] -fail_under = 60 +fail_under = 40 skip_empty = true [tool.coverage.html] @@ -48,6 +48,8 @@ packages = [{ include = "apps" }] pyutils-unusedcode = "apps.unused_code.unused_code:get_unused_functions" pyutils-polarion-verify-tc-requirements = "apps.polarion.polarion_verify_tc_requirements:has_verify" pyutils-polarion-set-automated = "apps.polarion.polarion_set_automated:polarion_approve_automate" +pyutils-jira = "apps.jira_utils.jira_information:get_jira_mismatch" + [tool.poetry.dependencies] python = "^3.8" @@ -56,6 +58,8 @@ pylero = "^0.1.0" pyhelper-utils = "^0.0.42" pytest-mock = "^3.14.0" pyyaml = "^6.0.1" +jira = "^3.6.0" +tenacity = "^9.0.0" [tool.poetry.group.dev.dependencies] ipdb = "^0.13.13"