Skip to content

Commit 38e5d63

Browse files
authored
Feature/722 nox session check workflow orchestrator (#844)
* Clean up unit tests and add coverage case * Clean up unit tests and add coverage case, as well as simplify code ifs * Rename tests so clearer what is being tested * Start with WorkflowOrchestrator class by moving select_template * Move workflow_patcher to WorkflowOrchestrator * Move is_new_project to WorkflowOrchestrator * Add _extract_workflow_patch * Add _skip_workflow * Fix missed tests * Add _load_generated_workflow * Move update_workflows to be in WorkflowOrchestrator * Move _is_new_project to be a function * fixup! Move update_workflows to be in WorkflowOrchestrator * Move remaining tests for writing to workflow_orchestrator_test.py * Change write_workflows to generate_workflows * Centralize tests to be under _iter as shared for preparation of comparison * Add tests specific to write * Add changelog entry * Update missed workflows * Update poetry.lock to resolve transitive vulnerability * Add closing paranethesis
1 parent 32c7fa8 commit 38e5d63

10 files changed

Lines changed: 649 additions & 413 deletions

File tree

.github/workflows/fast-tests-extension.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
- name: Set up Python & Poetry Environment
1818
id: set-up-python-and-poetry-environment
19-
uses: exasol/python-toolbox/.github/actions/python-environment@v7
19+
uses: exasol/python-toolbox/.github/actions/python-environment@v8
2020
with:
2121
python-version: "3.10"
2222
poetry-version: "2.3.0"

.github/workflows/slow-checks.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
contents: read
1212

1313
run-integration-tests:
14-
name: Run Integration Tests (Python-${{ matrix.python-version }}, Exasol-${{ matrix.exasol-version}})
14+
name: Run Integration Tests (Python-${{ matrix.python-version }})
1515
needs:
1616
- build-matrix
1717
runs-on: "ubuntu-24.04"
@@ -29,7 +29,7 @@ jobs:
2929

3030
- name: Set up Python & Poetry Environment
3131
id: set-up-python-and-poetry-environment
32-
uses: exasol/python-toolbox/.github/actions/python-environment@v7
32+
uses: exasol/python-toolbox/.github/actions/python-environment@v8
3333
with:
3434
python-version: ${{ matrix.python-version }}
3535
poetry-version: "2.3.0"

doc/changes/unreleased.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@
99
## Feature
1010

1111
* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out
12+
13+
## Refactoring
14+
15+
* #722: Modified `workflow:generate` backend function to class `WorkflowOrchestrator`

exasol/toolbox/nox/_workflow.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import nox
66
from nox import Session
77

8-
from exasol.toolbox.util.workflows.workflow import (
8+
from exasol.toolbox.util.workflows.workflow_orchestrator import (
99
WORKFLOW_CHOICES,
10-
update_workflow,
10+
WorkflowOrchestrator,
1111
)
1212
from noxconfig import PROJECT_CONFIG
1313

@@ -37,4 +37,7 @@ def generate_workflow(session: Session) -> None:
3737

3838
PROJECT_CONFIG.github_workflow_directory.mkdir(parents=True, exist_ok=True)
3939

40-
update_workflow(workflow_choice=args.workflow_choice, config=PROJECT_CONFIG)
40+
WorkflowOrchestrator(
41+
workflow_choice=args.workflow_choice,
42+
config=PROJECT_CONFIG,
43+
).generate_workflows()
Lines changed: 4 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import difflib
2-
from collections.abc import Mapping
32
from pathlib import Path
43
from typing import (
5-
Annotated,
64
Any,
7-
Final,
85
)
96

107
from pydantic import (
@@ -15,28 +12,15 @@
1512
bound_contextvars,
1613
)
1714

18-
from exasol.toolbox.config import BaseConfig
1915
from exasol.toolbox.util.workflows import logger
2016
from exasol.toolbox.util.workflows.exceptions import (
21-
InvalidWorkflowPatcherEntryError,
22-
NotMaintainedWorkflowError,
2317
YamlError,
2418
YamlKeyError,
2519
)
2620
from exasol.toolbox.util.workflows.patch_workflow import (
2721
WorkflowCommentedMap,
28-
WorkflowPatcher,
2922
)
3023
from exasol.toolbox.util.workflows.process_template import WorkflowRenderer
31-
from exasol.toolbox.util.workflows.templates import (
32-
WORKFLOW_TEMPLATE_OPTIONS,
33-
validate_workflow_name,
34-
)
35-
36-
ALL: Final[str] = "all"
37-
WORKFLOW_CHOICES: Final[list[str]] = [ALL, *WORKFLOW_TEMPLATE_OPTIONS.keys()]
38-
39-
WorkflowChoice = Annotated[str, f"Should be a value from {WORKFLOW_CHOICES}"]
4024

4125

4226
class Workflow(BaseModel):
@@ -78,9 +62,10 @@ def load_from_template(
7862
raise ValueError(f"Error rendering file: {template_path}") from ex
7963

8064
def compare_to_file(self) -> str:
81-
existing_content = (
82-
self.output_path.read_text().strip() if self.output_path.exists() else ""
83-
)
65+
existing_content = ""
66+
if self.output_path.is_file():
67+
existing_content = self.output_path.read_text().strip()
68+
8469
generated_content = self.content.strip()
8570

8671
diff = difflib.unified_diff(
@@ -98,59 +83,3 @@ def write_to_file(self) -> None:
9883
return
9984
logger.info("Write workflow file %s", self.output_path.name)
10085
self.output_path.write_text(self.content + "\n")
101-
102-
103-
def _select_workflow_template(workflow_name: WorkflowChoice) -> Mapping[str, Path]:
104-
"""
105-
Returns a mapping of workflow names to paths. Can be a single item or all workflow
106-
templates.
107-
"""
108-
if workflow_name == ALL:
109-
return WORKFLOW_TEMPLATE_OPTIONS
110-
return {workflow_name: WORKFLOW_TEMPLATE_OPTIONS[workflow_name]}
111-
112-
113-
def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None:
114-
"""
115-
Updates a selected workflow or all workflows.
116-
"""
117-
workflow_dict = _select_workflow_template(workflow_choice)
118-
logger.info(f"Selected workflow(s) to update: {list(workflow_dict.keys())}")
119-
120-
workflow_patcher = None
121-
if config.github_workflow_patcher_yaml:
122-
workflow_patcher = WorkflowPatcher(
123-
github_template_dict=config.github_template_dict,
124-
file_path=config.github_workflow_patcher_yaml,
125-
)
126-
127-
is_new_project = not any(config.github_workflow_directory.glob("*.yml"))
128-
for workflow_name in workflow_dict:
129-
patch_yaml = None
130-
if workflow_patcher:
131-
patch_yaml = workflow_patcher.extract_by_workflow(
132-
workflow_name=workflow_name
133-
)
134-
135-
try:
136-
validate_workflow_name(workflow_name)
137-
except NotMaintainedWorkflowError:
138-
if not is_new_project:
139-
logger.debug(
140-
"Skipping not-maintained workflow in older project: %s",
141-
workflow_name,
142-
)
143-
continue
144-
145-
try:
146-
workflow = Workflow.load_from_template(
147-
template_path=workflow_dict[workflow_name],
148-
output_directory=config.github_workflow_directory,
149-
github_template_dict=config.github_template_dict,
150-
patch_yaml=patch_yaml,
151-
)
152-
workflow.write_to_file()
153-
except YamlKeyError as ex:
154-
raise InvalidWorkflowPatcherEntryError(
155-
file_path=config.github_workflow_patcher_yaml, entry=ex.entry # type: ignore
156-
) from ex
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import (
4+
Iterator,
5+
Mapping,
6+
)
7+
from functools import cached_property
8+
from pathlib import Path
9+
from typing import (
10+
Annotated,
11+
Final,
12+
)
13+
14+
from pydantic import BaseModel
15+
16+
from exasol.toolbox.config import BaseConfig
17+
from exasol.toolbox.util.workflows import logger
18+
from exasol.toolbox.util.workflows.exceptions import (
19+
InvalidWorkflowPatcherEntryError,
20+
NotMaintainedWorkflowError,
21+
YamlKeyError,
22+
)
23+
from exasol.toolbox.util.workflows.patch_workflow import (
24+
WorkflowCommentedMap,
25+
WorkflowPatcher,
26+
)
27+
from exasol.toolbox.util.workflows.templates import (
28+
WORKFLOW_TEMPLATE_OPTIONS,
29+
validate_workflow_name,
30+
)
31+
from exasol.toolbox.util.workflows.workflow import Workflow
32+
33+
ALL: Final[str] = "all"
34+
WorkflowChoice = Annotated[
35+
str, f"Should be a value from {[ALL, *WORKFLOW_TEMPLATE_OPTIONS.keys()]}"
36+
]
37+
WORKFLOW_CHOICES: Final[list[str]] = [ALL, *WORKFLOW_TEMPLATE_OPTIONS.keys()]
38+
39+
40+
class WorkflowOrchestrator(BaseModel):
41+
"""Orchestrate workflow rendering, comparison, and writing."""
42+
43+
workflow_choice: WorkflowChoice
44+
config: BaseConfig
45+
46+
@cached_property
47+
def templates(self) -> Mapping[str, Path]:
48+
"""
49+
A mapping of workflow templates names to paths. This can be a single
50+
item or all workflow templates.
51+
"""
52+
if self.workflow_choice == ALL:
53+
return WORKFLOW_TEMPLATE_OPTIONS
54+
return {self.workflow_choice: WORKFLOW_TEMPLATE_OPTIONS[self.workflow_choice]}
55+
56+
@cached_property
57+
def workflow_patcher(self) -> WorkflowPatcher | None:
58+
if not self.config.github_workflow_patcher_yaml:
59+
return None
60+
return WorkflowPatcher(
61+
github_template_dict=self.config.github_template_dict,
62+
file_path=self.config.github_workflow_patcher_yaml,
63+
)
64+
65+
def _extract_workflow_patch(
66+
self, workflow_name: str
67+
) -> WorkflowCommentedMap | None:
68+
"""
69+
Return the patch data for a workflow, or ``None`` if no patcher is configured.
70+
"""
71+
if self.workflow_patcher is None:
72+
return None
73+
return self.workflow_patcher.extract_by_workflow(workflow_name=workflow_name)
74+
75+
def _is_new_project(self) -> bool:
76+
"""
77+
A project is considered new if no YML files are present in the GitHub directory.
78+
"""
79+
return not any(self.config.github_workflow_directory.glob("*.yml"))
80+
81+
def _iter_workflows(self) -> Iterator[Workflow]:
82+
logger.info(f"Selected workflow(s) to update: {list(self.templates.keys())}")
83+
is_new_project = self._is_new_project()
84+
for workflow_name, template_path in self.templates.items():
85+
patch_yaml = self._extract_workflow_patch(workflow_name=workflow_name)
86+
87+
if self._skip_workflow(workflow_name, is_new_project):
88+
continue
89+
90+
yield self._load_workflow(
91+
template_path=template_path, patch_yaml=patch_yaml
92+
)
93+
94+
def _load_workflow(
95+
self, template_path: Path, patch_yaml: WorkflowCommentedMap | None
96+
):
97+
try:
98+
return Workflow.load_from_template(
99+
template_path=template_path,
100+
output_directory=self.config.github_workflow_directory,
101+
github_template_dict=self.config.github_template_dict,
102+
patch_yaml=patch_yaml,
103+
)
104+
except YamlKeyError as ex:
105+
raise InvalidWorkflowPatcherEntryError(
106+
file_path=self.config.github_workflow_patcher_yaml, # type: ignore
107+
entry=ex.entry,
108+
) from ex
109+
110+
def _skip_workflow(self, workflow_name: str, is_new_project: bool) -> bool:
111+
"""
112+
Return ``True`` if the workflow should be skipped because it is not maintained
113+
by the PTB, otherwise return ``False``.
114+
"""
115+
try:
116+
validate_workflow_name(workflow_name)
117+
except NotMaintainedWorkflowError:
118+
if not is_new_project:
119+
logger.debug(
120+
"Skipping not-maintained workflow in older project: %s",
121+
workflow_name,
122+
)
123+
return True
124+
return False
125+
126+
def generate_workflows(self) -> None:
127+
"""
128+
Render the selected workflows and write them to disk.
129+
"""
130+
for workflow in self._iter_workflows():
131+
workflow.write_to_file()

0 commit comments

Comments
 (0)