pypeline-runner 1.21.1__py3-none-any.whl → 1.23.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 +1 -1
- pypeline/bootstrap/run.py +30 -16
- pypeline/steps/create_venv.py +98 -15
- pypeline/steps/env_setup_script.py +67 -15
- {pypeline_runner-1.21.1.dist-info → pypeline_runner-1.23.0.dist-info}/METADATA +1 -1
- {pypeline_runner-1.21.1.dist-info → pypeline_runner-1.23.0.dist-info}/RECORD +9 -9
- {pypeline_runner-1.21.1.dist-info → pypeline_runner-1.23.0.dist-info}/WHEEL +0 -0
- {pypeline_runner-1.21.1.dist-info → pypeline_runner-1.23.0.dist-info}/entry_points.txt +0 -0
- {pypeline_runner-1.21.1.dist-info → pypeline_runner-1.23.0.dist-info}/licenses/LICENSE +0 -0
pypeline/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.23.0"
|
pypeline/bootstrap/run.py
CHANGED
|
@@ -692,22 +692,27 @@ class CreateVirtualEnvironment(Runnable):
|
|
|
692
692
|
return "create-virtual-environment"
|
|
693
693
|
|
|
694
694
|
def get_inputs(self) -> List[Path]:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
".
|
|
701
|
-
"
|
|
702
|
-
"Pipfile.lock",
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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)])
|
pypeline/steps/create_venv.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
#
|
|
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.
|
|
147
|
-
2.
|
|
148
|
-
3.
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import platform
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from typing import Dict, List
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
3
4
|
|
|
4
|
-
from py_app_dev.core.env_setup_scripts import BatEnvSetupScriptGenerator, Ps1EnvSetupScriptGenerator
|
|
5
|
+
from py_app_dev.core.env_setup_scripts import BatEnvSetupScriptGenerator, EnvSetupScriptGenerator, Ps1EnvSetupScriptGenerator
|
|
5
6
|
from py_app_dev.core.logging import logger
|
|
6
7
|
|
|
7
8
|
from pypeline.domain.execution_context import ExecutionContext
|
|
@@ -20,16 +21,47 @@ def read_dot_env_file(dot_env_file: Path) -> Dict[str, str]:
|
|
|
20
21
|
return env_vars
|
|
21
22
|
|
|
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
|
+
|
|
23
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
|
+
|
|
24
55
|
def run(self) -> None:
|
|
25
56
|
logger.info(f"Generating environment setup scripts under {self.output_dir} ...")
|
|
57
|
+
|
|
26
58
|
# Read the .env file and set up the environment variables
|
|
27
59
|
dot_env_file = self.execution_context.project_root_dir.joinpath(".env")
|
|
28
60
|
if dot_env_file.exists():
|
|
29
61
|
logger.debug(f"Reading .env file: {dot_env_file}")
|
|
30
62
|
env_vars = read_dot_env_file(dot_env_file)
|
|
31
63
|
else:
|
|
32
|
-
logger.
|
|
64
|
+
logger.info(f".env file not found: {dot_env_file}.")
|
|
33
65
|
env_vars = {}
|
|
34
66
|
|
|
35
67
|
# Merge execution context environment variables
|
|
@@ -37,23 +69,43 @@ class GenerateEnvSetupScript(PipelineStep[ExecutionContext]):
|
|
|
37
69
|
# Update the execution context with the merged environment variables to ensure they are available for subsequent steps
|
|
38
70
|
self.execution_context.env_vars.update(env_vars)
|
|
39
71
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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)
|
|
51
103
|
|
|
52
104
|
def get_inputs(self) -> List[Path]:
|
|
53
105
|
return []
|
|
54
106
|
|
|
55
107
|
def get_outputs(self) -> List[Path]:
|
|
56
|
-
return
|
|
108
|
+
return self._generated_scripts
|
|
57
109
|
|
|
58
110
|
def get_name(self) -> str:
|
|
59
111
|
return self.__class__.__name__
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
pypeline/__init__.py,sha256=
|
|
1
|
+
pypeline/__init__.py,sha256=ytSrJKh9KOuUfqP3DPPNXc0__n_XE-EgHA5DOGwts50,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
|
|
4
|
+
pypeline/bootstrap/run.py,sha256=tFyi5LeH8OY9EjqvJww2Ruq6XVv0t3dDpmG5sU-VfyY,32167
|
|
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=k1CkeFGRvQ-zLv6C-AMLC2ed1iyFzDUdvEam3HLHy2E,4210
|
|
|
21
21
|
pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
pypeline/pypeline.py,sha256=mDKUnTuMDw8l-kSDJCHRNbn6zrxAfXhAIAqc5HyHd5M,8758
|
|
23
23
|
pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
pypeline/steps/create_venv.py,sha256=
|
|
25
|
-
pypeline/steps/env_setup_script.py,sha256=
|
|
24
|
+
pypeline/steps/create_venv.py,sha256=MrA1zfwabVIjFLP47wagyR2JVUP3SSrLTQNqQdtWi-Y,16751
|
|
25
|
+
pypeline/steps/env_setup_script.py,sha256=L8TwGo_Ugo2r4Z10MxtE0P8w0ApAxMKCHMnW-NkyG3w,4968
|
|
26
26
|
pypeline/steps/scoop_install.py,sha256=2MhsJ0iPmL8ueQhI52sKjVY9fqzj5xOQweQ65C0onfE,4117
|
|
27
27
|
pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
|
|
28
|
-
pypeline_runner-1.
|
|
29
|
-
pypeline_runner-1.
|
|
30
|
-
pypeline_runner-1.
|
|
31
|
-
pypeline_runner-1.
|
|
32
|
-
pypeline_runner-1.
|
|
28
|
+
pypeline_runner-1.23.0.dist-info/METADATA,sha256=i8_6yzoi_RG4t7Hznpu5x12Rqk5-_qkmqL6wiE49xho,7659
|
|
29
|
+
pypeline_runner-1.23.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
30
|
+
pypeline_runner-1.23.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
|
|
31
|
+
pypeline_runner-1.23.0.dist-info/licenses/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
|
|
32
|
+
pypeline_runner-1.23.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|