pypeline-runner 0.3.0__py3-none-any.whl → 1.0.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__ = "0.3.0"
1
+ __version__ = "1.0.0"
@@ -4,11 +4,13 @@ from pathlib import Path
4
4
  from typing import (
5
5
  Any,
6
6
  Dict,
7
+ Generic,
7
8
  List,
8
9
  Optional,
9
10
  OrderedDict,
10
11
  Type,
11
12
  TypeAlias,
13
+ TypeVar,
12
14
  )
13
15
 
14
16
  from mashumaro import DataClassDictMixin
@@ -39,25 +41,39 @@ class PipelineStepConfig(DataClassDictMixin):
39
41
 
40
42
  PipelineConfig: TypeAlias = OrderedDict[str, List[PipelineStepConfig]]
41
43
 
44
+ TExecutionContext = TypeVar("TExecutionContext", bound=ExecutionContext)
42
45
 
43
- class PipelineStep(Runnable):
44
- def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
46
+
47
+ class PipelineStep(Generic[TExecutionContext], Runnable):
48
+ """One can create subclasses of PipelineStep that specify the type of ExecutionContext they require."""
49
+
50
+ def __init__(self, execution_context: TExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
45
51
  super().__init__(self.get_needs_dependency_management())
46
52
  self.execution_context = execution_context
47
- self.output_dir = output_dir
53
+ self.group_name = group_name
48
54
  self.config = config
49
55
  self.project_root_dir = self.execution_context.project_root_dir
50
56
 
57
+ @property
58
+ def output_dir(self) -> Path:
59
+ return self.execution_context.create_artifacts_locator().build_dir.joinpath(self.group_name)
60
+
51
61
  @abstractmethod
52
62
  def update_execution_context(self) -> None:
63
+ """
64
+ Even if the step does not need to run ( because it is not outdated ), it can still update the execution context.
65
+
66
+ A typical use case is for steps installing software that need to provide the install directories in the execution context even if all tools are already installed.
67
+ """
53
68
  pass
54
69
 
55
70
  def get_needs_dependency_management(self) -> bool:
71
+ """If false, the step executor will not check for outdated dependencies. This is useful for steps consisting of command lines which shall always run."""
56
72
  return True
57
73
 
58
74
 
59
- class PipelineStepReference:
60
- def __init__(self, group_name: str, _class: Type[PipelineStep], config: Optional[Dict[str, Any]] = None) -> None:
75
+ class PipelineStepReference(Generic[TExecutionContext]):
76
+ def __init__(self, group_name: str, _class: Type[PipelineStep[TExecutionContext]], config: Optional[Dict[str, Any]] = None) -> None:
61
77
  self.group_name = group_name
62
78
  self._class = _class
63
79
  self.config = config
@@ -3,10 +3,11 @@ from typing import List
3
3
 
4
4
  from py_app_dev.core.logging import logger
5
5
 
6
+ from pypeline.domain.execution_context import ExecutionContext
6
7
  from pypeline.domain.pipeline import PipelineStep
7
8
 
8
9
 
9
- class MyStep(PipelineStep):
10
+ class MyStep(PipelineStep[ExecutionContext]):
10
11
  def run(self) -> None:
11
12
  logger.info(f"Run {self.get_name()} found install dirs:")
12
13
  for install_dir in self.execution_context.install_dirs:
pypeline/main.py CHANGED
@@ -7,6 +7,7 @@ from py_app_dev.core.exceptions import UserNotificationException
7
7
  from py_app_dev.core.logging import logger, setup_logger, time_it
8
8
 
9
9
  from pypeline import __version__
10
+ from pypeline.domain.execution_context import ExecutionContext
10
11
  from pypeline.domain.project_slurper import ProjectSlurper
11
12
  from pypeline.kickstart.create import KickstartProject
12
13
  from pypeline.pypeline import PipelineScheduler, PipelineStepsExecutor
@@ -76,14 +77,14 @@ def run(
76
77
  if not project_slurper.pipeline:
77
78
  raise UserNotificationException("No pipeline found in the configuration.")
78
79
  # Schedule the steps to run
79
- steps_references = PipelineScheduler(project_slurper.pipeline, project_dir).get_steps_to_run(step, single)
80
+ steps_references = PipelineScheduler[ExecutionContext](project_slurper.pipeline, project_dir).get_steps_to_run(step, single)
80
81
  if not steps_references:
81
82
  if step:
82
83
  raise UserNotificationException(f"Step '{step}' not found in the pipeline.")
83
84
  logger.info("No steps to run.")
84
85
  return
85
86
 
86
- PipelineStepsExecutor(project_slurper.artifacts_locator, steps_references, force_run, dry_run).run()
87
+ PipelineStepsExecutor[ExecutionContext](ExecutionContext(project_dir), steps_references, force_run, dry_run).run()
87
88
 
88
89
 
89
90
  def main() -> None:
pypeline/pypeline.py CHANGED
@@ -4,9 +4,11 @@ from pathlib import Path
4
4
  from typing import (
5
5
  Any,
6
6
  Dict,
7
+ Generic,
7
8
  List,
8
9
  Optional,
9
10
  Type,
11
+ cast,
10
12
  )
11
13
 
12
14
  from py_app_dev.core.exceptions import UserNotificationException
@@ -15,10 +17,10 @@ from py_app_dev.core.runnable import Executor
15
17
 
16
18
  from .domain.artifacts import ProjectArtifactsLocator
17
19
  from .domain.execution_context import ExecutionContext
18
- from .domain.pipeline import PipelineConfig, PipelineStep, PipelineStepConfig, PipelineStepReference
20
+ from .domain.pipeline import PipelineConfig, PipelineStep, PipelineStepConfig, PipelineStepReference, TExecutionContext
19
21
 
20
22
 
21
- class PipelineLoader:
23
+ class PipelineLoader(Generic[TExecutionContext]):
22
24
  """
23
25
  Loads pipeline steps from a pipeline configuration.
24
26
 
@@ -31,7 +33,7 @@ class PipelineLoader:
31
33
  self.pipeline_config = pipeline_config
32
34
  self.project_root_dir = project_root_dir
33
35
 
34
- def load_steps_references(self) -> List[PipelineStepReference]:
36
+ def load_steps_references(self) -> List[PipelineStepReference[TExecutionContext]]:
35
37
  result = []
36
38
  for group_name, steps_config in self.pipeline_config.items():
37
39
  result.extend(self._load_steps(group_name, steps_config, self.project_root_dir))
@@ -42,7 +44,7 @@ class PipelineLoader:
42
44
  group_name: str,
43
45
  steps_config: List[PipelineStepConfig],
44
46
  project_root_dir: Path,
45
- ) -> List[PipelineStepReference]:
47
+ ) -> List[PipelineStepReference[TExecutionContext]]:
46
48
  result = []
47
49
  for step_config in steps_config:
48
50
  step_class_name = step_config.class_name or step_config.step
@@ -54,11 +56,11 @@ class PipelineLoader:
54
56
  step_class = PipelineLoader._create_run_command_step_class(step_config.run, step_class_name)
55
57
  else:
56
58
  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))
59
+ result.append(PipelineStepReference[TExecutionContext](group_name, cast(Type[PipelineStep[TExecutionContext]], step_class), step_config.config))
58
60
  return result
59
61
 
60
62
  @staticmethod
61
- def _load_user_step(python_file: Path, step_class_name: str) -> Type[PipelineStep]:
63
+ def _load_user_step(python_file: Path, step_class_name: str) -> Type[PipelineStep[ExecutionContext]]:
62
64
  # Create a module specification from the file path
63
65
  spec = spec_from_file_location(f"user__{step_class_name}", python_file)
64
66
  if spec and spec.loader:
@@ -73,7 +75,7 @@ class PipelineLoader:
73
75
  raise UserNotificationException(f"Could not load file '{python_file}'." " Please check the file for any errors.")
74
76
 
75
77
  @staticmethod
76
- def _load_module_step(module_name: str, step_class_name: str) -> Type[PipelineStep]:
78
+ def _load_module_step(module_name: str, step_class_name: str) -> Type[PipelineStep[ExecutionContext]]:
77
79
  try:
78
80
  module = importlib.import_module(module_name)
79
81
  step_class = getattr(module, step_class_name)
@@ -84,14 +86,14 @@ class PipelineLoader:
84
86
  return step_class
85
87
 
86
88
  @staticmethod
87
- def _create_run_command_step_class(command: str, name: str) -> Type[PipelineStep]:
89
+ def _create_run_command_step_class(command: str, name: str) -> Type[PipelineStep[ExecutionContext]]:
88
90
  """Dynamically creates a step class for a given command."""
89
91
 
90
- class DynamicRunCommandStep(PipelineStep):
92
+ class TmpDynamicRunCommandStep(PipelineStep[ExecutionContext]):
91
93
  """A simple step that runs a command."""
92
94
 
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
+ def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
96
+ super().__init__(execution_context, group_name, config)
95
97
  self.command = command
96
98
  self.name = name
97
99
 
@@ -118,32 +120,35 @@ class PipelineLoader:
118
120
  def update_execution_context(self) -> None:
119
121
  pass
120
122
 
121
- return DynamicRunCommandStep
123
+ # Dynamically create the class with the given name
124
+ return type(name, (TmpDynamicRunCommandStep,), {})
122
125
 
123
126
 
124
- class PipelineStepsExecutor:
127
+ class PipelineStepsExecutor(Generic[TExecutionContext]):
125
128
  """Executes a list of pipeline steps sequentially."""
126
129
 
127
130
  def __init__(
128
131
  self,
129
- artifacts_locator: ProjectArtifactsLocator,
130
- steps_references: List[PipelineStepReference],
132
+ execution_context: TExecutionContext,
133
+ steps_references: List[PipelineStepReference[TExecutionContext]],
131
134
  force_run: bool = False,
132
135
  dry_run: bool = False,
133
136
  ) -> None:
134
137
  self.logger = logger.bind()
135
- self.artifacts_locator = artifacts_locator
138
+ self.execution_context = execution_context
136
139
  self.steps_references = steps_references
137
140
  self.force_run = force_run
138
141
  self.dry_run = dry_run
139
142
 
143
+ @property
144
+ def artifacts_locator(self) -> ProjectArtifactsLocator:
145
+ return self.execution_context.create_artifacts_locator()
146
+
140
147
  def run(self) -> None:
141
- execution_context = ExecutionContext(project_root_dir=self.artifacts_locator.project_root_dir, install_dirs=[])
142
148
  for step_reference in self.steps_references:
143
- step_output_dir = self.artifacts_locator.build_dir / step_reference.group_name
149
+ step = step_reference._class(self.execution_context, step_reference.group_name, step_reference.config)
144
150
  # 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)
151
+ step.output_dir.mkdir(parents=True, exist_ok=True)
147
152
  # Execute the step is necessary. If the step is not dirty, it will not be executed
148
153
  Executor(step.output_dir, self.force_run, self.dry_run).execute(step)
149
154
  # Independent if the step was executed or not, every step shall update the context
@@ -152,7 +157,7 @@ class PipelineStepsExecutor:
152
157
  return
153
158
 
154
159
 
155
- class PipelineScheduler:
160
+ class PipelineScheduler(Generic[TExecutionContext]):
156
161
  """
157
162
  Schedules which steps must be executed based on the provided configuration.
158
163
 
@@ -167,16 +172,16 @@ class PipelineScheduler:
167
172
  self.project_root_dir = project_root_dir
168
173
  self.logger = logger.bind()
169
174
 
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)
175
+ def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference[TExecutionContext]]:
176
+ pipeline_loader = PipelineLoader[TExecutionContext](self.pipeline, self.project_root_dir)
172
177
  return self.filter_steps_references(pipeline_loader.load_steps_references(), step_name, single)
173
178
 
174
179
  @staticmethod
175
180
  def filter_steps_references(
176
- steps_references: List[PipelineStepReference],
181
+ steps_references: List[PipelineStepReference[TExecutionContext]],
177
182
  step_name: Optional[str],
178
183
  single: Optional[bool],
179
- ) -> List[PipelineStepReference]:
184
+ ) -> List[PipelineStepReference[TExecutionContext]]:
180
185
  if step_name:
181
186
  step_reference = next((step for step in steps_references if step.name == step_name), None)
182
187
  if not step_reference:
@@ -15,9 +15,9 @@ class CreateVEnvConfig(DataClassDictMixin):
15
15
  bootstrap_script: str = "bootstrap.py"
16
16
 
17
17
 
18
- class CreateVEnv(PipelineStep):
19
- def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
20
- super().__init__(execution_context, output_dir, config)
18
+ class CreateVEnv(PipelineStep[ExecutionContext]):
19
+ def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
20
+ super().__init__(execution_context, group_name, config)
21
21
  self.logger = logger.bind()
22
22
  self.logger = logger.bind()
23
23
 
@@ -45,9 +45,9 @@ def create_scoop_wrapper() -> ScoopWrapper:
45
45
  return ScoopWrapper()
46
46
 
47
47
 
48
- class ScoopInstall(PipelineStep):
49
- def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
50
- super().__init__(execution_context, output_dir, config)
48
+ class ScoopInstall(PipelineStep[ExecutionContext]):
49
+ def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
50
+ super().__init__(execution_context, group_name, config)
51
51
  self.logger = logger.bind()
52
52
  self.execution_info = ScoopInstallExecutionInfo([])
53
53
  # One needs to keep track of the installed apps to get the required paths
@@ -8,9 +8,9 @@ from ..domain.execution_context import ExecutionContext
8
8
  from ..domain.pipeline import PipelineStep
9
9
 
10
10
 
11
- class WestInstall(PipelineStep):
12
- def __init__(self, execution_context: ExecutionContext, output_dir: Path, config: Optional[Dict[str, Any]] = None) -> None:
13
- super().__init__(execution_context, output_dir, config)
11
+ class WestInstall(PipelineStep[ExecutionContext]):
12
+ def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
13
+ super().__init__(execution_context, group_name, config)
14
14
  self.logger = logger.bind()
15
15
  self.artifacts_locator = execution_context.create_artifacts_locator()
16
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pypeline-runner
3
- Version: 0.3.0
3
+ Version: 1.0.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
@@ -1,10 +1,10 @@
1
- pypeline/__init__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
1
+ pypeline/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
2
2
  pypeline/__run.py,sha256=TCdaX05Qm3g8T4QYryKB25Xxf0L5Km7hFOHe1mK9vI0,350
3
3
  pypeline/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pypeline/domain/artifacts.py,sha256=qXshnk9umi0AVGV4m5iEiy_MQ5Ad2LDZwI8OULU-qMk,1355
5
5
  pypeline/domain/config.py,sha256=AlavAaz5hSxa6yaKYnj-x71ClhOtA41yv5Qf2JIE47k,1650
6
6
  pypeline/domain/execution_context.py,sha256=0zc3OgXeIMDpgWWYMaDGub7fY5urLLR79yuCaXVxoTQ,1101
7
- pypeline/domain/pipeline.py,sha256=TbnM1JI1AFEdDfMSfdDVi2DZjFAAWBRMzIycOsui4Vs,1997
7
+ pypeline/domain/pipeline.py,sha256=PlNrRXBaLw3Eu62ytyH3sgbTmt5pKXC4l24GXXhCNII,2925
8
8
  pypeline/domain/project_slurper.py,sha256=YCho7V1BHjFmC_foxHFaWX8c_VbMJ16XEB4CQBlMrhc,894
9
9
  pypeline/kickstart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  pypeline/kickstart/create.py,sha256=Gz2rVfhoQCKgYWlJoefh5Azcgamc9JJtiNsP1DTKdQo,2235
@@ -16,16 +16,16 @@ pypeline/kickstart/templates/project/pypeline.ps1,sha256=tkiXrsMsNM43WJdmWJRhis3
16
16
  pypeline/kickstart/templates/project/pypeline.yaml,sha256=i9YSqpS2o-_Bzl69fvdOFVyYRMsDCtDy8usBpk-hqnE,219
17
17
  pypeline/kickstart/templates/project/pyproject.toml,sha256=yc6RCo-bUo1PXF91XfM-dButgfxU16Uud34NidgJ0zQ,225
18
18
  pypeline/kickstart/templates/project/scoopfile.json,sha256=DcfZ8jYf9hmPHM-AWwnPKQJCzRG3fCuYtMeoY01nkag,219
19
- pypeline/kickstart/templates/project/steps/my_step.py,sha256=YNRdb7ofqo3PxMQe8Vhtf7K9eZclv-6J0IwMrd3E70w,651
20
- pypeline/main.py,sha256=gwzbTUu-9_LXHpMDUDJU4gUInA4wGRmngO0VaTAqptk,3260
19
+ pypeline/kickstart/templates/project/steps/my_step.py,sha256=_zx01qAVuwn6IMPBUBwKY-IBjS9Gs2m-d51L9sayGug,733
20
+ pypeline/main.py,sha256=20gfpGlYWN3wlAifTkES4VY3lXDQjwKBYd8KkSr33EE,3355
21
21
  pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- pypeline/pypeline.py,sha256=YiKZ8IH1t1FsTqJ45dVBkr2I2XRN2Su0ChlJFbUIqBc,8335
22
+ pypeline/pypeline.py,sha256=GUEIPtK6TU9yt-izuIqbRUjbxM3JASoj8nNJ63mN388,8756
23
23
  pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- pypeline/steps/create_venv.py,sha256=od1zLVPhNdN-sWqEqYMkX19c5IEt4lIIOcASBeoqUUw,1954
25
- pypeline/steps/scoop_install.py,sha256=MmoC4Yy1lOhHrfKBrgJKHMcRUNPbRztzUbSgIlazer8,3356
26
- pypeline/steps/west_install.py,sha256=J1p-62owTDOY6bVTGCr5d9YFOL8DeRUPTnq0TbiaWlA,1930
27
- pypeline_runner-0.3.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
28
- pypeline_runner-0.3.0.dist-info/METADATA,sha256=f0qMzL-SHEe-n5BJw1DHJ-9WovmEmIaBh1X4bUMaYiY,7156
29
- pypeline_runner-0.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
- pypeline_runner-0.3.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
31
- pypeline_runner-0.3.0.dist-info/RECORD,,
24
+ pypeline/steps/create_venv.py,sha256=hXRklcYR38uUPWRDyhUjWu8WEPIaQPSQggM_ocwG7k8,1971
25
+ pypeline/steps/scoop_install.py,sha256=_YdoCMXLON0eIwck8PJOcNhayx_ka1krBAidw_oRuFE,3373
26
+ pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
27
+ pypeline_runner-1.0.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
28
+ pypeline_runner-1.0.0.dist-info/METADATA,sha256=fDbeBXYd0Lt_7_euh4eG3kUhNC40PVWanF0jY8mysJg,7156
29
+ pypeline_runner-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
+ pypeline_runner-1.0.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
31
+ pypeline_runner-1.0.0.dist-info/RECORD,,