pypeline-runner 0.1.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 -0
- pypeline/__run.py +12 -0
- pypeline/domain/__init__.py +0 -0
- pypeline/domain/artifacts.py +36 -0
- pypeline/domain/config.py +36 -0
- pypeline/domain/execution_context.py +27 -0
- pypeline/domain/pipeline.py +28 -0
- pypeline/domain/project_slurper.py +22 -0
- pypeline/kickstart/__init__.py +0 -0
- pypeline/kickstart/create.py +58 -0
- pypeline/kickstart/templates/bootstrap/bootstrap.ps1 +137 -0
- pypeline/kickstart/templates/bootstrap/bootstrap.py +428 -0
- pypeline/kickstart/templates/project/.gitignore +2 -0
- pypeline/kickstart/templates/project/poetry.toml +2 -0
- pypeline/kickstart/templates/project/pypeline.yaml +10 -0
- pypeline/kickstart/templates/project/pyproject.toml +8 -0
- pypeline/kickstart/templates/project/scoopfile.json +14 -0
- pypeline/kickstart/templates/project/steps/my_step.py +25 -0
- pypeline/main.py +101 -0
- pypeline/py.typed +0 -0
- pypeline/pypeline.py +93 -0
- pypeline/steps/__init__.py +0 -0
- pypeline/steps/create_venv.py +43 -0
- pypeline/steps/scoop_install.py +87 -0
- pypeline/steps/west_install.py +54 -0
- pypeline_runner-0.1.0.dist-info/LICENSE +22 -0
- pypeline_runner-0.1.0.dist-info/METADATA +120 -0
- pypeline_runner-0.1.0.dist-info/RECORD +30 -0
- pypeline_runner-0.1.0.dist-info/WHEEL +4 -0
- pypeline_runner-0.1.0.dist-info/entry_points.txt +3 -0
pypeline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
pypeline/__run.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Used to run pypeline from the command line when run from this repository.
|
|
3
|
+
|
|
4
|
+
This is required because pypeline module is not visible when running from the repository.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import runpy
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, Path(__file__).parent.parent.absolute().as_posix())
|
|
12
|
+
runpy.run_module("pypeline.main", run_name="__main__")
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
|
6
|
+
|
|
7
|
+
CONFIG_FILENAME = "pypeline.yaml"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectArtifactsLocator:
|
|
11
|
+
"""Provides paths to project artifacts."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
project_root_dir: Path,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.project_root_dir = project_root_dir
|
|
18
|
+
self.build_dir = project_root_dir / "build"
|
|
19
|
+
self.config_file = project_root_dir / CONFIG_FILENAME
|
|
20
|
+
self.external_dependencies_dir = self.build_dir / "external"
|
|
21
|
+
scripts_dir = "Scripts" if sys.platform.startswith("win32") else "bin"
|
|
22
|
+
self.venv_scripts_dir = self.project_root_dir.joinpath(".venv").joinpath(scripts_dir)
|
|
23
|
+
|
|
24
|
+
def locate_artifact(self, artifact: str, first_search_paths: List[Optional[Path]]) -> Path:
|
|
25
|
+
search_paths = []
|
|
26
|
+
for path in first_search_paths:
|
|
27
|
+
if path:
|
|
28
|
+
search_paths.append(path.parent if path.is_file() else path)
|
|
29
|
+
for dir in [
|
|
30
|
+
*search_paths,
|
|
31
|
+
self.project_root_dir,
|
|
32
|
+
]:
|
|
33
|
+
if dir and (artifact_path := Path(dir).joinpath(artifact)).exists():
|
|
34
|
+
return artifact_path
|
|
35
|
+
else:
|
|
36
|
+
raise UserNotificationException(f"Artifact '{artifact}' not found in the project.")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from mashumaro import DataClassDictMixin
|
|
7
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
|
8
|
+
from py_app_dev.core.pipeline import PipelineConfig
|
|
9
|
+
from yaml.parser import ParserError
|
|
10
|
+
from yaml.scanner import ScannerError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ProjectConfig(DataClassDictMixin):
|
|
15
|
+
pipeline: PipelineConfig
|
|
16
|
+
# This field is intended to keep track of where configuration was loaded from and
|
|
17
|
+
# it is automatically added when configuration is loaded from file
|
|
18
|
+
file: Optional[Path] = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_file(cls, config_file: Path) -> "ProjectConfig":
|
|
22
|
+
config_dict = cls.parse_to_dict(config_file)
|
|
23
|
+
return cls.from_dict(config_dict)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def parse_to_dict(config_file: Path) -> Dict[str, Any]:
|
|
27
|
+
try:
|
|
28
|
+
with open(config_file) as fs:
|
|
29
|
+
config_dict = yaml.safe_load(fs)
|
|
30
|
+
# Add file name to config to keep track of where configuration was loaded from
|
|
31
|
+
config_dict["file"] = config_file
|
|
32
|
+
return config_dict
|
|
33
|
+
except ScannerError as e:
|
|
34
|
+
raise UserNotificationException(f"Failed scanning configuration file '{config_file}'. \nError: {e}") from e
|
|
35
|
+
except ParserError as e:
|
|
36
|
+
raise UserNotificationException(f"Failed parsing configuration file '{config_file}'. \nError: {e}") from e
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from py_app_dev.core.subprocess import SubprocessExecutor
|
|
7
|
+
|
|
8
|
+
from .artifacts import ProjectArtifactsLocator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ExecutionContext:
|
|
13
|
+
project_root_dir: Path
|
|
14
|
+
# Keep track of all install directories, updated by any step for the subsequent steps
|
|
15
|
+
install_dirs: List[Path] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
def add_install_dirs(self, install_dirs: List[Path]) -> None:
|
|
18
|
+
self.install_dirs.extend(install_dirs)
|
|
19
|
+
|
|
20
|
+
def create_process_executor(self, command: List[str | Path], cwd: Optional[Path] = None) -> SubprocessExecutor:
|
|
21
|
+
# Add the install directories to the PATH
|
|
22
|
+
env = os.environ.copy()
|
|
23
|
+
env["PATH"] = os.pathsep.join([path.absolute().as_posix() for path in self.install_dirs] + [env["PATH"]])
|
|
24
|
+
return SubprocessExecutor(command, cwd=cwd, env=env, shell=True) # noqa: S604
|
|
25
|
+
|
|
26
|
+
def create_artifacts_locator(self) -> ProjectArtifactsLocator:
|
|
27
|
+
return ProjectArtifactsLocator(self.project_root_dir)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Type
|
|
4
|
+
|
|
5
|
+
from py_app_dev.core.runnable import Runnable
|
|
6
|
+
|
|
7
|
+
from .execution_context import ExecutionContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PipelineStep(Runnable):
|
|
11
|
+
def __init__(self, execution_context: ExecutionContext, output_dir: Path) -> None:
|
|
12
|
+
self.execution_context = execution_context
|
|
13
|
+
self.output_dir = output_dir
|
|
14
|
+
self.project_root_dir = self.execution_context.project_root_dir
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def update_execution_context(self) -> None:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PipelineStepReference:
|
|
22
|
+
def __init__(self, group_name: str, _class: Type[PipelineStep]) -> None:
|
|
23
|
+
self.group_name = group_name
|
|
24
|
+
self._class = _class
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return self._class.__name__
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
|
4
|
+
from py_app_dev.core.logging import logger
|
|
5
|
+
|
|
6
|
+
from .artifacts import ProjectArtifactsLocator
|
|
7
|
+
from .config import PipelineConfig, ProjectConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectSlurper:
|
|
11
|
+
def __init__(self, project_dir: Path) -> None:
|
|
12
|
+
self.logger = logger.bind()
|
|
13
|
+
self.artifacts_locator = ProjectArtifactsLocator(project_dir)
|
|
14
|
+
try:
|
|
15
|
+
self.user_config: ProjectConfig = ProjectConfig.from_file(self.artifacts_locator.config_file)
|
|
16
|
+
except FileNotFoundError:
|
|
17
|
+
raise UserNotificationException(f"Project configuration file '{self.artifacts_locator.config_file}' not found.") from None
|
|
18
|
+
self.pipeline: PipelineConfig = self.user_config.pipeline
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def project_dir(self) -> Path:
|
|
22
|
+
return self.artifacts_locator.project_root_dir
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
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." " The target directory shall either be empty or not exist.")
|
|
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, bootstrap_only: bool = False, force: bool = False) -> None:
|
|
45
|
+
self.logger = logger.bind()
|
|
46
|
+
self.project_dir = project_dir
|
|
47
|
+
self.bootstrap_only = bootstrap_only
|
|
48
|
+
self.force = force
|
|
49
|
+
|
|
50
|
+
def run(self) -> None:
|
|
51
|
+
self.logger.info(f"Kickstart new project in '{self.project_dir.absolute().as_posix()}'")
|
|
52
|
+
project_builder = ProjectBuilder(self.project_dir)
|
|
53
|
+
project_builder.with_dir("bootstrap")
|
|
54
|
+
if self.bootstrap_only or self.force:
|
|
55
|
+
project_builder.with_disable_target_directory_check()
|
|
56
|
+
if not self.bootstrap_only:
|
|
57
|
+
project_builder.with_dir("project")
|
|
58
|
+
project_builder.build()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.DESCRIPTION
|
|
3
|
+
Script to install python and start the bootstrap.py.
|
|
4
|
+
This file was generated by yanga.
|
|
5
|
+
VERSION: 0.6.1
|
|
6
|
+
#>
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
|
|
10
|
+
###################################################################################################
|
|
11
|
+
# Configuration
|
|
12
|
+
###################################################################################################
|
|
13
|
+
$bootstrapJsonPath = Join-Path $PSScriptRoot "bootstrap.json"
|
|
14
|
+
if (Test-Path $bootstrapJsonPath) {
|
|
15
|
+
$json = Get-Content $bootstrapJsonPath | ConvertFrom-Json
|
|
16
|
+
$config = @{
|
|
17
|
+
pythonVersion = $json.python_version
|
|
18
|
+
scoopInstaller = $json.scoop_installer
|
|
19
|
+
scoopMainBucketBaseUrl = $json.scoop_main_bucket_base_url
|
|
20
|
+
scoopPythonBucketBaseUrl = $json.scoop_python_bucket_base_url
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
$config = @{
|
|
24
|
+
pythonVersion = "3.10"
|
|
25
|
+
scoopInstaller = "https://raw.githubusercontent.com/ScoopInstaller/Install/master/install.ps1"
|
|
26
|
+
scoopMainBucketBaseUrl = "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket"
|
|
27
|
+
scoopPythonBucketBaseUrl = "https://raw.githubusercontent.com/ScoopInstaller/Versions/master/bucket"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
###################################################################################################
|
|
32
|
+
# Utility functions
|
|
33
|
+
###################################################################################################
|
|
34
|
+
|
|
35
|
+
# Update/Reload current environment variable PATH with settings from registry
|
|
36
|
+
Function Initialize-EnvPath {
|
|
37
|
+
# workaround for system-wide installations
|
|
38
|
+
if ($Env:USER_PATH_FIRST) {
|
|
39
|
+
$Env:Path = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "Machine")
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
$Env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Function Invoke-CommandLine {
|
|
47
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Usually this statement must be avoided (https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/avoid-using-invoke-expression?view=powershell-7.3), here it is OK as it does not execute unknown code.')]
|
|
48
|
+
param (
|
|
49
|
+
[Parameter(Mandatory = $true, Position = 0)]
|
|
50
|
+
[string]$CommandLine,
|
|
51
|
+
[Parameter(Mandatory = $false, Position = 1)]
|
|
52
|
+
[bool]$StopAtError = $true,
|
|
53
|
+
[Parameter(Mandatory = $false, Position = 2)]
|
|
54
|
+
[bool]$Silent = $false
|
|
55
|
+
)
|
|
56
|
+
if (-Not $Silent) {
|
|
57
|
+
Write-Output "Executing: $CommandLine"
|
|
58
|
+
}
|
|
59
|
+
$global:LASTEXITCODE = 0
|
|
60
|
+
Invoke-Expression $CommandLine
|
|
61
|
+
if ($global:LASTEXITCODE -ne 0) {
|
|
62
|
+
if ($StopAtError) {
|
|
63
|
+
Write-Error "Command line call `"$CommandLine`" failed with exit code $global:LASTEXITCODE"
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (-Not $Silent) {
|
|
67
|
+
Write-Output "Command line call `"$CommandLine`" failed with exit code $global:LASTEXITCODE, continuing ..."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Function Install-Scoop {
|
|
74
|
+
# Initial Scoop installation
|
|
75
|
+
if (-Not (Get-Command 'scoop' -ErrorAction SilentlyContinue)) {
|
|
76
|
+
$tempDir = [System.IO.Path]::GetTempPath()
|
|
77
|
+
$tempFile = "$tempDir\install.ps1"
|
|
78
|
+
Invoke-RestMethod $config.scoopInstaller -OutFile $tempFile
|
|
79
|
+
if ((New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
80
|
+
& $tempFile -RunAsAdmin
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
& $tempFile
|
|
84
|
+
}
|
|
85
|
+
Initialize-EnvPath
|
|
86
|
+
Remove-Item $tempFile
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Install needed tools
|
|
90
|
+
Invoke-CommandLine "scoop update"
|
|
91
|
+
# Avoid deadlocks while updating scoop buckets
|
|
92
|
+
Invoke-CommandLine "scoop config autostash_on_conflict $true" -Silent $true
|
|
93
|
+
|
|
94
|
+
Initialize-EnvPath
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
###################################################################################################
|
|
98
|
+
# Main
|
|
99
|
+
###################################################################################################
|
|
100
|
+
|
|
101
|
+
# python executable name
|
|
102
|
+
$python = "python" + $config.pythonVersion.Replace(".", "")
|
|
103
|
+
|
|
104
|
+
# Check if scoop is installed
|
|
105
|
+
$scoopPath = (Get-Command scoop -ErrorAction SilentlyContinue).Source
|
|
106
|
+
if ($scoopPath -eq $null) {
|
|
107
|
+
Write-Output "Scoop not found. Trying to install scoop ..."
|
|
108
|
+
Install-Scoop
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
Write-Output "Found scoop under $scoopPath."
|
|
112
|
+
}
|
|
113
|
+
# Check if python is installed
|
|
114
|
+
$pythonPath = (Get-Command $python -ErrorAction SilentlyContinue).Source
|
|
115
|
+
if ($pythonPath -eq $null) {
|
|
116
|
+
Write-Output "$python not found. Try to install $python via scoop ..."
|
|
117
|
+
# Install Python installer dependencies
|
|
118
|
+
Invoke-CommandLine "scoop install $($config.scoopMainBucketBaseUrl)/dark.json"
|
|
119
|
+
Invoke-CommandLine "scoop install $($config.scoopMainBucketBaseUrl)/lessmsi.json"
|
|
120
|
+
# Install python
|
|
121
|
+
Invoke-CommandLine "scoop install $($config.scoopPythonBucketBaseUrl)/$python.json"
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
Write-Output "$python found in $pythonPath"
|
|
125
|
+
# Extract the directory of python exe file and add it to PATH. It needs to be the first entry in PATH
|
|
126
|
+
# such that this version is used when the user calls python and not python311
|
|
127
|
+
$pythonDir = [System.IO.Path]::GetDirectoryName($pythonPath)
|
|
128
|
+
Write-Output "Adding $pythonDir to PATH"
|
|
129
|
+
$Env:Path += ";$pythonDir"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Call the bootstrap.py if it exists with all provided arguments
|
|
133
|
+
$buildScript = Join-Path $PSScriptRoot "bootstrap.py"
|
|
134
|
+
if (Test-Path $buildScript) {
|
|
135
|
+
Write-Output "Calling $buildScript ..."
|
|
136
|
+
& $python $buildScript $args
|
|
137
|
+
}
|