diff --git a/.gitignore b/.gitignore index 09628947..3de11f24 100644 --- a/.gitignore +++ b/.gitignore @@ -139,10 +139,11 @@ dmypy.json cython_debug/ # App -config.yaml +/config.yaml docker-compose.yaml github-webhook-server.json -config-dev.yaml +/config-dev.yaml local-run.sh .scannerwork/ webhook-server.private-key.pem +log-colors.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1338325..e898b24e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,4 +55,5 @@ repos: rev: v1.11.2 hooks: - id: mypy + exclude: (tests/*) additional_dependencies: [types-requests, types-PyYAML] diff --git a/example.config.yaml b/example.config.yaml index 61570db6..e3088ded 100644 --- a/example.config.yaml +++ b/example.config.yaml @@ -24,7 +24,7 @@ auto-verified-and-merged-users: jira: server: project: - tokan: + token: user-mapping: : # if github user is not the same as jira diff --git a/poetry.lock b/poetry.lock index aba73e6c..15bcd38b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -782,6 +782,17 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] type = ["pytest-mypy"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "installer" version = "0.7.0" @@ -1410,6 +1421,21 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "poetry" version = "1.8.3" @@ -1774,6 +1800,45 @@ files = [ {file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, ] +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-rrmngmnt" version = "0.1.32" @@ -2561,4 +2626,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "d3747b4de46275d1bf82a9efb842e8ee4d79cf58f7e70a5e54d49066de39fdb9" +content-hash = "1392f5b2bb7e2c1402f566003f2ae07bcd90346fc6ceb9a9419e25d4e8894b8b" diff --git a/pyproject.toml b/pyproject.toml index d9e0c3ba..e3cd6d4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,11 @@ string-color = "^1.2.3" ipdb = "^0.13.13" ipython = "*" + +[tool.poetry.group.tests.dependencies] +pytest-mock = "^3.14.0" +pytest = "^8.3.3" + [tool.poetry-dynamic-versioning] enable = true pattern = "((?P\\d+)!)?(?P\\d+(\\.\\d+)*)" diff --git a/tox.ini b/tox.ini index 5e84e3ee..cf779487 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,16 @@ [tox] +envlist = unused-code, pytest skipsdist = True -[testenv] +[testenv:unused-code] deps = python-utility-scripts commands = pyutils-unusedcode --exclude-function-prefixes 'process_webhook' + +[testenv:pytest] +deps = + poetry +commands = + poetry install + poetry run pytest webhook_server_container/tests diff --git a/webhook_server_container/libs/github_api.py b/webhook_server_container/libs/github_api.py index 7ac1feb2..53fce6dd 100644 --- a/webhook_server_container/libs/github_api.py +++ b/webhook_server_container/libs/github_api.py @@ -617,18 +617,58 @@ def assign_reviewers(self) -> None: self.pull_request.create_issue_comment(f"{reviewer} can not be added as reviewer. {ex}") def get_size(self) -> str: - """Calculate size label based on additions and deletions.""" + """Calculates size label based on additions and deletions.""" + size = self.pull_request.additions + self.pull_request.deletions - prefixes = ["XS", "S", "M", "L", "XL", "XXL"] - for prefix in prefixes: - if size < 20 * (prefix[:-1].upper() == "S") + 10: - return f"{SIZE_LABEL_PREFIX}{prefix}" - return "" + # Define label thresholds in a more readable way + threshold_sizes = [20, 50, 100, 300, 500] + prefixes = ["XS", "S", "M", "L", "XL"] + + for i, size_threshold in enumerate(threshold_sizes): + if size < size_threshold: + _label = prefixes[i] + return f"{SIZE_LABEL_PREFIX}{_label}" + + return f"{SIZE_LABEL_PREFIX}XXL" + + # def get_size(self) -> str: + # """Calculate size label based on additions and deletions.""" + # size: int = self.pull_request.additions + self.pull_request.deletions + # if size < 20: + # _label = "XS" + # + # elif size < 50: + # _label = "S" + # + # elif size < 100: + # _label = "M" + # + # elif size < 300: + # _label = "L" + # + # elif size < 500: + # _label = "XL" + # + # else: + # _label = "XXL" + # + # return f"{SIZE_LABEL_PREFIX}{_label}" + # # size = self.pull_request.additions + self.pull_request.deletions + # # prefixes = ["XS", "S", "M", "L", "XL", "XXL"] + # # for prefix in prefixes: + # # if size < 20 * (prefix[:-1].upper() == "S") + 10: + # # return f"{SIZE_LABEL_PREFIX}{prefix}" + # # + # # return "" def add_size_label(self) -> None: """Add a size label to the pull request based on its additions and deletions.""" size_label = self.get_size() + if not size_label: + self.logger.debug(f"{self.log_prefix} Size label not found") + return + if size_label in self.pull_request_labels_names(): return diff --git a/webhook_server_container/tests/manifests/config.yaml b/webhook_server_container/tests/manifests/config.yaml new file mode 100644 index 00000000..6ce8f623 --- /dev/null +++ b/webhook_server_container/tests/manifests/config.yaml @@ -0,0 +1,92 @@ +log-level: INFO # Set global log level, change take effect immediately without server restart +log-file: webhook-server.log # Set global log file, change take effect immediately without server restart + +github-app-id: 123456 # GitHub app id +github-toekns: + - + - + +webhook_ip: + +docker: # Used to pull images from docker.io + username: + password: + +default-status-checks: + - "WIP" + - "dpulls" + - "can-be-merged" + +auto-verified-and-merged-users: + - "renovate[bot]" + - "pre-commit-ci[bot]" + +jira: + server: + project: + token: + user-mapping: + : # if github user is not the same as jira + +repositories: + test-repo: + name: my-org/test-repo + log-level: DEBUG # Override global log-level for repository + log-file: test-repo.log # Override global log-file for repository + slack_webhook_url: # Send notification to slack on several operations + verified_job: true + pypi: + token: + + events: # To listen to all events do not send events + - push + - pull_request + - issue_comment + - check_run + - pull_request_review + tox: + main: all # Run all tests in tox.ini when pull request parent branch is main + dev: testenv1,testenv2 # Run testenv1 and testenv2 tests in tox.ini when pull request parent branch is dev + + pre-commit: true # Run pre-commit check + + protected-branches: + dev: [] + main: # set [] in order to set all defaults run included + include-runs: + - "pre-commit.ci - pr" + - "WIP" + exclude-runs: + - "SonarCloud Code Analysis" + container: + username: + password: + repository: + tag: + release: true # Push image to registry on new release with release as the tag + build-args: # build args to send to podman build command + - my-build-arg1=1 + - my-build-arg2=2 + args: # args to send to podman build command + - --format docker + + auto-verified-and-merged-users: # override auto verified users per repository + - "my[bot]" + + github-tokens: # override GitHub tokens per repository + - + - + + can-be-merged-required-labels: # check for extra labels to set PR as can be merged + - my-label1 + - my-label2 + + jira-tracking: true + + jira: # override Jira global settings + server: + project: + token: + epic: # Optional + user-mapping: + : # if github user is not the same as jira diff --git a/webhook_server_container/tests/test_github_api.py b/webhook_server_container/tests/test_github_api.py new file mode 100644 index 00000000..61288948 --- /dev/null +++ b/webhook_server_container/tests/test_github_api.py @@ -0,0 +1,52 @@ +import pytest +from starlette.datastructures import Headers + +from simple_logger.logger import logging +from stringcolor.ops import os +from webhook_server_container.libs.github_api import ProcessGithubWehook +from webhook_server_container.utils.constants import SIZE_LABEL_PREFIX + + +class Repository: + def __init__(self): + self.name = "test-repo" + + +class PullRequest: + def __init__(self, additions: int, deletions: int): + self.additions = additions + self.deletions = deletions + + +@pytest.fixture(scope="function") +def process_github_webhook(mocker): + base_import_path = "webhook_server_container.libs.github_api" + os.environ["WEBHOOK_SERVER_DATA_DIR"] = "webhook_server_container/tests/manifests" + + mocker.patch(f"{base_import_path}.get_repository_github_app_api", return_value=True) + mocker.patch("github.AuthenticatedUser", return_value=True) + mocker.patch(f"{base_import_path}.get_api_with_highest_rate_limit", return_value=("API", "TOKEN")) + mocker.patch(f"{base_import_path}.get_github_repo_api", return_value=Repository()) + + return ProcessGithubWehook( + {"repository": {"name": Repository().name}}, Headers({"X-GitHub-Event": "test-event"}), logging.getLogger() + ) + + +@pytest.mark.parametrize( + "additions, deletions, expected_label", + [ + (0, 0, "XS"), + (18, 1, "XS"), + (48, 1, "S"), + (98, 1, "M"), + (298, 1, "L"), + (498, 1, "XL"), + (1000, 1, "XXL"), + ], +) +def test_get_size_thresholds(process_github_webhook, additions, deletions, expected_label): + process_github_webhook.pull_request = PullRequest(additions=additions, deletions=deletions) + result = process_github_webhook.get_size() + + assert result == f"{SIZE_LABEL_PREFIX}{expected_label}" diff --git a/webhook_server_container/utils/github_repository_settings.py b/webhook_server_container/utils/github_repository_settings.py index e05c782f..41cec5ef 100644 --- a/webhook_server_container/utils/github_repository_settings.py +++ b/webhook_server_container/utils/github_repository_settings.py @@ -30,8 +30,6 @@ get_logger_with_params, ) -LOGGER = get_logger_with_params(name="github-repository-settings") - def get_branch_sampler(repo: Repository, branch_name: str) -> Branch: return repo.get_branch(branch=branch_name) @@ -43,8 +41,10 @@ def set_branch_protection( required_status_checks: List[str], github_api: Github, ) -> bool: + logger = get_logger_with_params(name="github-repository-settings") + api_user = github_api.get_user().login - LOGGER.info(f"Set branch {branch} setting for {repository.name}. enabled checks: {required_status_checks}") + logger.info(f"Set branch {branch} setting for {repository.name}. enabled checks: {required_status_checks}") branch.edit_protection( strict=True, required_conversation_resolution=True, @@ -62,14 +62,16 @@ def set_branch_protection( def set_repository_settings(repository: Repository) -> None: - LOGGER.info(f"Set repository {repository.name} settings") + logger = get_logger_with_params(name="github-repository-settings") + + logger.info(f"Set repository {repository.name} settings") repository.edit(delete_branch_on_merge=True, allow_auto_merge=True, allow_update_branch=True) if repository.private: - LOGGER.warning(f"{repository.name}: Repository is private, skipping setting security settings") + logger.warning(f"{repository.name}: Repository is private, skipping setting security settings") return - LOGGER.info(f"Set repository {repository.name} security settings") + logger.info(f"Set repository {repository.name} security settings") repository._requester.requestJsonAndCheck( "PATCH", f"{repository.url}/code-scanning/default-setup", @@ -128,7 +130,9 @@ def get_user_configures_status_checks(status_checks: Dict[str, Any]) -> Tuple[Li def set_repository_labels(repository: Repository) -> str: - LOGGER.info(f"Set repository {repository.name} labels") + logger = get_logger_with_params(name="github-repository-settings") + + logger.info(f"Set repository {repository.name} labels") repository_labels: Dict[str, Dict[str, Any]] = {} for label in repository.get_labels(): repository_labels[label.name.lower()] = { @@ -143,22 +147,24 @@ def set_repository_labels(repository: Repository) -> str: if repository_labels[label_lower]["color"] == color: continue else: - LOGGER.debug(f"{repository.name}: Edit repository label {label} with color {color}") + logger.debug(f"{repository.name}: Edit repository label {label} with color {color}") repo_label.edit(name=repo_label.name, color=color) else: - LOGGER.debug(f"{repository.name}: Add repository label {label} with color {color}") + logger.debug(f"{repository.name}: Add repository label {label} with color {color}") repository.create_label(name=label, color=color) return f"{repository}: Setting repository labels is done" def set_repositories_settings(config_: Config, github_api: Github) -> None: - LOGGER.info("Processing repositories") + logger = get_logger_with_params(name="github-repository-settings") + + logger.info("Processing repositories") config_data = config_.data default_status_checks: List[str] = config_data.get("default-status-checks", []) docker: Optional[Dict[str, str]] = config_data.get("docker") if docker: - LOGGER.info("Login in to docker.io") + logger.info("Login in to docker.io") docker_username: str = docker["username"] docker_password: str = docker["password"] os.system(f"podman login -u {docker_username} -p {docker_password} docker.io") @@ -183,28 +189,30 @@ def set_repositories_settings(config_: Config, github_api: Github) -> None: def set_repository( data: Dict[str, Any], github_api: Github, default_status_checks: List[str] ) -> Tuple[bool, str, Callable]: + logger = get_logger_with_params(name="github-repository-settings") + repository: str = data["name"] - LOGGER.info(f"Processing repository {repository}") + logger.info(f"Processing repository {repository}") protected_branches: Dict[str, Any] = data.get("protected-branches", {}) repo = get_github_repo_api(github_api=github_api, repository=repository) if not repo: - return False, f"{repository}: Failed to get repository", LOGGER.error + return False, f"{repository}: Failed to get repository", logger.error try: set_repository_labels(repository=repo) set_repository_settings(repository=repo) if repo.private: - return False, f"{repository}: Repository is private, skipping setting branch settings", LOGGER.warning + return False, f"{repository}: Repository is private, skipping setting branch settings", logger.warning futures: List["Future"] = [] with ThreadPoolExecutor() as executor: for branch_name, status_checks in protected_branches.items(): - LOGGER.debug(f"{repository}: Getting branch {branch_name}") + logger.debug(f"{repository}: Getting branch {branch_name}") branch = get_branch_sampler(repo=repo, branch_name=branch_name) if not branch: - LOGGER.error(f"{repository}: Failed to get branch {branch_name}") + logger.error(f"{repository}: Failed to get branch {branch_name}") continue _default_status_checks = deepcopy(default_status_checks) @@ -234,12 +242,12 @@ def set_repository( for result in as_completed(futures): if result.exception(): - LOGGER.error(result.exception()) + logger.error(result.exception()) except UnknownObjectException as ex: - return False, f"{repository}: Failed to get repository settings, ex: {ex}", LOGGER.error + return False, f"{repository}: Failed to get repository settings, ex: {ex}", logger.error - return True, f"{repository}: Setting repository settings is done", LOGGER.info + return True, f"{repository}: Setting repository settings is done", logger.info def set_all_in_progress_check_runs_to_queued(config_: Config, github_api: Github) -> None: @@ -272,29 +280,33 @@ def set_all_in_progress_check_runs_to_queued(config_: Config, github_api: Github def set_repository_check_runs_to_queued( config_: Config, data: Dict[str, Any], github_api: Github, check_runs: Tuple[str] ) -> Tuple[bool, str, Callable]: + logger = get_logger_with_params(name="github-repository-settings") + repository: str = data["name"] repository_app_api = get_repository_github_app_api(config_=config_, repository_name=repository) if not repository_app_api: - return False, "Failed to get repositories GitHub app API", LOGGER.error + return False, "Failed to get repositories GitHub app API", logger.error app_api = get_github_repo_api(github_api=repository_app_api, repository=repository) repo = get_github_repo_api(github_api=github_api, repository=repository) - LOGGER.info(f"{repository}: Set all {IN_PROGRESS_STR} check runs to {QUEUED_STR}") + logger.info(f"{repository}: Set all {IN_PROGRESS_STR} check runs to {QUEUED_STR}") for pull_request in repo.get_pulls(state="open"): last_commit: Commit = list(pull_request.get_commits())[-1] for check_run in last_commit.get_check_runs(): if check_run.name in check_runs and check_run.status == IN_PROGRESS_STR: - LOGGER.warning( + logger.warning( f"{repository}: [PR:{pull_request.number}] {check_run.name} status is {IN_PROGRESS_STR}, " f"Setting check run {check_run.name} to {QUEUED_STR}" ) app_api.create_check_run(name=check_run.name, head_sha=last_commit.sha, status=QUEUED_STR) - return True, f"{repository}: Set check run status to {QUEUED_STR} is done", LOGGER.debug + return True, f"{repository}: Set check run status to {QUEUED_STR} is done", logger.debug def get_repository_github_app_api(config_: Config, repository_name: str) -> Optional[Github]: - LOGGER.debug("Getting repositories GitHub app API") + logger = get_logger_with_params(name="github-repository-settings") + + logger.debug("Getting repositories GitHub app API") with open(os.path.join(config_.data_dir, "webhook-server.private-key.pem")) as fd: private_key = fd.read() @@ -307,7 +319,7 @@ def get_repository_github_app_api(config_: Config, repository_name: str) -> Opti try: return app_instance.get_repo_installation(owner=owner, repo=repo).get_github_for_installation() except UnknownObjectException: - LOGGER.error( + logger.error( f"Repository {repository_name} not found by manage-repositories-app, " f"make sure the app installed (https://github.com/apps/manage-repositories-app)" ) @@ -315,6 +327,8 @@ def get_repository_github_app_api(config_: Config, repository_name: str) -> Opti if __name__ == "__main__": + logger = get_logger_with_params(name="github-repository-settings") + config = Config() api, _ = get_api_with_highest_rate_limit(config=config) if api: @@ -325,4 +339,4 @@ def get_repository_github_app_api(config_: Config, repository_name: str) -> Opti ) else: - LOGGER.error("Failed to get GitHub API") + logger.error("Failed to get GitHub API") diff --git a/webhook_server_container/utils/helpers.py b/webhook_server_container/utils/helpers.py index 6a2bb594..4a2f0833 100644 --- a/webhook_server_container/utils/helpers.py +++ b/webhook_server_container/utils/helpers.py @@ -43,9 +43,6 @@ def get_logger_with_params(name: str, repository_name: Optional[str] = "") -> Lo return get_logger(name=name, filename=log_file, level=log_level, file_max_bytes=1048576 * 50) # 50MB -LOGGER = get_logger_with_params(name="helpers") - - def extract_key_from_dict(key: Any, _dict: Dict[Any, Any]) -> Any: if isinstance(_dict, dict): for _key, _val in _dict.items(): @@ -90,10 +87,12 @@ def run_command( Returns: tuple: True, out if command succeeded, False, err otherwise. """ + logger = get_logger_with_params(name="helpers") + out_decoded: str = "" err_decoded: str = "" try: - LOGGER.debug(f"{log_prefix} Running '{command}' command") + logger.debug(f"{log_prefix} Running '{command}' command") sub_process = subprocess.run( shlex.split(command), capture_output=capture_output, @@ -112,17 +111,17 @@ def run_command( ) if sub_process.returncode != 0: - LOGGER.error(error_msg) + logger.error(error_msg) return False, out_decoded, err_decoded # From this point and onwards we are guaranteed that sub_process.returncode == 0 if err_decoded and verify_stderr: - LOGGER.error(error_msg) + logger.error(error_msg) return False, out_decoded, err_decoded return True, out_decoded, err_decoded except Exception as ex: - LOGGER.error(f"{log_prefix} Failed to run '{command}' command: {ex}") + logger.error(f"{log_prefix} Failed to run '{command}' command: {ex}") return False, out_decoded, err_decoded @@ -153,6 +152,8 @@ def get_api_with_highest_rate_limit(config: Config, repository_name: str = "") - Returns: tuple: API, token """ + logger = get_logger_with_params(name="helpers") + api: Optional[Github] = None token: Optional[str] = None _api_user: str = "" @@ -166,17 +167,19 @@ def get_api_with_highest_rate_limit(config: Config, repository_name: str = "") - rate_limit = _api.get_rate_limit() if rate_limit.core.remaining > remaining: remaining = rate_limit.core.remaining - LOGGER.debug(f"API user {_api_user} remaining rate limit: {remaining}") + logger.debug(f"API user {_api_user} remaining rate limit: {remaining}") api, token = _api, _token if rate_limit: log_rate_limit(rate_limit=rate_limit, api_user=_api_user) - LOGGER.info(f"API user {_api_user} selected with highest rate limit: {remaining}") + logger.info(f"API user {_api_user} selected with highest rate limit: {remaining}") return api, token def log_rate_limit(rate_limit: RateLimit, api_user: str) -> None: + logger = get_logger_with_params(name="helpers") + rate_limit_str: str time_for_limit_reset: int = (rate_limit.core.reset - datetime.datetime.now(tz=datetime.timezone.utc)).seconds below_minimum: bool = rate_limit.core.remaining < 700 @@ -195,9 +198,9 @@ def log_rate_limit(rate_limit: RateLimit, api_user: str) -> None: f"Reset in {rate_limit.core.reset} [{datetime.timedelta(seconds=time_for_limit_reset)}] " f"(UTC time is {datetime.datetime.now(tz=datetime.timezone.utc)})" ) - LOGGER.debug(msg) + logger.debug(msg) if below_minimum: - LOGGER.warning(msg) + logger.warning(msg) def get_future_results(futures: List["Future"]) -> None: