pypeline-runner 1.15.1__tar.gz → 1.16.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-1.15.1 → pypeline_runner-1.16.0}/PKG-INFO +1 -1
  2. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/pyproject.toml +1 -1
  3. pypeline_runner-1.16.0/src/pypeline/__init__.py +1 -0
  4. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/bootstrap/run.py +3 -126
  5. pypeline_runner-1.16.0/src/pypeline/steps/create_venv.py +110 -0
  6. pypeline_runner-1.15.1/src/pypeline/__init__.py +0 -1
  7. pypeline_runner-1.15.1/src/pypeline/steps/create_venv.py +0 -62
  8. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/LICENSE +0 -0
  9. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/README.md +0 -0
  10. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/__run.py +0 -0
  11. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/bootstrap/__init__.py +0 -0
  12. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/domain/__init__.py +0 -0
  13. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/domain/artifacts.py +0 -0
  14. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/domain/config.py +0 -0
  15. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/domain/execution_context.py +0 -0
  16. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/domain/pipeline.py +0 -0
  17. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/domain/project_slurper.py +0 -0
  18. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/inputs_parser.py +0 -0
  19. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/__init__.py +0 -0
  20. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/create.py +0 -0
  21. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/templates/project/.gitignore +0 -0
  22. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/templates/project/pypeline.ps1 +0 -0
  23. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/templates/project/pypeline.yaml +0 -0
  24. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/templates/project/pyproject.toml +0 -0
  25. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/templates/project/steps/my_step.py +0 -0
  26. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/kickstart/templates/project/west.yaml +0 -0
  27. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/main.py +0 -0
  28. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/py.typed +0 -0
  29. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/pypeline.py +0 -0
  30. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/steps/__init__.py +0 -0
  31. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/steps/env_setup_script.py +0 -0
  32. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/steps/scoop_install.py +0 -0
  33. {pypeline_runner-1.15.1 → pypeline_runner-1.16.0}/src/pypeline/steps/west_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypeline-runner
3
- Version: 1.15.1
3
+ Version: 1.16.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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pypeline-runner"
3
- version = "1.15.1"
3
+ version = "1.16.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"
@@ -0,0 +1 @@
1
+ __version__ = "1.16.0"
@@ -1,7 +1,6 @@
1
1
  import argparse
2
2
  import configparser
3
3
  import ensurepip
4
- import hashlib
5
4
  import json
6
5
  import logging
7
6
  import os
@@ -12,7 +11,6 @@ import tempfile
12
11
  import venv
13
12
  from abc import ABC, abstractmethod
14
13
  from dataclasses import dataclass
15
- from enum import Enum
16
14
  from functools import total_ordering
17
15
  from pathlib import Path
18
16
  from typing import List, Optional, Tuple
@@ -153,106 +151,6 @@ class PyPiSourceParser:
153
151
  return sections
154
152
 
155
153
 
156
- class Runnable(ABC):
157
- @abstractmethod
158
- def run(self) -> int:
159
- """Run stage."""
160
-
161
- @abstractmethod
162
- def get_name(self) -> str:
163
- """Get stage name."""
164
-
165
- @abstractmethod
166
- def get_inputs(self) -> List[Path]:
167
- """Get stage dependencies."""
168
-
169
- @abstractmethod
170
- def get_outputs(self) -> List[Path]:
171
- """Get stage outputs."""
172
-
173
-
174
- class RunInfoStatus(Enum):
175
- MATCH = (False, "Nothing has changed, previous execution information matches.")
176
- NO_INFO = (True, "No previous execution information found.")
177
- FILE_CHANGED = (True, "Dependencies have been changed.")
178
-
179
- def __init__(self, should_run: bool, message: str) -> None:
180
- self.should_run = should_run
181
- self.message = message
182
-
183
-
184
- class Executor:
185
- """
186
- Accepts Runnable objects and executes them.
187
-
188
- It create a file with the same name as the runnable's name
189
- and stores the inputs and outputs with their hashes.
190
- If the file exists, it checks the hashes of the inputs and outputs
191
- and if they match, it skips the execution.
192
- """
193
-
194
- RUN_INFO_FILE_EXTENSION = ".deps.json"
195
-
196
- def __init__(self, cache_dir: Path) -> None:
197
- self.cache_dir = cache_dir
198
-
199
- @staticmethod
200
- def get_file_hash(path: Path) -> str:
201
- """
202
- Get the hash of a file.
203
-
204
- Returns an empty string if the file does not exist.
205
- """
206
- if path.is_file():
207
- with open(path, "rb") as file:
208
- bytes = file.read()
209
- readable_hash = hashlib.sha256(bytes).hexdigest()
210
- return readable_hash
211
- else:
212
- return ""
213
-
214
- def store_run_info(self, runnable: Runnable) -> None:
215
- file_info = {
216
- "inputs": {str(path): self.get_file_hash(path) for path in runnable.get_inputs()},
217
- "outputs": {str(path): self.get_file_hash(path) for path in runnable.get_outputs()},
218
- }
219
-
220
- run_info_path = self.get_runnable_run_info_file(runnable)
221
- run_info_path.parent.mkdir(parents=True, exist_ok=True)
222
- with run_info_path.open("w") as f:
223
- # pretty print the json file
224
- json.dump(file_info, f, indent=4)
225
-
226
- def get_runnable_run_info_file(self, runnable: Runnable) -> Path:
227
- return self.cache_dir / f"{runnable.get_name()}{self.RUN_INFO_FILE_EXTENSION}"
228
-
229
- def previous_run_info_matches(self, runnable: Runnable) -> RunInfoStatus:
230
- run_info_path = self.get_runnable_run_info_file(runnable)
231
- if not run_info_path.exists():
232
- return RunInfoStatus.NO_INFO
233
-
234
- with run_info_path.open() as f:
235
- previous_info = json.load(f)
236
-
237
- for file_type in ["inputs", "outputs"]:
238
- for path_str, previous_hash in previous_info[file_type].items():
239
- path = Path(path_str)
240
- if self.get_file_hash(path) != previous_hash:
241
- return RunInfoStatus.FILE_CHANGED
242
- return RunInfoStatus.MATCH
243
-
244
- def execute(self, runnable: Runnable) -> int:
245
- run_info_status = self.previous_run_info_matches(runnable)
246
- if run_info_status.should_run:
247
- logger.info(f"Runnable '{runnable.get_name()}' must run. {run_info_status.message}")
248
- exit_code = runnable.run()
249
- self.store_run_info(runnable)
250
- return exit_code
251
- logger.info(f"Runnable '{runnable.get_name()}' execution skipped. {run_info_status.message}")
252
-
253
- return 0
254
-
255
-
256
154
  class UserNotificationException(Exception):
257
155
  pass
258
156
 
@@ -399,7 +297,7 @@ class UnixVirtualEnvironment(VirtualEnvironment):
399
297
  os.remove(temp_script_path)
400
298
 
401
299
 
402
- class CreateVirtualEnvironment(Runnable):
300
+ class CreateVirtualEnvironment:
403
301
  def __init__(self, root_dir: Path, package_manager: str) -> None:
404
302
  self.root_dir = root_dir
405
303
  self.venv_dir = self.root_dir / ".venv"
@@ -422,9 +320,7 @@ class CreateVirtualEnvironment(Runnable):
422
320
  return "install"
423
321
 
424
322
  def run(self) -> int:
425
- # Create the virtual environment if pip executable does not exist
426
- if not self.virtual_env.pip_path().exists():
427
- self.virtual_env.create()
323
+ self.virtual_env.create()
428
324
 
429
325
  # Get the PyPi source from pyproject.toml or Pipfile if it is defined
430
326
  pypi_source = PyPiSourceParser.from_pyproject(self.root_dir)
@@ -454,24 +350,6 @@ class CreateVirtualEnvironment(Runnable):
454
350
  else:
455
351
  raise UserNotificationException(f"Unsupported operating system: {sys.platform}")
456
352
 
457
- def get_name(self) -> str:
458
- return "create-virtual-environment"
459
-
460
- def get_inputs(self) -> List[Path]:
461
- venv_relevant_files = [
462
- "uv.lock",
463
- "poetry.lock",
464
- "poetry.toml",
465
- "pyproject.toml",
466
- ".env",
467
- "Pipfile",
468
- "Pipfile.lock",
469
- ]
470
- return [self.root_dir / file for file in venv_relevant_files] + [get_bootstrap_script()]
471
-
472
- def get_outputs(self) -> List[Path]:
473
- return []
474
-
475
353
 
476
354
  def print_environment_info() -> None:
477
355
  str_bar = "".join(["-" for _ in range(80)])
@@ -501,8 +379,7 @@ def main() -> int:
501
379
  )
502
380
  args = parser.parse_args()
503
381
 
504
- creator = CreateVirtualEnvironment(args.project_dir, package_manager=args.package_manager)
505
- Executor(creator.venv_dir).execute(creator)
382
+ CreateVirtualEnvironment(args.project_dir, package_manager=args.package_manager).run()
506
383
  except UserNotificationException as e:
507
384
  logger.error(e)
508
385
  return 1
@@ -0,0 +1,110 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from enum import Enum, auto
4
+ from pathlib import Path
5
+ from typing import Any, ClassVar, Dict, List, Optional
6
+
7
+ from mashumaro import DataClassDictMixin
8
+ from py_app_dev.core.exceptions import UserNotificationException
9
+ from py_app_dev.core.logging import logger
10
+
11
+ from pypeline.bootstrap.run import get_bootstrap_script
12
+
13
+ from ..domain.execution_context import ExecutionContext
14
+ from ..domain.pipeline import PipelineStep
15
+
16
+
17
+ @dataclass
18
+ class CreateVEnvConfig(DataClassDictMixin):
19
+ bootstrap_script: Optional[str] = None
20
+ python_executable: Optional[str] = None
21
+ package_manager: Optional[str] = None
22
+
23
+
24
+ class BootstrapScriptType(Enum):
25
+ CUSTOM = auto()
26
+ INTERNAL = auto()
27
+
28
+
29
+ class CreateVEnv(PipelineStep[ExecutionContext]):
30
+ DEFAULT_PACKAGE_MANAGER = "uv>=0.6"
31
+ DEFAULT_PYTHON_EXECUTABLE = "python311"
32
+ SUPPORTED_PACKAGE_MANAGERS: ClassVar[Dict[str, List[str]]] = {
33
+ "uv": ["uv.lock", "pyproject.toml"],
34
+ "pipenv": ["Pipfile", "Pipfile.lock"],
35
+ "poetry": ["pyproject.toml", "poetry.lock"],
36
+ }
37
+
38
+ def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
39
+ self.user_config = CreateVEnvConfig.from_dict(config) if config else CreateVEnvConfig()
40
+ self.bootstrap_script_type = BootstrapScriptType.CUSTOM if self.user_config.bootstrap_script else BootstrapScriptType.INTERNAL
41
+ super().__init__(execution_context, group_name, config)
42
+ self.logger = logger.bind()
43
+ self.bootstrap_script = get_bootstrap_script()
44
+ self.package_manager = self.user_config.package_manager if self.user_config.package_manager else self.DEFAULT_PACKAGE_MANAGER
45
+ self.python_executable = self.user_config.python_executable if self.user_config.python_executable else self.DEFAULT_PYTHON_EXECUTABLE
46
+ self.venv_dir = self.project_root_dir / ".venv"
47
+
48
+ @property
49
+ def install_dirs(self) -> List[Path]:
50
+ return [self.project_root_dir / dir for dir in [".venv/Scripts", ".venv/bin"] if (self.project_root_dir / dir).exists()]
51
+
52
+ @property
53
+ def package_manager_name(self) -> str:
54
+ match = re.match(r"^([a-zA-Z0-9_-]+)", self.package_manager)
55
+ if match:
56
+ result = match.group(1)
57
+ if result in self.SUPPORTED_PACKAGE_MANAGERS:
58
+ return result
59
+ else:
60
+ raise UserNotificationException(f"Package manager {result} is not supported. Supported package managers are: {', '.join(self.SUPPORTED_PACKAGE_MANAGERS)}")
61
+ else:
62
+ raise UserNotificationException(f"Could not extract the package manager name from {self.package_manager}")
63
+
64
+ def get_name(self) -> str:
65
+ return self.__class__.__name__
66
+
67
+ def run(self) -> int:
68
+ self.logger.debug(f"Run {self.get_name()} step. Output dir: {self.output_dir}")
69
+
70
+ if self.user_config.bootstrap_script:
71
+ bootstrap_script = self.project_root_dir / self.user_config.bootstrap_script
72
+ if not bootstrap_script.exists():
73
+ raise UserNotificationException(f"Bootstrap script {bootstrap_script} does not exist.")
74
+ self.execution_context.create_process_executor(
75
+ [self.python_executable, bootstrap_script.as_posix()],
76
+ cwd=self.project_root_dir,
77
+ ).execute()
78
+ else:
79
+ # The internal bootstrap script supports arguments.
80
+ bootstrap_args = [
81
+ "--project-dir",
82
+ self.project_root_dir.as_posix(),
83
+ "--package-manager",
84
+ f'"{self.package_manager}"',
85
+ ]
86
+ self.execution_context.create_process_executor(
87
+ [self.python_executable, self.bootstrap_script.as_posix(), *bootstrap_args],
88
+ cwd=self.project_root_dir,
89
+ ).execute()
90
+ return 0
91
+
92
+ def get_inputs(self) -> List[Path]:
93
+ package_manager_relevant_file = self.SUPPORTED_PACKAGE_MANAGERS.get(self.package_manager_name, [])
94
+ return [self.project_root_dir / file for file in package_manager_relevant_file] + [self.bootstrap_script]
95
+
96
+ def get_outputs(self) -> List[Path]:
97
+ return [self.venv_dir]
98
+
99
+ def get_config(self) -> Optional[dict[str, str]]:
100
+ return {
101
+ "bootstrap_script": self.bootstrap_script.as_posix(),
102
+ "python_executable": self.python_executable,
103
+ "package_manager": self.package_manager,
104
+ }
105
+
106
+ def update_execution_context(self) -> None:
107
+ self.execution_context.add_install_dirs(self.install_dirs)
108
+
109
+ def get_needs_dependency_management(self) -> bool:
110
+ return False if self.bootstrap_script_type == BootstrapScriptType.CUSTOM else True
@@ -1 +0,0 @@
1
- __version__ = "1.15.1"
@@ -1,62 +0,0 @@
1
- from dataclasses import dataclass
2
- from pathlib import Path
3
- from typing import Any, Dict, List, Optional
4
-
5
- from mashumaro import DataClassDictMixin
6
- from py_app_dev.core.logging import logger
7
-
8
- from pypeline.bootstrap.run import get_bootstrap_script
9
-
10
- from ..domain.execution_context import ExecutionContext
11
- from ..domain.pipeline import PipelineStep
12
-
13
-
14
- @dataclass
15
- class CreateVEnvConfig(DataClassDictMixin):
16
- bootstrap_script: str = "bootstrap.py"
17
- python_executable: str = "python3"
18
- package_manager: Optional[str] = None
19
-
20
-
21
- class CreateVEnv(PipelineStep[ExecutionContext]):
22
- def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[Dict[str, Any]] = None) -> None:
23
- super().__init__(execution_context, group_name, config)
24
- self.logger = logger.bind()
25
-
26
- @property
27
- def install_dirs(self) -> List[Path]:
28
- return [self.project_root_dir / dir for dir in [".venv/Scripts", ".venv/bin"] if (self.project_root_dir / dir).exists()]
29
-
30
- def get_name(self) -> str:
31
- return self.__class__.__name__
32
-
33
- def run(self) -> int:
34
- self.logger.debug(f"Run {self.get_name()} step. Output dir: {self.output_dir}")
35
- config = CreateVEnvConfig.from_dict(self.config) if self.config else CreateVEnvConfig()
36
- bootstrap_script = self.project_root_dir / config.bootstrap_script
37
- bootstrap_args = []
38
- if not bootstrap_script.exists():
39
- self.logger.warning(f"Bootstrap script {bootstrap_script} does not exist. Use pypeline internal `bootstrap.py`.")
40
- bootstrap_script = get_bootstrap_script()
41
- # Only the internal bootstrap.py script supports arguments.
42
- bootstrap_args = ["--project-dir", self.project_root_dir.as_posix()]
43
- if config.package_manager:
44
- bootstrap_args.extend(["--package-manager", f'"{config.package_manager}"'])
45
- self.execution_context.create_process_executor(
46
- [config.python_executable, bootstrap_script.as_posix(), *bootstrap_args],
47
- cwd=self.project_root_dir,
48
- ).execute()
49
- self.execution_context.add_install_dirs(self.install_dirs)
50
- return 0
51
-
52
- def get_inputs(self) -> List[Path]:
53
- return []
54
-
55
- def get_outputs(self) -> List[Path]:
56
- return []
57
-
58
- def update_execution_context(self) -> None:
59
- pass
60
-
61
- def get_needs_dependency_management(self) -> bool:
62
- return False