diff --git a/.github/workflows/fast-tests-extension.yml b/.github/workflows/fast-tests-extension.yml index ed1c038ba..b99aaeeb3 100644 --- a/.github/workflows/fast-tests-extension.yml +++ b/.github/workflows/fast-tests-extension.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python & Poetry Environment id: set-up-python-and-poetry-environment - uses: exasol/python-toolbox/.github/actions/python-environment@v7 + uses: exasol/python-toolbox/.github/actions/python-environment@v8 with: python-version: "3.10" poetry-version: "2.3.0" diff --git a/.github/workflows/slow-checks.yml b/.github/workflows/slow-checks.yml index d04176fc1..8c4cd585f 100644 --- a/.github/workflows/slow-checks.yml +++ b/.github/workflows/slow-checks.yml @@ -11,7 +11,7 @@ jobs: contents: read run-integration-tests: - name: Run Integration Tests (Python-${{ matrix.python-version }}, Exasol-${{ matrix.exasol-version}}) + name: Run Integration Tests (Python-${{ matrix.python-version }}) needs: - build-matrix runs-on: "ubuntu-24.04" @@ -29,7 +29,7 @@ jobs: - name: Set up Python & Poetry Environment id: set-up-python-and-poetry-environment - uses: exasol/python-toolbox/.github/actions/python-environment@v7 + uses: exasol/python-toolbox/.github/actions/python-environment@v8 with: python-version: ${{ matrix.python-version }} poetry-version: "2.3.0" diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index dd51210ed..7ea1b208d 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -9,3 +9,7 @@ ## Feature * #722: Added check in `workflow:generate` to compare the generated and existing content before writing out + +## Refactoring + +* #722: Modified `workflow:generate` backend function to class `WorkflowOrchestrator` diff --git a/exasol/toolbox/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index 4f2085b1b..9508941a8 100644 --- a/exasol/toolbox/nox/_workflow.py +++ b/exasol/toolbox/nox/_workflow.py @@ -5,9 +5,9 @@ import nox from nox import Session -from exasol.toolbox.util.workflows.workflow import ( +from exasol.toolbox.util.workflows.workflow_orchestrator import ( WORKFLOW_CHOICES, - update_workflow, + WorkflowOrchestrator, ) from noxconfig import PROJECT_CONFIG @@ -37,4 +37,7 @@ def generate_workflow(session: Session) -> None: PROJECT_CONFIG.github_workflow_directory.mkdir(parents=True, exist_ok=True) - update_workflow(workflow_choice=args.workflow_choice, config=PROJECT_CONFIG) + WorkflowOrchestrator( + workflow_choice=args.workflow_choice, + config=PROJECT_CONFIG, + ).generate_workflows() diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index ba08d14a5..577b88574 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -1,10 +1,7 @@ import difflib -from collections.abc import Mapping from pathlib import Path from typing import ( - Annotated, Any, - Final, ) from pydantic import ( @@ -15,28 +12,15 @@ bound_contextvars, ) -from exasol.toolbox.config import BaseConfig from exasol.toolbox.util.workflows import logger from exasol.toolbox.util.workflows.exceptions import ( - InvalidWorkflowPatcherEntryError, - NotMaintainedWorkflowError, YamlError, YamlKeyError, ) from exasol.toolbox.util.workflows.patch_workflow import ( WorkflowCommentedMap, - WorkflowPatcher, ) from exasol.toolbox.util.workflows.process_template import WorkflowRenderer -from exasol.toolbox.util.workflows.templates import ( - WORKFLOW_TEMPLATE_OPTIONS, - validate_workflow_name, -) - -ALL: Final[str] = "all" -WORKFLOW_CHOICES: Final[list[str]] = [ALL, *WORKFLOW_TEMPLATE_OPTIONS.keys()] - -WorkflowChoice = Annotated[str, f"Should be a value from {WORKFLOW_CHOICES}"] class Workflow(BaseModel): @@ -78,9 +62,10 @@ def load_from_template( raise ValueError(f"Error rendering file: {template_path}") from ex def compare_to_file(self) -> str: - existing_content = ( - self.output_path.read_text().strip() if self.output_path.exists() else "" - ) + existing_content = "" + if self.output_path.is_file(): + existing_content = self.output_path.read_text().strip() + generated_content = self.content.strip() diff = difflib.unified_diff( @@ -98,59 +83,3 @@ def write_to_file(self) -> None: return logger.info("Write workflow file %s", self.output_path.name) self.output_path.write_text(self.content + "\n") - - -def _select_workflow_template(workflow_name: WorkflowChoice) -> Mapping[str, Path]: - """ - Returns a mapping of workflow names to paths. Can be a single item or all workflow - templates. - """ - if workflow_name == ALL: - return WORKFLOW_TEMPLATE_OPTIONS - return {workflow_name: WORKFLOW_TEMPLATE_OPTIONS[workflow_name]} - - -def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None: - """ - Updates a selected workflow or all workflows. - """ - workflow_dict = _select_workflow_template(workflow_choice) - logger.info(f"Selected workflow(s) to update: {list(workflow_dict.keys())}") - - workflow_patcher = None - if config.github_workflow_patcher_yaml: - workflow_patcher = WorkflowPatcher( - github_template_dict=config.github_template_dict, - file_path=config.github_workflow_patcher_yaml, - ) - - is_new_project = not any(config.github_workflow_directory.glob("*.yml")) - for workflow_name in workflow_dict: - patch_yaml = None - if workflow_patcher: - patch_yaml = workflow_patcher.extract_by_workflow( - workflow_name=workflow_name - ) - - try: - validate_workflow_name(workflow_name) - except NotMaintainedWorkflowError: - if not is_new_project: - logger.debug( - "Skipping not-maintained workflow in older project: %s", - workflow_name, - ) - continue - - try: - workflow = Workflow.load_from_template( - template_path=workflow_dict[workflow_name], - output_directory=config.github_workflow_directory, - github_template_dict=config.github_template_dict, - patch_yaml=patch_yaml, - ) - workflow.write_to_file() - except YamlKeyError as ex: - raise InvalidWorkflowPatcherEntryError( - file_path=config.github_workflow_patcher_yaml, entry=ex.entry # type: ignore - ) from ex diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py new file mode 100644 index 000000000..95977454d --- /dev/null +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from collections.abc import ( + Iterator, + Mapping, +) +from functools import cached_property +from pathlib import Path +from typing import ( + Annotated, + Final, +) + +from pydantic import BaseModel + +from exasol.toolbox.config import BaseConfig +from exasol.toolbox.util.workflows import logger +from exasol.toolbox.util.workflows.exceptions import ( + InvalidWorkflowPatcherEntryError, + NotMaintainedWorkflowError, + YamlKeyError, +) +from exasol.toolbox.util.workflows.patch_workflow import ( + WorkflowCommentedMap, + WorkflowPatcher, +) +from exasol.toolbox.util.workflows.templates import ( + WORKFLOW_TEMPLATE_OPTIONS, + validate_workflow_name, +) +from exasol.toolbox.util.workflows.workflow import Workflow + +ALL: Final[str] = "all" +WorkflowChoice = Annotated[ + str, f"Should be a value from {[ALL, *WORKFLOW_TEMPLATE_OPTIONS.keys()]}" +] +WORKFLOW_CHOICES: Final[list[str]] = [ALL, *WORKFLOW_TEMPLATE_OPTIONS.keys()] + + +class WorkflowOrchestrator(BaseModel): + """Orchestrate workflow rendering, comparison, and writing.""" + + workflow_choice: WorkflowChoice + config: BaseConfig + + @cached_property + def templates(self) -> Mapping[str, Path]: + """ + A mapping of workflow templates names to paths. This can be a single + item or all workflow templates. + """ + if self.workflow_choice == ALL: + return WORKFLOW_TEMPLATE_OPTIONS + return {self.workflow_choice: WORKFLOW_TEMPLATE_OPTIONS[self.workflow_choice]} + + @cached_property + def workflow_patcher(self) -> WorkflowPatcher | None: + if not self.config.github_workflow_patcher_yaml: + return None + return WorkflowPatcher( + github_template_dict=self.config.github_template_dict, + file_path=self.config.github_workflow_patcher_yaml, + ) + + def _extract_workflow_patch( + self, workflow_name: str + ) -> WorkflowCommentedMap | None: + """ + Return the patch data for a workflow, or ``None`` if no patcher is configured. + """ + if self.workflow_patcher is None: + return None + return self.workflow_patcher.extract_by_workflow(workflow_name=workflow_name) + + def _is_new_project(self) -> bool: + """ + A project is considered new if no YML files are present in the GitHub directory. + """ + return not any(self.config.github_workflow_directory.glob("*.yml")) + + def _iter_workflows(self) -> Iterator[Workflow]: + logger.info(f"Selected workflow(s) to update: {list(self.templates.keys())}") + is_new_project = self._is_new_project() + for workflow_name, template_path in self.templates.items(): + patch_yaml = self._extract_workflow_patch(workflow_name=workflow_name) + + if self._skip_workflow(workflow_name, is_new_project): + continue + + yield self._load_workflow( + template_path=template_path, patch_yaml=patch_yaml + ) + + def _load_workflow( + self, template_path: Path, patch_yaml: WorkflowCommentedMap | None + ): + try: + return Workflow.load_from_template( + template_path=template_path, + output_directory=self.config.github_workflow_directory, + github_template_dict=self.config.github_template_dict, + patch_yaml=patch_yaml, + ) + except YamlKeyError as ex: + raise InvalidWorkflowPatcherEntryError( + file_path=self.config.github_workflow_patcher_yaml, # type: ignore + entry=ex.entry, + ) from ex + + def _skip_workflow(self, workflow_name: str, is_new_project: bool) -> bool: + """ + Return ``True`` if the workflow should be skipped because it is not maintained + by the PTB, otherwise return ``False``. + """ + try: + validate_workflow_name(workflow_name) + except NotMaintainedWorkflowError: + if not is_new_project: + logger.debug( + "Skipping not-maintained workflow in older project: %s", + workflow_name, + ) + return True + return False + + def generate_workflows(self) -> None: + """ + Render the selected workflows and write them to disk. + """ + for workflow in self._iter_workflows(): + workflow.write_to_file() diff --git a/poetry.lock b/poetry.lock index 093d95048..1c1c2222c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -130,43 +130,45 @@ test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock [[package]] name = "ast-serialize" -version = "0.3.0" +version = "0.5.0" description = "Python bindings for mypy AST serialization" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1"}, - {file = "ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0"}, - {file = "ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9"}, - {file = "ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc"}, - {file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4"}, - {file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7"}, - {file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94"}, - {file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61"}, - {file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0"}, - {file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f"}, - {file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a"}, - {file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b"}, - {file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9"}, - {file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57"}, - {file = "ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2"}, - {file = "ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549"}, - {file = "ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a"}, - {file = "ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb"}, + {file = "ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101"}, + {file = "ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d"}, + {file = "ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a"}, + {file = "ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590"}, + {file = "ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642"}, + {file = "ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6"}, ] [[package]] @@ -342,39 +344,39 @@ files = [ [[package]] name = "black" -version = "26.3.1" +version = "26.5.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}, - {file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}, - {file = "black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"}, - {file = "black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"}, - {file = "black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"}, - {file = "black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"}, - {file = "black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"}, - {file = "black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"}, - {file = "black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"}, - {file = "black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"}, - {file = "black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"}, - {file = "black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"}, - {file = "black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"}, - {file = "black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"}, - {file = "black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"}, - {file = "black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"}, - {file = "black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"}, - {file = "black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"}, - {file = "black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"}, - {file = "black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"}, - {file = "black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"}, - {file = "black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"}, - {file = "black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"}, - {file = "black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"}, - {file = "black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"}, - {file = "black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"}, - {file = "black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"}, + {file = "black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893"}, + {file = "black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90"}, + {file = "black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4"}, + {file = "black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef"}, + {file = "black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22"}, + {file = "black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c"}, + {file = "black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7"}, + {file = "black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59"}, + {file = "black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3"}, + {file = "black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe"}, + {file = "black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8"}, + {file = "black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217"}, + {file = "black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d"}, + {file = "black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264"}, + {file = "black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418"}, + {file = "black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3"}, + {file = "black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0"}, + {file = "black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294"}, + {file = "black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a"}, + {file = "black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52"}, + {file = "black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168"}, + {file = "black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3"}, + {file = "black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18"}, + {file = "black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50"}, + {file = "black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae"}, + {file = "black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2"}, + {file = "black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73"}, ] [package.dependencies] @@ -435,14 +437,14 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.5.20" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, - {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, + {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"}, + {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"}, ] [[package]] @@ -696,14 +698,14 @@ files = [ [[package]] name = "click" -version = "8.3.3" +version = "8.4.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, - {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, + {file = "click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81"}, + {file = "click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973"}, ] [package.dependencies] @@ -948,22 +950,6 @@ typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3. [package.extras] ssh = ["bcrypt (>=3.1.5)"] -[[package]] -name = "cssutils" -version = "2.11.0" -description = "A CSS Cascading Style Sheets library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cssutils-2.11.0-py3-none-any.whl", hash = "sha256:220816dc6d413e81281bbd568c473a8ae28f73b1af008b1bacf3a7ebd21e0334"}, - {file = "cssutils-2.11.0.tar.gz", hash = "sha256:cd24a30b9a848ca92d80f0d1b362139c0b69de31394d585dbf1b17a5dc4aa627"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["cssselect", "importlib-resources ; python_version < \"3.9\"", "jaraco.test (>=5.1)", "lxml ; python_version < \"3.11\"", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - [[package]] name = "cyclonedx-python-lib" version = "11.7.0" @@ -1021,19 +1007,19 @@ cli = ["tomli ; python_version < \"3.11\""] [[package]] name = "dict2css" -version = "0.4.0" +version = "0.6.0" description = "A μ-library for constructing cascading style sheets from Python dictionaries." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "dict2css-0.4.0-py3-none-any.whl", hash = "sha256:d6058b953a35bd56c0574e8b85540df8da26348d16a21063d9063dd6fd1cd188"}, - {file = "dict2css-0.4.0.tar.gz", hash = "sha256:fbb01ebf86c5b667aa9d0348294ef7141a1ee0b64df75d468ea423e1aaef561a"}, + {file = "dict2css-0.6.0-py3-none-any.whl", hash = "sha256:5251f1df1c78ffdf09313657a7f88add0ad219127d9aeb18fb343b052d6bfbbe"}, + {file = "dict2css-0.6.0.tar.gz", hash = "sha256:143e55cb71c98a88c79f2c41e08a5fa4d875659275756f794e31ccd69936ce88"}, ] [package.dependencies] -cssutils = ">=2.2.0,<=2.11.0" domdf-python-tools = ">=2.2.0" +tinycss2 = ">=1.2.1" [[package]] name = "dill" @@ -1337,14 +1323,14 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.14" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69"}, - {file = "idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] [package.extras] @@ -1517,27 +1503,27 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "jaraco-functools" -version = "4.4.0" +version = "4.5.0" description = "Functools like those found in stdlib" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, - {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, + {file = "jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4"}, + {file = "jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03"}, ] [package.dependencies] more_itertools = "*" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=3.4)"] test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "jeepney" @@ -1894,14 +1880,14 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.6.0" +version = "0.6.1" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "mdit_py_plugins-0.6.0-py3-none-any.whl", hash = "sha256:f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90"}, - {file = "mdit_py_plugins-0.6.0.tar.gz", hash = "sha256:2436f14a7295837ac9228a36feeabda867c4abc488c8d019ad5c0bda88eee040"}, + {file = "mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d"}, + {file = "mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0"}, ] [package.dependencies] @@ -2663,8 +2649,8 @@ astroid = ">=4.0.2,<=4.1.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=5,<5.13 || >5.13,<9" mccabe = ">=0.6,<0.8" @@ -2764,14 +2750,14 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.3.0" +version = "1.3.1" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f"}, - {file = "python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0"}, + {file = "python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c"}, + {file = "python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6"}, ] [package.dependencies] @@ -2779,7 +2765,7 @@ filelock = ">=3.15.4" platformdirs = ">=4.3.6,<5" [package.extras] -docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)", "sphinxcontrib-towncrier (>=0.4)", "towncrier (>=25.8)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] [[package]] @@ -2988,14 +2974,14 @@ md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "requests" -version = "2.34.0" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60"}, - {file = "requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] @@ -3232,14 +3218,14 @@ files = [ [[package]] name = "shibuya" -version = "2026.1.9" +version = "2026.5.19" description = "A clean, responsive, and customizable Sphinx documentation theme with light/dark mode." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "shibuya-2026.1.9-py3-none-any.whl", hash = "sha256:b58a3cc6e5619c71d00fcf0be4a3060c87040c2a62a1b3f1a93a6a41ca8eaf45"}, - {file = "shibuya-2026.1.9.tar.gz", hash = "sha256:b389f10fd9c07b048e940f32d1e1ac096a2d49736389173ac771b37a10b51fdf"}, + {file = "shibuya-2026.5.19-py3-none-any.whl", hash = "sha256:7e94f6d5d9e269510138f4742c993c875beaa32344bf5fc55d104bc89e62028e"}, + {file = "shibuya-2026.5.19.tar.gz", hash = "sha256:f39e012e945ecb93f8aeaeab48e7bfaa204d570fe86796034a1f01fe4f1a3709"}, ] [package.dependencies] @@ -3710,14 +3696,14 @@ files = [ [[package]] name = "stevedore" -version = "5.7.0" +version = "5.8.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed"}, - {file = "stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3"}, + {file = "stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b"}, + {file = "stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715"}, ] [[package]] @@ -3762,6 +3748,25 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] +[[package]] +name = "tinycss2" +version = "1.5.1" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661"}, + {file = "tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["furo", "sphinx"] +test = ["pytest", "ruff"] + [[package]] name = "tokenize-rt" version = "6.2.0" @@ -3914,14 +3919,14 @@ shellingham = ">=1.3.0" [[package]] name = "types-pyyaml" -version = "6.0.12.20260510" +version = "6.0.12.20260518" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_pyyaml-6.0.12.20260510-py3-none-any.whl", hash = "sha256:3492eb9ba4d9d833473214c4d5736cccf5f37d93f5854059721e1c84f785309d"}, - {file = "types_pyyaml-6.0.12.20260510.tar.gz", hash = "sha256:09c1f1cb65a6eebea1e2e51ccf4918b8288e152909609a35cdb0d805efd125ad"}, + {file = "types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd"}, + {file = "types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466"}, ] [[package]] @@ -3983,21 +3988,21 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" -version = "21.3.1" +version = "21.3.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35"}, - {file = "virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b"}, + {file = "virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3"}, + {file = "virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" -python-discovery = ">=1.2.2" +python-discovery = ">=1.3.1" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [[package]] @@ -4038,24 +4043,24 @@ files = [ [[package]] name = "zipp" -version = "3.23.1" +version = "4.1.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"}, - {file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"}, + {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, + {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [metadata] lock-version = "2.1" diff --git a/test/unit/nox/_workflow_test.py b/test/unit/nox/_workflow_test.py index 44eed55cd..75cf445a6 100644 --- a/test/unit/nox/_workflow_test.py +++ b/test/unit/nox/_workflow_test.py @@ -6,7 +6,7 @@ from exasol.toolbox.config import BaseConfig from exasol.toolbox.nox._workflow import generate_workflow from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS -from exasol.toolbox.util.workflows.workflow import ALL +from exasol.toolbox.util.workflows.workflow_orchestrator import ALL @pytest.fixture diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py new file mode 100644 index 000000000..6a79df611 --- /dev/null +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -0,0 +1,267 @@ +from pathlib import Path +from unittest.mock import patch + +import pytest + +from exasol.toolbox.util.workflows.exceptions import ( + InvalidWorkflowPatcherEntryError, + YamlJobValueError, +) +from exasol.toolbox.util.workflows.templates import ( + NOT_MAINTAINED_WORKFLOW_NAMES, + WORKFLOW_TEMPLATE_OPTIONS, +) +from exasol.toolbox.util.workflows.workflow_orchestrator import WorkflowOrchestrator + + +class TestTemplates: + @staticmethod + def test_all_works_as_expected(project_config): + result = WorkflowOrchestrator( + workflow_choice="all", config=project_config + ).templates + assert result == WORKFLOW_TEMPLATE_OPTIONS + + @staticmethod + @pytest.mark.parametrize("workflow_name", WORKFLOW_TEMPLATE_OPTIONS) + def test_individual_workflows_works_as_expected(workflow_name, project_config): + result = WorkflowOrchestrator( + workflow_choice=workflow_name, config=project_config + ).templates + assert result == {workflow_name: WORKFLOW_TEMPLATE_OPTIONS[workflow_name]} + + +class TestIsNewProject: + @staticmethod + def test_returns_true_when_no_yml_files_exist(project_config): + project_config.github_workflow_directory.mkdir(parents=True) + + result = WorkflowOrchestrator( + workflow_choice="all", + config=project_config, + )._is_new_project() + + assert result is True + + @staticmethod + def test_returns_false_when_yml_files_exist(project_config): + workflow_directory = project_config.github_workflow_directory + workflow_directory.mkdir(parents=True) + (workflow_directory / "existing.yml").touch() + + result = WorkflowOrchestrator( + workflow_choice="all", + config=project_config, + )._is_new_project() + + assert result is False + + +class TestSkipWorkflow: + @staticmethod + def test_returns_true_for_not_maintained_workflow_in_existing_project( + project_config, + ): + workflow_directory = project_config.github_workflow_directory + workflow_directory.mkdir(parents=True) + (workflow_directory / "existing.yml").touch() + + result = WorkflowOrchestrator( + workflow_choice=NOT_MAINTAINED_WORKFLOW_NAMES[0], + config=project_config, + )._skip_workflow( + workflow_name=NOT_MAINTAINED_WORKFLOW_NAMES[0], + is_new_project=False, + ) + + assert result is True + + @staticmethod + def test_returns_false_for_maintained_workflow(project_config): + result = WorkflowOrchestrator( + workflow_choice="checks", + config=project_config, + )._skip_workflow(workflow_name="checks", is_new_project=False) + + assert result is False + + +class TestIterWorkflows: + @staticmethod + def test_works_as_expected_without_patcher(project_config_without_patcher): + workflow_name = "merge-gate" + project_config_without_patcher.github_workflow_directory.mkdir(parents=True) + input_text = WORKFLOW_TEMPLATE_OPTIONS[workflow_name].read_text() + + result = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + )._iter_workflows() + + result = list(result) + assert len(result) == 1 + assert result[0].template_path == WORKFLOW_TEMPLATE_OPTIONS[workflow_name] + assert result[0].output_path.name == f"{workflow_name}.yml" + assert result[0].content[:10] == input_text[:10] + + @staticmethod + def test_works_as_expected_with_relevant_patcher(project_config, remove_job_yaml): + workflow_name = "checks" + project_config.github_workflow_directory.mkdir(parents=True) + input_text = WORKFLOW_TEMPLATE_OPTIONS[workflow_name].read_text() + removed_job_name = "build-documentation-and-check-links" + assert removed_job_name in remove_job_yaml + assert removed_job_name in input_text + + result = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config, + )._iter_workflows() + + result = list(result) + assert len(result) == 1 + assert result[0].output_path.name == f"{workflow_name}.yml" + assert result[0].content[:10] == input_text[:10] + assert removed_job_name not in result[0].content + + @staticmethod + def test_works_as_expected_with_not_relevant_patcher( + project_config, remove_job_yaml + ): + workflow_name = "merge-gate" + project_config.github_workflow_directory.mkdir(parents=True) + input_text = WORKFLOW_TEMPLATE_OPTIONS[workflow_name].read_text() + + result = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config, + )._iter_workflows() + + result = list(result) + assert len(result) == 1 + assert result[0].output_path.name == f"{workflow_name}.yml" + assert result[0].content[:10] == input_text[:10] + + @staticmethod + def test_not_maintained_workflows_added_to_new_project( + project_config_without_patcher, + ): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True) + + result = WorkflowOrchestrator( + workflow_choice="all", + config=project_config_without_patcher, + )._iter_workflows() + + assert {f"{name}.yml" for name in NOT_MAINTAINED_WORKFLOW_NAMES}.issubset( + {workflow.output_path.name for workflow in result} + ) + + @staticmethod + @pytest.mark.parametrize("workflow_name", NOT_MAINTAINED_WORKFLOW_NAMES) + def test_not_maintained_workflows_not_modified_in_old_project( + project_config_without_patcher, workflow_name + ): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True, exist_ok=True) + workflow = "slow-checks.yml" + (directory / workflow).touch() + + result = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + )._iter_workflows() + + assert list(result) == [] + + @staticmethod + @pytest.mark.parametrize("workflow_name", NOT_MAINTAINED_WORKFLOW_NAMES) + def test_not_maintained_workflows_not_added_to_old_project( + project_config_without_patcher, workflow_name + ): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True, exist_ok=True) + (directory / "dummy.yml").touch() + + result = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + )._iter_workflows() + + assert list(result) == [] + + @staticmethod + def test_raises_invalidworkflowpatcherentryerror(project_config): + patcher_yml = """ + workflows: + - name: "checks" + remove_jobs: + - unknown-job + """ + project_config.github_workflow_patcher_yaml.write_text(patcher_yml) + + with pytest.raises(InvalidWorkflowPatcherEntryError) as ex: + for _ in WorkflowOrchestrator( + workflow_choice="checks", + config=project_config, + )._iter_workflows(): + pass + + assert ( + f"In file '{project_config.github_workflow_patcher_yaml}', " + "an entry '{'job_name': 'unknown-job'}' does not exist in" + ) in str(ex.value) + assert isinstance(ex.value.__cause__, YamlJobValueError) + + +class TestGenerateWorkflows: + @staticmethod + def test_writes_all_workflows_on_fresh_project(project_config_without_patcher): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True) + + WorkflowOrchestrator( + workflow_choice="all", + config=project_config_without_patcher, + ).generate_workflows() + + assert all( + (directory / f"{name}.yml").exists() for name in WORKFLOW_TEMPLATE_OPTIONS + ) + + @staticmethod + def test_does_not_write_when_all_workflows_are_up_to_date( + project_config_without_patcher, + ): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True) + + WorkflowOrchestrator( + workflow_choice="all", + config=project_config_without_patcher, + ).generate_workflows() + + with patch.object(Path, "write_text") as write_text: + WorkflowOrchestrator( + workflow_choice="all", + config=project_config_without_patcher, + ).generate_workflows() + + write_text.assert_not_called() + + @staticmethod + def test_overwrites_existing_workflow_file(project_config_without_patcher): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True) + + workflow_name = "merge-gate" + workflow_path = directory / f"{workflow_name}.yml" + original_content = "line 3\n" + workflow_path.write_text(original_content) + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).generate_workflows() + assert workflow_path.read_text() != original_content diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index d17790bca..8148fcc75 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -5,23 +5,41 @@ import pytest from exasol.toolbox.util.workflows.exceptions import ( - InvalidWorkflowPatcherEntryError, TemplateRenderingError, - YamlJobValueError, YamlOutputError, YamlParsingError, ) from exasol.toolbox.util.workflows.process_template import WorkflowRenderer from exasol.toolbox.util.workflows.templates import ( - NOT_MAINTAINED_WORKFLOW_NAMES, WORKFLOW_TEMPLATE_OPTIONS, ) -from exasol.toolbox.util.workflows.workflow import ( - ALL, - Workflow, - _select_workflow_template, - update_workflow, -) +from exasol.toolbox.util.workflows.workflow import Workflow + + +@pytest.fixture +def workflow_template_path(tmp_path): + template_directory = tmp_path / "templates" + template_directory.mkdir() + template_path = template_directory / "workflow.yml" + + content = """ + jobs: + check-release-tag: + name: Check Release Tag + uses: ./.github/workflows/check-release-tag.yml + permissions: + contents: read + """ + + template_path.write_text(cleandoc(content)) + return template_path + + +@pytest.fixture +def workflow_output_directory(tmp_path): + output_directory = tmp_path / "output" + output_directory.mkdir() + return output_directory class TestWorkflow: @@ -60,59 +78,93 @@ def test_works_as_expected(tmp_path, project_config): assert output_file_path.read_text() == cleandoc(expected_yaml) + "\n" @staticmethod - def test_compare_to_file_accepts_matching_content(tmp_path): - content = "line 1\nline 2" - file_path = tmp_path / "workflow.yml" - file_path.write_text(f"\n{content}\n") + def test_compare_to_file_has_identical_content( + project_config, workflow_template_path, workflow_output_directory + ): + content = workflow_template_path.read_text() + output_path = workflow_output_directory / workflow_template_path.name + output_path.write_text(content) - workflow = Workflow( - template_path=file_path, - output_path=file_path, - content=f"\n{content}\n", + workflow = Workflow.load_from_template( + template_path=workflow_template_path, + output_directory=workflow_output_directory, + github_template_dict=project_config.github_template_dict, ) assert workflow.compare_to_file() == "" @staticmethod - def test_compare_to_file_reports_diff(tmp_path): - file_path = tmp_path / "workflow.yml" - file_path.write_text("line 1\nline 3\n") - workflow = Workflow( - template_path=file_path, - output_path=file_path, - content="line 1\nline 2", + def test_compare_to_file_lacks_existing_content( + project_config, workflow_template_path, workflow_output_directory + ): + workflow = Workflow.load_from_template( + template_path=workflow_template_path, + output_directory=workflow_output_directory, + github_template_dict=project_config.github_template_dict, + ) + + assert workflow.compare_to_file() == ( + f"--- existing: {workflow.output_path.name}\n" + "+++ generated\n" + "@@ -0,0 +1,6 @@\n" + "+jobs:\n" + "+check-release-tag:\n" + "+ name: Check Release Tag\n" + "+ uses: ./.github/workflows/check-release-tag.yml\n" + "+ permissions:\n" + "+ contents: read" ) - diff = workflow.compare_to_file() + @staticmethod + def test_compare_to_file_reports_diff( + project_config, workflow_template_path, workflow_output_directory + ): + output_path = workflow_output_directory / workflow_template_path.name + output_path.write_text("line 3\n") - assert diff == ( - f"--- existing: {file_path.name}\n" + workflow = Workflow.load_from_template( + template_path=workflow_template_path, + output_directory=workflow_output_directory, + github_template_dict=project_config.github_template_dict, + ) + + assert workflow.compare_to_file() == ( + f"--- existing: {workflow.output_path.name}\n" "+++ generated\n" - "@@ -1,2 +1,2 @@\n" - " line 1\n" + "@@ -1 +1,6 @@\n" "-line 3\n" - "+line 2" + "+jobs:\n" + "+check-release-tag:\n" + "+ name: Check Release Tag\n" + "+ uses: ./.github/workflows/check-release-tag.yml\n" + "+ permissions:\n" + "+ contents: read" ) @staticmethod - def test_write_to_file_skips_up_to_date_file(tmp_path): - file_path = tmp_path / "workflow.yml" - file_path.write_text("line 1\nline 2\n") - workflow = Workflow( - template_path=file_path, - output_path=file_path, - content="line 1\nline 2", + def test_write_to_file_skips_up_to_date_file( + project_config, workflow_template_path, workflow_output_directory + ): + content = workflow_template_path.read_text() + output_path = workflow_output_directory / workflow_template_path.name + output_path.write_text(content) + + workflow = Workflow.load_from_template( + template_path=workflow_template_path, + output_directory=workflow_output_directory, + github_template_dict=project_config.github_template_dict, ) with patch.object(Path, "write_text") as write_text: workflow.write_to_file() write_text.assert_not_called() - assert file_path.read_text() == "line 1\nline 2\n" @staticmethod @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) - def test_works_for_all_templates(tmp_path, project_config, template_path): + def test_write_to_file_works_for_all_templates( + tmp_path, project_config, template_path + ): workflow = Workflow.load_from_template( template_path=template_path, output_directory=tmp_path, @@ -124,7 +176,9 @@ def test_works_for_all_templates(tmp_path, project_config, template_path): assert file_path.read_text() != "" @staticmethod - def test_fails_when_yaml_does_not_exist(tmp_path, project_config): + def test_load_from_template_fails_when_yaml_does_not_exist( + tmp_path, project_config + ): file_path = tmp_path / "test.yaml" with pytest.raises(FileNotFoundError, match="test.yaml"): Workflow.load_from_template( @@ -137,7 +191,9 @@ def test_fails_when_yaml_does_not_exist(tmp_path, project_config): @pytest.mark.parametrize( "raised_exc", [TemplateRenderingError, YamlParsingError, YamlOutputError] ) - def test_raises_custom_exceptions(tmp_path, project_config, raised_exc): + def test_load_from_template_raises_custom_exceptions( + tmp_path, project_config, raised_exc + ): file_path = tmp_path / "test.yaml" file_path.write_text("dummy content") @@ -152,7 +208,9 @@ def test_raises_custom_exceptions(tmp_path, project_config, raised_exc): ) @staticmethod - def test_other_exceptions_raised_as_valuerror(tmp_path, project_config): + def test_load_from_template_reraises_other_exceptions_raised_as_valuerror( + tmp_path, project_config + ): file_path = tmp_path / "test.yaml" file_path.write_text("dummy content") @@ -165,164 +223,3 @@ def test_other_exceptions_raised_as_valuerror(tmp_path, project_config): output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) - - -class TestSelectWorkflowTemplate: - @staticmethod - def test_for_all_works_as_expected(): - result = _select_workflow_template(ALL) - assert result == WORKFLOW_TEMPLATE_OPTIONS - - @staticmethod - @pytest.mark.parametrize("workflow_name", WORKFLOW_TEMPLATE_OPTIONS) - def test_for_individual_workflows_works_as_expected(workflow_name): - result = _select_workflow_template(workflow_name) - assert result == {workflow_name: WORKFLOW_TEMPLATE_OPTIONS[workflow_name]} - - -class TestUpdateWorkflow: - @staticmethod - def test_works_as_expected_without_patcher(project_config_without_patcher): - workflow_name = "merge-gate" - # setup - project_config_without_patcher.github_workflow_directory.mkdir(parents=True) - input_text = WORKFLOW_TEMPLATE_OPTIONS[workflow_name].read_text() - expected_file_path = ( - project_config_without_patcher.github_workflow_directory - / f"{workflow_name}.yml" - ) - - update_workflow( - workflow_choice=workflow_name, config=project_config_without_patcher - ) - result = expected_file_path.read_text() - - # Currently, we check only a subselection as we must preserve formatting for tbx - # endpoints, and there are 2 minor whitespace differences. - assert result[:10] == input_text[:10] - - @staticmethod - def test_works_as_expected_with_relevant_patcher(project_config, remove_job_yaml): - # remove_job_yaml modifies "checks" and that's also the workflow being updated - workflow_name = "checks" - # setup - project_config.github_workflow_directory.mkdir(parents=True) - input_text = WORKFLOW_TEMPLATE_OPTIONS[workflow_name].read_text() - expected_file_path = ( - project_config.github_workflow_directory / f"{workflow_name}.yml" - ) - # setup checks - removed_job_name = "build-documentation-and-check-links" - assert removed_job_name in remove_job_yaml - assert removed_job_name in input_text - - update_workflow(workflow_choice="checks", config=project_config) - result = expected_file_path.read_text() - - # We compare only a subselection to verify that the files are roughly the - # same, and we expect them to differ as the 'result' does not contain - # the 'removed_job_name' - assert result[:10] == input_text[:10] - assert removed_job_name not in result - - @staticmethod - def test_works_as_expected_with_not_relevant_patcher( - project_config, remove_job_yaml - ): - # remove_job_yaml modifies "checks" and that's NOT the workflow being updated - workflow_name = "merge-gate" - # setup - project_config.github_workflow_directory.mkdir(parents=True) - input_text = WORKFLOW_TEMPLATE_OPTIONS[workflow_name].read_text() - expected_file_path = ( - project_config.github_workflow_directory / f"{workflow_name}.yml" - ) - - update_workflow(workflow_choice=workflow_name, config=project_config) - result = expected_file_path.read_text() - - # Currently, we check only a subselection as we must preserve formatting for tbx - # endpoints, and there are 2 minor whitespace differences. - assert result[:10] == input_text[:10] - - @staticmethod - def test_not_maintained_workflows_added_to_new_project( - project_config_without_patcher, - ): - directory = project_config_without_patcher.github_workflow_directory - directory.mkdir(parents=True) - - update_workflow(workflow_choice="all", config=project_config_without_patcher) - - assert all( - (directory / f"{name}.yml").exists() - for name in NOT_MAINTAINED_WORKFLOW_NAMES - ) - - @staticmethod - @pytest.mark.parametrize("workflow_name", NOT_MAINTAINED_WORKFLOW_NAMES) - def test_not_maintained_workflows_not_modified_in_old_project( - project_config_without_patcher, workflow_name - ): - directory = project_config_without_patcher.github_workflow_directory - directory.mkdir(parents=True, exist_ok=True) - workflow = "slow-checks.yml" - (directory / workflow).touch() - - update_workflow( - workflow_choice=workflow_name, config=project_config_without_patcher - ) - - assert {file_path.name for file_path in directory.iterdir()} == {workflow} - assert (directory / workflow).read_text() == "" - - @staticmethod - @pytest.mark.parametrize("workflow_name", NOT_MAINTAINED_WORKFLOW_NAMES) - def test_not_maintained_workflows_not_added_to_old_project( - project_config_without_patcher, workflow_name - ): - directory = project_config_without_patcher.github_workflow_directory - directory.mkdir(parents=True, exist_ok=True) - (directory / "dummy.yml").touch() - - update_workflow( - workflow_choice=workflow_name, config=project_config_without_patcher - ) - - assert {file_path.name for file_path in directory.iterdir()} == {"dummy.yml"} - - @staticmethod - @pytest.mark.parametrize("workflow_name", NOT_MAINTAINED_WORKFLOW_NAMES) - def test_not_maintained_workflows_not_modified_in_old_project( - project_config_without_patcher, workflow_name - ): - directory = project_config_without_patcher.github_workflow_directory - directory.mkdir(parents=True, exist_ok=True) - workflow = "slow-checks.yml" - (directory / workflow).touch() - - update_workflow( - workflow_choice=workflow_name, config=project_config_without_patcher - ) - - assert {file_path.name for file_path in directory.iterdir()} == {workflow} - assert (directory / workflow).read_text() == "" - - @staticmethod - def test_raises_invalidworkflowpatcherentryerror(project_config): - patcher_yml = """ - workflows: - - name: "checks" - remove_jobs: - - unknown-job - """ - project_config.github_workflow_patcher_yaml.write_text(patcher_yml) - - with pytest.raises(InvalidWorkflowPatcherEntryError) as ex: - update_workflow(workflow_choice="checks", config=project_config) - - assert ( - f"In file '{project_config.github_workflow_patcher_yaml}', " - "an entry '{'job_name': 'unknown-job'}' does not exist in" - ) in str(ex.value) - assert isinstance(ex.value.__cause__, YamlJobValueError)