pypeline-runner 1.21.1__tar.gz → 1.23.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.21.1 → pypeline_runner-1.23.0}/PKG-INFO +1 -1
  2. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/pyproject.toml +1 -1
  3. pypeline_runner-1.23.0/src/pypeline/__init__.py +1 -0
  4. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/bootstrap/run.py +30 -16
  5. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/steps/create_venv.py +98 -15
  6. pypeline_runner-1.23.0/src/pypeline/steps/env_setup_script.py +117 -0
  7. pypeline_runner-1.21.1/src/pypeline/__init__.py +0 -1
  8. pypeline_runner-1.21.1/src/pypeline/steps/env_setup_script.py +0 -65
  9. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/LICENSE +0 -0
  10. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/README.md +0 -0
  11. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/__run.py +0 -0
  12. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/bootstrap/__init__.py +0 -0
  13. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/domain/__init__.py +0 -0
  14. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/domain/artifacts.py +0 -0
  15. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/domain/config.py +0 -0
  16. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/domain/execution_context.py +0 -0
  17. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/domain/pipeline.py +0 -0
  18. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/domain/project_slurper.py +0 -0
  19. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/inputs_parser.py +0 -0
  20. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/__init__.py +0 -0
  21. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/create.py +0 -0
  22. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/templates/project/.gitignore +0 -0
  23. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/templates/project/pypeline.ps1 +0 -0
  24. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/templates/project/pypeline.yaml +0 -0
  25. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/templates/project/pyproject.toml +0 -0
  26. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/templates/project/steps/my_step.py +0 -0
  27. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/kickstart/templates/project/west.yaml +0 -0
  28. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/main.py +0 -0
  29. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/py.typed +0 -0
  30. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/pypeline.py +0 -0
  31. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/steps/__init__.py +0 -0
  32. {pypeline_runner-1.21.1 → pypeline_runner-1.23.0}/src/pypeline/steps/scoop_install.py +0 -0
  33. {pypeline_runner-1.21.1 → pypeline_runner-1.23.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.21.1
3
+ Version: 1.23.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.21.1"
3
+ version = "1.23.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.23.0"
@@ -692,22 +692,27 @@ class CreateVirtualEnvironment(Runnable):
692
692
  return "create-virtual-environment"
693
693
 
694
694
  def get_inputs(self) -> List[Path]:
695
- venv_relevant_files = [
696
- "uv.lock",
697
- "poetry.lock",
698
- "poetry.toml",
699
- "pyproject.toml",
700
- ".env",
701
- "Pipfile",
702
- "Pipfile.lock",
703
- "bootstrap.json",
704
- ".bootstrap/bootstrap.ps1",
705
- ".bootstrap/bootstrap.py",
706
- "bootstrap.ps1",
707
- "bootstrap.py",
708
- str(self.bootstrap_env.marker_file),
709
- ]
710
- return [self.root_dir / file for file in venv_relevant_files]
695
+ """Get input dependencies based on package manager and actual bootstrap configuration."""
696
+ inputs = []
697
+
698
+ # Add package manager specific lock/config files
699
+ package_manager_files = {
700
+ "uv": ["uv.lock", "pyproject.toml"],
701
+ "poetry": ["poetry.lock", "poetry.toml", "pyproject.toml"],
702
+ "pipenv": ["Pipfile", "Pipfile.lock", ".env"],
703
+ }
704
+
705
+ if self.package_manager_name in package_manager_files:
706
+ for file in package_manager_files[self.package_manager_name]:
707
+ inputs.append(self.root_dir / file)
708
+
709
+ # Add bootstrap script (always tracked since CreateVEnv creates it)
710
+ inputs.append(self.bootstrap_dir / "bootstrap.py")
711
+
712
+ # Add the bootstrap environment marker
713
+ inputs.append(self.bootstrap_env.marker_file)
714
+
715
+ return inputs
711
716
 
712
717
  def get_outputs(self) -> List[Path]:
713
718
  """
@@ -721,6 +726,15 @@ class CreateVirtualEnvironment(Runnable):
721
726
  self.bootstrap_env.virtual_env.scripts_path(),
722
727
  ]
723
728
 
729
+ def get_config(self) -> Optional[dict[str, Any]]:
730
+ """Return configuration that affects the project environment."""
731
+ return {
732
+ "package_manager": self.config.package_manager,
733
+ "package_manager_args": self.config.package_manager_args,
734
+ "venv_install_command": self.config.venv_install_command,
735
+ "python_version": self.config.python_version,
736
+ }
737
+
724
738
 
725
739
  def print_environment_info() -> None:
726
740
  str_bar = "".join(["-" for _ in range(80)])
@@ -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
@@ -16,9 +17,8 @@ from py_app_dev.core.logging import logger
16
17
 
17
18
  from pypeline import __version__
18
19
  from pypeline.bootstrap.run import get_bootstrap_script
19
-
20
- from ..domain.execution_context import ExecutionContext
21
- from ..domain.pipeline import PipelineStep
20
+ from pypeline.domain.execution_context import ExecutionContext
21
+ from pypeline.domain.pipeline import PipelineStep
22
22
 
23
23
 
24
24
  @dataclass
@@ -86,17 +86,73 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
86
86
  ]
87
87
  )
88
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
+
89
144
  def _find_python_executable(self, python_version: str) -> Optional[str]:
90
145
  """
91
146
  Find Python executable based on version string.
92
147
 
93
148
  Supports version formats:
94
- - "3.11.5" or "3.11" -> tries python3.11, python311
95
- - "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
96
151
 
97
- 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.
98
154
 
99
- 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.
100
156
  """
101
157
  # Handle empty string
102
158
  if not python_version:
@@ -134,7 +190,16 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
134
190
  self.logger.debug(f"Found Python executable: {executable_path} (candidate: {candidate})")
135
191
  return candidate
136
192
 
137
- # 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
138
203
  return None
139
204
 
140
205
  @property
@@ -143,15 +208,27 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
143
208
  Get python executable to use.
144
209
 
145
210
  Priority:
146
- 1. User-specified python_executable config
147
- 2. Auto-detect from python_version config
148
- 3. Current Python interpreter (sys.executable)
211
+ 1. Input from execution context (execution_context.get_input("python_version"))
212
+ 2. User-specified python_executable config
213
+ 3. Auto-detect from python_version config
214
+ 4. Current Python interpreter (sys.executable)
149
215
  """
150
- # Priority 1: User explicitly specified executable
216
+ # Priority 1: Check execution context inputs first
217
+ input_python_version = self.execution_context.get_input("python_version")
218
+ if input_python_version:
219
+ found_executable = self._find_python_executable(input_python_version)
220
+ if found_executable:
221
+ return found_executable
222
+ # If version specified via input but not found, fail with helpful error
223
+ raise UserNotificationException(
224
+ f"Could not find Python {input_python_version} in PATH. Please install Python {input_python_version} or specify python_executable explicitly."
225
+ )
226
+
227
+ # Priority 2: User explicitly specified executable
151
228
  if self.user_config.python_executable:
152
229
  return self.user_config.python_executable
153
230
 
154
- # Priority 2: Auto-detect from python_version
231
+ # Priority 3: Auto-detect from python_version config
155
232
  if self.user_config.python_version:
156
233
  found_executable = self._find_python_executable(self.user_config.python_version)
157
234
  if found_executable:
@@ -161,7 +238,7 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
161
238
  f"Could not find Python {self.user_config.python_version} in PATH. Please install Python {self.user_config.python_version} or specify python_executable explicitly."
162
239
  )
163
240
 
164
- # Priority 3: Use current interpreter
241
+ # Priority 4: Use current interpreter
165
242
  return sys.executable
166
243
 
167
244
  @property
@@ -220,8 +297,14 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
220
297
  bootstrap_config = {}
221
298
  if self.user_config.package_manager:
222
299
  bootstrap_config["python_package_manager"] = self.user_config.package_manager
223
- if self.user_config.python_version:
300
+
301
+ # Priority: input python_version takes precedence over config python_version
302
+ input_python_version = self.execution_context.get_input("python_version")
303
+ if input_python_version:
304
+ bootstrap_config["python_version"] = input_python_version
305
+ elif self.user_config.python_version:
224
306
  bootstrap_config["python_version"] = self.user_config.python_version
307
+
225
308
  if self.user_config.package_manager_args:
226
309
  bootstrap_config["python_package_manager_args"] = self.user_config.package_manager_args
227
310
  if self.user_config.bootstrap_packages:
@@ -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.21.1"
@@ -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