pypeline-runner 1.5.0__py3-none-any.whl → 1.7.0__py3-none-any.whl

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.
pypeline/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.5.0"
1
+ __version__ = "1.7.0"
@@ -10,13 +10,10 @@ CONFIG_FILENAME = "pypeline.yaml"
10
10
  class ProjectArtifactsLocator:
11
11
  """Provides paths to project artifacts."""
12
12
 
13
- def __init__(
14
- self,
15
- project_root_dir: Path,
16
- ) -> None:
13
+ def __init__(self, project_root_dir: Path, config_file: Optional[str] = None) -> None:
17
14
  self.project_root_dir = project_root_dir
18
15
  self.build_dir = project_root_dir / "build"
19
- self.config_file = project_root_dir / CONFIG_FILENAME
16
+ self.config_file = project_root_dir.joinpath(config_file if config_file else CONFIG_FILENAME)
20
17
  self.external_dependencies_dir = self.build_dir / "external"
21
18
  scripts_dir = "Scripts" if sys.platform.startswith("win32") else "bin"
22
19
  self.venv_scripts_dir = self.project_root_dir.joinpath(".venv").joinpath(scripts_dir)
@@ -1,5 +1,7 @@
1
+ import importlib
1
2
  from abc import abstractmethod
2
3
  from dataclasses import dataclass
4
+ from importlib.util import module_from_spec, spec_from_file_location
3
5
  from pathlib import Path
4
6
  from typing import (
5
7
  Any,
@@ -9,6 +11,7 @@ from typing import (
9
11
  List,
10
12
  Optional,
11
13
  OrderedDict,
14
+ Protocol,
12
15
  Tuple,
13
16
  Type,
14
17
  TypeAlias,
@@ -17,6 +20,7 @@ from typing import (
17
20
  )
18
21
 
19
22
  from mashumaro import DataClassDictMixin
23
+ from py_app_dev.core.exceptions import UserNotificationException
20
24
  from py_app_dev.core.runnable import Runnable
21
25
 
22
26
  from .execution_context import ExecutionContext
@@ -44,6 +48,21 @@ class PipelineStepConfig(DataClassDictMixin):
44
48
 
45
49
  PipelineConfig: TypeAlias = Union[List[PipelineStepConfig], OrderedDict[str, List[PipelineStepConfig]]]
46
50
 
51
+ TPipelineStep = TypeVar("TPipelineStep", covariant=True)
52
+
53
+
54
+ @dataclass
55
+ class PipelineStepReference(Generic[TPipelineStep]):
56
+ """Once a Step is found, keep the Step class reference to be able to instantiate it later."""
57
+
58
+ group_name: Optional[str]
59
+ _class: Type[TPipelineStep]
60
+ config: Optional[Dict[str, Any]] = None
61
+
62
+ @property
63
+ def name(self) -> str:
64
+ return self._class.__name__
65
+
47
66
 
48
67
  class PipelineConfigIterator:
49
68
  """
@@ -64,6 +83,70 @@ class PipelineConfigIterator:
64
83
  yield from self._items
65
84
 
66
85
 
86
+ class StepClassFactory(Generic[TPipelineStep], Protocol):
87
+ def create_step_class(self, step_config: PipelineStepConfig, project_root_dir: Path) -> Type[TPipelineStep]: ...
88
+
89
+
90
+ class PipelineLoader(Generic[TPipelineStep]):
91
+ def __init__(self, pipeline_config: PipelineConfig, project_root_dir: Path, step_class_factory: Optional[StepClassFactory[TPipelineStep]] = None) -> None:
92
+ self.pipeline_config = pipeline_config
93
+ self.project_root_dir = project_root_dir
94
+ self.step_class_factory = step_class_factory
95
+
96
+ def load_steps_references(self) -> List[PipelineStepReference[TPipelineStep]]:
97
+ result = []
98
+ for group_name, steps_config in PipelineConfigIterator(self.pipeline_config):
99
+ result.extend(self._load_steps(group_name, steps_config, self.project_root_dir, self.step_class_factory))
100
+ return result
101
+
102
+ @staticmethod
103
+ def _load_steps(
104
+ group_name: Optional[str], steps_config: List[PipelineStepConfig], project_root_dir: Path, step_class_factory: Optional[StepClassFactory[TPipelineStep]] = None
105
+ ) -> List[PipelineStepReference[TPipelineStep]]:
106
+ result = []
107
+ for step_config in steps_config:
108
+ step_class_name = step_config.class_name or step_config.step
109
+ if step_config.module:
110
+ step_class = PipelineLoader[TPipelineStep]._load_module_step(step_config.module, step_class_name)
111
+ elif step_config.file:
112
+ step_class = PipelineLoader[TPipelineStep]._load_user_step(project_root_dir.joinpath(step_config.file), step_class_name)
113
+ else:
114
+ if step_class_factory:
115
+ step_class = step_class_factory.create_step_class(step_config, project_root_dir)
116
+ else:
117
+ raise UserNotificationException(
118
+ f"Step '{step_class_name}' has no 'module' nor 'file' defined nor a custom step class factory was provided. Please check your pipeline configuration."
119
+ )
120
+ result.append(PipelineStepReference(group_name, step_class, step_config.config))
121
+ return result
122
+
123
+ @staticmethod
124
+ def _load_user_step(python_file: Path, step_class_name: str) -> Type[TPipelineStep]:
125
+ # Create a module specification from the file path
126
+ spec = spec_from_file_location(f"user__{python_file.stem}", python_file)
127
+ if spec and spec.loader:
128
+ step_module = module_from_spec(spec)
129
+ # Import the module
130
+ spec.loader.exec_module(step_module)
131
+ try:
132
+ step_class = getattr(step_module, step_class_name)
133
+ except AttributeError:
134
+ raise UserNotificationException(f"Could not load class '{step_class_name}' from file '{python_file}'. Please check your pipeline configuration.") from None
135
+ return step_class
136
+ raise UserNotificationException(f"Could not load file '{python_file}'. Please check the file for any errors.")
137
+
138
+ @staticmethod
139
+ def _load_module_step(module_name: str, step_class_name: str) -> Type[TPipelineStep]:
140
+ try:
141
+ module = importlib.import_module(module_name)
142
+ step_class = getattr(module, step_class_name)
143
+ except ImportError:
144
+ raise UserNotificationException(f"Could not load module '{module_name}'. Please check your pipeline configuration.") from None
145
+ except AttributeError:
146
+ raise UserNotificationException(f"Could not load class '{step_class_name}' from module '{module_name}'. Please check your pipeline configuration.") from None
147
+ return step_class
148
+
149
+
67
150
  TExecutionContext = TypeVar("TExecutionContext", bound=ExecutionContext)
68
151
 
69
152
 
@@ -96,14 +179,3 @@ class PipelineStep(Generic[TExecutionContext], Runnable):
96
179
  def get_needs_dependency_management(self) -> bool:
97
180
  """If false, the step executor will not check for outdated dependencies. This is useful for steps consisting of command lines which shall always run."""
98
181
  return True
99
-
100
-
101
- class PipelineStepReference(Generic[TExecutionContext]):
102
- def __init__(self, group_name: Optional[str], _class: Type[PipelineStep[TExecutionContext]], config: Optional[Dict[str, Any]] = None) -> None:
103
- self.group_name = group_name
104
- self._class = _class
105
- self.config = config
106
-
107
- @property
108
- def name(self) -> str:
109
- return self._class.__name__
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from typing import Optional
2
3
 
3
4
  from py_app_dev.core.exceptions import UserNotificationException
4
5
  from py_app_dev.core.logging import logger
@@ -8,9 +9,9 @@ from .config import PipelineConfig, ProjectConfig
8
9
 
9
10
 
10
11
  class ProjectSlurper:
11
- def __init__(self, project_dir: Path) -> None:
12
+ def __init__(self, project_dir: Path, config_file: Optional[str] = None) -> None:
12
13
  self.logger = logger.bind()
13
- self.artifacts_locator = ProjectArtifactsLocator(project_dir)
14
+ self.artifacts_locator = ProjectArtifactsLocator(project_dir, config_file)
14
15
  try:
15
16
  self.user_config: ProjectConfig = ProjectConfig.from_file(self.artifacts_locator.config_file)
16
17
  except FileNotFoundError:
pypeline/main.py CHANGED
@@ -42,6 +42,7 @@ def init(
42
42
  @time_it("run")
43
43
  def run(
44
44
  project_dir: Path = typer.Option(Path.cwd().absolute(), help="The project directory"), # noqa: B008,
45
+ config_file: Optional[str] = typer.Option(None, help="The name of the YAML configuration file containing the pypeline definition."),
45
46
  step: Optional[str] = typer.Option(
46
47
  None,
47
48
  help="Name of the step to run (as written in the pipeline config).",
@@ -67,7 +68,7 @@ def run(
67
68
  is_flag=True,
68
69
  ),
69
70
  ) -> None:
70
- project_slurper = ProjectSlurper(project_dir)
71
+ project_slurper = ProjectSlurper(project_dir, config_file)
71
72
  if print:
72
73
  logger.info("Pipeline steps:")
73
74
  for group, step_configs in PipelineConfigIterator(project_slurper.pipeline):
pypeline/pypeline.py CHANGED
@@ -1,5 +1,3 @@
1
- import importlib
2
- from importlib.util import module_from_spec, spec_from_file_location
3
1
  from pathlib import Path
4
2
  from typing import (
5
3
  Any,
@@ -8,7 +6,6 @@ from typing import (
8
6
  List,
9
7
  Optional,
10
8
  Type,
11
- cast,
12
9
  )
13
10
 
14
11
  from py_app_dev.core.exceptions import UserNotificationException
@@ -17,75 +14,18 @@ from py_app_dev.core.runnable import Executor
17
14
 
18
15
  from .domain.artifacts import ProjectArtifactsLocator
19
16
  from .domain.execution_context import ExecutionContext
20
- from .domain.pipeline import PipelineConfig, PipelineConfigIterator, PipelineStep, PipelineStepConfig, PipelineStepReference, TExecutionContext
17
+ from .domain.pipeline import PipelineConfig, PipelineLoader, PipelineStep, PipelineStepConfig, PipelineStepReference, StepClassFactory, TExecutionContext
21
18
 
22
19
 
23
- class PipelineLoader(Generic[TExecutionContext]):
24
- """
25
- Loads pipeline steps from a pipeline configuration.
26
-
27
- The steps are not instantiated, only the references are returned (lazy load).
28
- The pipeline loader needs to know the project root directory to be able to find the
29
- user custom local steps.
30
- """
31
-
32
- def __init__(self, pipeline_config: PipelineConfig, project_root_dir: Path) -> None:
33
- self.pipeline_config = pipeline_config
34
- self.project_root_dir = project_root_dir
35
-
36
- def load_steps_references(self) -> List[PipelineStepReference[TExecutionContext]]:
37
- result = []
38
- for group_name, steps_config in PipelineConfigIterator(self.pipeline_config):
39
- result.extend(self._load_steps(group_name, steps_config, self.project_root_dir))
40
- return result
41
-
42
- @staticmethod
43
- def _load_steps(
44
- group_name: Optional[str],
45
- steps_config: List[PipelineStepConfig],
46
- project_root_dir: Path,
47
- ) -> List[PipelineStepReference[TExecutionContext]]:
48
- result = []
49
- for step_config in steps_config:
50
- step_class_name = step_config.class_name or step_config.step
51
- if step_config.module:
52
- step_class = PipelineLoader._load_module_step(step_config.module, step_class_name)
53
- elif step_config.file:
54
- step_class = PipelineLoader._load_user_step(project_root_dir.joinpath(step_config.file), step_class_name)
55
- elif step_config.run:
56
- # We want the run field to always return a list of strings (the command and its arguments).
57
- run_command = step_config.run.split(" ") if isinstance(step_config.run, str) else step_config.run
58
- step_class = PipelineLoader._create_run_command_step_class(run_command, step_class_name)
59
- else:
60
- raise UserNotificationException(f"Step '{step_class_name}' has no 'module' nor 'file' nor `run` defined." " Please check your pipeline configuration.")
61
- result.append(PipelineStepReference[TExecutionContext](group_name, cast(Type[PipelineStep[TExecutionContext]], step_class), step_config.config))
62
- return result
63
-
64
- @staticmethod
65
- def _load_user_step(python_file: Path, step_class_name: str) -> Type[PipelineStep[ExecutionContext]]:
66
- # Create a module specification from the file path
67
- spec = spec_from_file_location(f"user__{step_class_name}", python_file)
68
- if spec and spec.loader:
69
- step_module = module_from_spec(spec)
70
- # Import the module
71
- spec.loader.exec_module(step_module)
72
- try:
73
- step_class = getattr(step_module, step_class_name)
74
- except AttributeError:
75
- raise UserNotificationException(f"Could not load class '{step_class_name}' from file '{python_file}'." " Please check your pipeline configuration.") from None
76
- return step_class
77
- raise UserNotificationException(f"Could not load file '{python_file}'." " Please check the file for any errors.")
78
-
79
- @staticmethod
80
- def _load_module_step(module_name: str, step_class_name: str) -> Type[PipelineStep[ExecutionContext]]:
81
- try:
82
- module = importlib.import_module(module_name)
83
- step_class = getattr(module, step_class_name)
84
- except ImportError:
85
- raise UserNotificationException(f"Could not load module '{module_name}'. Please check your pipeline configuration.") from None
86
- except AttributeError:
87
- raise UserNotificationException(f"Could not load class '{step_class_name}' from module '{module_name}'." " Please check your pipeline configuration.") from None
88
- return step_class
20
+ class RunCommandClassFactory(StepClassFactory[PipelineStep[TExecutionContext]]):
21
+ def create_step_class(self, step_config: PipelineStepConfig, project_root_dir: Path) -> Type[PipelineStep[ExecutionContext]]:
22
+ _ = project_root_dir # Unused because we do not need to locate files relative to the project root directory
23
+ step_name = step_config.class_name or step_config.step
24
+ if step_config.run:
25
+ # We want the run field to always return a list of strings (the command and its arguments).
26
+ run_command = step_config.run.split(" ") if isinstance(step_config.run, str) else step_config.run
27
+ return self._create_run_command_step_class(run_command, step_name)
28
+ raise UserNotificationException(f"Step '{step_name}' has `run` command defined. Please check your pipeline configuration.")
89
29
 
90
30
  @staticmethod
91
31
  def _create_run_command_step_class(command: List[str], name: str) -> Type[PipelineStep[ExecutionContext]]:
@@ -133,7 +73,7 @@ class PipelineStepsExecutor(Generic[TExecutionContext]):
133
73
  def __init__(
134
74
  self,
135
75
  execution_context: TExecutionContext,
136
- steps_references: List[PipelineStepReference[TExecutionContext]],
76
+ steps_references: List[PipelineStepReference[PipelineStep[TExecutionContext]]],
137
77
  force_run: bool = False,
138
78
  dry_run: bool = False,
139
79
  ) -> None:
@@ -175,16 +115,15 @@ class PipelineScheduler(Generic[TExecutionContext]):
175
115
  self.project_root_dir = project_root_dir
176
116
  self.logger = logger.bind()
177
117
 
178
- def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference[TExecutionContext]]:
179
- pipeline_loader = PipelineLoader[TExecutionContext](self.pipeline, self.project_root_dir)
180
- return self.filter_steps_references(pipeline_loader.load_steps_references(), step_name, single)
118
+ def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference[PipelineStep[TExecutionContext]]]:
119
+ return self.filter_steps_references(self.create_pipeline_loader(self.pipeline, self.project_root_dir).load_steps_references(), step_name, single)
181
120
 
182
121
  @staticmethod
183
122
  def filter_steps_references(
184
- steps_references: List[PipelineStepReference[TExecutionContext]],
123
+ steps_references: List[PipelineStepReference[PipelineStep[TExecutionContext]]],
185
124
  step_name: Optional[str],
186
125
  single: Optional[bool],
187
- ) -> List[PipelineStepReference[TExecutionContext]]:
126
+ ) -> List[PipelineStepReference[PipelineStep[TExecutionContext]]]:
188
127
  if step_name:
189
128
  step_reference = next((step for step in steps_references if step.name == step_name), None)
190
129
  if not step_reference:
@@ -193,3 +132,7 @@ class PipelineScheduler(Generic[TExecutionContext]):
193
132
  return [step_reference]
194
133
  return [step for step in steps_references if steps_references.index(step) <= steps_references.index(step_reference)]
195
134
  return steps_references
135
+
136
+ @staticmethod
137
+ def create_pipeline_loader(pipeline: PipelineConfig, project_root_dir: Path) -> PipelineLoader[PipelineStep[TExecutionContext]]:
138
+ return PipelineLoader[PipelineStep[TExecutionContext]](pipeline, project_root_dir, RunCommandClassFactory())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypeline-runner
3
- Version: 1.5.0
3
+ Version: 1.7.0
4
4
  Summary: Configure and execute pipelines with Python (similar to GitHub workflows or Jenkins pipelines).
5
5
  License: MIT
6
6
  Author: cuinixam
@@ -1,11 +1,11 @@
1
- pypeline/__init__.py,sha256=X9pTeGHIYZtDOmfb9pnn2DEqWZMAyaPlqdFwEhsuSvk,22
1
+ pypeline/__init__.py,sha256=lEEx5lwinUby-YNl7w-VFGUcjcibsJ9EuzgJ2W7ocrU,22
2
2
  pypeline/__run.py,sha256=TCdaX05Qm3g8T4QYryKB25Xxf0L5Km7hFOHe1mK9vI0,350
3
3
  pypeline/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pypeline/domain/artifacts.py,sha256=qXshnk9umi0AVGV4m5iEiy_MQ5Ad2LDZwI8OULU-qMk,1355
4
+ pypeline/domain/artifacts.py,sha256=5k7cVfHhLmvWXNuHKxXb9ca4Lxu0JytGQqazENCeKEU,1404
5
5
  pypeline/domain/config.py,sha256=AlavAaz5hSxa6yaKYnj-x71ClhOtA41yv5Qf2JIE47k,1650
6
6
  pypeline/domain/execution_context.py,sha256=ho-WvCVRMUfYo1532eQYabXCHtXDgvSNUkX8S3Cr7Xo,1278
7
- pypeline/domain/pipeline.py,sha256=mO5tc18nxiAtnLs_MR56u7PAoTJDMZ_KVkCqMzV-YG8,3916
8
- pypeline/domain/project_slurper.py,sha256=YCho7V1BHjFmC_foxHFaWX8c_VbMJ16XEB4CQBlMrhc,894
7
+ pypeline/domain/pipeline.py,sha256=2BsN2lw2znUxLH--Novyqe6SubVKs6XeHQSQf9yxirw,7788
8
+ pypeline/domain/project_slurper.py,sha256=e3BLV88GvfW3efh0agUWKqMk3oWnL602P5u9jER_o9U,971
9
9
  pypeline/kickstart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  pypeline/kickstart/create.py,sha256=dWqSdDsqNh7_AnNEsJzsmheR2-xyO6rpUwF-7AwzCYY,2188
11
11
  pypeline/kickstart/templates/project/.gitignore,sha256=y8GJoVvRPez1LBokf1NaDOt2X1XtGwKFMF5yjA8AVS0,24
@@ -16,15 +16,15 @@ pypeline/kickstart/templates/project/pypeline.yaml,sha256=EV5Tnu3H33gMT3Ov0t14-j
16
16
  pypeline/kickstart/templates/project/pyproject.toml,sha256=yc6RCo-bUo1PXF91XfM-dButgfxU16Uud34NidgJ0zQ,225
17
17
  pypeline/kickstart/templates/project/scoopfile.json,sha256=DcfZ8jYf9hmPHM-AWwnPKQJCzRG3fCuYtMeoY01nkag,219
18
18
  pypeline/kickstart/templates/project/steps/my_step.py,sha256=_zx01qAVuwn6IMPBUBwKY-IBjS9Gs2m-d51L9sayGug,733
19
- pypeline/main.py,sha256=GtuOgB9OeNFgbWLHZux80fppYQVwMYNFZoZhDIlJW9c,3457
19
+ pypeline/main.py,sha256=54_-aINmJY6IILSp6swL2kYqNoBQqJw4W2duxY7gcQ4,3607
20
20
  pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- pypeline/pypeline.py,sha256=fHGocevkWywVVTOkHJ0YSPBKjDwyJ8Zjs37wP4eMAO0,9209
21
+ pypeline/pypeline.py,sha256=FHGS2iNtiuiM4dZDHCbeL-UW1apdCtZeFzl1xZW9qBw,6482
22
22
  pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  pypeline/steps/create_venv.py,sha256=vTPSA0gGGzq_QhI1jAgsGchS2s8_ZpEtnWcb0uL8BHE,1934
24
24
  pypeline/steps/scoop_install.py,sha256=_YdoCMXLON0eIwck8PJOcNhayx_ka1krBAidw_oRuFE,3373
25
25
  pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
26
- pypeline_runner-1.5.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
27
- pypeline_runner-1.5.0.dist-info/METADATA,sha256=BBR8eGiEfCmhMBS-Dgn6lmx7RRYuWUbxitVehIHRMOo,7154
28
- pypeline_runner-1.5.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
29
- pypeline_runner-1.5.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
30
- pypeline_runner-1.5.0.dist-info/RECORD,,
26
+ pypeline_runner-1.7.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
27
+ pypeline_runner-1.7.0.dist-info/METADATA,sha256=O-Y7JgczzWmfE0m2W7CRH23JJmNDNTrCdxc0P0ZLpkI,7154
28
+ pypeline_runner-1.7.0.dist-info/WHEEL,sha256=7dDg4QLnNKTvwIDR9Ac8jJaAmBC_owJrckbC0jjThyA,88
29
+ pypeline_runner-1.7.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
30
+ pypeline_runner-1.7.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.0.1
2
+ Generator: poetry-core 2.1.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any