pypeline-runner 1.7.0__py3-none-any.whl → 1.8.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/kickstart/create.py +55 -55
- pypeline/kickstart/templates/project/bootstrap.py +461 -0
- pypeline/kickstart/templates/project/poetry.toml +2 -2
- pypeline/kickstart/templates/project/pypeline.ps1 +7 -7
- pypeline/kickstart/templates/project/pypeline.yaml +3 -2
- pypeline/kickstart/templates/project/pyproject.toml +11 -9
- pypeline/kickstart/templates/project/steps/my_step.py +26 -26
- pypeline/kickstart/templates/project/west.yaml +10 -0
- pypeline/steps/create_venv.py +10 -3
- {pypeline_runner-1.7.0.dist-info → pypeline_runner-1.8.0.dist-info}/METADATA +61 -33
- {pypeline_runner-1.7.0.dist-info → pypeline_runner-1.8.0.dist-info}/RECORD +15 -13
- {pypeline_runner-1.7.0.dist-info → pypeline_runner-1.8.0.dist-info}/LICENSE +0 -0
- {pypeline_runner-1.7.0.dist-info → pypeline_runner-1.8.0.dist-info}/WHEEL +0 -0
- {pypeline_runner-1.7.0.dist-info → pypeline_runner-1.8.0.dist-info}/entry_points.txt +0 -0
pypeline/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.8.0"
|
pypeline/kickstart/create.py
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
import shutil
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List, Optional, Union
|
|
4
|
-
|
|
5
|
-
from py_app_dev.core.exceptions import UserNotificationException
|
|
6
|
-
from py_app_dev.core.logging import logger
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class ProjectBuilder:
|
|
10
|
-
def __init__(self, project_dir: Path, input_dir: Optional[Path] = None) -> None:
|
|
11
|
-
self.project_dir = project_dir
|
|
12
|
-
self.input_dir = input_dir if input_dir else Path(__file__).parent.joinpath("templates")
|
|
13
|
-
|
|
14
|
-
self.dirs: List[Path] = []
|
|
15
|
-
self.check_target_directory_flag = True
|
|
16
|
-
|
|
17
|
-
def with_disable_target_directory_check(self) -> "ProjectBuilder":
|
|
18
|
-
self.check_target_directory_flag = False
|
|
19
|
-
return self
|
|
20
|
-
|
|
21
|
-
def with_dir(self, dir: Union[Path, str]) -> "ProjectBuilder":
|
|
22
|
-
self.dirs.append(self.resolve_file_path(dir))
|
|
23
|
-
return self
|
|
24
|
-
|
|
25
|
-
def resolve_file_paths(self, files: List[Path | str]) -> List[Path]:
|
|
26
|
-
return [self.resolve_file_path(file) for file in files]
|
|
27
|
-
|
|
28
|
-
def resolve_file_path(self, file: Union[Path, str]) -> Path:
|
|
29
|
-
return self.input_dir.joinpath(file) if isinstance(file, str) else file
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def _check_target_directory(project_dir: Path) -> None:
|
|
33
|
-
if project_dir.is_dir() and any(project_dir.iterdir()):
|
|
34
|
-
raise UserNotificationException(f"Project directory '{project_dir}' is not empty. Use --force to override.")
|
|
35
|
-
|
|
36
|
-
def build(self) -> None:
|
|
37
|
-
if self.check_target_directory_flag:
|
|
38
|
-
self._check_target_directory(self.project_dir)
|
|
39
|
-
for dir in self.dirs:
|
|
40
|
-
shutil.copytree(dir, self.project_dir, dirs_exist_ok=True)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class KickstartProject:
|
|
44
|
-
def __init__(self, project_dir: Path, force: bool = False) -> None:
|
|
45
|
-
self.logger = logger.bind()
|
|
46
|
-
self.project_dir = project_dir
|
|
47
|
-
self.force = force
|
|
48
|
-
|
|
49
|
-
def run(self) -> None:
|
|
50
|
-
self.logger.info(f"Kickstart new project in '{self.project_dir.absolute().as_posix()}'")
|
|
51
|
-
project_builder = ProjectBuilder(self.project_dir)
|
|
52
|
-
if self.force:
|
|
53
|
-
project_builder.with_disable_target_directory_check()
|
|
54
|
-
project_builder.with_dir("project")
|
|
55
|
-
project_builder.build()
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
|
6
|
+
from py_app_dev.core.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectBuilder:
|
|
10
|
+
def __init__(self, project_dir: Path, input_dir: Optional[Path] = None) -> None:
|
|
11
|
+
self.project_dir = project_dir
|
|
12
|
+
self.input_dir = input_dir if input_dir else Path(__file__).parent.joinpath("templates")
|
|
13
|
+
|
|
14
|
+
self.dirs: List[Path] = []
|
|
15
|
+
self.check_target_directory_flag = True
|
|
16
|
+
|
|
17
|
+
def with_disable_target_directory_check(self) -> "ProjectBuilder":
|
|
18
|
+
self.check_target_directory_flag = False
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def with_dir(self, dir: Union[Path, str]) -> "ProjectBuilder":
|
|
22
|
+
self.dirs.append(self.resolve_file_path(dir))
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def resolve_file_paths(self, files: List[Path | str]) -> List[Path]:
|
|
26
|
+
return [self.resolve_file_path(file) for file in files]
|
|
27
|
+
|
|
28
|
+
def resolve_file_path(self, file: Union[Path, str]) -> Path:
|
|
29
|
+
return self.input_dir.joinpath(file) if isinstance(file, str) else file
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _check_target_directory(project_dir: Path) -> None:
|
|
33
|
+
if project_dir.is_dir() and any(project_dir.iterdir()):
|
|
34
|
+
raise UserNotificationException(f"Project directory '{project_dir}' is not empty. Use --force to override.")
|
|
35
|
+
|
|
36
|
+
def build(self) -> None:
|
|
37
|
+
if self.check_target_directory_flag:
|
|
38
|
+
self._check_target_directory(self.project_dir)
|
|
39
|
+
for dir in self.dirs:
|
|
40
|
+
shutil.copytree(dir, self.project_dir, dirs_exist_ok=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class KickstartProject:
|
|
44
|
+
def __init__(self, project_dir: Path, force: bool = False) -> None:
|
|
45
|
+
self.logger = logger.bind()
|
|
46
|
+
self.project_dir = project_dir
|
|
47
|
+
self.force = force
|
|
48
|
+
|
|
49
|
+
def run(self) -> None:
|
|
50
|
+
self.logger.info(f"Kickstart new project in '{self.project_dir.absolute().as_posix()}'")
|
|
51
|
+
project_builder = ProjectBuilder(self.project_dir)
|
|
52
|
+
if self.force:
|
|
53
|
+
project_builder.with_disable_target_directory_check()
|
|
54
|
+
project_builder.with_dir("project")
|
|
55
|
+
project_builder.build()
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# Source: https://github.com/avengineers/bootstrap
|
|
3
|
+
# Tag: v1.15.1
|
|
4
|
+
import configparser
|
|
5
|
+
import ensurepip
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess # nosec
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import venv
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from functools import total_ordering
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional, Tuple
|
|
21
|
+
from urllib.parse import urlparse
|
|
22
|
+
|
|
23
|
+
logging.basicConfig(level=logging.INFO)
|
|
24
|
+
logger = logging.getLogger("bootstrap")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
bootstrap_json_path = Path.cwd() / "bootstrap.json"
|
|
28
|
+
if bootstrap_json_path.exists():
|
|
29
|
+
with bootstrap_json_path.open("r") as f:
|
|
30
|
+
config = json.load(f)
|
|
31
|
+
package_manager = config.get("python_package_manager", "poetry>=2.0")
|
|
32
|
+
package_manager_args = config.get("python_package_manager_args", [])
|
|
33
|
+
else:
|
|
34
|
+
package_manager = "poetry>=2.0"
|
|
35
|
+
package_manager_args = []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@total_ordering
|
|
39
|
+
class Version:
|
|
40
|
+
def __init__(self, version_str: str) -> None:
|
|
41
|
+
self.version = self.parse_version(version_str)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def parse_version(version_str: str) -> Tuple[int, ...]:
|
|
45
|
+
"""Convert a version string into a tuple of integers for comparison."""
|
|
46
|
+
return tuple(map(int, re.split(r"\D+", version_str)))
|
|
47
|
+
|
|
48
|
+
def __eq__(self, other: object) -> bool:
|
|
49
|
+
return isinstance(other, Version) and self.version == other.version
|
|
50
|
+
|
|
51
|
+
def __lt__(self, other: object) -> bool:
|
|
52
|
+
return isinstance(other, Version) and self.version < other.version
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return f"Version({'.'.join(map(str, self.version))})"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PyPiSource:
|
|
60
|
+
name: str
|
|
61
|
+
url: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class TomlSection:
|
|
66
|
+
name: str
|
|
67
|
+
content: str
|
|
68
|
+
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
return f"[{self.name}]\n{self.content}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PyPiSourceParser:
|
|
74
|
+
@staticmethod
|
|
75
|
+
def from_pyproject(project_dir: Path) -> Optional[PyPiSource]:
|
|
76
|
+
pyproject_toml = project_dir / "pyproject.toml"
|
|
77
|
+
pipfile = project_dir / "Pipfile"
|
|
78
|
+
if pyproject_toml.exists():
|
|
79
|
+
return PyPiSourceParser.from_toml_content(pyproject_toml.read_text(), "tool.poetry.source")
|
|
80
|
+
elif pipfile.exists():
|
|
81
|
+
return PyPiSourceParser.from_toml_content(pipfile.read_text(), "source")
|
|
82
|
+
else:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def from_toml_content(content: str, source_section_name: str) -> Optional[PyPiSource]:
|
|
87
|
+
sections = PyPiSourceParser.get_toml_sections(content)
|
|
88
|
+
for section in sections:
|
|
89
|
+
if section.name == source_section_name:
|
|
90
|
+
try:
|
|
91
|
+
parser = configparser.ConfigParser()
|
|
92
|
+
parser.read_string(str(section))
|
|
93
|
+
name = parser[section.name]["name"].strip('"')
|
|
94
|
+
url = parser[section.name]["url"].strip('"')
|
|
95
|
+
return PyPiSource(name, url)
|
|
96
|
+
except KeyError:
|
|
97
|
+
raise UserNotificationException(
|
|
98
|
+
f"Could not parse PyPi source from section {section.name}. "
|
|
99
|
+
f"Please make sure the section has the following format:\n"
|
|
100
|
+
f"[{source_section_name}]\n"
|
|
101
|
+
f'name = "name"\n'
|
|
102
|
+
f'url = "https://url"\n'
|
|
103
|
+
f"verify_ssl = true"
|
|
104
|
+
) from None
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def get_toml_sections(toml_content: str) -> List[TomlSection]:
|
|
109
|
+
# Use a regular expression to find all sections with [ or [[ at the beginning of the line
|
|
110
|
+
raw_sections = re.findall(r"^\[+.*\]+\n(?:[^[]*\n)*", toml_content, re.MULTILINE)
|
|
111
|
+
|
|
112
|
+
# Process each section
|
|
113
|
+
sections = []
|
|
114
|
+
for section in raw_sections:
|
|
115
|
+
# Split the lines, from the first line extract the section name
|
|
116
|
+
# and merge all the other lines into the content
|
|
117
|
+
lines = section.splitlines()
|
|
118
|
+
name_match = re.match(r"^\[+([^]]*)\]+", lines[0])
|
|
119
|
+
if name_match:
|
|
120
|
+
name = name_match.group(1).strip()
|
|
121
|
+
content = "\n".join(lines[1:]).strip()
|
|
122
|
+
sections.append(TomlSection(name, content))
|
|
123
|
+
|
|
124
|
+
return sections
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Runnable(ABC):
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def run(self) -> int:
|
|
130
|
+
"""Run stage"""
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def get_name(self) -> str:
|
|
134
|
+
"""Get stage name"""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def get_inputs(self) -> List[Path]:
|
|
138
|
+
"""Get stage dependencies"""
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def get_outputs(self) -> List[Path]:
|
|
142
|
+
"""Get stage outputs"""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RunInfoStatus(Enum):
|
|
146
|
+
MATCH = (False, "Nothing has changed, previous execution information matches.")
|
|
147
|
+
NO_INFO = (True, "No previous execution information found.")
|
|
148
|
+
FILE_CHANGED = (True, "Dependencies have been changed.")
|
|
149
|
+
|
|
150
|
+
def __init__(self, should_run: bool, message: str) -> None:
|
|
151
|
+
self.should_run = should_run
|
|
152
|
+
self.message = message
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Executor:
|
|
156
|
+
"""Accepts Runnable objects and executes them.
|
|
157
|
+
It create a file with the same name as the runnable's name
|
|
158
|
+
and stores the inputs and outputs with their hashes.
|
|
159
|
+
If the file exists, it checks the hashes of the inputs and outputs
|
|
160
|
+
and if they match, it skips the execution."""
|
|
161
|
+
|
|
162
|
+
RUN_INFO_FILE_EXTENSION = ".deps.json"
|
|
163
|
+
|
|
164
|
+
def __init__(self, cache_dir: Path) -> None:
|
|
165
|
+
self.cache_dir = cache_dir
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def get_file_hash(path: Path) -> str:
|
|
169
|
+
"""Get the hash of a file.
|
|
170
|
+
Returns an empty string if the file does not exist."""
|
|
171
|
+
if path.is_file():
|
|
172
|
+
with open(path, "rb") as file:
|
|
173
|
+
bytes = file.read()
|
|
174
|
+
readable_hash = hashlib.sha256(bytes).hexdigest()
|
|
175
|
+
return readable_hash
|
|
176
|
+
else:
|
|
177
|
+
return ""
|
|
178
|
+
|
|
179
|
+
def store_run_info(self, runnable: Runnable) -> None:
|
|
180
|
+
file_info = {
|
|
181
|
+
"inputs": {str(path): self.get_file_hash(path) for path in runnable.get_inputs()},
|
|
182
|
+
"outputs": {str(path): self.get_file_hash(path) for path in runnable.get_outputs()},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
run_info_path = self.get_runnable_run_info_file(runnable)
|
|
186
|
+
run_info_path.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
with run_info_path.open("w") as f:
|
|
188
|
+
# pretty print the json file
|
|
189
|
+
json.dump(file_info, f, indent=4)
|
|
190
|
+
|
|
191
|
+
def get_runnable_run_info_file(self, runnable: Runnable) -> Path:
|
|
192
|
+
return self.cache_dir / f"{runnable.get_name()}{self.RUN_INFO_FILE_EXTENSION}"
|
|
193
|
+
|
|
194
|
+
def previous_run_info_matches(self, runnable: Runnable) -> RunInfoStatus:
|
|
195
|
+
run_info_path = self.get_runnable_run_info_file(runnable)
|
|
196
|
+
if not run_info_path.exists():
|
|
197
|
+
return RunInfoStatus.NO_INFO
|
|
198
|
+
|
|
199
|
+
with run_info_path.open() as f:
|
|
200
|
+
previous_info = json.load(f)
|
|
201
|
+
|
|
202
|
+
for file_type in ["inputs", "outputs"]:
|
|
203
|
+
for path_str, previous_hash in previous_info[file_type].items():
|
|
204
|
+
path = Path(path_str)
|
|
205
|
+
if self.get_file_hash(path) != previous_hash:
|
|
206
|
+
return RunInfoStatus.FILE_CHANGED
|
|
207
|
+
return RunInfoStatus.MATCH
|
|
208
|
+
|
|
209
|
+
def execute(self, runnable: Runnable) -> int:
|
|
210
|
+
run_info_status = self.previous_run_info_matches(runnable)
|
|
211
|
+
if run_info_status.should_run:
|
|
212
|
+
logger.info(f"Runnable '{runnable.get_name()}' must run. {run_info_status.message}")
|
|
213
|
+
exit_code = runnable.run()
|
|
214
|
+
self.store_run_info(runnable)
|
|
215
|
+
return exit_code
|
|
216
|
+
logger.info(f"Runnable '{runnable.get_name()}' execution skipped. {run_info_status.message}")
|
|
217
|
+
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class UserNotificationException(Exception):
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class SubprocessExecutor:
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
command: List[str | Path],
|
|
229
|
+
cwd: Optional[Path] = None,
|
|
230
|
+
capture_output: bool = True,
|
|
231
|
+
):
|
|
232
|
+
self.command = " ".join([str(cmd) for cmd in command])
|
|
233
|
+
self.current_working_directory = cwd
|
|
234
|
+
self.capture_output = capture_output
|
|
235
|
+
|
|
236
|
+
def execute(self) -> None:
|
|
237
|
+
result = None
|
|
238
|
+
try:
|
|
239
|
+
current_dir = (self.current_working_directory or Path.cwd()).as_posix()
|
|
240
|
+
logger.info(f"Running command: {self.command} in {current_dir}")
|
|
241
|
+
# print all virtual environment variables
|
|
242
|
+
logger.debug(json.dumps(dict(os.environ), indent=4))
|
|
243
|
+
result = subprocess.run(
|
|
244
|
+
self.command.split(),
|
|
245
|
+
cwd=current_dir,
|
|
246
|
+
capture_output=self.capture_output,
|
|
247
|
+
text=True, # to get stdout and stderr as strings instead of bytes
|
|
248
|
+
) # nosec
|
|
249
|
+
result.check_returncode()
|
|
250
|
+
except subprocess.CalledProcessError as e:
|
|
251
|
+
raise UserNotificationException(f"Command '{self.command}' failed with:\n" f"{result.stdout if result else ''}\n" f"{result.stderr if result else e}") from e
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class VirtualEnvironment(ABC):
|
|
255
|
+
def __init__(self, venv_dir: Path) -> None:
|
|
256
|
+
self.venv_dir = venv_dir
|
|
257
|
+
|
|
258
|
+
def create(self) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Create a new virtual environment. This should configure the virtual environment such that
|
|
261
|
+
subsequent calls to `pip` and `run` operate within this environment.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
venv.create(env_dir=self.venv_dir, with_pip=True)
|
|
265
|
+
except PermissionError as e:
|
|
266
|
+
if "python.exe" in str(e):
|
|
267
|
+
raise UserNotificationException(
|
|
268
|
+
f"Failed to create virtual environment in {self.venv_dir}.\n" f"Virtual environment python.exe is still running. Please kill all instances and run again.\n" f"Error: {e}"
|
|
269
|
+
) from e
|
|
270
|
+
raise UserNotificationException(f"Failed to create virtual environment in {self.venv_dir}.\n" f"Please make sure you have the necessary permissions.\n" f"Error: {e}") from e
|
|
271
|
+
|
|
272
|
+
def pip_configure(self, index_url: str, verify_ssl: bool = True) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Configure pip to use the given index URL and SSL verification setting. This method should
|
|
275
|
+
behave as if the user had activated the virtual environment and run `pip config set
|
|
276
|
+
global.index-url <index_url>` and `pip config set global.cert <verify_ssl>` from the
|
|
277
|
+
command line.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
----
|
|
281
|
+
index_url: The index URL to use for pip.
|
|
282
|
+
verify_ssl: Whether to verify SSL certificates when using pip.
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
# The pip configuration file should be in the virtual environment directory %VIRTUAL_ENV%
|
|
286
|
+
pip_ini_path = self.pip_config_path()
|
|
287
|
+
with open(pip_ini_path, "w") as pip_ini_file:
|
|
288
|
+
pip_ini_file.write(f"[global]\nindex-url = {index_url}\n")
|
|
289
|
+
if not verify_ssl:
|
|
290
|
+
pip_ini_file.write("cert = false\n")
|
|
291
|
+
|
|
292
|
+
def pip(self, args: List[str]) -> None:
|
|
293
|
+
SubprocessExecutor([self.pip_path().as_posix(), *args]).execute()
|
|
294
|
+
|
|
295
|
+
@abstractmethod
|
|
296
|
+
def pip_path(self) -> Path:
|
|
297
|
+
"""
|
|
298
|
+
Get the path to the pip executable within the virtual environment.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
@abstractmethod
|
|
302
|
+
def pip_config_path(self) -> Path:
|
|
303
|
+
"""
|
|
304
|
+
Get the path to the pip configuration file within the virtual environment.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
@abstractmethod
|
|
308
|
+
def run(self, args: List[str], capture_output: bool = True) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Run an arbitrary command within the virtual environment. This method should behave as if the
|
|
311
|
+
user had activated the virtual environment and run the given command from the command line.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
----
|
|
315
|
+
*args: Command-line arguments. For example, `run('python', 'setup.py', 'install')`
|
|
316
|
+
should behave similarly to `python setup.py install` at the command line.
|
|
317
|
+
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class WindowsVirtualEnvironment(VirtualEnvironment):
|
|
322
|
+
def __init__(self, venv_dir: Path) -> None:
|
|
323
|
+
super().__init__(venv_dir)
|
|
324
|
+
self.activate_script = self.venv_dir.joinpath("Scripts/activate")
|
|
325
|
+
|
|
326
|
+
def pip_path(self) -> Path:
|
|
327
|
+
return self.venv_dir.joinpath("Scripts/pip.exe")
|
|
328
|
+
|
|
329
|
+
def pip_config_path(self) -> Path:
|
|
330
|
+
return self.venv_dir.joinpath("pip.ini")
|
|
331
|
+
|
|
332
|
+
def run(self, args: List[str], capture_output: bool = True) -> None:
|
|
333
|
+
SubprocessExecutor(
|
|
334
|
+
command=[f"cmd /c {self.activate_script.as_posix()} && ", *args],
|
|
335
|
+
capture_output=capture_output,
|
|
336
|
+
).execute()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class UnixVirtualEnvironment(VirtualEnvironment):
|
|
340
|
+
def __init__(self, venv_dir: Path) -> None:
|
|
341
|
+
super().__init__(venv_dir)
|
|
342
|
+
self.activate_script = self.venv_dir.joinpath("bin/activate")
|
|
343
|
+
|
|
344
|
+
def pip_path(self) -> Path:
|
|
345
|
+
return self.venv_dir.joinpath("bin/pip")
|
|
346
|
+
|
|
347
|
+
def pip_config_path(self) -> Path:
|
|
348
|
+
return self.venv_dir.joinpath("pip.conf")
|
|
349
|
+
|
|
350
|
+
def run(self, args: List[str], capture_output: bool = True) -> None:
|
|
351
|
+
# Create a temporary shell script
|
|
352
|
+
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".sh") as f:
|
|
353
|
+
f.write("#!/bin/bash\n") # Add a shebang line
|
|
354
|
+
f.write(f"source {self.activate_script.as_posix()}\n") # Write the activate command
|
|
355
|
+
f.write(" ".join(args)) # Write the provided command
|
|
356
|
+
temp_script_path = f.name # Get the path of the temporary script
|
|
357
|
+
|
|
358
|
+
# Make the temporary script executable
|
|
359
|
+
SubprocessExecutor(["chmod", "+x", temp_script_path]).execute()
|
|
360
|
+
# Run the temporary script
|
|
361
|
+
SubprocessExecutor(
|
|
362
|
+
command=[f"{Path(temp_script_path).as_posix()}"],
|
|
363
|
+
capture_output=capture_output,
|
|
364
|
+
).execute()
|
|
365
|
+
# Delete the temporary script
|
|
366
|
+
os.remove(temp_script_path)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class CreateVirtualEnvironment(Runnable):
|
|
370
|
+
def __init__(self, root_dir: Path) -> None:
|
|
371
|
+
self.root_dir = root_dir
|
|
372
|
+
self.venv_dir = self.root_dir / ".venv"
|
|
373
|
+
self.bootstrap_dir = self.root_dir / ".bootstrap"
|
|
374
|
+
self.virtual_env = self.instantiate_os_specific_venv(self.venv_dir)
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def package_manager_name(self) -> str:
|
|
378
|
+
match = re.match(r"^([a-zA-Z0-9_-]+)", package_manager)
|
|
379
|
+
|
|
380
|
+
if match:
|
|
381
|
+
return match.group(1)
|
|
382
|
+
else:
|
|
383
|
+
raise UserNotificationException(f"Could not extract the package manager name from {package_manager}")
|
|
384
|
+
|
|
385
|
+
def run(self) -> int:
|
|
386
|
+
# Create the virtual environment if pip executable does not exist
|
|
387
|
+
if not self.virtual_env.pip_path().exists():
|
|
388
|
+
self.virtual_env.create()
|
|
389
|
+
|
|
390
|
+
# Get the PyPi source from pyproject.toml or Pipfile if it is defined
|
|
391
|
+
pypi_source = PyPiSourceParser.from_pyproject(self.root_dir)
|
|
392
|
+
if pypi_source:
|
|
393
|
+
self.virtual_env.pip_configure(index_url=pypi_source.url, verify_ssl=True)
|
|
394
|
+
# We need pip-system-certs in venv to use certificates, that are stored in the system's trust store,
|
|
395
|
+
pip_args = ["install", package_manager, "pip-system-certs"]
|
|
396
|
+
# but to install it, we need either a pip version with the trust store feature or to trust the host
|
|
397
|
+
# (trust store feature enabled by default since 24.2)
|
|
398
|
+
if Version(ensurepip.version()) < Version("24.2"):
|
|
399
|
+
# Add trusted host of configured source for older Python versions
|
|
400
|
+
if pypi_source:
|
|
401
|
+
pip_args.extend(["--trusted-host", urlparse(pypi_source.url).hostname])
|
|
402
|
+
else:
|
|
403
|
+
pip_args.extend(["--trusted-host", "pypi.org", "--trusted-host", "pypi.python.org", "--trusted-host", "files.pythonhosted.org"])
|
|
404
|
+
self.virtual_env.pip(pip_args)
|
|
405
|
+
self.virtual_env.run(["python", "-m", self.package_manager_name, "install", *package_manager_args])
|
|
406
|
+
return 0
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def instantiate_os_specific_venv(venv_dir: Path) -> VirtualEnvironment:
|
|
410
|
+
if sys.platform.startswith("win32"):
|
|
411
|
+
return WindowsVirtualEnvironment(venv_dir)
|
|
412
|
+
elif sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
|
|
413
|
+
return UnixVirtualEnvironment(venv_dir)
|
|
414
|
+
else:
|
|
415
|
+
raise UserNotificationException(f"Unsupported operating system: {sys.platform}")
|
|
416
|
+
|
|
417
|
+
def get_name(self) -> str:
|
|
418
|
+
return "create-virtual-environment"
|
|
419
|
+
|
|
420
|
+
def get_inputs(self) -> List[Path]:
|
|
421
|
+
venv_relevant_files = [
|
|
422
|
+
"poetry.lock",
|
|
423
|
+
"poetry.toml",
|
|
424
|
+
"pyproject.toml",
|
|
425
|
+
".env",
|
|
426
|
+
"Pipfile",
|
|
427
|
+
"Pipfile.lock",
|
|
428
|
+
"bootstrap.json",
|
|
429
|
+
".bootstrap/bootstrap.ps1",
|
|
430
|
+
".bootstrap/bootstrap.py",
|
|
431
|
+
"bootstrap.ps1",
|
|
432
|
+
"bootstrap.py",
|
|
433
|
+
]
|
|
434
|
+
return [self.root_dir / file for file in venv_relevant_files]
|
|
435
|
+
|
|
436
|
+
def get_outputs(self) -> List[Path]:
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def print_environment_info() -> None:
|
|
441
|
+
str_bar = "".join(["-" for _ in range(80)])
|
|
442
|
+
logger.debug(str_bar)
|
|
443
|
+
logger.debug("Environment: \n" + json.dumps(dict(os.environ), indent=4))
|
|
444
|
+
logger.info(str_bar)
|
|
445
|
+
logger.info(f"Arguments: {sys.argv[1:]}")
|
|
446
|
+
logger.info(str_bar)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def main() -> int:
|
|
450
|
+
try:
|
|
451
|
+
# print_environment_info()
|
|
452
|
+
creator = CreateVirtualEnvironment(Path.cwd())
|
|
453
|
+
Executor(creator.venv_dir).execute(creator)
|
|
454
|
+
except UserNotificationException as e:
|
|
455
|
+
logger.error(e)
|
|
456
|
+
return 1
|
|
457
|
+
return 0
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
if __name__ == "__main__":
|
|
461
|
+
sys.exit(main())
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[virtualenvs]
|
|
2
|
-
in-project = true
|
|
1
|
+
[virtualenvs]
|
|
2
|
+
in-project = true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Push-Location $PSScriptRoot
|
|
2
|
-
try {
|
|
3
|
-
.\.venv\Scripts\pypeline.exe $args
|
|
4
|
-
}
|
|
5
|
-
finally {
|
|
6
|
-
Pop-Location
|
|
7
|
-
}
|
|
1
|
+
Push-Location $PSScriptRoot
|
|
2
|
+
try {
|
|
3
|
+
.\.venv\Scripts\pypeline.exe $args
|
|
4
|
+
}
|
|
5
|
+
finally {
|
|
6
|
+
Pop-Location
|
|
7
|
+
}
|
|
@@ -3,7 +3,8 @@ pipeline:
|
|
|
3
3
|
module: pypeline.steps.create_venv
|
|
4
4
|
config:
|
|
5
5
|
bootstrap_script: .bootstrap/bootstrap.py
|
|
6
|
-
- step:
|
|
7
|
-
module: pypeline.steps.
|
|
6
|
+
- step: WestInstall
|
|
7
|
+
module: pypeline.steps.west_install
|
|
8
|
+
description: Download external modules
|
|
8
9
|
- step: MyStep
|
|
9
10
|
file: steps/my_step.py
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "Hello Pypeline"
|
|
3
|
-
version = "0.0.1"
|
|
4
|
-
description = "A simple generated project to get you started"
|
|
5
|
-
authors = ["Your Name"]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "Hello Pypeline"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A simple generated project to get you started"
|
|
5
|
+
authors = ["Your Name"]
|
|
6
|
+
package-mode = false
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = ">=3.10,<3.13"
|
|
10
|
+
pypeline-runner = "*"
|
|
11
|
+
west = "*"
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import List
|
|
3
|
-
|
|
4
|
-
from py_app_dev.core.logging import logger
|
|
5
|
-
|
|
6
|
-
from pypeline.domain.execution_context import ExecutionContext
|
|
7
|
-
from pypeline.domain.pipeline import PipelineStep
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class MyStep(PipelineStep[ExecutionContext]):
|
|
11
|
-
def run(self) -> None:
|
|
12
|
-
logger.info(f"Run {self.get_name()} found install dirs:")
|
|
13
|
-
for install_dir in self.execution_context.install_dirs:
|
|
14
|
-
logger.info(f" {install_dir}")
|
|
15
|
-
|
|
16
|
-
def get_inputs(self) -> List[Path]:
|
|
17
|
-
return []
|
|
18
|
-
|
|
19
|
-
def get_outputs(self) -> List[Path]:
|
|
20
|
-
return []
|
|
21
|
-
|
|
22
|
-
def get_name(self) -> str:
|
|
23
|
-
return self.__class__.__name__
|
|
24
|
-
|
|
25
|
-
def update_execution_context(self) -> None:
|
|
26
|
-
pass
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from py_app_dev.core.logging import logger
|
|
5
|
+
|
|
6
|
+
from pypeline.domain.execution_context import ExecutionContext
|
|
7
|
+
from pypeline.domain.pipeline import PipelineStep
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MyStep(PipelineStep[ExecutionContext]):
|
|
11
|
+
def run(self) -> None:
|
|
12
|
+
logger.info(f"Run {self.get_name()} found install dirs:")
|
|
13
|
+
for install_dir in self.execution_context.install_dirs:
|
|
14
|
+
logger.info(f" {install_dir}")
|
|
15
|
+
|
|
16
|
+
def get_inputs(self) -> List[Path]:
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
def get_outputs(self) -> List[Path]:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
def get_name(self) -> str:
|
|
23
|
+
return self.__class__.__name__
|
|
24
|
+
|
|
25
|
+
def update_execution_context(self) -> None:
|
|
26
|
+
pass
|
pypeline/steps/create_venv.py
CHANGED
|
@@ -9,10 +9,12 @@ from py_app_dev.core.logging import logger
|
|
|
9
9
|
from ..domain.execution_context import ExecutionContext
|
|
10
10
|
from ..domain.pipeline import PipelineStep
|
|
11
11
|
|
|
12
|
+
DEFAULT_BOOTSTRAP_SCRIPT = "bootstrap.py"
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclass
|
|
14
16
|
class CreateVEnvConfig(DataClassDictMixin):
|
|
15
|
-
bootstrap_script: str =
|
|
17
|
+
bootstrap_script: str = DEFAULT_BOOTSTRAP_SCRIPT
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class CreateVEnv(PipelineStep[ExecutionContext]):
|
|
@@ -32,9 +34,14 @@ class CreateVEnv(PipelineStep[ExecutionContext]):
|
|
|
32
34
|
config = CreateVEnvConfig.from_dict(self.config) if self.config else CreateVEnvConfig()
|
|
33
35
|
bootstrap_script = self.project_root_dir / config.bootstrap_script
|
|
34
36
|
if not bootstrap_script.exists():
|
|
35
|
-
|
|
37
|
+
if config.bootstrap_script == DEFAULT_BOOTSTRAP_SCRIPT:
|
|
38
|
+
raise UserNotificationException(f"Failed to find bootstrap script '{config.bootstrap_script}'. Make sure that the project is initialized correctly.")
|
|
39
|
+
else: # Fallback to default bootstrap script
|
|
40
|
+
bootstrap_script = self.project_root_dir / DEFAULT_BOOTSTRAP_SCRIPT
|
|
41
|
+
if not bootstrap_script.exists():
|
|
42
|
+
raise UserNotificationException("Failed to find bootstrap script. Make sure that the project is initialized correctly.")
|
|
36
43
|
self.execution_context.create_process_executor(
|
|
37
|
-
["
|
|
44
|
+
["python3", bootstrap_script.as_posix()],
|
|
38
45
|
cwd=self.project_root_dir,
|
|
39
46
|
).execute()
|
|
40
47
|
self.execution_context.add_install_dirs(self.install_dirs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pypeline-runner
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Configure and execute pipelines with Python (similar to GitHub workflows or Jenkins pipelines).
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: cuinixam
|
|
@@ -26,7 +26,13 @@ Project-URL: Documentation, https://pypeline-runner.readthedocs.io
|
|
|
26
26
|
Project-URL: Repository, https://github.com/cuinixam/pypeline
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="https://pypeline-runner.readthedocs.io">
|
|
31
|
+
<img align="center" src="https://github.com/cuinixam/pypeline/raw/main/logo.png" width="400"/>
|
|
32
|
+
</a>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
# pypeline - Define Your CI/CD Pipeline Once, Run It Anywhere
|
|
30
36
|
|
|
31
37
|
<p align="center">
|
|
32
38
|
<a href="https://github.com/cuinixam/pypeline/actions/workflows/ci.yml?query=branch%3Amain">
|
|
@@ -58,28 +64,71 @@ Description-Content-Type: text/markdown
|
|
|
58
64
|
<img src="https://img.shields.io/pypi/l/pypeline-runner.svg?style=flat-square" alt="License">
|
|
59
65
|
</p>
|
|
60
66
|
|
|
61
|
-
Pypeline
|
|
62
|
-
The primary motivation for developing Pypeline stemmed from the need to unify and simplify the creation of build, test, and deployment pipelines that are traditionally defined separately across these platforms using GitHub workflows (YAML) and Jenkins pipelines (Jenkinsfile).
|
|
67
|
+
Pypeline lets you define your build, test, and deployment pipeline in a single YAML file and run it _consistently_ across your local development environment and _any_ CI/CD platform (GitHub Actions, Jenkins, etc.). No more platform-specific configurations – write once, run anywhere.
|
|
63
68
|
|
|
64
69
|
**Key Features**
|
|
65
70
|
|
|
66
71
|
- **Unified Pipeline Definition**: Users can define their entire pipeline in a single YAML file, eliminating the need to switch between different syntaxes and configurations for different CI/CD tools.
|
|
67
72
|
|
|
68
|
-
- **Extensibility**: Pypeline supports execution steps defined not only through
|
|
69
|
-
|
|
70
|
-
- **Execution Context**: Each step in the pipeline receives an execution context that can be updated during step execution. This allows for the sharing of information and state between steps.
|
|
73
|
+
- **Extensibility**: Pypeline supports execution steps defined not only through installed Python packages but also from local scripts.
|
|
71
74
|
|
|
72
|
-
- **
|
|
75
|
+
- **Execution Context**: Allow sharing information and state between steps. Each step in the pipeline receives an execution context that can be updated during step execution.
|
|
73
76
|
|
|
74
|
-
- **
|
|
77
|
+
- **Dependency Handling**: Every step can register its dependencies and will only be scheduled if anything has changed.
|
|
75
78
|
|
|
76
79
|
## Installation
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
Use pipx (or your favorite package manager) to install and run it in an isolated environment:
|
|
82
|
+
|
|
83
|
+
```shell
|
|
84
|
+
pipx install pypeline-runner
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This will install the `pypeline` command globally, which you can use to run your pipelines.
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
> [!NOTE]
|
|
90
|
+
> The Python package is called `pypeline-runner` because the name `pypeline` was already taken on PyPI.
|
|
91
|
+
> The command-line interface is `pypeline`.
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
Documentation: [pypeline-runner.readthedocs.io](https://pypeline-runner.readthedocs.io)
|
|
94
|
+
|
|
95
|
+
## Walkthrough: Getting Started with Pypeline
|
|
96
|
+
|
|
97
|
+
To get started run the `init` command to create a sample project:
|
|
98
|
+
|
|
99
|
+
```shell
|
|
100
|
+
pypeline init --project-dir my-pipeline
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The example project pipeline is defined in the `pipeline.yaml` file.
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
pipeline:
|
|
107
|
+
- step: CreateVEnv
|
|
108
|
+
module: pypeline.steps.create_venv
|
|
109
|
+
config:
|
|
110
|
+
bootstrap_script: .bootstrap/bootstrap.py
|
|
111
|
+
- step: WestInstall
|
|
112
|
+
module: pypeline.steps.west_install
|
|
113
|
+
description: Download external modules
|
|
114
|
+
- step: MyStep
|
|
115
|
+
file: steps/my_step.py
|
|
116
|
+
description: Run a custom script
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This pipeline consists of three steps:
|
|
120
|
+
|
|
121
|
+
- `CreateVEnv`: This is a built-in step that creates a Python virtual environment.
|
|
122
|
+
- `WestInstall`: This is a built-in step that downloads external modules using the `west` tool.
|
|
123
|
+
- `MyStep`: This is a custom step that runs a script defined in the `steps/my_step.py` file.
|
|
124
|
+
|
|
125
|
+
You can run the pipeline using the `run` command:
|
|
126
|
+
|
|
127
|
+
```shell
|
|
128
|
+
pypeline run --project-dir my-pipeline
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
83
132
|
|
|
84
133
|
The project uses Poetry for dependencies management and packaging.
|
|
85
134
|
Run the `bootstrap.ps1` script to install Python and create the virtual environment.
|
|
@@ -88,43 +137,22 @@ Run the `bootstrap.ps1` script to install Python and create the virtual environm
|
|
|
88
137
|
.\bootstrap.ps1
|
|
89
138
|
```
|
|
90
139
|
|
|
91
|
-
This will also generate a `poetry.lock` file, you should track this file in version control.
|
|
92
|
-
|
|
93
140
|
To execute the test suite, call pytest inside Poetry's virtual environment via `poetry run`:
|
|
94
141
|
|
|
95
142
|
```shell
|
|
96
143
|
.venv/Scripts/poetry run pytest
|
|
97
144
|
```
|
|
98
145
|
|
|
99
|
-
Check out the Poetry documentation for more information on the available commands.
|
|
100
|
-
|
|
101
146
|
For those using [VS Code](https://code.visualstudio.com/) there are tasks defined for the most common commands:
|
|
102
147
|
|
|
103
|
-
- bootstrap
|
|
104
|
-
- install dependencies
|
|
105
148
|
- run tests
|
|
106
149
|
- run all checks configured for pre-commit
|
|
107
150
|
- generate documentation
|
|
108
151
|
|
|
109
152
|
See the `.vscode/tasks.json` for more details.
|
|
110
153
|
|
|
111
|
-
## Committing changes
|
|
112
|
-
|
|
113
154
|
This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en).
|
|
114
155
|
|
|
115
|
-
## Contributors ✨
|
|
116
|
-
|
|
117
|
-
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
118
|
-
|
|
119
|
-
<!-- prettier-ignore-start -->
|
|
120
|
-
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
121
|
-
<!-- markdownlint-disable -->
|
|
122
|
-
<!-- markdownlint-enable -->
|
|
123
|
-
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
124
|
-
<!-- prettier-ignore-end -->
|
|
125
|
-
|
|
126
|
-
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
127
|
-
|
|
128
156
|
## Credits
|
|
129
157
|
|
|
130
158
|
This package was created with [Copier](https://copier.readthedocs.io/) and the [cuinixam/pypackage-template](https://github.com/cuinixam/pypackage-template) project template.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
pypeline/__init__.py,sha256=
|
|
1
|
+
pypeline/__init__.py,sha256=Oc_xF94AMAHKZkZlB5rBt1iO0TXWFalg65MP4T2qt-A,22
|
|
2
2
|
pypeline/__run.py,sha256=TCdaX05Qm3g8T4QYryKB25Xxf0L5Km7hFOHe1mK9vI0,350
|
|
3
3
|
pypeline/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
pypeline/domain/artifacts.py,sha256=5k7cVfHhLmvWXNuHKxXb9ca4Lxu0JytGQqazENCeKEU,1404
|
|
@@ -7,24 +7,26 @@ pypeline/domain/execution_context.py,sha256=ho-WvCVRMUfYo1532eQYabXCHtXDgvSNUkX8
|
|
|
7
7
|
pypeline/domain/pipeline.py,sha256=2BsN2lw2znUxLH--Novyqe6SubVKs6XeHQSQf9yxirw,7788
|
|
8
8
|
pypeline/domain/project_slurper.py,sha256=e3BLV88GvfW3efh0agUWKqMk3oWnL602P5u9jER_o9U,971
|
|
9
9
|
pypeline/kickstart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
pypeline/kickstart/create.py,sha256=
|
|
10
|
+
pypeline/kickstart/create.py,sha256=iaB8MMC7PinpPBwRmz3rWZuE-DRbsLh2NtvczYaVgi0,2133
|
|
11
11
|
pypeline/kickstart/templates/project/.gitignore,sha256=y8GJoVvRPez1LBokf1NaDOt2X1XtGwKFMF5yjA8AVS0,24
|
|
12
12
|
pypeline/kickstart/templates/project/bootstrap.ps1,sha256=eR8cyIJwDVt-bA2H3GWmxUew3igJaKYKv4rtg7MqhsY,766
|
|
13
|
-
pypeline/kickstart/templates/project/
|
|
14
|
-
pypeline/kickstart/templates/project/
|
|
15
|
-
pypeline/kickstart/templates/project/pypeline.
|
|
16
|
-
pypeline/kickstart/templates/project/
|
|
13
|
+
pypeline/kickstart/templates/project/bootstrap.py,sha256=9cJp_sbU0SKvDjJluvyQfh0_xIsf6E1ct7sa7rRecNU,17244
|
|
14
|
+
pypeline/kickstart/templates/project/poetry.toml,sha256=qgVxBdPcJZOHdHCTOBoZYna3cke4VGgRkNZ0bKgN6rs,32
|
|
15
|
+
pypeline/kickstart/templates/project/pypeline.ps1,sha256=PjCJULG8XA3AHKbNt3oHrIgD04huvvpIue_gjSo3PMA,104
|
|
16
|
+
pypeline/kickstart/templates/project/pypeline.yaml,sha256=7FeIu7OOqq7iToPVdP_a4MehIi9lUkBPbXnl8mYpxSo,279
|
|
17
|
+
pypeline/kickstart/templates/project/pyproject.toml,sha256=frr2i_bresciD1LsW5_y65Mwh_QTHia5MVipwsrc1-Q,248
|
|
17
18
|
pypeline/kickstart/templates/project/scoopfile.json,sha256=DcfZ8jYf9hmPHM-AWwnPKQJCzRG3fCuYtMeoY01nkag,219
|
|
18
|
-
pypeline/kickstart/templates/project/steps/my_step.py,sha256=
|
|
19
|
+
pypeline/kickstart/templates/project/steps/my_step.py,sha256=iZYTzWtL-qxEW_t7q079d-xpnRST_tumSzxqiQDW7sM,707
|
|
20
|
+
pypeline/kickstart/templates/project/west.yaml,sha256=ZfVym7M4yzzC-Nm0vESdhqNYs6EaJuMQWGJBht_i0b4,188
|
|
19
21
|
pypeline/main.py,sha256=54_-aINmJY6IILSp6swL2kYqNoBQqJw4W2duxY7gcQ4,3607
|
|
20
22
|
pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
23
|
pypeline/pypeline.py,sha256=FHGS2iNtiuiM4dZDHCbeL-UW1apdCtZeFzl1xZW9qBw,6482
|
|
22
24
|
pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
pypeline/steps/create_venv.py,sha256=
|
|
25
|
+
pypeline/steps/create_venv.py,sha256=xCc73Hk62bbuJIM3btvapuoWeQy_Se5MsgffokYc-r0,2429
|
|
24
26
|
pypeline/steps/scoop_install.py,sha256=_YdoCMXLON0eIwck8PJOcNhayx_ka1krBAidw_oRuFE,3373
|
|
25
27
|
pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
|
|
26
|
-
pypeline_runner-1.
|
|
27
|
-
pypeline_runner-1.
|
|
28
|
-
pypeline_runner-1.
|
|
29
|
-
pypeline_runner-1.
|
|
30
|
-
pypeline_runner-1.
|
|
28
|
+
pypeline_runner-1.8.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
|
|
29
|
+
pypeline_runner-1.8.0.dist-info/METADATA,sha256=PNVemc7HHUUK7pjR6pzw9EWlTfn-FFm506Ca798Y5Gs,7557
|
|
30
|
+
pypeline_runner-1.8.0.dist-info/WHEEL,sha256=7dDg4QLnNKTvwIDR9Ac8jJaAmBC_owJrckbC0jjThyA,88
|
|
31
|
+
pypeline_runner-1.8.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
|
|
32
|
+
pypeline_runner-1.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|