prepare-assignment 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- prepare_assignment-0.3.0/.github/workflows/release.yml +30 -0
- prepare_assignment-0.3.0/.github/workflows/test.yml +63 -0
- prepare_assignment-0.3.0/.gitignore +165 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/PKG-INFO +17 -23
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/cli/main.py +35 -4
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/cli/task.py +1 -1
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/command.py +19 -2
- prepare_assignment-0.3.0/prepare_assignment/core/expression.py +137 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/main.py +11 -5
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/preparer.py +80 -10
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/runner.py +45 -12
- prepare_assignment-0.3.0/prepare_assignment/core/subsituter.py +84 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/task_handler.py +3 -4
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/constants.py +1 -10
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/errors.py +7 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/job_environment.py +2 -1
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/prepare.py +5 -2
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/task_properties.py +2 -2
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/schemas/prepare.schema.json +13 -1
- prepare_assignment-0.3.0/pyproject.toml +49 -0
- prepare_assignment-0.3.0/tests/__init__.py +0 -0
- prepare_assignment-0.3.0/tests/core/__init__.py +0 -0
- prepare_assignment-0.3.0/tests/core/test_command.py +121 -0
- prepare_assignment-0.3.0/tests/core/test_expression.py +212 -0
- prepare_assignment-0.3.0/tests/core/test_preparer.py +159 -0
- prepare_assignment-0.3.0/tests/core/test_resolve_version.py +139 -0
- prepare_assignment-0.3.0/tests/core/test_runner.py +376 -0
- prepare_assignment-0.3.0/tests/core/test_substituter.py +101 -0
- prepare_assignment-0.3.0/tests/core/test_validator.py +140 -0
- prepare_assignment-0.3.0/tests/test_main.py +66 -0
- prepare_assignment-0.3.0/tests/testproject/prepare.yml +22 -0
- prepare_assignment-0.3.0/tests/testproject/solution/pom.xml +19 -0
- prepare_assignment-0.3.0/tests/testproject/solution/src/main/java/gradelist/AssessmentResult.java +36 -0
- prepare_assignment-0.3.0/tests/testproject/solution/src/main/java/gradelist/GradeList.java +40 -0
- prepare_assignment-0.3.0/tests/testproject/solution/src/main/java/gradelist/Test.java +3 -0
- prepare_assignment-0.3.0/tests/testproject/solution/src/test/java/gradelist/AssessmentResultTest.java +65 -0
- prepare_assignment-0.3.0/tests/testproject/solution/src/test/java/gradelist/GradeListTest.java +67 -0
- prepare_assignment-0.3.0/tests/utils/__init__.py +0 -0
- prepare_assignment-0.3.0/tests/utils/test_default_validator.py +39 -0
- prepare_assignment-0.3.0/tests/utils/test_logger.py +90 -0
- prepare_assignment-0.3.0/tests/utils/test_paths.py +98 -0
- prepare_assignment-0.3.0/uv.lock +1401 -0
- prepare_assignment-0.2.0/prepare_assignment/core/subsituter.py +0 -102
- prepare_assignment-0.2.0/pyproject.toml +0 -42
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/LICENSE +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/README.md +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/__init__.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/__main__.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/cli/__init__.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/cli/tasks.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/__init__.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/core/validator.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/__init__.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/config.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/data/task_definition.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/schemas/config.schema.json +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/schemas/task.schema.json +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/schemas/task.schema.json_template +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/__init__.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/config.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/default_validator.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/dependency.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/executables.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/logger.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/paths.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/tasks.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/virtual_env.py +0 -0
- {prepare_assignment-0.2.0 → prepare_assignment-0.3.0}/prepare_assignment/utils/yml_loader.py +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Release Prepare to PyPi
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- 'releases/**'
|
|
7
|
+
tags:
|
|
8
|
+
- 'v*'
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9"]
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v7
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
cache-dependency-glob: "uv.lock"
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
run: uv python install ${{ matrix.python-version }}
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync
|
|
27
|
+
- name: Build
|
|
28
|
+
run: uv build
|
|
29
|
+
- name: Publish
|
|
30
|
+
run: uv publish --token ${{ secrets.PYPI_TOKEN }}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
defaults:
|
|
4
|
+
run:
|
|
5
|
+
shell: bash
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [ "main" ]
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [ "main" ]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
|
|
20
|
+
runs-on: ${{ matrix.os }}
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
checks: write
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@v7
|
|
28
|
+
with:
|
|
29
|
+
enable-cache: true
|
|
30
|
+
cache-dependency-glob: "uv.lock"
|
|
31
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
32
|
+
run: uv python install ${{ matrix.python-version }}
|
|
33
|
+
- name: Install dependencies
|
|
34
|
+
run: uv sync --group test
|
|
35
|
+
- name: Run tests
|
|
36
|
+
env:
|
|
37
|
+
PREPARE_TEST_GIT_MODE: https
|
|
38
|
+
run: uv run pytest --cov --cov-report=xml --junitxml=tests.xml
|
|
39
|
+
- name: Test Report
|
|
40
|
+
uses: mikepenz/action-junit-report@v6
|
|
41
|
+
if: success() || failure()
|
|
42
|
+
with:
|
|
43
|
+
check_name: Test Results (Python version ${{ matrix.python-version }})
|
|
44
|
+
report_paths: tests.xml
|
|
45
|
+
|
|
46
|
+
typecheck:
|
|
47
|
+
strategy:
|
|
48
|
+
matrix:
|
|
49
|
+
python-version: [ "3.12" ]
|
|
50
|
+
runs-on: "ubuntu-latest"
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- name: Install uv
|
|
54
|
+
uses: astral-sh/setup-uv@v7
|
|
55
|
+
with:
|
|
56
|
+
enable-cache: true
|
|
57
|
+
cache-dependency-glob: "uv.lock"
|
|
58
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
59
|
+
run: uv python install ${{ matrix.python-version }}
|
|
60
|
+
- name: Install dependencies
|
|
61
|
+
run: uv sync --group test
|
|
62
|
+
- name: Type checker
|
|
63
|
+
run: uv run mypy prepare_assignment
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Cython debug symbols
|
|
153
|
+
cython_debug/
|
|
154
|
+
|
|
155
|
+
# PyCharm
|
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
|
+
.idea/
|
|
161
|
+
exampleproject/
|
|
162
|
+
tests.xml
|
|
163
|
+
out.txt
|
|
164
|
+
out/
|
|
165
|
+
.vscode
|
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: prepare-assignment
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Prepare assignment
|
|
5
|
+
Author-email: Bonajo <m.bonajo@fontys.nl>
|
|
5
6
|
License: MIT
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Requires-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist: multipledispatch (>=1.0.0,<2.0.0)
|
|
21
|
-
Requires-Dist: prepare-toolbox (==0.4.0)
|
|
22
|
-
Requires-Dist: ruamel-yaml (>=0.18.5,<0.19.0)
|
|
23
|
-
Requires-Dist: treelib (>=1.7.0,<2.0.0)
|
|
24
|
-
Requires-Dist: typer[all] (>=0.9.0,<0.10.0)
|
|
25
|
-
Requires-Dist: virtualenv (>=20.25.0,<21.0.0)
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: dacite>=1.9.2
|
|
10
|
+
Requires-Dist: gitpython>=3.1.41
|
|
11
|
+
Requires-Dist: importlib-resources>=6.1.1
|
|
12
|
+
Requires-Dist: jsonschema>=4.21.1
|
|
13
|
+
Requires-Dist: multipledispatch>=1.0.0
|
|
14
|
+
Requires-Dist: packaging>=24.0
|
|
15
|
+
Requires-Dist: prepare-toolbox==0.4.0
|
|
16
|
+
Requires-Dist: ruamel-yaml>=0.18.5
|
|
17
|
+
Requires-Dist: simpleeval>=1.0.0
|
|
18
|
+
Requires-Dist: treelib>=1.7.0
|
|
19
|
+
Requires-Dist: typer==0.17.3
|
|
20
|
+
Requires-Dist: virtualenv>=20.25.0
|
|
26
21
|
Description-Content-Type: text/markdown
|
|
27
22
|
|
|
28
23
|
# Prepare assignment
|
|
@@ -124,4 +119,3 @@ It is possible to create custom (python/composite) tasks.
|
|
|
124
119
|
3. Validate that the task definition is correct against the [json schema](https://github.com/prepare-assignment/core/blob/main/prepare_assignment/schemas/task.schema.json)
|
|
125
120
|
4. If python task, create a script that implements desired functionality
|
|
126
121
|
|
|
127
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os.path
|
|
2
2
|
import sys
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from typing_extensions import Annotated
|
|
@@ -31,8 +31,9 @@ def main(ctx: typer.Context):
|
|
|
31
31
|
typer.echo(f"Virtual env: {get_virtualenv_name()}")
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
@app.command()
|
|
34
|
+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
35
35
|
def run(
|
|
36
|
+
ctx: typer.Context,
|
|
36
37
|
file_name: Annotated[
|
|
37
38
|
Optional[str],
|
|
38
39
|
typer.Option("--file", "-f", help="Configuration file")
|
|
@@ -48,7 +49,11 @@ def run(
|
|
|
48
49
|
verbose: Annotated[
|
|
49
50
|
int,
|
|
50
51
|
typer.Option("--verbose", "-v", count=True, help="increase task output verbosity")
|
|
51
|
-
] = CONFIG.core.verbose
|
|
52
|
+
] = CONFIG.core.verbose,
|
|
53
|
+
env: Annotated[
|
|
54
|
+
Optional[List[str]],
|
|
55
|
+
typer.Option("-e", "--env", help="Set environment variable (KEY=VALUE)")
|
|
56
|
+
] = None,
|
|
52
57
|
):
|
|
53
58
|
"""
|
|
54
59
|
Parse 'prepare_assignment.y(a)ml' and execute all jobs
|
|
@@ -57,5 +62,31 @@ def run(
|
|
|
57
62
|
CONFIG.core.git_mode = git # type: ignore
|
|
58
63
|
CONFIG.core.verbose = verbose # type: ignore
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
env_vars: Dict[str, str] = {}
|
|
66
|
+
for item in (env or []):
|
|
67
|
+
if "=" not in item:
|
|
68
|
+
raise typer.BadParameter("Environment variables must be in KEY=VALUE format.", param_hint="-e/--env")
|
|
69
|
+
key, value = item.split("=", 1)
|
|
70
|
+
if not key:
|
|
71
|
+
raise typer.BadParameter("Environment variable name cannot be empty.", param_hint="-e/--env")
|
|
72
|
+
env_vars[key] = value
|
|
73
|
+
# ctx.args only contains arguments unknown to Click (known options like --debug,
|
|
74
|
+
# --verbose are consumed by the parser and never appear here)
|
|
75
|
+
for arg in ctx.args:
|
|
76
|
+
if not arg.startswith("--"):
|
|
77
|
+
continue
|
|
78
|
+
key_value = arg[2:]
|
|
79
|
+
if not key_value:
|
|
80
|
+
continue
|
|
81
|
+
if "=" in key_value:
|
|
82
|
+
key, value = key_value.split("=", 1)
|
|
83
|
+
if not key:
|
|
84
|
+
continue
|
|
85
|
+
else:
|
|
86
|
+
key, value = key_value, "true"
|
|
87
|
+
env_vars[key] = value
|
|
61
88
|
|
|
89
|
+
try:
|
|
90
|
+
prepare(file_name, env_vars)
|
|
91
|
+
except Exception:
|
|
92
|
+
raise typer.Exit(code=1)
|
|
@@ -21,6 +21,9 @@ def handle_set_failed(environment: JobEnvironment, params: List[str]) -> None:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def handle_set_output(environment: JobEnvironment, params: List[str]) -> None:
|
|
24
|
+
if environment.current_task_definition is None:
|
|
25
|
+
logger.warning("'set-output' is not supported for shell commands and will be ignored")
|
|
26
|
+
return
|
|
24
27
|
if len(params) < 2:
|
|
25
28
|
raise AssertionError(f"Missing required params for 'set_failed'")
|
|
26
29
|
try:
|
|
@@ -33,11 +36,11 @@ def handle_set_output(environment: JobEnvironment, params: List[str]) -> None:
|
|
|
33
36
|
definition = environment.current_task_definition.outputs.get(key, None) # type: ignore
|
|
34
37
|
if definition is None:
|
|
35
38
|
logger.warning(f"Trying to set output '{key}', but is not defined in task. Skipping for now.")
|
|
36
|
-
|
|
39
|
+
continue
|
|
37
40
|
expected_type = TYPE_MAPPING.get(definition.type, None)
|
|
38
41
|
if expected_type is None or not isinstance(value, expected_type):
|
|
39
42
|
logger.warning(f"Output '{key}' is of type '{type(value)}', but expected '{definition.type}'")
|
|
40
|
-
|
|
43
|
+
continue
|
|
41
44
|
environment.outputs[environment.current_task.key][key] = value # type: ignore
|
|
42
45
|
|
|
43
46
|
|
|
@@ -65,9 +68,23 @@ def handle_debug(environment: JobEnvironment, params: List[str]) -> None:
|
|
|
65
68
|
logger.debug(__handle_message(params[0]))
|
|
66
69
|
|
|
67
70
|
|
|
71
|
+
def handle_set_env(environment: JobEnvironment, params: List[str]) -> None:
|
|
72
|
+
if len(params) < 2:
|
|
73
|
+
raise AssertionError(f"Missing required params for 'set-env'")
|
|
74
|
+
name = unquote_plus(params[0])
|
|
75
|
+
try:
|
|
76
|
+
value = json.loads(params[1])
|
|
77
|
+
except JSONDecodeError:
|
|
78
|
+
raise AssertionError(f"'set-env' expects a JSON-encoded value as second param")
|
|
79
|
+
if not isinstance(value, str):
|
|
80
|
+
raise AssertionError(f"'set-env' expects a string value")
|
|
81
|
+
environment.environment[name] = value
|
|
82
|
+
|
|
83
|
+
|
|
68
84
|
COMMAND_MAPPING = {
|
|
69
85
|
"set-failed": handle_set_failed,
|
|
70
86
|
"set-output": handle_set_output,
|
|
87
|
+
"set-env": handle_set_env,
|
|
71
88
|
"error": handle_error,
|
|
72
89
|
"warning": handle_warning,
|
|
73
90
|
"info": handle_info,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from simpleeval import EvalWithCompoundTypes, InvalidExpression
|
|
5
|
+
|
|
6
|
+
from prepare_assignment.data.job_environment import JobEnvironment
|
|
7
|
+
|
|
8
|
+
_OP_RE = re.compile(r'&&|\|\||!(?!=)')
|
|
9
|
+
# Match attribute access where the name contains hyphens (not valid Python identifiers)
|
|
10
|
+
# e.g. .stripped-files → ["stripped-files"]
|
|
11
|
+
_HYPHEN_ATTR_RE = re.compile(r'\.([a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)+)')
|
|
12
|
+
# Matches quoted strings (to preserve them) OR bare true/false literals to capitalise.
|
|
13
|
+
# Group 1 is set only for bare boolean literals; quoted strings are captured but not replaced.
|
|
14
|
+
_BOOL_RE = re.compile(r"'[^']*'|\"[^\"]*\"|\b(true|false)\b")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _preprocess(expr: str) -> str:
|
|
18
|
+
def replace_op(m: re.Match) -> str: # type: ignore
|
|
19
|
+
token = m.group(0)
|
|
20
|
+
if token == "&&":
|
|
21
|
+
return " and "
|
|
22
|
+
if token == "||":
|
|
23
|
+
return " or "
|
|
24
|
+
return "not "
|
|
25
|
+
|
|
26
|
+
def replace_bool(m: re.Match) -> str: # type: ignore
|
|
27
|
+
# Only replace when group 1 matched (bare literal, not inside quotes)
|
|
28
|
+
if m.group(1) is not None:
|
|
29
|
+
return str(m.group(1)).capitalize()
|
|
30
|
+
return str(m.group(0))
|
|
31
|
+
|
|
32
|
+
expr = _OP_RE.sub(replace_op, expr)
|
|
33
|
+
expr = _HYPHEN_ATTR_RE.sub(lambda m: f'["{m.group(1)}"]', expr)
|
|
34
|
+
expr = _BOOL_RE.sub(replace_bool, expr)
|
|
35
|
+
return expr
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Namespace:
|
|
39
|
+
"""Wraps a dict to allow attribute-style access (e.g. inputs.foo, tasks.step.outputs.bar)."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, d: Dict[str, Any]) -> None:
|
|
42
|
+
object.__setattr__(self, "_d", d)
|
|
43
|
+
|
|
44
|
+
def __getattr__(self, name: str) -> Any:
|
|
45
|
+
if name.startswith("_"):
|
|
46
|
+
raise AttributeError(name)
|
|
47
|
+
d: Dict[str, Any] = object.__getattribute__(self, "_d")
|
|
48
|
+
_MISSING = object()
|
|
49
|
+
val = d.get(name, _MISSING)
|
|
50
|
+
if val is _MISSING:
|
|
51
|
+
raise AttributeError(name)
|
|
52
|
+
return _Namespace(val) if isinstance(val, dict) else val
|
|
53
|
+
|
|
54
|
+
def __getitem__(self, key: str) -> Any:
|
|
55
|
+
d: Dict[str, Any] = object.__getattribute__(self, "_d")
|
|
56
|
+
val = d[key]
|
|
57
|
+
return _Namespace(val) if isinstance(val, dict) else val
|
|
58
|
+
|
|
59
|
+
def __contains__(self, item: Any) -> bool:
|
|
60
|
+
d: Dict[str, Any] = object.__getattribute__(self, "_d")
|
|
61
|
+
return item in d
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _coerce_env(env: Dict[str, str]) -> Dict[str, Any]:
|
|
65
|
+
"""Convert string "true"/"false" env var values to Python booleans."""
|
|
66
|
+
result: Dict[str, Any] = {}
|
|
67
|
+
for k, v in env.items():
|
|
68
|
+
if v.lower() == "true":
|
|
69
|
+
result[k] = True
|
|
70
|
+
elif v.lower() == "false":
|
|
71
|
+
result[k] = False
|
|
72
|
+
else:
|
|
73
|
+
result[k] = v
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_names(environment: JobEnvironment) -> Dict[str, Any]:
|
|
78
|
+
tasks_ctx = {
|
|
79
|
+
step: {"outputs": outputs}
|
|
80
|
+
for step, outputs in environment.outputs.items()
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
"inputs": _Namespace(environment.inputs),
|
|
84
|
+
"env": _Namespace(_coerce_env(environment.environment)),
|
|
85
|
+
"tasks": _Namespace(tasks_ctx),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _contains(collection: Any, item: Any) -> bool:
|
|
90
|
+
return item in collection
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _starts_with(s: str, prefix: str) -> bool:
|
|
94
|
+
return s.startswith(prefix)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ends_with(s: str, suffix: str) -> bool:
|
|
98
|
+
return s.endswith(suffix)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_functions(environment: JobEnvironment) -> Dict[str, Any]:
|
|
102
|
+
return {
|
|
103
|
+
"contains": _contains,
|
|
104
|
+
"startsWith": _starts_with,
|
|
105
|
+
"endsWith": _ends_with,
|
|
106
|
+
"success": lambda: not environment.job_failed,
|
|
107
|
+
"failure": lambda: environment.job_failed,
|
|
108
|
+
"always": lambda: True,
|
|
109
|
+
"never": lambda: False,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def evaluate(expr: str, environment: JobEnvironment) -> Any:
|
|
114
|
+
"""
|
|
115
|
+
Evaluate an expression, optionally wrapped in ${{ }}.
|
|
116
|
+
Returns the typed result.
|
|
117
|
+
"""
|
|
118
|
+
stripped = expr.strip()
|
|
119
|
+
m = re.fullmatch(r'\${{\s*(.*?)\s*}}', stripped, re.DOTALL)
|
|
120
|
+
if m:
|
|
121
|
+
stripped = m.group(1).strip()
|
|
122
|
+
|
|
123
|
+
processed = _preprocess(stripped)
|
|
124
|
+
names = _build_names(environment)
|
|
125
|
+
evaluator = EvalWithCompoundTypes(names=names, functions=_build_functions(environment))
|
|
126
|
+
return evaluator.eval(processed)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def evaluate_condition(expr: str, environment: JobEnvironment) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Evaluate an expression as a boolean condition.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
result = evaluate(expr, environment)
|
|
135
|
+
return bool(result)
|
|
136
|
+
except (InvalidExpression, Exception):
|
|
137
|
+
return False
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
4
|
+
from typing import Dict, Optional
|
|
5
5
|
|
|
6
6
|
from prepare_toolbox.file import get_matching_files
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ from prepare_assignment.core.preparer import prepare_tasks
|
|
|
9
9
|
from prepare_assignment.core.runner import run
|
|
10
10
|
from prepare_assignment.core.validator import validate_prepare
|
|
11
11
|
from prepare_assignment.data.constants import CONFIG
|
|
12
|
-
from prepare_assignment.data.errors import PrepareTaskError, PrepareError
|
|
12
|
+
from prepare_assignment.data.errors import PrepareTaskError, PrepareError, TaskExecutionError
|
|
13
13
|
from prepare_assignment.data.prepare import Prepare
|
|
14
14
|
from prepare_assignment.utils.logger import add_logging_level, set_logger_level
|
|
15
15
|
from prepare_assignment.utils.yml_loader import YAML_LOADER
|
|
@@ -40,7 +40,8 @@ def __get_prepare_file(file: Optional[str]) -> str:
|
|
|
40
40
|
return file
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def prepare(file_name: Optional[str]) -> None:
|
|
43
|
+
def prepare(file_name: Optional[str], env_vars: Optional[Dict[str, str]] = None) -> None:
|
|
44
|
+
env_vars = env_vars or {}
|
|
44
45
|
# Set the logger
|
|
45
46
|
add_logging_level("TRACE", logging.DEBUG - 5, "trace")
|
|
46
47
|
logger = logging.getLogger("prepare_assignment")
|
|
@@ -68,12 +69,17 @@ def prepare(file_name: Optional[str]) -> None:
|
|
|
68
69
|
prepare = Prepare.of(yaml)
|
|
69
70
|
|
|
70
71
|
# Execute
|
|
71
|
-
run(prepare, mapping)
|
|
72
|
+
run(prepare, mapping, env_vars)
|
|
73
|
+
except TaskExecutionError as e:
|
|
74
|
+
logger.error(e.message)
|
|
75
|
+
raise
|
|
72
76
|
except PrepareTaskError as PE:
|
|
73
77
|
logger.error(PE.message)
|
|
74
78
|
if isinstance(PE.cause, PrepareError):
|
|
75
79
|
logger.error(PE.cause.message)
|
|
76
80
|
else:
|
|
77
81
|
logger.error(str(PE.cause))
|
|
82
|
+
raise
|
|
78
83
|
except Exception as e:
|
|
79
|
-
logger.error(str(e))
|
|
84
|
+
logger.error(str(e))
|
|
85
|
+
raise
|