pypeline-runner 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/PKG-INFO +4 -4
  2. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/pyproject.toml +12 -12
  3. pypeline_runner-0.3.0/src/pypeline/__init__.py +1 -0
  4. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/domain/config.py +2 -1
  5. pypeline_runner-0.3.0/src/pypeline/domain/pipeline.py +67 -0
  6. pypeline_runner-0.3.0/src/pypeline/pypeline.py +187 -0
  7. pypeline_runner-0.2.2/src/pypeline/__init__.py +0 -1
  8. pypeline_runner-0.2.2/src/pypeline/domain/pipeline.py +0 -30
  9. pypeline_runner-0.2.2/src/pypeline/pypeline.py +0 -95
  10. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/LICENSE +0 -0
  11. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/README.md +0 -0
  12. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/__run.py +0 -0
  13. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/domain/__init__.py +0 -0
  14. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/domain/artifacts.py +0 -0
  15. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/domain/execution_context.py +0 -0
  16. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/domain/project_slurper.py +0 -0
  17. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/__init__.py +0 -0
  18. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/create.py +0 -0
  19. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/bootstrap/bootstrap.ps1 +0 -0
  20. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/bootstrap/bootstrap.py +0 -0
  21. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/.gitignore +0 -0
  22. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/poetry.toml +0 -0
  23. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/pypeline.ps1 +0 -0
  24. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/pypeline.yaml +0 -0
  25. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/pyproject.toml +0 -0
  26. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/scoopfile.json +0 -0
  27. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/kickstart/templates/project/steps/my_step.py +0 -0
  28. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/main.py +0 -0
  29. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/py.typed +0 -0
  30. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/steps/__init__.py +0 -0
  31. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/steps/create_venv.py +0 -0
  32. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/steps/scoop_install.py +0 -0
  33. {pypeline_runner-0.2.2 → pypeline_runner-0.3.0}/src/pypeline/steps/west_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pypeline-runner
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Configure and execute pipelines with Python (similar to GitHub workflows or Jenkins pipelines).
5
5
  Home-page: https://github.com/cuinixam/pypeline
6
6
  License: MIT
@@ -17,9 +17,9 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Software Development :: Libraries
20
- Requires-Dist: py-app-dev (>=2.1.1,<3.0.0)
21
- Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
22
- Requires-Dist: typer[all] (>=0.12.0,<0.13.0)
20
+ Requires-Dist: py-app-dev (>=2.2,<3.0)
21
+ Requires-Dist: pyyaml (>=6.0,<7.0)
22
+ Requires-Dist: typer[all] (>=0.12,<0.13)
23
23
  Project-URL: Bug Tracker, https://github.com/cuinixam/pypeline/issues
24
24
  Project-URL: Changelog, https://github.com/cuinixam/pypeline/blob/main/CHANGELOG.md
25
25
  Project-URL: Documentation, https://pypeline-runner.readthedocs.io
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pypeline-runner"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "Configure and execute pipelines with Python (similar to GitHub workflows or Jenkins pipelines)."
5
5
  authors = ["cuinixam <me@cuinixam.com>"]
6
6
  license = "MIT"
@@ -27,25 +27,25 @@ pypeline = "pypeline.main:main"
27
27
 
28
28
  [tool.poetry.dependencies]
29
29
  python = "^3.10"
30
- py-app-dev = "^2.1.1"
31
- typer = {extras = ["all"], version = "^0.12.0"}
32
- pyyaml = "^6.0.1"
30
+ py-app-dev = "^2.2"
31
+ typer = {extras = ["all"], version = "^0.12"}
32
+ pyyaml = "^6.0"
33
33
 
34
34
  [tool.poetry.group.dev.dependencies]
35
35
  pytest = "^7.0"
36
36
  pytest-cov = "^4.0"
37
- pre-commit = "^3.1.1"
38
- ruff = "^0.3.0"
37
+ pre-commit = "^3.1"
38
+ ruff = "^0.3"
39
39
 
40
40
  [tool.poetry.group.docs.dependencies]
41
41
  myst-parser = ">=0.16"
42
42
  sphinx = ">=4.0"
43
- sphinxcontrib-mermaid = "^0.8.1"
44
- mlx-traceability = "^10.0.0"
45
- sphinx-copybutton = "^0.5.2"
46
- sphinx-new-tab-link = "^0.2.2"
47
- sphinx-book-theme = "^1.1.2"
48
- sphinx-design = "^0.5.0"
43
+ sphinxcontrib-mermaid = "^0.8"
44
+ mlx-traceability = "^10.0"
45
+ sphinx-copybutton = "^0.5"
46
+ sphinx-new-tab-link = "^0.2"
47
+ sphinx-book-theme = "^1.1"
48
+ sphinx-design = "^0.5"
49
49
 
50
50
  [tool.semantic_release]
51
51
  version_toml = ["pyproject.toml:tool.poetry.version"]
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -5,10 +5,11 @@ from typing import Any, Dict, Optional
5
5
  import yaml
6
6
  from mashumaro import DataClassDictMixin
7
7
  from py_app_dev.core.exceptions import UserNotificationException
8
- from py_app_dev.core.pipeline import PipelineConfig
9
8
  from yaml.parser import ParserError
10
9
  from yaml.scanner import ScannerError
11
10
 
11
+ from .pipeline import PipelineConfig
12
+
12
13
 
13
14
  @dataclass
14
15
  class ProjectConfig(DataClassDictMixin):
@@ -0,0 +1,67 @@
1
+ from abc import abstractmethod
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ List,
8
+ Optional,
9
+ OrderedDict,
10
+ Type,
11
+ TypeAlias,
12
+ )
13
+
14
+ from mashumaro import DataClassDictMixin
15
+ from py_app_dev.core.runnable import Runnable
16
+
17
+ from .execution_context import ExecutionContext
18
+
19
+
20
+ @dataclass
21
+ class PipelineStepConfig(DataClassDictMixin):
22
+ #: Step name or class name if file is not specified
23
+ step: str
24
+ #: Path to file with step class
25
+ file: Optional[str] = None
26
+ #: Python module with step class
27
+ module: Optional[str] = None
28
+ #: Step class name
29
+ class_name: Optional[str] = None
30
+ #: Command to run. For simple steps that don't need a class
31
+ run: Optional[str] = None
32
+ #: Step description
33
+ description: Optional[str] = None
34
+ #: Step timeout in seconds
35
+ timeout_sec: Optional[int] = None
36
+ #: Custom step configuration
37
+ config: Optional[Dict[str, Any]] = None
38
+
39
+
40
+ PipelineConfig: TypeAlias = OrderedDict[str, List[PipelineStepConfig]]
41
+
42
+
43
+ class PipelineStep(Runnable):
44
+ def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
45
+ super().__init__(self.get_needs_dependency_management())
46
+ self.execution_context = execution_context
47
+ self.output_dir = output_dir
48
+ self.config = config
49
+ self.project_root_dir = self.execution_context.project_root_dir
50
+
51
+ @abstractmethod
52
+ def update_execution_context(self) -> None:
53
+ pass
54
+
55
+ def get_needs_dependency_management(self) -> bool:
56
+ return True
57
+
58
+
59
+ class PipelineStepReference:
60
+ def __init__(self, group_name: str, _class: Type[PipelineStep], config: Optional[Dict[str, Any]] = None) -> None:
61
+ self.group_name = group_name
62
+ self._class = _class
63
+ self.config = config
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ return self._class.__name__
@@ -0,0 +1,187 @@
1
+ import importlib
2
+ from importlib.util import module_from_spec, spec_from_file_location
3
+ from pathlib import Path
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ List,
8
+ Optional,
9
+ Type,
10
+ )
11
+
12
+ from py_app_dev.core.exceptions import UserNotificationException
13
+ from py_app_dev.core.logging import logger
14
+ from py_app_dev.core.runnable import Executor
15
+
16
+ from .domain.artifacts import ProjectArtifactsLocator
17
+ from .domain.execution_context import ExecutionContext
18
+ from .domain.pipeline import PipelineConfig, PipelineStep, PipelineStepConfig, PipelineStepReference
19
+
20
+
21
+ class PipelineLoader:
22
+ """
23
+ Loads pipeline steps from a pipeline configuration.
24
+
25
+ The steps are not instantiated, only the references are returned (lazy load).
26
+ The pipeline loader needs to know the project root directory to be able to find the
27
+ user custom local steps.
28
+ """
29
+
30
+ def __init__(self, pipeline_config: PipelineConfig, project_root_dir: Path) -> None:
31
+ self.pipeline_config = pipeline_config
32
+ self.project_root_dir = project_root_dir
33
+
34
+ def load_steps_references(self) -> List[PipelineStepReference]:
35
+ result = []
36
+ for group_name, steps_config in self.pipeline_config.items():
37
+ result.extend(self._load_steps(group_name, steps_config, self.project_root_dir))
38
+ return result
39
+
40
+ @staticmethod
41
+ def _load_steps(
42
+ group_name: str,
43
+ steps_config: List[PipelineStepConfig],
44
+ project_root_dir: Path,
45
+ ) -> List[PipelineStepReference]:
46
+ result = []
47
+ for step_config in steps_config:
48
+ step_class_name = step_config.class_name or step_config.step
49
+ if step_config.module:
50
+ step_class = PipelineLoader._load_module_step(step_config.module, step_class_name)
51
+ elif step_config.file:
52
+ step_class = PipelineLoader._load_user_step(project_root_dir.joinpath(step_config.file), step_class_name)
53
+ elif step_config.run:
54
+ step_class = PipelineLoader._create_run_command_step_class(step_config.run, step_class_name)
55
+ else:
56
+ raise UserNotificationException(f"Step '{step_class_name}' has no 'module' nor 'file' nor `run` defined." " Please check your pipeline configuration.")
57
+ result.append(PipelineStepReference(group_name, step_class, step_config.config))
58
+ return result
59
+
60
+ @staticmethod
61
+ def _load_user_step(python_file: Path, step_class_name: str) -> Type[PipelineStep]:
62
+ # Create a module specification from the file path
63
+ spec = spec_from_file_location(f"user__{step_class_name}", python_file)
64
+ if spec and spec.loader:
65
+ step_module = module_from_spec(spec)
66
+ # Import the module
67
+ spec.loader.exec_module(step_module)
68
+ try:
69
+ step_class = getattr(step_module, step_class_name)
70
+ except AttributeError:
71
+ raise UserNotificationException(f"Could not load class '{step_class_name}' from file '{python_file}'." " Please check your pipeline configuration.") from None
72
+ return step_class
73
+ raise UserNotificationException(f"Could not load file '{python_file}'." " Please check the file for any errors.")
74
+
75
+ @staticmethod
76
+ def _load_module_step(module_name: str, step_class_name: str) -> Type[PipelineStep]:
77
+ try:
78
+ module = importlib.import_module(module_name)
79
+ step_class = getattr(module, step_class_name)
80
+ except ImportError:
81
+ raise UserNotificationException(f"Could not load module '{module_name}'. Please check your pipeline configuration.") from None
82
+ except AttributeError:
83
+ raise UserNotificationException(f"Could not load class '{step_class_name}' from module '{module_name}'." " Please check your pipeline configuration.") from None
84
+ return step_class
85
+
86
+ @staticmethod
87
+ def _create_run_command_step_class(command: str, name: str) -> Type[PipelineStep]:
88
+ """Dynamically creates a step class for a given command."""
89
+
90
+ class DynamicRunCommandStep(PipelineStep):
91
+ """A simple step that runs a command."""
92
+
93
+ def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
94
+ super().__init__(execution_context, output_dir, config)
95
+ self.command = command
96
+ self.name = name
97
+
98
+ def get_needs_dependency_management(self) -> bool:
99
+ """A command step does not need dependency management."""
100
+ return False
101
+
102
+ def run(self) -> int:
103
+ self.execution_context.create_process_executor(
104
+ [self.command],
105
+ cwd=self.project_root_dir,
106
+ ).execute()
107
+ return 0
108
+
109
+ def get_name(self) -> str:
110
+ return self.name
111
+
112
+ def get_inputs(self) -> List[Path]:
113
+ return []
114
+
115
+ def get_outputs(self) -> List[Path]:
116
+ return []
117
+
118
+ def update_execution_context(self) -> None:
119
+ pass
120
+
121
+ return DynamicRunCommandStep
122
+
123
+
124
+ class PipelineStepsExecutor:
125
+ """Executes a list of pipeline steps sequentially."""
126
+
127
+ def __init__(
128
+ self,
129
+ artifacts_locator: ProjectArtifactsLocator,
130
+ steps_references: List[PipelineStepReference],
131
+ force_run: bool = False,
132
+ dry_run: bool = False,
133
+ ) -> None:
134
+ self.logger = logger.bind()
135
+ self.artifacts_locator = artifacts_locator
136
+ self.steps_references = steps_references
137
+ self.force_run = force_run
138
+ self.dry_run = dry_run
139
+
140
+ def run(self) -> None:
141
+ execution_context = ExecutionContext(project_root_dir=self.artifacts_locator.project_root_dir, install_dirs=[])
142
+ for step_reference in self.steps_references:
143
+ step_output_dir = self.artifacts_locator.build_dir / step_reference.group_name
144
+ # Create the step output directory, to make sure that files can be created.
145
+ step_output_dir.mkdir(parents=True, exist_ok=True)
146
+ step = step_reference._class(execution_context, step_output_dir, step_reference.config)
147
+ # Execute the step is necessary. If the step is not dirty, it will not be executed
148
+ Executor(step.output_dir, self.force_run, self.dry_run).execute(step)
149
+ # Independent if the step was executed or not, every step shall update the context
150
+ step.update_execution_context()
151
+
152
+ return
153
+
154
+
155
+ class PipelineScheduler:
156
+ """
157
+ Schedules which steps must be executed based on the provided configuration.
158
+
159
+ * If a step name is provided and the single flag is set, only that step will be executed.
160
+ * If a step name is provided and the single flag is not set, all steps up to the provided step will be executed.
161
+ * In case a command is provided, only the steps up to that command will be executed.
162
+ * If no step name is provided, all steps will be executed.
163
+ """
164
+
165
+ def __init__(self, pipeline: PipelineConfig, project_root_dir: Path) -> None:
166
+ self.pipeline = pipeline
167
+ self.project_root_dir = project_root_dir
168
+ self.logger = logger.bind()
169
+
170
+ def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference]:
171
+ pipeline_loader = PipelineLoader(self.pipeline, self.project_root_dir)
172
+ return self.filter_steps_references(pipeline_loader.load_steps_references(), step_name, single)
173
+
174
+ @staticmethod
175
+ def filter_steps_references(
176
+ steps_references: List[PipelineStepReference],
177
+ step_name: Optional[str],
178
+ single: Optional[bool],
179
+ ) -> List[PipelineStepReference]:
180
+ if step_name:
181
+ step_reference = next((step for step in steps_references if step.name == step_name), None)
182
+ if not step_reference:
183
+ return []
184
+ if single:
185
+ return [step_reference]
186
+ return [step for step in steps_references if steps_references.index(step) <= steps_references.index(step_reference)]
187
+ return steps_references
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
@@ -1,30 +0,0 @@
1
- from abc import abstractmethod
2
- from pathlib import Path
3
- from typing import Any, Dict, Optional, Type
4
-
5
- from py_app_dev.core.runnable import Runnable
6
-
7
- from .execution_context import ExecutionContext
8
-
9
-
10
- class PipelineStep(Runnable):
11
- def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
12
- self.execution_context = execution_context
13
- self.output_dir = output_dir
14
- self.config = config
15
- self.project_root_dir = self.execution_context.project_root_dir
16
-
17
- @abstractmethod
18
- def update_execution_context(self) -> None:
19
- pass
20
-
21
-
22
- class PipelineStepReference:
23
- def __init__(self, group_name: str, _class: Type[PipelineStep], config: Optional[Dict[str, Any]] = None) -> None:
24
- self.group_name = group_name
25
- self._class = _class
26
- self.config = config
27
-
28
- @property
29
- def name(self) -> str:
30
- return self._class.__name__
@@ -1,95 +0,0 @@
1
- from pathlib import Path
2
- from typing import List, Optional
3
-
4
- from py_app_dev.core.logging import logger
5
- from py_app_dev.core.pipeline import PipelineConfig
6
- from py_app_dev.core.pipeline import PipelineLoader as GenericPipelineLoader
7
- from py_app_dev.core.runnable import Executor
8
-
9
- from .domain.artifacts import ProjectArtifactsLocator
10
- from .domain.execution_context import ExecutionContext
11
- from .domain.pipeline import PipelineStep, PipelineStepReference
12
-
13
-
14
- class PipelineLoader:
15
- """
16
- Loads pipeline steps from a pipeline configuration.
17
-
18
- The steps are not instantiated, only the references are returned (lazy load).
19
- The pipeline loader needs to know the project root directory to be able to find the
20
- user custom local steps.
21
- """
22
-
23
- def __init__(self, pipeline_config: PipelineConfig, project_root_dir: Path) -> None:
24
- self.pipeline_config = pipeline_config
25
- self.project_root_dir = project_root_dir
26
- self._loader = GenericPipelineLoader[PipelineStep](self.pipeline_config, self.project_root_dir)
27
-
28
- def load_steps_references(self) -> List[PipelineStepReference]:
29
- return [PipelineStepReference(step_reference.group_name, step_reference._class, step_reference.config) for step_reference in self._loader.load_steps()]
30
-
31
-
32
- class PipelineStepsExecutor:
33
- """Executes a list of pipeline steps sequentially."""
34
-
35
- def __init__(
36
- self,
37
- artifacts_locator: ProjectArtifactsLocator,
38
- steps_references: List[PipelineStepReference],
39
- force_run: bool = False,
40
- dry_run: bool = False,
41
- ) -> None:
42
- self.logger = logger.bind()
43
- self.artifacts_locator = artifacts_locator
44
- self.steps_references = steps_references
45
- self.force_run = force_run
46
- self.dry_run = dry_run
47
-
48
- def run(self) -> None:
49
- execution_context = ExecutionContext(project_root_dir=self.artifacts_locator.project_root_dir, install_dirs=[])
50
- for step_reference in self.steps_references:
51
- step_output_dir = self.artifacts_locator.build_dir / step_reference.group_name
52
- # Create the step output directory, to make sure that files can be created.
53
- step_output_dir.mkdir(parents=True, exist_ok=True)
54
- step = step_reference._class(execution_context, step_output_dir, step_reference.config)
55
- # Execute the step is necessary. If the step is not dirty, it will not be executed
56
- Executor(step.output_dir, self.force_run, self.dry_run).execute(step)
57
- # Independent if the step was executed or not, every step shall update the context
58
- step.update_execution_context()
59
-
60
- return
61
-
62
-
63
- class PipelineScheduler:
64
- """
65
- Schedules which steps must be executed based on the provided configuration.
66
-
67
- * If a step name is provided and the single flag is set, only that step will be executed.
68
- * If a step name is provided and the single flag is not set, all steps up to the provided step will be executed.
69
- * In case a command is provided, only the steps up to that command will be executed.
70
- * If no step name is provided, all steps will be executed.
71
- """
72
-
73
- def __init__(self, pipeline: PipelineConfig, project_root_dir: Path) -> None:
74
- self.pipeline = pipeline
75
- self.project_root_dir = project_root_dir
76
- self.logger = logger.bind()
77
-
78
- def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference]:
79
- pipeline_loader = PipelineLoader(self.pipeline, self.project_root_dir)
80
- return self.filter_steps_references(pipeline_loader.load_steps_references(), step_name, single)
81
-
82
- @staticmethod
83
- def filter_steps_references(
84
- steps_references: List[PipelineStepReference],
85
- step_name: Optional[str],
86
- single: Optional[bool],
87
- ) -> List[PipelineStepReference]:
88
- if step_name:
89
- step_reference = next((step for step in steps_references if step.name == step_name), None)
90
- if not step_reference:
91
- return []
92
- if single:
93
- return [step_reference]
94
- return [step for step in steps_references if steps_references.index(step) <= steps_references.index(step_reference)]
95
- return steps_references
File without changes