pypeline-runner 1.6.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/domain/artifacts.py +2 -5
- pypeline/domain/pipeline.py +83 -11
- pypeline/domain/project_slurper.py +3 -2
- 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/main.py +2 -1
- pypeline/pypeline.py +19 -76
- pypeline/steps/create_venv.py +10 -3
- {pypeline_runner-1.6.0.dist-info → pypeline_runner-1.8.0.dist-info}/METADATA +61 -33
- pypeline_runner-1.8.0.dist-info/RECORD +32 -0
- {pypeline_runner-1.6.0.dist-info → pypeline_runner-1.8.0.dist-info}/WHEEL +1 -1
- pypeline_runner-1.6.0.dist-info/RECORD +0 -30
- {pypeline_runner-1.6.0.dist-info → pypeline_runner-1.8.0.dist-info}/LICENSE +0 -0
- {pypeline_runner-1.6.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/domain/artifacts.py
CHANGED
|
@@ -10,13 +10,10 @@ CONFIG_FILENAME = "pypeline.yaml"
|
|
|
10
10
|
class ProjectArtifactsLocator:
|
|
11
11
|
"""Provides paths to project artifacts."""
|
|
12
12
|
|
|
13
|
-
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
project_root_dir: Path,
|
|
16
|
-
) -> None:
|
|
13
|
+
def __init__(self, project_root_dir: Path, config_file: Optional[str] = None) -> None:
|
|
17
14
|
self.project_root_dir = project_root_dir
|
|
18
15
|
self.build_dir = project_root_dir / "build"
|
|
19
|
-
self.config_file = project_root_dir
|
|
16
|
+
self.config_file = project_root_dir.joinpath(config_file if config_file else CONFIG_FILENAME)
|
|
20
17
|
self.external_dependencies_dir = self.build_dir / "external"
|
|
21
18
|
scripts_dir = "Scripts" if sys.platform.startswith("win32") else "bin"
|
|
22
19
|
self.venv_scripts_dir = self.project_root_dir.joinpath(".venv").joinpath(scripts_dir)
|
pypeline/domain/pipeline.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import importlib
|
|
1
2
|
from abc import abstractmethod
|
|
2
3
|
from dataclasses import dataclass
|
|
4
|
+
from importlib.util import module_from_spec, spec_from_file_location
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import (
|
|
5
7
|
Any,
|
|
@@ -9,6 +11,7 @@ from typing import (
|
|
|
9
11
|
List,
|
|
10
12
|
Optional,
|
|
11
13
|
OrderedDict,
|
|
14
|
+
Protocol,
|
|
12
15
|
Tuple,
|
|
13
16
|
Type,
|
|
14
17
|
TypeAlias,
|
|
@@ -17,6 +20,7 @@ from typing import (
|
|
|
17
20
|
)
|
|
18
21
|
|
|
19
22
|
from mashumaro import DataClassDictMixin
|
|
23
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
|
20
24
|
from py_app_dev.core.runnable import Runnable
|
|
21
25
|
|
|
22
26
|
from .execution_context import ExecutionContext
|
|
@@ -44,6 +48,21 @@ class PipelineStepConfig(DataClassDictMixin):
|
|
|
44
48
|
|
|
45
49
|
PipelineConfig: TypeAlias = Union[List[PipelineStepConfig], OrderedDict[str, List[PipelineStepConfig]]]
|
|
46
50
|
|
|
51
|
+
TPipelineStep = TypeVar("TPipelineStep", covariant=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class PipelineStepReference(Generic[TPipelineStep]):
|
|
56
|
+
"""Once a Step is found, keep the Step class reference to be able to instantiate it later."""
|
|
57
|
+
|
|
58
|
+
group_name: Optional[str]
|
|
59
|
+
_class: Type[TPipelineStep]
|
|
60
|
+
config: Optional[Dict[str, Any]] = None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def name(self) -> str:
|
|
64
|
+
return self._class.__name__
|
|
65
|
+
|
|
47
66
|
|
|
48
67
|
class PipelineConfigIterator:
|
|
49
68
|
"""
|
|
@@ -64,6 +83,70 @@ class PipelineConfigIterator:
|
|
|
64
83
|
yield from self._items
|
|
65
84
|
|
|
66
85
|
|
|
86
|
+
class StepClassFactory(Generic[TPipelineStep], Protocol):
|
|
87
|
+
def create_step_class(self, step_config: PipelineStepConfig, project_root_dir: Path) -> Type[TPipelineStep]: ...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PipelineLoader(Generic[TPipelineStep]):
|
|
91
|
+
def __init__(self, pipeline_config: PipelineConfig, project_root_dir: Path, step_class_factory: Optional[StepClassFactory[TPipelineStep]] = None) -> None:
|
|
92
|
+
self.pipeline_config = pipeline_config
|
|
93
|
+
self.project_root_dir = project_root_dir
|
|
94
|
+
self.step_class_factory = step_class_factory
|
|
95
|
+
|
|
96
|
+
def load_steps_references(self) -> List[PipelineStepReference[TPipelineStep]]:
|
|
97
|
+
result = []
|
|
98
|
+
for group_name, steps_config in PipelineConfigIterator(self.pipeline_config):
|
|
99
|
+
result.extend(self._load_steps(group_name, steps_config, self.project_root_dir, self.step_class_factory))
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _load_steps(
|
|
104
|
+
group_name: Optional[str], steps_config: List[PipelineStepConfig], project_root_dir: Path, step_class_factory: Optional[StepClassFactory[TPipelineStep]] = None
|
|
105
|
+
) -> List[PipelineStepReference[TPipelineStep]]:
|
|
106
|
+
result = []
|
|
107
|
+
for step_config in steps_config:
|
|
108
|
+
step_class_name = step_config.class_name or step_config.step
|
|
109
|
+
if step_config.module:
|
|
110
|
+
step_class = PipelineLoader[TPipelineStep]._load_module_step(step_config.module, step_class_name)
|
|
111
|
+
elif step_config.file:
|
|
112
|
+
step_class = PipelineLoader[TPipelineStep]._load_user_step(project_root_dir.joinpath(step_config.file), step_class_name)
|
|
113
|
+
else:
|
|
114
|
+
if step_class_factory:
|
|
115
|
+
step_class = step_class_factory.create_step_class(step_config, project_root_dir)
|
|
116
|
+
else:
|
|
117
|
+
raise UserNotificationException(
|
|
118
|
+
f"Step '{step_class_name}' has no 'module' nor 'file' defined nor a custom step class factory was provided. Please check your pipeline configuration."
|
|
119
|
+
)
|
|
120
|
+
result.append(PipelineStepReference(group_name, step_class, step_config.config))
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _load_user_step(python_file: Path, step_class_name: str) -> Type[TPipelineStep]:
|
|
125
|
+
# Create a module specification from the file path
|
|
126
|
+
spec = spec_from_file_location(f"user__{python_file.stem}", python_file)
|
|
127
|
+
if spec and spec.loader:
|
|
128
|
+
step_module = module_from_spec(spec)
|
|
129
|
+
# Import the module
|
|
130
|
+
spec.loader.exec_module(step_module)
|
|
131
|
+
try:
|
|
132
|
+
step_class = getattr(step_module, step_class_name)
|
|
133
|
+
except AttributeError:
|
|
134
|
+
raise UserNotificationException(f"Could not load class '{step_class_name}' from file '{python_file}'. Please check your pipeline configuration.") from None
|
|
135
|
+
return step_class
|
|
136
|
+
raise UserNotificationException(f"Could not load file '{python_file}'. Please check the file for any errors.")
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _load_module_step(module_name: str, step_class_name: str) -> Type[TPipelineStep]:
|
|
140
|
+
try:
|
|
141
|
+
module = importlib.import_module(module_name)
|
|
142
|
+
step_class = getattr(module, step_class_name)
|
|
143
|
+
except ImportError:
|
|
144
|
+
raise UserNotificationException(f"Could not load module '{module_name}'. Please check your pipeline configuration.") from None
|
|
145
|
+
except AttributeError:
|
|
146
|
+
raise UserNotificationException(f"Could not load class '{step_class_name}' from module '{module_name}'. Please check your pipeline configuration.") from None
|
|
147
|
+
return step_class
|
|
148
|
+
|
|
149
|
+
|
|
67
150
|
TExecutionContext = TypeVar("TExecutionContext", bound=ExecutionContext)
|
|
68
151
|
|
|
69
152
|
|
|
@@ -96,14 +179,3 @@ class PipelineStep(Generic[TExecutionContext], Runnable):
|
|
|
96
179
|
def get_needs_dependency_management(self) -> bool:
|
|
97
180
|
"""If false, the step executor will not check for outdated dependencies. This is useful for steps consisting of command lines which shall always run."""
|
|
98
181
|
return True
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class PipelineStepReference(Generic[TExecutionContext]):
|
|
102
|
-
def __init__(self, group_name: Optional[str], _class: Type[PipelineStep[TExecutionContext]], config: Optional[Dict[str, Any]] = None) -> None:
|
|
103
|
-
self.group_name = group_name
|
|
104
|
-
self._class = _class
|
|
105
|
-
self.config = config
|
|
106
|
-
|
|
107
|
-
@property
|
|
108
|
-
def name(self) -> str:
|
|
109
|
-
return self._class.__name__
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
4
|
from py_app_dev.core.exceptions import UserNotificationException
|
|
4
5
|
from py_app_dev.core.logging import logger
|
|
@@ -8,9 +9,9 @@ from .config import PipelineConfig, ProjectConfig
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ProjectSlurper:
|
|
11
|
-
def __init__(self, project_dir: Path) -> None:
|
|
12
|
+
def __init__(self, project_dir: Path, config_file: Optional[str] = None) -> None:
|
|
12
13
|
self.logger = logger.bind()
|
|
13
|
-
self.artifacts_locator = ProjectArtifactsLocator(project_dir)
|
|
14
|
+
self.artifacts_locator = ProjectArtifactsLocator(project_dir, config_file)
|
|
14
15
|
try:
|
|
15
16
|
self.user_config: ProjectConfig = ProjectConfig.from_file(self.artifacts_locator.config_file)
|
|
16
17
|
except FileNotFoundError:
|
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/main.py
CHANGED
|
@@ -42,6 +42,7 @@ def init(
|
|
|
42
42
|
@time_it("run")
|
|
43
43
|
def run(
|
|
44
44
|
project_dir: Path = typer.Option(Path.cwd().absolute(), help="The project directory"), # noqa: B008,
|
|
45
|
+
config_file: Optional[str] = typer.Option(None, help="The name of the YAML configuration file containing the pypeline definition."),
|
|
45
46
|
step: Optional[str] = typer.Option(
|
|
46
47
|
None,
|
|
47
48
|
help="Name of the step to run (as written in the pipeline config).",
|
|
@@ -67,7 +68,7 @@ def run(
|
|
|
67
68
|
is_flag=True,
|
|
68
69
|
),
|
|
69
70
|
) -> None:
|
|
70
|
-
project_slurper = ProjectSlurper(project_dir)
|
|
71
|
+
project_slurper = ProjectSlurper(project_dir, config_file)
|
|
71
72
|
if print:
|
|
72
73
|
logger.info("Pipeline steps:")
|
|
73
74
|
for group, step_configs in PipelineConfigIterator(project_slurper.pipeline):
|
pypeline/pypeline.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
from importlib.util import module_from_spec, spec_from_file_location
|
|
3
1
|
from pathlib import Path
|
|
4
2
|
from typing import (
|
|
5
3
|
Any,
|
|
@@ -8,7 +6,6 @@ from typing import (
|
|
|
8
6
|
List,
|
|
9
7
|
Optional,
|
|
10
8
|
Type,
|
|
11
|
-
cast,
|
|
12
9
|
)
|
|
13
10
|
|
|
14
11
|
from py_app_dev.core.exceptions import UserNotificationException
|
|
@@ -17,75 +14,18 @@ from py_app_dev.core.runnable import Executor
|
|
|
17
14
|
|
|
18
15
|
from .domain.artifacts import ProjectArtifactsLocator
|
|
19
16
|
from .domain.execution_context import ExecutionContext
|
|
20
|
-
from .domain.pipeline import PipelineConfig,
|
|
17
|
+
from .domain.pipeline import PipelineConfig, PipelineLoader, PipelineStep, PipelineStepConfig, PipelineStepReference, StepClassFactory, TExecutionContext
|
|
21
18
|
|
|
22
19
|
|
|
23
|
-
class
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def __init__(self, pipeline_config: PipelineConfig, project_root_dir: Path) -> None:
|
|
33
|
-
self.pipeline_config = pipeline_config
|
|
34
|
-
self.project_root_dir = project_root_dir
|
|
35
|
-
|
|
36
|
-
def load_steps_references(self) -> List[PipelineStepReference[TExecutionContext]]:
|
|
37
|
-
result = []
|
|
38
|
-
for group_name, steps_config in PipelineConfigIterator(self.pipeline_config):
|
|
39
|
-
result.extend(self._load_steps(group_name, steps_config, self.project_root_dir))
|
|
40
|
-
return result
|
|
41
|
-
|
|
42
|
-
@staticmethod
|
|
43
|
-
def _load_steps(
|
|
44
|
-
group_name: Optional[str],
|
|
45
|
-
steps_config: List[PipelineStepConfig],
|
|
46
|
-
project_root_dir: Path,
|
|
47
|
-
) -> List[PipelineStepReference[TExecutionContext]]:
|
|
48
|
-
result = []
|
|
49
|
-
for step_config in steps_config:
|
|
50
|
-
step_class_name = step_config.class_name or step_config.step
|
|
51
|
-
if step_config.module:
|
|
52
|
-
step_class = PipelineLoader._load_module_step(step_config.module, step_class_name)
|
|
53
|
-
elif step_config.file:
|
|
54
|
-
step_class = PipelineLoader._load_user_step(project_root_dir.joinpath(step_config.file), step_class_name)
|
|
55
|
-
elif step_config.run:
|
|
56
|
-
# We want the run field to always return a list of strings (the command and its arguments).
|
|
57
|
-
run_command = step_config.run.split(" ") if isinstance(step_config.run, str) else step_config.run
|
|
58
|
-
step_class = PipelineLoader._create_run_command_step_class(run_command, step_class_name)
|
|
59
|
-
else:
|
|
60
|
-
raise UserNotificationException(f"Step '{step_class_name}' has no 'module' nor 'file' nor `run` defined. Please check your pipeline configuration.")
|
|
61
|
-
result.append(PipelineStepReference[TExecutionContext](group_name, cast(Type[PipelineStep[TExecutionContext]], step_class), step_config.config))
|
|
62
|
-
return result
|
|
63
|
-
|
|
64
|
-
@staticmethod
|
|
65
|
-
def _load_user_step(python_file: Path, step_class_name: str) -> Type[PipelineStep[ExecutionContext]]:
|
|
66
|
-
# Create a module specification from the file path
|
|
67
|
-
spec = spec_from_file_location(f"user__{python_file.stem}", python_file)
|
|
68
|
-
if spec and spec.loader:
|
|
69
|
-
step_module = module_from_spec(spec)
|
|
70
|
-
# Import the module
|
|
71
|
-
spec.loader.exec_module(step_module)
|
|
72
|
-
try:
|
|
73
|
-
step_class = getattr(step_module, step_class_name)
|
|
74
|
-
except AttributeError:
|
|
75
|
-
raise UserNotificationException(f"Could not load class '{step_class_name}' from file '{python_file}'. Please check your pipeline configuration.") from None
|
|
76
|
-
return step_class
|
|
77
|
-
raise UserNotificationException(f"Could not load file '{python_file}'. Please check the file for any errors.")
|
|
78
|
-
|
|
79
|
-
@staticmethod
|
|
80
|
-
def _load_module_step(module_name: str, step_class_name: str) -> Type[PipelineStep[ExecutionContext]]:
|
|
81
|
-
try:
|
|
82
|
-
module = importlib.import_module(module_name)
|
|
83
|
-
step_class = getattr(module, step_class_name)
|
|
84
|
-
except ImportError:
|
|
85
|
-
raise UserNotificationException(f"Could not load module '{module_name}'. Please check your pipeline configuration.") from None
|
|
86
|
-
except AttributeError:
|
|
87
|
-
raise UserNotificationException(f"Could not load class '{step_class_name}' from module '{module_name}'. Please check your pipeline configuration.") from None
|
|
88
|
-
return step_class
|
|
20
|
+
class RunCommandClassFactory(StepClassFactory[PipelineStep[TExecutionContext]]):
|
|
21
|
+
def create_step_class(self, step_config: PipelineStepConfig, project_root_dir: Path) -> Type[PipelineStep[ExecutionContext]]:
|
|
22
|
+
_ = project_root_dir # Unused because we do not need to locate files relative to the project root directory
|
|
23
|
+
step_name = step_config.class_name or step_config.step
|
|
24
|
+
if step_config.run:
|
|
25
|
+
# We want the run field to always return a list of strings (the command and its arguments).
|
|
26
|
+
run_command = step_config.run.split(" ") if isinstance(step_config.run, str) else step_config.run
|
|
27
|
+
return self._create_run_command_step_class(run_command, step_name)
|
|
28
|
+
raise UserNotificationException(f"Step '{step_name}' has `run` command defined. Please check your pipeline configuration.")
|
|
89
29
|
|
|
90
30
|
@staticmethod
|
|
91
31
|
def _create_run_command_step_class(command: List[str], name: str) -> Type[PipelineStep[ExecutionContext]]:
|
|
@@ -133,7 +73,7 @@ class PipelineStepsExecutor(Generic[TExecutionContext]):
|
|
|
133
73
|
def __init__(
|
|
134
74
|
self,
|
|
135
75
|
execution_context: TExecutionContext,
|
|
136
|
-
steps_references: List[PipelineStepReference[TExecutionContext]],
|
|
76
|
+
steps_references: List[PipelineStepReference[PipelineStep[TExecutionContext]]],
|
|
137
77
|
force_run: bool = False,
|
|
138
78
|
dry_run: bool = False,
|
|
139
79
|
) -> None:
|
|
@@ -175,16 +115,15 @@ class PipelineScheduler(Generic[TExecutionContext]):
|
|
|
175
115
|
self.project_root_dir = project_root_dir
|
|
176
116
|
self.logger = logger.bind()
|
|
177
117
|
|
|
178
|
-
def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference[TExecutionContext]]:
|
|
179
|
-
|
|
180
|
-
return self.filter_steps_references(pipeline_loader.load_steps_references(), step_name, single)
|
|
118
|
+
def get_steps_to_run(self, step_name: Optional[str] = None, single: bool = False) -> List[PipelineStepReference[PipelineStep[TExecutionContext]]]:
|
|
119
|
+
return self.filter_steps_references(self.create_pipeline_loader(self.pipeline, self.project_root_dir).load_steps_references(), step_name, single)
|
|
181
120
|
|
|
182
121
|
@staticmethod
|
|
183
122
|
def filter_steps_references(
|
|
184
|
-
steps_references: List[PipelineStepReference[TExecutionContext]],
|
|
123
|
+
steps_references: List[PipelineStepReference[PipelineStep[TExecutionContext]]],
|
|
185
124
|
step_name: Optional[str],
|
|
186
125
|
single: Optional[bool],
|
|
187
|
-
) -> List[PipelineStepReference[TExecutionContext]]:
|
|
126
|
+
) -> List[PipelineStepReference[PipelineStep[TExecutionContext]]]:
|
|
188
127
|
if step_name:
|
|
189
128
|
step_reference = next((step for step in steps_references if step.name == step_name), None)
|
|
190
129
|
if not step_reference:
|
|
@@ -193,3 +132,7 @@ class PipelineScheduler(Generic[TExecutionContext]):
|
|
|
193
132
|
return [step_reference]
|
|
194
133
|
return [step for step in steps_references if steps_references.index(step) <= steps_references.index(step_reference)]
|
|
195
134
|
return steps_references
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def create_pipeline_loader(pipeline: PipelineConfig, project_root_dir: Path) -> PipelineLoader[PipelineStep[TExecutionContext]]:
|
|
138
|
+
return PipelineLoader[PipelineStep[TExecutionContext]](pipeline, project_root_dir, RunCommandClassFactory())
|
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.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
pypeline/__init__.py,sha256=Oc_xF94AMAHKZkZlB5rBt1iO0TXWFalg65MP4T2qt-A,22
|
|
2
|
+
pypeline/__run.py,sha256=TCdaX05Qm3g8T4QYryKB25Xxf0L5Km7hFOHe1mK9vI0,350
|
|
3
|
+
pypeline/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pypeline/domain/artifacts.py,sha256=5k7cVfHhLmvWXNuHKxXb9ca4Lxu0JytGQqazENCeKEU,1404
|
|
5
|
+
pypeline/domain/config.py,sha256=AlavAaz5hSxa6yaKYnj-x71ClhOtA41yv5Qf2JIE47k,1650
|
|
6
|
+
pypeline/domain/execution_context.py,sha256=ho-WvCVRMUfYo1532eQYabXCHtXDgvSNUkX8S3Cr7Xo,1278
|
|
7
|
+
pypeline/domain/pipeline.py,sha256=2BsN2lw2znUxLH--Novyqe6SubVKs6XeHQSQf9yxirw,7788
|
|
8
|
+
pypeline/domain/project_slurper.py,sha256=e3BLV88GvfW3efh0agUWKqMk3oWnL602P5u9jER_o9U,971
|
|
9
|
+
pypeline/kickstart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
pypeline/kickstart/create.py,sha256=iaB8MMC7PinpPBwRmz3rWZuE-DRbsLh2NtvczYaVgi0,2133
|
|
11
|
+
pypeline/kickstart/templates/project/.gitignore,sha256=y8GJoVvRPez1LBokf1NaDOt2X1XtGwKFMF5yjA8AVS0,24
|
|
12
|
+
pypeline/kickstart/templates/project/bootstrap.ps1,sha256=eR8cyIJwDVt-bA2H3GWmxUew3igJaKYKv4rtg7MqhsY,766
|
|
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
|
|
18
|
+
pypeline/kickstart/templates/project/scoopfile.json,sha256=DcfZ8jYf9hmPHM-AWwnPKQJCzRG3fCuYtMeoY01nkag,219
|
|
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
|
|
21
|
+
pypeline/main.py,sha256=54_-aINmJY6IILSp6swL2kYqNoBQqJw4W2duxY7gcQ4,3607
|
|
22
|
+
pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
pypeline/pypeline.py,sha256=FHGS2iNtiuiM4dZDHCbeL-UW1apdCtZeFzl1xZW9qBw,6482
|
|
24
|
+
pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
pypeline/steps/create_venv.py,sha256=xCc73Hk62bbuJIM3btvapuoWeQy_Se5MsgffokYc-r0,2429
|
|
26
|
+
pypeline/steps/scoop_install.py,sha256=_YdoCMXLON0eIwck8PJOcNhayx_ka1krBAidw_oRuFE,3373
|
|
27
|
+
pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
|
|
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,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
pypeline/__init__.py,sha256=8EjIC8Er4Bn8PhErizTXrZVYTgb6tHgj00LrrBVNYXA,22
|
|
2
|
-
pypeline/__run.py,sha256=TCdaX05Qm3g8T4QYryKB25Xxf0L5Km7hFOHe1mK9vI0,350
|
|
3
|
-
pypeline/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
pypeline/domain/artifacts.py,sha256=qXshnk9umi0AVGV4m5iEiy_MQ5Ad2LDZwI8OULU-qMk,1355
|
|
5
|
-
pypeline/domain/config.py,sha256=AlavAaz5hSxa6yaKYnj-x71ClhOtA41yv5Qf2JIE47k,1650
|
|
6
|
-
pypeline/domain/execution_context.py,sha256=ho-WvCVRMUfYo1532eQYabXCHtXDgvSNUkX8S3Cr7Xo,1278
|
|
7
|
-
pypeline/domain/pipeline.py,sha256=mO5tc18nxiAtnLs_MR56u7PAoTJDMZ_KVkCqMzV-YG8,3916
|
|
8
|
-
pypeline/domain/project_slurper.py,sha256=YCho7V1BHjFmC_foxHFaWX8c_VbMJ16XEB4CQBlMrhc,894
|
|
9
|
-
pypeline/kickstart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
pypeline/kickstart/create.py,sha256=dWqSdDsqNh7_AnNEsJzsmheR2-xyO6rpUwF-7AwzCYY,2188
|
|
11
|
-
pypeline/kickstart/templates/project/.gitignore,sha256=y8GJoVvRPez1LBokf1NaDOt2X1XtGwKFMF5yjA8AVS0,24
|
|
12
|
-
pypeline/kickstart/templates/project/bootstrap.ps1,sha256=eR8cyIJwDVt-bA2H3GWmxUew3igJaKYKv4rtg7MqhsY,766
|
|
13
|
-
pypeline/kickstart/templates/project/poetry.toml,sha256=q5gF2MWexTFIahJ6StUa3y62HDUrRW7t8kGFszZhgp4,34
|
|
14
|
-
pypeline/kickstart/templates/project/pypeline.ps1,sha256=s7CDfnagg8BIO42fpCfLF1l8uK7PNzui-7t9_suokzc,111
|
|
15
|
-
pypeline/kickstart/templates/project/pypeline.yaml,sha256=EV5Tnu3H33gMT3Ov0t14-jKwnv9naSMb0wEDzaG0H2Q,238
|
|
16
|
-
pypeline/kickstart/templates/project/pyproject.toml,sha256=yc6RCo-bUo1PXF91XfM-dButgfxU16Uud34NidgJ0zQ,225
|
|
17
|
-
pypeline/kickstart/templates/project/scoopfile.json,sha256=DcfZ8jYf9hmPHM-AWwnPKQJCzRG3fCuYtMeoY01nkag,219
|
|
18
|
-
pypeline/kickstart/templates/project/steps/my_step.py,sha256=_zx01qAVuwn6IMPBUBwKY-IBjS9Gs2m-d51L9sayGug,733
|
|
19
|
-
pypeline/main.py,sha256=GtuOgB9OeNFgbWLHZux80fppYQVwMYNFZoZhDIlJW9c,3457
|
|
20
|
-
pypeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
pypeline/pypeline.py,sha256=ADg_HlVA6LxZX1jq2GJw-bRCsy1cWixTQcTzDNufauw,9198
|
|
22
|
-
pypeline/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
pypeline/steps/create_venv.py,sha256=vTPSA0gGGzq_QhI1jAgsGchS2s8_ZpEtnWcb0uL8BHE,1934
|
|
24
|
-
pypeline/steps/scoop_install.py,sha256=_YdoCMXLON0eIwck8PJOcNhayx_ka1krBAidw_oRuFE,3373
|
|
25
|
-
pypeline/steps/west_install.py,sha256=hPyr28ksdKsQ0tv0gMNytzupgk1IgjN9CpmaBdX5zps,1947
|
|
26
|
-
pypeline_runner-1.6.0.dist-info/LICENSE,sha256=sKxdoqSmW9ezvPvt0ZGJbneyA0SBcm0GiqzTv2jN230,1066
|
|
27
|
-
pypeline_runner-1.6.0.dist-info/METADATA,sha256=W1G0C5EOOD3lelRR3o4TDHkx_QY7K9ShxtwCXlr-FA4,7154
|
|
28
|
-
pypeline_runner-1.6.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
29
|
-
pypeline_runner-1.6.0.dist-info/entry_points.txt,sha256=pe1u0uuhPI_yeQ0KjEw6jK-EvQfPcZwBSajgbAdKz1o,47
|
|
30
|
-
pypeline_runner-1.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|