From e9d644524246ec97df0ba4d99f9bc41a107c1fd6 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 11:24:23 +0200 Subject: [PATCH 01/21] Clean up unit tests and add coverage case --- test/unit/util/workflows/workflow_test.py | 54 ++++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index d17790bca..24e9f06f6 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -60,33 +60,54 @@ 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(tmp_path): + template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] + content = template_path.read_text() + output_path = tmp_path / "workflow.yml" + output_path.write_text(f"\n{content}\n") workflow = Workflow( - template_path=file_path, - output_path=file_path, + template_path=template_path, + output_path=output_path, content=f"\n{content}\n", ) assert workflow.compare_to_file() == "" + @staticmethod + def test_compare_to_file_lacks_existing_content(tmp_path): + template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] + assert template_path.read_text() != "" + output_path = tmp_path / "does_not_exist_workflow.yml" + + workflow = Workflow( + template_path=template_path, + output_path=output_path, + content="line 1", + ) + + assert workflow.compare_to_file() == ( + f"--- existing: {output_path.name}\n" + "+++ generated\n" + "@@ -0,0 +1 @@\n" + "+line 1" + ) + @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") + template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] + output_path = tmp_path / "workflow.yml" + output_path.write_text("line 1\nline 3\n") workflow = Workflow( - template_path=file_path, - output_path=file_path, + template_path=template_path, + output_path=output_path, content="line 1\nline 2", ) diff = workflow.compare_to_file() assert diff == ( - f"--- existing: {file_path.name}\n" + f"--- existing: {output_path.name}\n" "+++ generated\n" "@@ -1,2 +1,2 @@\n" " line 1\n" @@ -96,11 +117,12 @@ def test_compare_to_file_reports_diff(tmp_path): @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") + template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] + output_path = tmp_path / "workflow.yml" + output_path.write_text("line 1\nline 2\n") workflow = Workflow( - template_path=file_path, - output_path=file_path, + template_path=template_path, + output_path=output_path, content="line 1\nline 2", ) @@ -108,7 +130,7 @@ def test_write_to_file_skips_up_to_date_file(tmp_path): workflow.write_to_file() write_text.assert_not_called() - assert file_path.read_text() == "line 1\nline 2\n" + assert output_path.read_text() == "line 1\nline 2\n" @staticmethod @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) From ad377d12121ac2d5e4eb6dd44a1e1a11cb9ce904 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 12:21:15 +0200 Subject: [PATCH 02/21] Clean up unit tests and add coverage case, as well as simplify code ifs --- exasol/toolbox/util/workflows/workflow.py | 7 +- test/unit/util/workflows/workflow_test.py | 126 ++++++++++++++-------- 2 files changed, 85 insertions(+), 48 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index ba08d14a5..fb88c63a8 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -78,9 +78,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( diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index 24e9f06f6..d7b5aa7eb 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -24,6 +24,32 @@ ) +@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: @staticmethod def test_works_as_expected(tmp_path, project_config): @@ -60,77 +86,87 @@ 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_has_identical_content(tmp_path): - template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] - content = template_path.read_text() - output_path = tmp_path / "workflow.yml" - output_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=template_path, - output_path=output_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_lacks_existing_content(tmp_path): - template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] - assert template_path.read_text() != "" - output_path = tmp_path / "does_not_exist_workflow.yml" - - workflow = Workflow( - template_path=template_path, - output_path=output_path, - content="line 1", + 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: {output_path.name}\n" + f"--- existing: {workflow.output_path.name}\n" "+++ generated\n" - "@@ -0,0 +1 @@\n" - "+line 1" + "@@ -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" ) @staticmethod - def test_compare_to_file_reports_diff(tmp_path): - template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] - output_path = tmp_path / "workflow.yml" - output_path.write_text("line 1\nline 3\n") - workflow = Workflow( - template_path=template_path, - output_path=output_path, - content="line 1\nline 2", - ) + 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") - diff = workflow.compare_to_file() + workflow = Workflow.load_from_template( + template_path=workflow_template_path, + output_directory=workflow_output_directory, + github_template_dict=project_config.github_template_dict, + ) - assert diff == ( - f"--- existing: {output_path.name}\n" + 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): - template_path = WORKFLOW_TEMPLATE_OPTIONS["checks"] - output_path = tmp_path / "workflow.yml" - output_path.write_text("line 1\nline 2\n") - workflow = Workflow( - template_path=template_path, - output_path=output_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 output_path.read_text() == "line 1\nline 2\n" @staticmethod @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) From 00d973f5660173cf6d81c84d3991cb0607424b2f Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 12:25:39 +0200 Subject: [PATCH 03/21] Rename tests so clearer what is being tested --- test/unit/util/workflows/workflow_test.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index d7b5aa7eb..79cb70c10 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -170,7 +170,9 @@ def test_write_to_file_skips_up_to_date_file( @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, @@ -182,7 +184,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( @@ -195,7 +199,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") @@ -210,7 +216,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") From fe38c75b447addf0c062793ba96e08f3686812eb Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 13:10:51 +0200 Subject: [PATCH 04/21] Start with WorkflowOrchestrator class by moving select_template --- exasol/toolbox/nox/_workflow.py | 6 ++-- exasol/toolbox/util/workflows/workflow.py | 29 ++++----------- .../util/workflows/workflow_orchestrator.py | 35 +++++++++++++++++++ test/unit/nox/_workflow_test.py | 2 +- .../workflows/workflow_orchestrator_test.py | 17 +++++++++ test/unit/util/workflows/workflow_test.py | 15 -------- 6 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 exasol/toolbox/util/workflows/workflow_orchestrator.py create mode 100644 test/unit/util/workflows/workflow_orchestrator_test.py diff --git a/exasol/toolbox/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index 4f2085b1b..acab6178d 100644 --- a/exasol/toolbox/nox/_workflow.py +++ b/exasol/toolbox/nox/_workflow.py @@ -5,10 +5,8 @@ import nox from nox import Session -from exasol.toolbox.util.workflows.workflow import ( - WORKFLOW_CHOICES, - update_workflow, -) +from exasol.toolbox.util.workflows.workflow import update_workflow +from exasol.toolbox.util.workflows.workflow_orchestrator import WORKFLOW_CHOICES from noxconfig import PROJECT_CONFIG diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index fb88c63a8..488f8d410 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 ( @@ -28,16 +25,12 @@ WorkflowPatcher, ) from exasol.toolbox.util.workflows.process_template import WorkflowRenderer -from exasol.toolbox.util.workflows.templates import ( - WORKFLOW_TEMPLATE_OPTIONS, - validate_workflow_name, +from exasol.toolbox.util.workflows.templates import validate_workflow_name +from exasol.toolbox.util.workflows.workflow_orchestrator import ( + WorkflowChoice, + WorkflowOrchestrator, ) -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): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) @@ -101,21 +94,13 @@ def write_to_file(self) -> None: 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) + + orchestrator = WorkflowOrchestrator(workflow_choice=workflow_choice) + workflow_dict = orchestrator.templates logger.info(f"Selected workflow(s) to update: {list(workflow_dict.keys())}") workflow_patcher = None diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py new file mode 100644 index 000000000..89f9dc9c0 --- /dev/null +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from collections.abc import Mapping +from functools import cached_property +from pathlib import Path +from typing import ( + Annotated, + Final, +) + +from pydantic import BaseModel + +from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS + +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 + + @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]} 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..f8e873e54 --- /dev/null +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -0,0 +1,17 @@ +import pytest + +from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS +from exasol.toolbox.util.workflows.workflow_orchestrator import WorkflowOrchestrator + + +class TestTemplates: + @staticmethod + def test_for_all_works_as_expected(): + result = WorkflowOrchestrator(workflow_choice="all").templates + 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 = WorkflowOrchestrator(workflow_choice=workflow_name).templates + assert result == {workflow_name: WORKFLOW_TEMPLATE_OPTIONS[workflow_name]} diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index 79cb70c10..45c5a82c7 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -17,9 +17,7 @@ WORKFLOW_TEMPLATE_OPTIONS, ) from exasol.toolbox.util.workflows.workflow import ( - ALL, Workflow, - _select_workflow_template, update_workflow, ) @@ -233,19 +231,6 @@ def test_load_from_template_reraises_other_exceptions_raised_as_valuerror( ) -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): From 2152b597b7ff52eabd42450a0cd218e457a89fa7 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:10:53 +0200 Subject: [PATCH 05/21] Move workflow_patcher to WorkflowOrchestrator --- exasol/toolbox/util/workflows/workflow.py | 14 +++++--------- .../util/workflows/workflow_orchestrator.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 488f8d410..c1f3854d1 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -22,7 +22,6 @@ ) 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 validate_workflow_name @@ -99,17 +98,14 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None Updates a selected workflow or all workflows. """ - orchestrator = WorkflowOrchestrator(workflow_choice=workflow_choice) + orchestrator = WorkflowOrchestrator( + workflow_choice=workflow_choice, + config=config, + ) workflow_dict = orchestrator.templates 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, - ) - + workflow_patcher = orchestrator.workflow_patcher is_new_project = not any(config.github_workflow_directory.glob("*.yml")) for workflow_name in workflow_dict: patch_yaml = None diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 89f9dc9c0..b3c985c88 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -10,6 +10,8 @@ from pydantic import BaseModel +from exasol.toolbox.config import BaseConfig +from exasol.toolbox.util.workflows.patch_workflow import WorkflowPatcher from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS ALL: Final[str] = "all" @@ -23,6 +25,7 @@ class WorkflowOrchestrator(BaseModel): """Orchestrate workflow rendering, comparison, and writing.""" workflow_choice: WorkflowChoice + config: BaseConfig @cached_property def templates(self) -> Mapping[str, Path]: @@ -33,3 +36,12 @@ def templates(self) -> Mapping[str, Path]: 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, + ) From 763a27f65f416f530ca3f61d400436c49329bc61 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:13:24 +0200 Subject: [PATCH 06/21] Move is_new_project to WorkflowOrchestrator --- exasol/toolbox/util/workflows/workflow.py | 2 +- .../util/workflows/workflow_orchestrator.py | 7 +++++ .../workflows/workflow_orchestrator_test.py | 30 +++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index c1f3854d1..0b925550d 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -106,7 +106,7 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None logger.info(f"Selected workflow(s) to update: {list(workflow_dict.keys())}") workflow_patcher = orchestrator.workflow_patcher - is_new_project = not any(config.github_workflow_directory.glob("*.yml")) + is_new_project = orchestrator.is_new_project for workflow_name in workflow_dict: patch_yaml = None if workflow_patcher: diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index b3c985c88..009506696 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -27,6 +27,13 @@ class WorkflowOrchestrator(BaseModel): workflow_choice: WorkflowChoice config: BaseConfig + @cached_property + 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")) + @cached_property def templates(self) -> Mapping[str, Path]: """ diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index f8e873e54..0e476990d 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -6,12 +6,38 @@ class TestTemplates: @staticmethod - def test_for_all_works_as_expected(): + def test_all_works_as_expected(): result = WorkflowOrchestrator(workflow_choice="all").templates assert result == WORKFLOW_TEMPLATE_OPTIONS @staticmethod @pytest.mark.parametrize("workflow_name", WORKFLOW_TEMPLATE_OPTIONS) - def test_for_individual_workflows_works_as_expected(workflow_name): + def test_individual_workflows_works_as_expected(workflow_name): result = WorkflowOrchestrator(workflow_choice=workflow_name).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 From 62c6cb88fd7b7de8dfff82571e652b002ddd3c72 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:24:53 +0200 Subject: [PATCH 07/21] Add _extract_workflow_patch --- exasol/toolbox/util/workflows/workflow.py | 7 +------ .../toolbox/util/workflows/workflow_orchestrator.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 0b925550d..2bc61c16a 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -105,14 +105,9 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None workflow_dict = orchestrator.templates logger.info(f"Selected workflow(s) to update: {list(workflow_dict.keys())}") - workflow_patcher = orchestrator.workflow_patcher is_new_project = orchestrator.is_new_project for workflow_name in workflow_dict: - patch_yaml = None - if workflow_patcher: - patch_yaml = workflow_patcher.extract_by_workflow( - workflow_name=workflow_name - ) + patch_yaml = orchestrator.patch_yaml_for_workflow(workflow_name=workflow_name) try: validate_workflow_name(workflow_name) diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 009506696..854299254 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -11,7 +11,10 @@ from pydantic import BaseModel from exasol.toolbox.config import BaseConfig -from exasol.toolbox.util.workflows.patch_workflow import WorkflowPatcher +from exasol.toolbox.util.workflows.patch_workflow import ( + WorkflowCommentedMap, + WorkflowPatcher, +) from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS ALL: Final[str] = "all" @@ -52,3 +55,10 @@ def workflow_patcher(self) -> WorkflowPatcher | None: 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: + if self.workflow_patcher is None: + return None + return self.workflow_patcher.extract_by_workflow(workflow_name=workflow_name) From 196e4b7a2f980391d6c54cc0eee03387a9a70125 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:28:28 +0200 Subject: [PATCH 08/21] Add _skip_workflow --- exasol/toolbox/util/workflows/workflow.py | 17 +++------- .../util/workflows/workflow_orchestrator.py | 23 ++++++++++++- .../workflows/workflow_orchestrator_test.py | 34 ++++++++++++++++++- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 2bc61c16a..8be49559d 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -16,7 +16,6 @@ from exasol.toolbox.util.workflows import logger from exasol.toolbox.util.workflows.exceptions import ( InvalidWorkflowPatcherEntryError, - NotMaintainedWorkflowError, YamlError, YamlKeyError, ) @@ -24,7 +23,6 @@ WorkflowCommentedMap, ) from exasol.toolbox.util.workflows.process_template import WorkflowRenderer -from exasol.toolbox.util.workflows.templates import validate_workflow_name from exasol.toolbox.util.workflows.workflow_orchestrator import ( WorkflowChoice, WorkflowOrchestrator, @@ -107,17 +105,12 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None is_new_project = orchestrator.is_new_project for workflow_name in workflow_dict: - patch_yaml = orchestrator.patch_yaml_for_workflow(workflow_name=workflow_name) + patch_yaml = orchestrator._extract_workflow_patch(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 + if orchestrator._skip_workflow( + workflow_name=workflow_name, is_new_project=is_new_project + ): + continue try: workflow = Workflow.load_from_template( diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 854299254..460667048 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -11,11 +11,16 @@ from pydantic import BaseModel from exasol.toolbox.config import BaseConfig +from exasol.toolbox.util.workflows import logger +from exasol.toolbox.util.workflows.exceptions import NotMaintainedWorkflowError from exasol.toolbox.util.workflows.patch_workflow import ( WorkflowCommentedMap, WorkflowPatcher, ) -from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS +from exasol.toolbox.util.workflows.templates import ( + WORKFLOW_TEMPLATE_OPTIONS, + validate_workflow_name, +) ALL: Final[str] = "all" WorkflowChoice = Annotated[ @@ -62,3 +67,19 @@ def _extract_workflow_patch( if self.workflow_patcher is None: return None return self.workflow_patcher.extract_by_workflow(workflow_name=workflow_name) + + 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 diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 0e476990d..3190c2f3a 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -1,6 +1,9 @@ import pytest -from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS +from exasol.toolbox.util.workflows.templates import ( + NOT_MAINTAINED_WORKFLOW_NAMES, + WORKFLOW_TEMPLATE_OPTIONS, +) from exasol.toolbox.util.workflows.workflow_orchestrator import WorkflowOrchestrator @@ -41,3 +44,32 @@ def test_returns_false_when_yml_files_exist(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 From a50f25af72c7ccb99f6efd1e1a5a39f8606326f1 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:38:41 +0200 Subject: [PATCH 09/21] Fix missed tests --- .../util/workflows/workflow_orchestrator_test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 3190c2f3a..164141a3c 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -9,14 +9,18 @@ class TestTemplates: @staticmethod - def test_all_works_as_expected(): - result = WorkflowOrchestrator(workflow_choice="all").templates + 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): - result = WorkflowOrchestrator(workflow_choice=workflow_name).templates + 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]} From 181337730689a905513fb08edb7c65efbb214908 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:43:37 +0200 Subject: [PATCH 10/21] Add _load_generated_workflow --- exasol/toolbox/util/workflows/workflow.py | 18 ++++--------- .../util/workflows/workflow_orchestrator.py | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 8be49559d..9939bf148 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -15,7 +15,6 @@ from exasol.toolbox.config import BaseConfig from exasol.toolbox.util.workflows import logger from exasol.toolbox.util.workflows.exceptions import ( - InvalidWorkflowPatcherEntryError, YamlError, YamlKeyError, ) @@ -112,15 +111,8 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None ): 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 + workflow = orchestrator._load_generated_workflow( + template_path=workflow_dict[workflow_name], + patch_yaml=patch_yaml, + ) + workflow.write_to_file() diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 460667048..10001fedd 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -12,7 +12,11 @@ from exasol.toolbox.config import BaseConfig from exasol.toolbox.util.workflows import logger -from exasol.toolbox.util.workflows.exceptions import NotMaintainedWorkflowError +from exasol.toolbox.util.workflows.exceptions import ( + InvalidWorkflowPatcherEntryError, + NotMaintainedWorkflowError, + YamlKeyError, +) from exasol.toolbox.util.workflows.patch_workflow import ( WorkflowCommentedMap, WorkflowPatcher, @@ -64,10 +68,30 @@ def workflow_patcher(self) -> WorkflowPatcher | None: 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 _load_generated_workflow( + self, template_path: Path, patch_yaml: WorkflowCommentedMap | None + ): + from exasol.toolbox.util.workflows.workflow import Workflow + + 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, 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 From b13ae204f0640b748e86edaac22e15c52ca9bad0 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:51:25 +0200 Subject: [PATCH 11/21] Move update_workflows to be in WorkflowOrchestrator --- exasol/toolbox/nox/_workflow.py | 11 +++++-- exasol/toolbox/util/workflows/workflow.py | 33 ------------------- .../util/workflows/workflow_orchestrator.py | 28 ++++++++++++++-- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/exasol/toolbox/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index acab6178d..ec50e24e3 100644 --- a/exasol/toolbox/nox/_workflow.py +++ b/exasol/toolbox/nox/_workflow.py @@ -5,8 +5,10 @@ import nox from nox import Session -from exasol.toolbox.util.workflows.workflow import update_workflow -from exasol.toolbox.util.workflows.workflow_orchestrator import WORKFLOW_CHOICES +from exasol.toolbox.util.workflows.workflow_orchestrator import ( + WORKFLOW_CHOICES, + WorkflowOrchestrator, +) from noxconfig import PROJECT_CONFIG @@ -35,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, + ).write_workflows() diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 9939bf148..577b88574 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -12,7 +12,6 @@ bound_contextvars, ) -from exasol.toolbox.config import BaseConfig from exasol.toolbox.util.workflows import logger from exasol.toolbox.util.workflows.exceptions import ( YamlError, @@ -22,10 +21,6 @@ WorkflowCommentedMap, ) from exasol.toolbox.util.workflows.process_template import WorkflowRenderer -from exasol.toolbox.util.workflows.workflow_orchestrator import ( - WorkflowChoice, - WorkflowOrchestrator, -) class Workflow(BaseModel): @@ -88,31 +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 update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None: - """ - Updates a selected workflow or all workflows. - """ - - orchestrator = WorkflowOrchestrator( - workflow_choice=workflow_choice, - config=config, - ) - workflow_dict = orchestrator.templates - logger.info(f"Selected workflow(s) to update: {list(workflow_dict.keys())}") - - is_new_project = orchestrator.is_new_project - for workflow_name in workflow_dict: - patch_yaml = orchestrator._extract_workflow_patch(workflow_name=workflow_name) - - if orchestrator._skip_workflow( - workflow_name=workflow_name, is_new_project=is_new_project - ): - continue - - workflow = orchestrator._load_generated_workflow( - template_path=workflow_dict[workflow_name], - patch_yaml=patch_yaml, - ) - workflow.write_to_file() diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 10001fedd..e63f7d1f9 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -1,6 +1,9 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import ( + Iterator, + Mapping, +) from functools import cached_property from pathlib import Path from typing import ( @@ -25,6 +28,7 @@ WORKFLOW_TEMPLATE_OPTIONS, validate_workflow_name, ) +from exasol.toolbox.util.workflows.workflow import Workflow ALL: Final[str] = "all" WorkflowChoice = Annotated[ @@ -75,7 +79,20 @@ def _extract_workflow_patch( return None return self.workflow_patcher.extract_by_workflow(workflow_name=workflow_name) - def _load_generated_workflow( + def _iter_workflows(self) -> Iterator[Workflow]: + 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 ): from exasol.toolbox.util.workflows.workflow import Workflow @@ -107,3 +124,10 @@ def _skip_workflow(self, workflow_name: str, is_new_project: bool) -> bool: ) return True return False + + def write_workflows(self) -> None: + """ + Render the selected workflows and write them to disk. + """ + for workflow in self._iter_workflows(): + workflow.write_to_file() From be5ae88d0aec962593a5531e32c7251c3e234c41 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:55:18 +0200 Subject: [PATCH 12/21] Move _is_new_project to be a function --- .../util/workflows/workflow_orchestrator.py | 15 +++++++-------- .../util/workflows/workflow_orchestrator_test.py | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index e63f7d1f9..e6c0ed841 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -43,13 +43,6 @@ class WorkflowOrchestrator(BaseModel): workflow_choice: WorkflowChoice config: BaseConfig - @cached_property - 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")) - @cached_property def templates(self) -> Mapping[str, Path]: """ @@ -79,8 +72,14 @@ def _extract_workflow_patch( 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]: - is_new_project = self.is_new_project + 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) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 164141a3c..e2ecab488 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -32,7 +32,7 @@ def test_returns_true_when_no_yml_files_exist(project_config): result = WorkflowOrchestrator( workflow_choice="all", config=project_config, - ).is_new_project + )._is_new_project() assert result is True @@ -45,7 +45,7 @@ def test_returns_false_when_yml_files_exist(project_config): result = WorkflowOrchestrator( workflow_choice="all", config=project_config, - ).is_new_project + )._is_new_project() assert result is False From 9c7f9c1e36ab38e42069eedb1670636e29689296 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 14:58:22 +0200 Subject: [PATCH 13/21] fixup! Move update_workflows to be in WorkflowOrchestrator --- exasol/toolbox/util/workflows/workflow_orchestrator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index e6c0ed841..d46ed5b09 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -79,8 +79,8 @@ def _is_new_project(self) -> bool: 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) From 522864a1392cd6c82fb4acb4a9556a530a99bc0f Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:15:12 +0200 Subject: [PATCH 14/21] Move remaining tests for writing to workflow_orchestrator_test.py --- .../workflows/workflow_orchestrator_test.py | 155 +++++++++++++++++ test/unit/util/workflows/workflow_test.py | 156 +----------------- 2 files changed, 156 insertions(+), 155 deletions(-) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index e2ecab488..8edfa56aa 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -1,5 +1,9 @@ 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, @@ -77,3 +81,154 @@ def test_returns_false_for_maintained_workflow(project_config): )._skip_workflow(workflow_name="checks", is_new_project=False) assert result is False + + +class TestWriteWorkflows: + @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() + expected_file_path = ( + project_config_without_patcher.github_workflow_directory + / f"{workflow_name}.yml" + ) + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).write_workflows() + result = expected_file_path.read_text() + + assert result[: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() + expected_file_path = ( + project_config.github_workflow_directory / f"{workflow_name}.yml" + ) + removed_job_name = "build-documentation-and-check-links" + assert removed_job_name in remove_job_yaml + assert removed_job_name in input_text + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config, + ).write_workflows() + result = expected_file_path.read_text() + + 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 + ): + workflow_name = "merge-gate" + 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" + ) + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config, + ).write_workflows() + result = expected_file_path.read_text() + + 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) + + WorkflowOrchestrator( + workflow_choice="all", + config=project_config_without_patcher, + ).write_workflows() + + 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() + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).write_workflows() + + 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() + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).write_workflows() + + 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() + + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).write_workflows() + + 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: + WorkflowOrchestrator( + workflow_choice="checks", + config=project_config, + ).write_workflows() + + 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) diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index 45c5a82c7..8148fcc75 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -5,21 +5,15 @@ 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 ( - Workflow, - update_workflow, -) +from exasol.toolbox.util.workflows.workflow import Workflow @pytest.fixture @@ -229,151 +223,3 @@ def test_load_from_template_reraises_other_exceptions_raised_as_valuerror( output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) - - -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) From 3405d46ecaa75a8152bbb3cb7d42c446fb4a93de Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:22:50 +0200 Subject: [PATCH 15/21] Change write_workflows to generate_workflows --- exasol/toolbox/nox/_workflow.py | 2 +- .../util/workflows/workflow_orchestrator.py | 4 +--- .../workflows/workflow_orchestrator_test.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/exasol/toolbox/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index ec50e24e3..9508941a8 100644 --- a/exasol/toolbox/nox/_workflow.py +++ b/exasol/toolbox/nox/_workflow.py @@ -40,4 +40,4 @@ def generate_workflow(session: Session) -> None: WorkflowOrchestrator( workflow_choice=args.workflow_choice, config=PROJECT_CONFIG, - ).write_workflows() + ).generate_workflows() diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index d46ed5b09..c0e836f87 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -94,8 +94,6 @@ def _iter_workflows(self) -> Iterator[Workflow]: def _load_workflow( self, template_path: Path, patch_yaml: WorkflowCommentedMap | None ): - from exasol.toolbox.util.workflows.workflow import Workflow - try: return Workflow.load_from_template( template_path=template_path, @@ -124,7 +122,7 @@ def _skip_workflow(self, workflow_name: str, is_new_project: bool) -> bool: return True return False - def write_workflows(self) -> None: + def generate_workflows(self) -> None: """ Render the selected workflows and write them to disk. """ diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 8edfa56aa..95dbc0c25 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -83,7 +83,7 @@ def test_returns_false_for_maintained_workflow(project_config): assert result is False -class TestWriteWorkflows: +class TestGenerateWorkflows: @staticmethod def test_works_as_expected_without_patcher(project_config_without_patcher): workflow_name = "merge-gate" @@ -97,7 +97,7 @@ def test_works_as_expected_without_patcher(project_config_without_patcher): WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).write_workflows() + ).generate_workflows() result = expected_file_path.read_text() assert result[:10] == input_text[:10] @@ -117,7 +117,7 @@ def test_works_as_expected_with_relevant_patcher(project_config, remove_job_yaml WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config, - ).write_workflows() + ).generate_workflows() result = expected_file_path.read_text() assert result[:10] == input_text[:10] @@ -137,7 +137,7 @@ def test_works_as_expected_with_not_relevant_patcher( WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config, - ).write_workflows() + ).generate_workflows() result = expected_file_path.read_text() assert result[:10] == input_text[:10] @@ -152,7 +152,7 @@ def test_not_maintained_workflows_added_to_new_project( WorkflowOrchestrator( workflow_choice="all", config=project_config_without_patcher, - ).write_workflows() + ).generate_workflows() assert all( (directory / f"{name}.yml").exists() @@ -172,7 +172,7 @@ def test_not_maintained_workflows_not_modified_in_old_project( WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).write_workflows() + ).generate_workflows() assert {file_path.name for file_path in directory.iterdir()} == {workflow} assert (directory / workflow).read_text() == "" @@ -189,7 +189,7 @@ def test_not_maintained_workflows_not_added_to_old_project( WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).write_workflows() + ).generate_workflows() assert {file_path.name for file_path in directory.iterdir()} == {"dummy.yml"} @@ -206,7 +206,7 @@ def test_not_maintained_workflows_not_modified_in_old_project( WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).write_workflows() + ).generate_workflows() assert {file_path.name for file_path in directory.iterdir()} == {workflow} assert (directory / workflow).read_text() == "" @@ -225,7 +225,7 @@ def test_raises_invalidworkflowpatcherentryerror(project_config): WorkflowOrchestrator( workflow_choice="checks", config=project_config, - ).write_workflows() + ).generate_workflows() assert ( f"In file '{project_config.github_workflow_patcher_yaml}', " From ff06fdf766842938e2ff055909b7576f11675215 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:34:03 +0200 Subject: [PATCH 16/21] Centralize tests to be under _iter as shared for preparation of comparison --- .../workflows/workflow_orchestrator_test.py | 90 +++++++------------ 1 file changed, 34 insertions(+), 56 deletions(-) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 95dbc0c25..bac6c48ff 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -83,45 +83,43 @@ def test_returns_false_for_maintained_workflow(project_config): assert result is False -class TestGenerateWorkflows: +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() - expected_file_path = ( - project_config_without_patcher.github_workflow_directory - / f"{workflow_name}.yml" - ) - WorkflowOrchestrator( + result = WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).generate_workflows() - result = expected_file_path.read_text() + )._iter_workflows() - assert result[:10] == input_text[:10] + 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() - expected_file_path = ( - project_config.github_workflow_directory / f"{workflow_name}.yml" - ) removed_job_name = "build-documentation-and-check-links" assert removed_job_name in remove_job_yaml assert removed_job_name in input_text - WorkflowOrchestrator( + result = WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config, - ).generate_workflows() - result = expected_file_path.read_text() + )._iter_workflows() - assert result[:10] == input_text[:10] - assert removed_job_name not in result + 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( @@ -130,17 +128,16 @@ def test_works_as_expected_with_not_relevant_patcher( workflow_name = "merge-gate" 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" - ) - WorkflowOrchestrator( + result = WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config, - ).generate_workflows() - result = expected_file_path.read_text() + )._iter_workflows() - assert result[:10] == input_text[:10] + 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( @@ -149,14 +146,13 @@ def test_not_maintained_workflows_added_to_new_project( directory = project_config_without_patcher.github_workflow_directory directory.mkdir(parents=True) - WorkflowOrchestrator( + result = WorkflowOrchestrator( workflow_choice="all", config=project_config_without_patcher, - ).generate_workflows() + )._iter_workflows() - assert all( - (directory / f"{name}.yml").exists() - for name in NOT_MAINTAINED_WORKFLOW_NAMES + assert {f"{name}.yml" for name in NOT_MAINTAINED_WORKFLOW_NAMES}.issubset( + {workflow.output_path.name for workflow in result} ) @staticmethod @@ -169,13 +165,12 @@ def test_not_maintained_workflows_not_modified_in_old_project( workflow = "slow-checks.yml" (directory / workflow).touch() - WorkflowOrchestrator( + result = WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).generate_workflows() + )._iter_workflows() - assert {file_path.name for file_path in directory.iterdir()} == {workflow} - assert (directory / workflow).read_text() == "" + assert list(result) == [] @staticmethod @pytest.mark.parametrize("workflow_name", NOT_MAINTAINED_WORKFLOW_NAMES) @@ -186,30 +181,12 @@ def test_not_maintained_workflows_not_added_to_old_project( directory.mkdir(parents=True, exist_ok=True) (directory / "dummy.yml").touch() - WorkflowOrchestrator( - workflow_choice=workflow_name, - config=project_config_without_patcher, - ).generate_workflows() - - 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() - - WorkflowOrchestrator( + result = WorkflowOrchestrator( workflow_choice=workflow_name, config=project_config_without_patcher, - ).generate_workflows() + )._iter_workflows() - assert {file_path.name for file_path in directory.iterdir()} == {workflow} - assert (directory / workflow).read_text() == "" + assert list(result) == [] @staticmethod def test_raises_invalidworkflowpatcherentryerror(project_config): @@ -222,10 +199,11 @@ def test_raises_invalidworkflowpatcherentryerror(project_config): project_config.github_workflow_patcher_yaml.write_text(patcher_yml) with pytest.raises(InvalidWorkflowPatcherEntryError) as ex: - WorkflowOrchestrator( + for _ in WorkflowOrchestrator( workflow_choice="checks", config=project_config, - ).generate_workflows() + )._iter_workflows(): + pass assert ( f"In file '{project_config.github_workflow_patcher_yaml}', " From 3cd49f0070d6cbb71bb7fef8f30f20aa47af95ec Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:44:13 +0200 Subject: [PATCH 17/21] Add tests specific to write --- .../workflows/workflow_orchestrator_test.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index bac6c48ff..6a79df611 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -1,3 +1,6 @@ +from pathlib import Path +from unittest.mock import patch + import pytest from exasol.toolbox.util.workflows.exceptions import ( @@ -210,3 +213,55 @@ def test_raises_invalidworkflowpatcherentryerror(project_config): "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 From c956a5fef5fc4324dcd43234d22f15de839580f2 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:48:13 +0200 Subject: [PATCH 18/21] Add changelog entry --- doc/changes/unreleased.md | 4 ++++ exasol/toolbox/util/workflows/workflow_orchestrator.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) 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/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index c0e836f87..95977454d 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -103,7 +103,8 @@ def _load_workflow( ) except YamlKeyError as ex: raise InvalidWorkflowPatcherEntryError( - file_path=self.config.github_workflow_patcher_yaml, entry=ex.entry + 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: From 98aedff57ba995d493e435f799d3121a9d155b79 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:53:29 +0200 Subject: [PATCH 19/21] Update missed workflows --- .github/workflows/fast-tests-extension.yml | 2 +- .github/workflows/slow-checks.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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..8d11ce9d5 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" From 6841e831dc56efd1cea3839db4daada75744e153 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:54:31 +0200 Subject: [PATCH 20/21] Update poetry.lock to resolve transitive vulnerability --- poetry.lock | 261 ++++++++++++++++++++++++++-------------------------- 1 file changed, 133 insertions(+), 128 deletions(-) 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" From fe95603170f5801c1a019668611659966d958cf6 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 15:58:16 +0200 Subject: [PATCH 21/21] Add closing paranethesis --- .github/workflows/slow-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slow-checks.yml b/.github/workflows/slow-checks.yml index 8d11ce9d5..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 }} + name: Run Integration Tests (Python-${{ matrix.python-version }}) needs: - build-matrix runs-on: "ubuntu-24.04"