pypeline-runner 1.23.1__tar.gz → 1.24.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 (32) hide show
  1. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/PKG-INFO +1 -1
  2. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/pyproject.toml +1 -1
  3. pypeline_runner-1.24.0/src/pypeline/__init__.py +1 -0
  4. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/bootstrap/run.py +26 -17
  5. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/templates/project/pypeline.yaml +1 -1
  6. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/steps/create_venv.py +118 -71
  7. pypeline_runner-1.23.1/src/pypeline/__init__.py +0 -1
  8. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/LICENSE +0 -0
  9. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/README.md +0 -0
  10. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/__run.py +0 -0
  11. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/bootstrap/__init__.py +0 -0
  12. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/domain/__init__.py +0 -0
  13. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/domain/artifacts.py +0 -0
  14. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/domain/config.py +0 -0
  15. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/domain/execution_context.py +0 -0
  16. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/domain/pipeline.py +0 -0
  17. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/domain/project_slurper.py +0 -0
  18. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/inputs_parser.py +0 -0
  19. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/__init__.py +0 -0
  20. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/create.py +0 -0
  21. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/templates/project/.gitignore +0 -0
  22. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/templates/project/pypeline.ps1 +0 -0
  23. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/templates/project/pyproject.toml +0 -0
  24. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/templates/project/steps/my_step.py +0 -0
  25. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/kickstart/templates/project/west.yaml +0 -0
  26. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/main.py +0 -0
  27. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/py.typed +0 -0
  28. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/pypeline.py +0 -0
  29. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/steps/__init__.py +0 -0
  30. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/steps/env_setup_script.py +0 -0
  31. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/steps/scoop_install.py +0 -0
  32. {pypeline_runner-1.23.1 → pypeline_runner-1.24.0}/src/pypeline/steps/west_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypeline-runner
3
- Version: 1.23.1
3
+ Version: 1.24.0
4
4
  Summary: Configure and execute pipelines with Python (similar to GitHub workflows or Jenkins pipelines).
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pypeline-runner"
3
- version = "1.23.1"
3
+ version = "1.24.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.24.0"
@@ -583,6 +583,7 @@ class CreateVirtualEnvironment(Runnable):
583
583
  self,
584
584
  root_dir: Path,
585
585
  bootstrap_env: CreateBootstrapEnvironment,
586
+ skip_venv_delete: bool = False,
586
587
  ) -> None:
587
588
  self.root_dir = root_dir
588
589
  self.venv_dir = self.root_dir / ".venv"
@@ -591,6 +592,7 @@ class CreateVirtualEnvironment(Runnable):
591
592
  self.bootstrap_env = bootstrap_env
592
593
  self.config = bootstrap_env.config
593
594
  self.python_version_marker = self.venv_dir / VENV_PYTHON_VERSION_MARKER
595
+ self.skip_venv_delete = skip_venv_delete
594
596
 
595
597
  @property
596
598
  def package_manager_name(self) -> str:
@@ -602,6 +604,7 @@ class CreateVirtualEnvironment(Runnable):
602
604
 
603
605
  If the Python version has changed (e.g., switching branches), delete the
604
606
  existing venv so it can be recreated by the package manager.
607
+ If skip_venv_delete is True, log a warning instead of deleting.
605
608
  """
606
609
  if not self.venv_dir.exists():
607
610
  return
@@ -609,19 +612,27 @@ class CreateVirtualEnvironment(Runnable):
609
612
  current_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
610
613
 
611
614
  if not self.python_version_marker.exists():
612
- logger.info(
613
- f"No Python version marker found in {self.venv_dir}. "
614
- f"This venv may have been created before version tracking was added. "
615
- f"Deleting {self.venv_dir} to ensure clean state."
616
- )
617
- shutil.rmtree(self.venv_dir)
615
+ if self.skip_venv_delete:
616
+ logger.warning(f"No Python version marker found in {self.venv_dir}. Cannot verify venv compatibility, but skipping deletion as requested.")
617
+ else:
618
+ logger.info(
619
+ f"No Python version marker found in {self.venv_dir}. "
620
+ f"This venv may have been created before version tracking was added. "
621
+ f"Deleting {self.venv_dir} to ensure clean state."
622
+ )
623
+ shutil.rmtree(self.venv_dir)
618
624
  return
619
625
 
620
626
  try:
621
627
  stored_version = self.python_version_marker.read_text().strip()
622
628
  if stored_version != current_version:
623
- logger.info(f"Python version changed from {stored_version} to {current_version}. Deleting {self.venv_dir} for recreation.")
624
- shutil.rmtree(self.venv_dir)
629
+ if self.skip_venv_delete:
630
+ logger.warning(
631
+ f"Python version changed from {stored_version} to {current_version}. Skipping venv deletion as requested - dependencies will be updated in place."
632
+ )
633
+ else:
634
+ logger.info(f"Python version changed from {stored_version} to {current_version}. Deleting {self.venv_dir} for recreation.")
635
+ shutil.rmtree(self.venv_dir)
625
636
  except OSError as exc:
626
637
  logger.warning(f"Could not read Python version marker: {exc}")
627
638
 
@@ -772,11 +783,11 @@ def main() -> int:
772
783
  help="Specify the project directory (default: current working directory).",
773
784
  )
774
785
  parser.add_argument(
775
- "--skip-venv-creation",
786
+ "--skip-venv-delete",
776
787
  action="store_true",
777
788
  required=False,
778
789
  default=False,
779
- help="Skip the virtual environment creation process.",
790
+ help="Skip deleting the virtual environment (used when running from within the venv). Dependencies will still be updated.",
780
791
  )
781
792
  parser.add_argument(
782
793
  "--config",
@@ -804,13 +815,11 @@ def main() -> int:
804
815
  bootstrap_executor.execute(bootstrap_env)
805
816
 
806
817
  # Step 2: Create the project virtual environment using the bootstrap env
807
- # Skip if requested (e.g., when running from within the venv)
808
- if not args.skip_venv_creation:
809
- project_venv = CreateVirtualEnvironment(project_dir, bootstrap_env)
810
- project_executor = Executor(project_venv.venv_dir)
811
- project_executor.execute(project_venv)
812
- else:
813
- logger.info("Skipping virtual environment creation as requested.")
818
+ # When skip_venv_delete is True (e.g., running from within the venv),
819
+ # we still update dependencies but skip deleting the venv to avoid write access exceptions
820
+ project_venv = CreateVirtualEnvironment(project_dir, bootstrap_env, skip_venv_delete=args.skip_venv_delete)
821
+ project_executor = Executor(project_venv.venv_dir)
822
+ project_executor.execute(project_venv)
814
823
 
815
824
  except UserNotificationException as exc:
816
825
  logger.error(exc)
@@ -8,8 +8,8 @@ pipeline:
8
8
  - step: CreateVEnv
9
9
  module: pypeline.steps.create_venv
10
10
  config:
11
- package_manager: uv>=0.9
12
11
  python_executable: python3
12
+ python_package_manager: uv>=0.9
13
13
  - step: WestInstall
14
14
  module: pypeline.steps.west_install
15
15
  description: Download external modules
@@ -5,34 +5,85 @@ import shutil
5
5
  import subprocess
6
6
  import sys
7
7
  import traceback
8
- from dataclasses import dataclass
8
+ from dataclasses import dataclass, fields
9
9
  from enum import Enum, auto
10
10
  from pathlib import Path
11
11
  from typing import Any, ClassVar, Dict, List, Optional
12
12
 
13
- from mashumaro import DataClassDictMixin
13
+ from mashumaro.config import TO_DICT_ADD_OMIT_NONE_FLAG, BaseConfig
14
14
  from mashumaro.mixins.json import DataClassJSONMixin
15
15
  from py_app_dev.core.exceptions import UserNotificationException
16
16
  from py_app_dev.core.logging import logger
17
17
 
18
- from pypeline import __version__
19
18
  from pypeline.bootstrap.run import get_bootstrap_script
20
19
  from pypeline.domain.execution_context import ExecutionContext
21
20
  from pypeline.domain.pipeline import PipelineStep
22
21
 
23
22
 
24
23
  @dataclass
25
- class CreateVEnvConfig(DataClassDictMixin):
24
+ class CreateVEnvConfig(DataClassJSONMixin):
26
25
  bootstrap_script: Optional[str] = None
27
26
  python_executable: Optional[str] = None
28
- # Bootstrap-specific configuration
29
- package_manager: Optional[str] = None
30
27
  python_version: Optional[str] = None
31
- package_manager_args: Optional[List[str]] = None
28
+ # deprecated: kept for backward compatibility
29
+ package_manager: Optional[str] = None
30
+ python_package_manager: Optional[str] = None
31
+ python_package_manager_args: Optional[List[str]] = None
32
32
  bootstrap_packages: Optional[List[str]] = None
33
33
  bootstrap_cache_dir: Optional[str] = None
34
34
  venv_install_command: Optional[str] = None
35
35
 
36
+ class Config(BaseConfig):
37
+ """Base configuration for JSON serialization with omitted None values."""
38
+
39
+ code_generation_options: ClassVar[List[str]] = [TO_DICT_ADD_OMIT_NONE_FLAG]
40
+
41
+ def __post_init__(self) -> None:
42
+ """
43
+ Migrate deprecated package_manager field to python_package_manager.
44
+
45
+ Ensures backward compatibility while preventing conflicting configurations.
46
+ """
47
+ # If both are set, they must match
48
+ if self.package_manager is not None and self.python_package_manager is not None:
49
+ if self.package_manager != self.python_package_manager:
50
+ raise UserNotificationException(
51
+ f"Conflicting package manager configuration: "
52
+ f"package_manager='{self.package_manager}' vs python_package_manager='{self.python_package_manager}'. "
53
+ f"Please use only 'python_package_manager' (package_manager is deprecated)."
54
+ )
55
+
56
+ # Migrate from deprecated package_manager to python_package_manager
57
+ if self.package_manager is not None and self.python_package_manager is None:
58
+ self.python_package_manager = self.package_manager
59
+
60
+ # Clear the deprecated field after migration
61
+ self.package_manager = None
62
+
63
+ @classmethod
64
+ def from_json_file(cls, file_path: Path) -> "CreateVEnvConfig":
65
+ try:
66
+ result = cls.from_dict(json.loads(file_path.read_text()))
67
+ except Exception as e:
68
+ output = io.StringIO()
69
+ traceback.print_exc(file=output)
70
+ raise UserNotificationException(output.getvalue()) from e
71
+ return result
72
+
73
+ def to_json_string(self) -> str:
74
+ return json.dumps(self.to_dict(omit_none=True), indent=2)
75
+
76
+ def to_json_file(self, file_path: Path) -> None:
77
+ file_path.write_text(self.to_json_string())
78
+
79
+ def get_all_properties_names(self, excluded_names: Optional[List[str]] = None) -> List[str]:
80
+ if excluded_names is None:
81
+ excluded_names = []
82
+ return [field.name for field in fields(self) if field.name not in excluded_names]
83
+
84
+ def is_any_property_set(self, excluded_fields: Optional[List[str]] = None) -> bool:
85
+ return any(getattr(self, field) is not None for field in self.get_all_properties_names(excluded_fields))
86
+
36
87
 
37
88
  class BootstrapScriptType(Enum):
38
89
  CUSTOM = auto()
@@ -69,22 +120,12 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
69
120
  super().__init__(execution_context, group_name, config)
70
121
  self.logger = logger.bind()
71
122
  self.internal_bootstrap_script = get_bootstrap_script()
72
- self.package_manager = self.user_config.package_manager if self.user_config.package_manager else self.DEFAULT_PACKAGE_MANAGER
123
+ self.package_manager = self.user_config.python_package_manager if self.user_config.python_package_manager else self.DEFAULT_PACKAGE_MANAGER
73
124
  self.venv_dir = self.project_root_dir / ".venv"
74
125
 
75
- @property
76
126
  def has_bootstrap_config(self) -> bool:
77
127
  """Check if user provided any bootstrap-specific configuration."""
78
- return any(
79
- [
80
- self.user_config.package_manager,
81
- self.user_config.python_version,
82
- self.user_config.package_manager_args,
83
- self.user_config.bootstrap_packages,
84
- self.user_config.bootstrap_cache_dir,
85
- self.user_config.venv_install_command,
86
- ]
87
- )
128
+ return self.user_config.is_any_property_set(["bootstrap_script", "python_executable"])
88
129
 
89
130
  def _verify_python_version(self, executable: str, expected_version: str) -> bool:
90
131
  """
@@ -275,49 +316,57 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
275
316
 
276
317
  def run(self) -> int:
277
318
  self.logger.debug(f"Run {self.get_name()} step. Output dir: {self.output_dir}")
278
-
319
+ bootstrap_config: Dict[str, Any] = {}
320
+ is_managed = False
321
+ # Determine target script and mode
279
322
  if self.user_config.bootstrap_script:
280
- # User provided a custom bootstrap script - run it directly
281
- bootstrap_script = self.project_root_dir / self.user_config.bootstrap_script
282
- if not bootstrap_script.exists():
283
- raise UserNotificationException(f"Bootstrap script {bootstrap_script} does not exist.")
323
+ target_script = self.project_root_dir / self.user_config.bootstrap_script
324
+ # If script exists, it's a "Custom Mode" execution (legacy behavior: run as-is)
325
+ # If it misses, we enter "Managed Mode" to auto-create and run it
326
+ is_managed = not target_script.exists()
327
+ if is_managed:
328
+ self.logger.warning(f"Bootstrap script {target_script} does not exist. Creating it from internal default.")
329
+ # If there is a custom bootstrap config (bootstrap.json) in the project root,
330
+ # we need to provide the internal script
331
+ default_bootstrap_config = self.project_root_dir / "bootstrap.json"
332
+ if default_bootstrap_config.exists():
333
+ self.logger.warning(f"Found bootstrap config {default_bootstrap_config}. Reading it.")
334
+ bootstrap_config = CreateVEnvConfig.from_json_file(default_bootstrap_config)
335
+ else:
336
+ target_script = self.target_internal_bootstrap_script
337
+ is_managed = True
338
+
339
+ if not is_managed:
340
+ # Custom Mode: Run existing user script directly without injection
284
341
  self.execution_context.create_process_executor(
285
- [self.python_executable, bootstrap_script.as_posix()],
342
+ [self.python_executable, target_script.as_posix()],
286
343
  cwd=self.project_root_dir,
287
344
  ).execute()
288
345
  else:
289
- # Use internal bootstrap script
290
- skip_venv_creation = False
346
+ # Managed Mode: Internal logic (Config generation + Args + File creation)
347
+ skip_venv_delete = False
291
348
  python_executable = Path(sys.executable).absolute()
292
349
  if python_executable.is_relative_to(self.project_root_dir):
293
- self.logger.info(f"Detected that the python executable '{python_executable}' is from the virtual environment. Skip updating the virtual environment.")
294
- skip_venv_creation = True
350
+ self.logger.info(f"Detected that the python executable '{python_executable}' is from the virtual environment. Will update dependencies but skip venv deletion.")
351
+ skip_venv_delete = True
295
352
 
296
353
  # Create bootstrap.json with all configuration
297
- bootstrap_config = {}
298
- if self.user_config.package_manager:
299
- bootstrap_config["python_package_manager"] = self.user_config.package_manager
354
+ # Populate config dynamically from CreateVEnvConfig fields
355
+ # excluding internal/local fields like bootstrap_script/python_executable
356
+ for field_name in self.user_config.get_all_properties_names(["bootstrap_script", "python_executable"]):
357
+ val = getattr(self.user_config, field_name)
358
+ if val is not None:
359
+ bootstrap_config[field_name] = val
300
360
 
301
361
  # Priority: input python_version takes precedence over config python_version
302
362
  input_python_version = self.execution_context.get_input("python_version")
303
363
  if input_python_version:
304
364
  bootstrap_config["python_version"] = input_python_version
305
- elif self.user_config.python_version:
306
- bootstrap_config["python_version"] = self.user_config.python_version
307
-
308
- if self.user_config.package_manager_args:
309
- bootstrap_config["python_package_manager_args"] = self.user_config.package_manager_args
310
- if self.user_config.bootstrap_packages:
311
- bootstrap_config["bootstrap_packages"] = self.user_config.bootstrap_packages
312
- if self.user_config.bootstrap_cache_dir:
313
- bootstrap_config["bootstrap_cache_dir"] = self.user_config.bootstrap_cache_dir
314
- if self.user_config.venv_install_command:
315
- bootstrap_config["venv_install_command"] = self.user_config.venv_install_command
316
365
 
317
366
  # Write bootstrap.json if any configuration is provided
318
367
  if bootstrap_config:
319
368
  self.bootstrap_config_file.parent.mkdir(exist_ok=True)
320
- self.bootstrap_config_file.write_text(json.dumps(bootstrap_config, indent=2))
369
+ CreateVEnvConfig.from_dict(bootstrap_config).to_json_file(self.bootstrap_config_file)
321
370
  self.logger.info(f"Created bootstrap configuration at {self.bootstrap_config_file}")
322
371
 
323
372
  # Build bootstrap script arguments
@@ -327,49 +376,47 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
327
376
  ]
328
377
 
329
378
  # Always use --config if bootstrap.json exists
379
+ # Note: We use the internal .bootstrap/bootstrap.json location for consistency
330
380
  if self.bootstrap_config_file.exists():
331
381
  bootstrap_args.extend(["--config", self.bootstrap_config_file.as_posix()])
332
382
 
333
- if skip_venv_creation:
334
- bootstrap_args.append("--skip-venv-creation")
383
+ if skip_venv_delete:
384
+ bootstrap_args.append("--skip-venv-delete")
335
385
 
336
- # Copy the internal bootstrap script to the project root .bootstrap/bootstrap.py
337
- self.target_internal_bootstrap_script.parent.mkdir(exist_ok=True)
338
- if not self.target_internal_bootstrap_script.exists() or self.target_internal_bootstrap_script.read_text() != self.internal_bootstrap_script.read_text():
339
- self.target_internal_bootstrap_script.write_text(self.internal_bootstrap_script.read_text())
340
- self.logger.warning(f"Updated bootstrap script at {self.target_internal_bootstrap_script}")
386
+ # Create/Update the target bootstrap script from internal template
387
+ target_script.parent.mkdir(parents=True, exist_ok=True)
341
388
 
342
- # Run the copied bootstrap script
389
+ # Check if we need to write/update the file
390
+ # If it's a missing custom file, we definitely write.
391
+ # If it's the internal file, we check content hash/diff.
392
+ should_write = False
393
+ if not target_script.exists():
394
+ should_write = True
395
+ elif target_script == self.target_internal_bootstrap_script:
396
+ if target_script.read_text() != self.internal_bootstrap_script.read_text():
397
+ should_write = True
398
+
399
+ if should_write:
400
+ target_script.write_text(self.internal_bootstrap_script.read_text())
401
+ if target_script == self.target_internal_bootstrap_script:
402
+ self.logger.warning(f"Updated bootstrap script at {target_script}")
403
+
404
+ # Run the bootstrap script
343
405
  self.execution_context.create_process_executor(
344
- [self.python_executable, self.target_internal_bootstrap_script.as_posix(), *bootstrap_args],
406
+ [self.python_executable, target_script.as_posix(), *bootstrap_args],
345
407
  cwd=self.project_root_dir,
346
408
  ).execute()
347
409
 
348
410
  return 0
349
411
 
350
412
  def get_inputs(self) -> List[Path]:
351
- package_manager_relevant_file = self.SUPPORTED_PACKAGE_MANAGERS.get(self.package_manager_name, [])
352
- inputs = [self.project_root_dir / file for file in package_manager_relevant_file]
353
- # Include bootstrap.json if it exists
354
- if self.bootstrap_config_file.exists():
355
- inputs.append(self.bootstrap_config_file)
356
- return inputs
413
+ return []
357
414
 
358
415
  def get_outputs(self) -> List[Path]:
359
- outputs = [self.venv_dir]
360
- if self.bootstrap_script_type == BootstrapScriptType.INTERNAL:
361
- outputs.append(self.target_internal_bootstrap_script)
362
- # Include bootstrap.json if it will be created
363
- if self.has_bootstrap_config:
364
- outputs.append(self.bootstrap_config_file)
365
- return outputs
416
+ return []
366
417
 
367
418
  def get_config(self) -> Optional[dict[str, str]]:
368
- return {
369
- "version": __version__,
370
- "python_executable": self.python_executable,
371
- "package_manager": self.package_manager,
372
- }
419
+ return None
373
420
 
374
421
  def update_execution_context(self) -> None:
375
422
  self.execution_context.add_install_dirs(self.install_dirs)
@@ -1 +0,0 @@
1
- __version__ = "1.23.1"