pypeline-runner 1.15.1__py3-none-any.whl → 1.16.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.15.1"
1
+ __version__ = "1.16.0"
pypeline/bootstrap/run.py CHANGED
@@ -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
@@ -1,8 +1,11 @@
1
+ import re
1
2
  from dataclasses import dataclass
3
+ from enum import Enum, auto
2
4
  from pathlib import Path
3
- from typing import Any, Dict, List, Optional
5
+ from typing import Any, ClassVar, Dict, List, Optional
4
6
 
5
7
  from mashumaro import DataClassDictMixin
8
+ from py_app_dev.core.exceptions import UserNotificationException
6
9
  from py_app_dev.core.logging import logger
7
10
 
8
11
  from pypeline.bootstrap.run import get_bootstrap_script
@@ -13,50 +16,95 @@ from ..domain.pipeline import PipelineStep
13
16
 
14
17
  @dataclass
15
18
  class CreateVEnvConfig(DataClassDictMixin):
16
- bootstrap_script: str = "bootstrap.py"
17
- python_executable: str = "python3"
19
+ bootstrap_script: Optional[str] = None
20
+ python_executable: Optional[str] = None
18
21
  package_manager: Optional[str] = None
19
22
 
20
23
 
24
+ class BootstrapScriptType(Enum):
25
+ CUSTOM = auto()
26
+ INTERNAL = auto()
27
+
28
+
21
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
+
22
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
23
41
  super().__init__(execution_context, group_name, config)
24
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"
25
47
 
26
48
  @property
27
49
  def install_dirs(self) -> List[Path]:
28
50
  return [self.project_root_dir / dir for dir in [".venv/Scripts", ".venv/bin"] if (self.project_root_dir / dir).exists()]
29
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
+
30
64
  def get_name(self) -> str:
31
65
  return self.__class__.__name__
32
66
 
33
67
  def run(self) -> int:
34
68
  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)
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()
50
90
  return 0
51
91
 
52
92
  def get_inputs(self) -> List[Path]:
53
- return []
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]
54
95
 
55
96
  def get_outputs(self) -> List[Path]:
56
- return []
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
+ }
57
105
 
58
106
  def update_execution_context(self) -> None:
59
- pass
107
+ self.execution_context.add_install_dirs(self.install_dirs)
60
108
 
61
109
  def get_needs_dependency_management(self) -> bool:
62
- return False
110
+ return False if self.bootstrap_script_type == BootstrapScriptType.CUSTOM else True
@@ -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,7 +1,7 @@
1
- pypeline/__init__.py,sha256=lJTHPTq_lku8cPevlR7mFSxJNTqHgZJVZuDdqSiDNeM,23
1
+ pypeline/__init__.py,sha256=zXjo5icgcGx5X40awUvUOk5_ohh0Pz1N3VFKYCfYEcM,23
2
2
  pypeline/__run.py,sha256=TCdaX05Qm3g8T4QYryKB25Xxf0L5Km7hFOHe1mK9vI0,350
3
3
  pypeline/bootstrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pypeline/bootstrap/run.py,sha256=VqBZI9eioGXpWI87GrGrQZaA6Zd-qtf1pi3efyGZezk,20201
4
+ pypeline/bootstrap/run.py,sha256=iZS-VZGk2KnMRVOb3QuYPrTeINQqVdWkvgjEDpl8oQA,15927
5
5
  pypeline/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  pypeline/domain/artifacts.py,sha256=5k7cVfHhLmvWXNuHKxXb9ca4Lxu0JytGQqazENCeKEU,1404
7
7
  pypeline/domain/config.py,sha256=6vWdHi7B6MA7NGi9wWXQE-YhSg1COSRmc3b1ji6AdAk,2053
@@ -21,12 +21,12 @@ pypeline/main.py,sha256=2mC2BDB1OWIXhaijBXG6Y1vfT8_yMZ4Dj55w5u7g7-w,4158
21
21
  pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  pypeline/pypeline.py,sha256=-mquLfFlEvESk-HORhvjRMESIzdlVAgBLPjwUDOPLqg,7452
23
23
  pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- pypeline/steps/create_venv.py,sha256=oNdgBvfYDI0obqgI-I9-vfJYjS6OsDmWOuYq1py9NGk,2488
24
+ pypeline/steps/create_venv.py,sha256=NeS2Q0a0USwEc-PpcqDxOOCGEDRENlnzAxI0OjTL3N4,4861
25
25
  pypeline/steps/env_setup_script.py,sha256=u08A6pvMccFQbcnU0xruFvpU30PbDrttnbOjl1gDqog,2340
26
26
  pypeline/steps/scoop_install.py,sha256=DDXBD-5TVaT-u6Yf7A85uWoCgBVmLvj9nPGrZ8OQCz0,3853
27
27
  pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
28
- pypeline_runner-1.15.1.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
29
- pypeline_runner-1.15.1.dist-info/METADATA,sha256=7EXTte2u8T8ZQDDOho7VbKAqqvOz7d_zEWsjKOw_b34,7553
30
- pypeline_runner-1.15.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
31
- pypeline_runner-1.15.1.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
32
- pypeline_runner-1.15.1.dist-info/RECORD,,
28
+ pypeline_runner-1.16.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
29
+ pypeline_runner-1.16.0.dist-info/METADATA,sha256=LdGV3SefgqFcV2tki0BBK2pr4wNzIBCOZ7Ws7Dx-Jew,7553
30
+ pypeline_runner-1.16.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
31
+ pypeline_runner-1.16.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
32
+ pypeline_runner-1.16.0.dist-info/RECORD,,