pypeline-runner 1.22.0__tar.gz → 1.23.1__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.22.0 → pypeline_runner-1.23.1}/PKG-INFO +1 -1
  2. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/pyproject.toml +1 -1
  3. pypeline_runner-1.23.1/src/pypeline/__init__.py +1 -0
  4. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/bootstrap/run.py +13 -4
  5. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/steps/create_venv.py +71 -5
  6. pypeline_runner-1.23.1/src/pypeline/steps/env_setup_script.py +117 -0
  7. pypeline_runner-1.22.0/src/pypeline/__init__.py +0 -1
  8. pypeline_runner-1.22.0/src/pypeline/steps/env_setup_script.py +0 -65
  9. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/LICENSE +0 -0
  10. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/README.md +0 -0
  11. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/__run.py +0 -0
  12. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/bootstrap/__init__.py +0 -0
  13. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/domain/__init__.py +0 -0
  14. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/domain/artifacts.py +0 -0
  15. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/domain/config.py +0 -0
  16. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/domain/execution_context.py +0 -0
  17. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/domain/pipeline.py +0 -0
  18. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/domain/project_slurper.py +0 -0
  19. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/inputs_parser.py +0 -0
  20. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/__init__.py +0 -0
  21. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/create.py +0 -0
  22. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/templates/project/.gitignore +0 -0
  23. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/templates/project/pypeline.ps1 +0 -0
  24. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/templates/project/pypeline.yaml +0 -0
  25. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/templates/project/pyproject.toml +0 -0
  26. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/templates/project/steps/my_step.py +0 -0
  27. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/kickstart/templates/project/west.yaml +0 -0
  28. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/main.py +0 -0
  29. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/py.typed +0 -0
  30. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/pypeline.py +0 -0
  31. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/steps/__init__.py +0 -0
  32. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/src/pypeline/steps/scoop_install.py +0 -0
  33. {pypeline_runner-1.22.0 → pypeline_runner-1.23.1}/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.22.0
3
+ Version: 1.23.1
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.22.0"
3
+ version = "1.23.1"
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.23.1"
@@ -632,22 +632,31 @@ class CreateVirtualEnvironment(Runnable):
632
632
  except OSError as exc:
633
633
  logger.warning(f"Could not write Python version marker: {exc}")
634
634
 
635
+ def _set_env_var(self, key: str, value: str) -> None:
636
+ """Helper method to set environment variables (easier to mock in tests)."""
637
+ os.environ[key] = value
638
+
635
639
  def _ensure_in_project_venv(self) -> None:
636
640
  """Configure package managers to create venv in-project (.venv in repository)."""
637
641
  if self.package_manager_name == "poetry":
638
642
  # Set environment variable for poetry to create venv in-project
639
- os.environ["POETRY_VIRTUALENVS_IN_PROJECT"] = "true"
643
+ self._set_env_var("POETRY_VIRTUALENVS_IN_PROJECT", "true")
640
644
  elif self.package_manager_name == "pipenv":
641
645
  # Set environment variable for pipenv
642
- os.environ["PIPENV_VENV_IN_PROJECT"] = "1"
646
+ self._set_env_var("PIPENV_VENV_IN_PROJECT", "1")
643
647
  # UV creates .venv in-project by default, no configuration needed
644
648
 
645
649
  def _ensure_correct_python_version(self) -> None:
646
650
  """Ensure the correct Python version is used in the virtual environment."""
647
651
  if self.package_manager_name == "poetry":
648
652
  # Make Poetry use the Python interpreter it's being run with
649
- os.environ["POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON"] = "false"
650
- os.environ["POETRY_VIRTUALENVS_USE_POETRY_PYTHON"] = "true"
653
+ self._set_env_var("POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON", "false")
654
+ self._set_env_var("POETRY_VIRTUALENVS_USE_POETRY_PYTHON", "true")
655
+ elif self.package_manager_name == "uv":
656
+ # Make UV use the Python interpreter it's being run with
657
+ self._set_env_var("UV_PYTHON", self.config.python_version)
658
+ self._set_env_var("UV_MANAGED_PYTHON", "false")
659
+ self._set_env_var("UV_NO_PYTHON_DOWNLOADS", "true")
651
660
 
652
661
  def _get_install_argument(self) -> str:
653
662
  if self.package_manager_name == "uv":
@@ -2,6 +2,7 @@ import io
2
2
  import json
3
3
  import re
4
4
  import shutil
5
+ import subprocess
5
6
  import sys
6
7
  import traceback
7
8
  from dataclasses import dataclass
@@ -85,17 +86,73 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
85
86
  ]
86
87
  )
87
88
 
89
+ def _verify_python_version(self, executable: str, expected_version: str) -> bool:
90
+ """
91
+ Verify that a Python executable matches the expected version.
92
+
93
+ Args:
94
+ ----
95
+ executable: Name or path of Python executable to check
96
+ expected_version: Expected version string (e.g., "3.11" or "3.11.5")
97
+
98
+ Returns:
99
+ -------
100
+ True if the executable's version matches expected_version (ignoring patch),
101
+ False otherwise or if the executable cannot be queried.
102
+
103
+ """
104
+ try:
105
+ # Run python --version to get the version string
106
+ result = subprocess.run(
107
+ [executable, "--version"], # noqa: S603
108
+ capture_output=True,
109
+ text=True,
110
+ check=False,
111
+ )
112
+
113
+ if result.returncode != 0:
114
+ return False
115
+
116
+ # Parse version from output (e.g., "Python 3.11.5")
117
+ version_output = result.stdout.strip()
118
+ match = re.match(r"Python\s+(\d+)\.(\d+)(?:\.\d+)?", version_output)
119
+ if not match:
120
+ self.logger.warning(f"Could not parse version from: {version_output}")
121
+ return False
122
+
123
+ actual_major = match.group(1)
124
+ actual_minor = match.group(2)
125
+
126
+ # Parse expected version
127
+ expected_parts = expected_version.split(".")
128
+ if len(expected_parts) == 0:
129
+ return False
130
+
131
+ expected_major = expected_parts[0]
132
+ # If only major version specified, only compare major
133
+ if len(expected_parts) == 1:
134
+ return actual_major == expected_major
135
+
136
+ # Compare major.minor
137
+ expected_minor = expected_parts[1]
138
+ return actual_major == expected_major and actual_minor == expected_minor
139
+
140
+ except (FileNotFoundError, OSError) as e:
141
+ self.logger.debug(f"Failed to verify Python version for {executable}: {e}")
142
+ return False
143
+
88
144
  def _find_python_executable(self, python_version: str) -> Optional[str]:
89
145
  """
90
146
  Find Python executable based on version string.
91
147
 
92
148
  Supports version formats:
93
- - "3.11.5" or "3.11" -> tries python3.11, python311
94
- - "3" -> tries python3
149
+ - "3.11.5" or "3.11" -> tries python3.11, python311, then falls back to python
150
+ - "3" -> tries python3, then falls back to python
95
151
 
96
- Always ignores patch version. No fallbacks to generic python.
152
+ Always ignores patch version. Falls back to generic 'python' if version-specific
153
+ executables are not found, but verifies the version matches.
97
154
 
98
- Returns the first executable found in PATH, or None if not found.
155
+ Returns the first executable found in PATH, or None if not found or version mismatch.
99
156
  """
100
157
  # Handle empty string
101
158
  if not python_version:
@@ -133,7 +190,16 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
133
190
  self.logger.debug(f"Found Python executable: {executable_path} (candidate: {candidate})")
134
191
  return candidate
135
192
 
136
- # No fallback - return None if specific version not found
193
+ # Fallback to generic 'python' executable with version verification
194
+ self.logger.debug(f"No version-specific Python executable found for {python_version}, trying generic 'python'")
195
+ if shutil.which("python"):
196
+ if self._verify_python_version("python", python_version):
197
+ self.logger.info(f"Using generic 'python' executable (verified as Python {python_version})")
198
+ return "python"
199
+ else:
200
+ self.logger.warning(f"Generic 'python' executable found but version does not match {python_version}")
201
+
202
+ # No suitable executable found
137
203
  return None
138
204
 
139
205
  @property
@@ -0,0 +1,117 @@
1
+ import platform
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from py_app_dev.core.env_setup_scripts import BatEnvSetupScriptGenerator, EnvSetupScriptGenerator, Ps1EnvSetupScriptGenerator
6
+ from py_app_dev.core.logging import logger
7
+
8
+ from pypeline.domain.execution_context import ExecutionContext
9
+ from pypeline.domain.pipeline import PipelineStep
10
+
11
+
12
+ def read_dot_env_file(dot_env_file: Path) -> Dict[str, str]:
13
+ """Reads a .env file and returns a dictionary of environment variables."""
14
+ env_vars = {}
15
+ with dot_env_file.open("r") as f:
16
+ for line in f:
17
+ line = line.strip()
18
+ if line and not line.startswith("#"):
19
+ key, value = line.split("=", 1)
20
+ env_vars[key.strip()] = value.strip().strip('"').strip("'")
21
+ return env_vars
22
+
23
+
24
+ class ShEnvSetupScriptGenerator(EnvSetupScriptGenerator):
25
+ """Generates a bash/sh script to set environment variables and update PATH."""
26
+
27
+ def generate_content(self) -> str:
28
+ lines = ["#!/bin/bash"]
29
+
30
+ for key, value in self.environment.items():
31
+ # Escape single quotes by replacing ' with '\''
32
+ # This closes the string, adds an escaped quote, then reopens the string
33
+ escaped_value = value.replace("'", "'\\''")
34
+ # Use single quotes for the value to prevent variable expansion
35
+ lines.append(f"export {key}='{escaped_value}'")
36
+
37
+ if self.install_dirs:
38
+ # Convert to POSIX paths to ensure forward slashes on all platforms
39
+ path_string = ":".join([path.as_posix() for path in self.install_dirs])
40
+ # Escape single quotes in paths
41
+ escaped_path_string = path_string.replace("'", "'\\''")
42
+ lines.append(f"export PATH='{escaped_path_string}':\"$PATH\"")
43
+ else:
44
+ self.logger.debug("No install directories provided for PATH update.")
45
+ lines.append("")
46
+
47
+ return "\n".join(lines)
48
+
49
+
50
+ class GenerateEnvSetupScript(PipelineStep[ExecutionContext]):
51
+ def __init__(self, execution_context: ExecutionContext, group_name: Optional[str], config: Optional[Dict[str, Any]] = None) -> None:
52
+ super().__init__(execution_context, group_name, config)
53
+ self._generated_scripts: List[Path] = []
54
+
55
+ def run(self) -> None:
56
+ logger.info(f"Generating environment setup scripts under {self.output_dir} ...")
57
+
58
+ # Read the .env file and set up the environment variables
59
+ dot_env_file = self.execution_context.project_root_dir.joinpath(".env")
60
+ if dot_env_file.exists():
61
+ logger.debug(f"Reading .env file: {dot_env_file}")
62
+ env_vars = read_dot_env_file(dot_env_file)
63
+ else:
64
+ logger.info(f".env file not found: {dot_env_file}.")
65
+ env_vars = {}
66
+
67
+ # Merge execution context environment variables
68
+ env_vars.update(self.execution_context.env_vars)
69
+ # Update the execution context with the merged environment variables to ensure they are available for subsequent steps
70
+ self.execution_context.env_vars.update(env_vars)
71
+
72
+ # Get generate-all option and detect OS
73
+ generate_all = self.execution_context.get_input("generate-all") or False
74
+ is_windows = platform.system() == "Windows"
75
+
76
+ # Generate Windows scripts if on Windows OR generate-all is True
77
+ if is_windows or generate_all:
78
+ bat_script = self.output_dir.joinpath("env_setup.bat")
79
+ BatEnvSetupScriptGenerator(
80
+ install_dirs=self.execution_context.install_dirs,
81
+ environment=env_vars,
82
+ output_file=bat_script,
83
+ ).to_file()
84
+ self._generated_scripts.append(bat_script)
85
+
86
+ ps1_script = self.output_dir.joinpath("env_setup.ps1")
87
+ Ps1EnvSetupScriptGenerator(
88
+ install_dirs=self.execution_context.install_dirs,
89
+ environment=env_vars,
90
+ output_file=ps1_script,
91
+ ).to_file()
92
+ self._generated_scripts.append(ps1_script)
93
+
94
+ # Generate Unix/Linux/macOS script if NOT on Windows OR generate-all is True
95
+ if not is_windows or generate_all:
96
+ sh_script = self.output_dir.joinpath("env_setup.sh")
97
+ ShEnvSetupScriptGenerator(
98
+ install_dirs=self.execution_context.install_dirs,
99
+ environment=env_vars,
100
+ output_file=sh_script,
101
+ ).to_file()
102
+ self._generated_scripts.append(sh_script)
103
+
104
+ def get_inputs(self) -> List[Path]:
105
+ return []
106
+
107
+ def get_outputs(self) -> List[Path]:
108
+ return self._generated_scripts
109
+
110
+ def get_name(self) -> str:
111
+ return self.__class__.__name__
112
+
113
+ def update_execution_context(self) -> None:
114
+ pass
115
+
116
+ def get_needs_dependency_management(self) -> bool:
117
+ return False
@@ -1 +0,0 @@
1
- __version__ = "1.22.0"
@@ -1,65 +0,0 @@
1
- from pathlib import Path
2
- from typing import Dict, List
3
-
4
- from py_app_dev.core.env_setup_scripts import BatEnvSetupScriptGenerator, Ps1EnvSetupScriptGenerator
5
- from py_app_dev.core.logging import logger
6
-
7
- from pypeline.domain.execution_context import ExecutionContext
8
- from pypeline.domain.pipeline import PipelineStep
9
-
10
-
11
- def read_dot_env_file(dot_env_file: Path) -> Dict[str, str]:
12
- """Reads a .env file and returns a dictionary of environment variables."""
13
- env_vars = {}
14
- with dot_env_file.open("r") as f:
15
- for line in f:
16
- line = line.strip()
17
- if line and not line.startswith("#"):
18
- key, value = line.split("=", 1)
19
- env_vars[key.strip()] = value.strip().strip('"').strip("'")
20
- return env_vars
21
-
22
-
23
- class GenerateEnvSetupScript(PipelineStep[ExecutionContext]):
24
- def run(self) -> None:
25
- logger.info(f"Generating environment setup scripts under {self.output_dir} ...")
26
- # Read the .env file and set up the environment variables
27
- dot_env_file = self.execution_context.project_root_dir.joinpath(".env")
28
- if dot_env_file.exists():
29
- logger.debug(f"Reading .env file: {dot_env_file}")
30
- env_vars = read_dot_env_file(dot_env_file)
31
- else:
32
- logger.warning(f".env file not found: {dot_env_file}")
33
- env_vars = {}
34
-
35
- # Merge execution context environment variables
36
- env_vars.update(self.execution_context.env_vars)
37
- # Update the execution context with the merged environment variables to ensure they are available for subsequent steps
38
- self.execution_context.env_vars.update(env_vars)
39
-
40
- # Generate the environment setup scripts
41
- BatEnvSetupScriptGenerator(
42
- install_dirs=self.execution_context.install_dirs,
43
- environment=env_vars,
44
- output_file=self.output_dir.joinpath("env_setup.bat"),
45
- ).to_file()
46
- Ps1EnvSetupScriptGenerator(
47
- install_dirs=self.execution_context.install_dirs,
48
- environment=env_vars,
49
- output_file=self.output_dir.joinpath("env_setup.ps1"),
50
- ).to_file()
51
-
52
- def get_inputs(self) -> List[Path]:
53
- return []
54
-
55
- def get_outputs(self) -> List[Path]:
56
- return []
57
-
58
- def get_name(self) -> str:
59
- return self.__class__.__name__
60
-
61
- def update_execution_context(self) -> None:
62
- pass
63
-
64
- def get_needs_dependency_management(self) -> bool:
65
- return False