pypeline-runner 1.22.0__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "1.22.0"
1
+ __version__ = "1.23.0"
@@ -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
@@ -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.warning(f".env file not found: {dot_env_file}")
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
- # 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()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypeline-runner
3
- Version: 1.22.0
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,4 +1,4 @@
1
- pypeline/__init__.py,sha256=Szrc0NsRYdxS5fUcYkEqrRJ7XLunDfkSaKDKZIhbemc,23
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
4
  pypeline/bootstrap/run.py,sha256=tFyi5LeH8OY9EjqvJww2Ruq6XVv0t3dDpmG5sU-VfyY,32167
@@ -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=O34M7GmsMnJTIw5aee4EwPAuwJD_gH3TI7EG6rkl-MY,13932
25
- pypeline/steps/env_setup_script.py,sha256=DRDCNMUDiW2rzkgEs0FhQfA_-WjPzPLb_e9dGc-mjLg,2526
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.22.0.dist-info/METADATA,sha256=q55q_iUj9db0iwELEZ2rJFd7iBIWPwuWO-dRZkJZdJ4,7659
29
- pypeline_runner-1.22.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
30
- pypeline_runner-1.22.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
31
- pypeline_runner-1.22.0.dist-info/licenses/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
32
- pypeline_runner-1.22.0.dist-info/RECORD,,
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,,