openjd-cli 0.7.5__tar.gz → 0.7.7__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.
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/PKG-INFO +3 -3
- openjd_cli-0.7.7/_version.py +24 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/hatch_version_hook.py +0 -1
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/pyproject.toml +12 -2
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/__main__.py +0 -1
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_check/_check_command.py +4 -1
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/__init__.py +9 -2
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/_extensions.py +12 -3
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/_job_from_template.py +24 -6
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/_validation_utils.py +17 -3
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_create_argparser.py +0 -1
- openjd_cli-0.7.7/src/openjd/cli/_run/_local_session/_actions.py +174 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_session_manager.py +164 -20
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_run_command.py +36 -6
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_summary/_summary_command.py +1 -1
- openjd_cli-0.7.7/src/openjd/cli/_version.py +24 -0
- openjd_cli-0.7.5/_version.py +0 -34
- openjd_cli-0.7.5/src/openjd/cli/_run/_local_session/_actions.py +0 -89
- openjd_cli-0.7.5/src/openjd/cli/_version.py +0 -34
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/.gitignore +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/LICENSE +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/NOTICE +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/README.md +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/hatch.toml +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/__init__.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_check/__init__.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/__init__.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_help_formatter.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/__init__.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_logs.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_schema/__init__.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_schema/_schema_command.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_summary/__init__.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_summary/_summary_output.py +0 -0
- {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openjd-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.7
|
|
4
4
|
Summary: Provides a command-line interface for working with Open Job Description templates.
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenJobDescription/openjd-cli
|
|
6
6
|
Project-URL: Source, https://github.com/OpenJobDescription/openjd-cli
|
|
@@ -22,8 +22,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
|
-
Requires-Dist: openjd-model<0.
|
|
26
|
-
Requires-Dist: openjd-sessions<0.
|
|
25
|
+
Requires-Dist: openjd-model<0.12,>=0.11.4
|
|
26
|
+
Requires-Dist: openjd-sessions<0.13,>=0.12.0
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
## Open Job Description - CLI
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.7.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 7)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -7,7 +7,6 @@ from dataclasses import dataclass
|
|
|
7
7
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
8
8
|
from typing import Any, Optional
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
_logger = logging.Logger(__name__, logging.INFO)
|
|
12
11
|
_stdout_handler = logging.StreamHandler(sys.stdout)
|
|
13
12
|
_stdout_handler.addFilter(lambda record: record.levelno <= logging.INFO)
|
|
@@ -29,8 +29,18 @@ classifiers = [
|
|
|
29
29
|
"Intended Audience :: End Users/Desktop"
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# 0.12.0 is the first release carrying `resolved_symtab` on all three of
|
|
33
|
+
# Session.enter_environment, Session.exit_environment and Session.run_task.
|
|
34
|
+
# It is also the release that removed `extra_let_bindings` from
|
|
35
|
+
# enter_environment: 0.10.11 through 0.11.0 have that keyword and no
|
|
36
|
+
# `resolved_symtab`, so there is no version satisfying both channels and the
|
|
37
|
+
# floor has to move rather than being feature-detected. Below 0.12.0 every
|
|
38
|
+
# call site here raises TypeError on the unexpected `resolved_symtab`.
|
|
39
|
+
"openjd-sessions >= 0.12.0,< 0.13",
|
|
40
|
+
# 0.11.4 is the first release exporting `create_job_with_symbol_tables` and
|
|
41
|
+
# `JobWithSymbolTables`. 0.11.3 and below have `create_job` only, so the
|
|
42
|
+
# step-scope resolved symbol tables cannot be obtained at all.
|
|
43
|
+
"openjd-model >= 0.11.4,< 0.12"
|
|
34
44
|
]
|
|
35
45
|
|
|
36
46
|
[project.urls]
|
|
@@ -41,7 +41,10 @@ def do_check(args: Namespace) -> OpenJDCliResult:
|
|
|
41
41
|
if TemplateSpecificationVersion.is_job_template(template_version):
|
|
42
42
|
decode_job_template(template=template_object, supported_extensions=extensions)
|
|
43
43
|
elif TemplateSpecificationVersion.is_environment_template(template_version):
|
|
44
|
-
|
|
44
|
+
# Pass `extensions` so env templates can declare e.g.
|
|
45
|
+
# WRAP_ACTIONS (RFC 0008) — without this the model rejects
|
|
46
|
+
# the extension declaration even though the CLI accepts it.
|
|
47
|
+
decode_environment_template(template=template_object, supported_extensions=extensions)
|
|
45
48
|
else:
|
|
46
49
|
return OpenJDCliResult(
|
|
47
50
|
status="error",
|
|
@@ -4,7 +4,7 @@ from argparse import ArgumentParser, Namespace, _SubParsersAction
|
|
|
4
4
|
from dataclasses import asdict, dataclass
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Callable, Literal
|
|
7
|
+
from typing import TYPE_CHECKING, Callable, Literal
|
|
8
8
|
import json
|
|
9
9
|
import yaml
|
|
10
10
|
import os
|
|
@@ -23,6 +23,10 @@ from ._validation_utils import (
|
|
|
23
23
|
)
|
|
24
24
|
from openjd.model import DecodeValidationError, Job, JobParameterValues, EnvironmentTemplate
|
|
25
25
|
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
# Annotations only; see the note in _job_from_template.py.
|
|
28
|
+
from openjd.expr import SerializedSymbolTable
|
|
29
|
+
|
|
26
30
|
__all__ = [
|
|
27
31
|
"add_extensions_argument",
|
|
28
32
|
"get_doc_type",
|
|
@@ -118,7 +122,10 @@ def generate_job(
|
|
|
118
122
|
environments: list[EnvironmentTemplate] = [],
|
|
119
123
|
*,
|
|
120
124
|
supported_extensions: list[str],
|
|
121
|
-
) -> tuple[Job, JobParameterValues]:
|
|
125
|
+
) -> tuple[Job, JobParameterValues, dict[str, "SerializedSymbolTable"]]:
|
|
126
|
+
"""Returns the job, its parameter values, and the per-step resolved symbol
|
|
127
|
+
tables keyed by step name. See :func:`job_from_template` for why the tables
|
|
128
|
+
must be carried alongside the job rather than discarded."""
|
|
122
129
|
try:
|
|
123
130
|
# Raises: RuntimeError, DecodeValidationError
|
|
124
131
|
template = read_job_template(args.path, supported_extensions=supported_extensions)
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
from argparse import ArgumentParser
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
-
# This is the list of Open Job Description extensions with implemented support
|
|
7
|
-
|
|
6
|
+
# This is the list of Open Job Description extensions with implemented support.
|
|
7
|
+
# EXPR (RFCs 0005/0006) and WRAP_ACTIONS (RFC 0008) are added so the CLI can run
|
|
8
|
+
# templates that declare them. WRAP_ACTIONS implies EXPR; the model library
|
|
9
|
+
# handles that implication when it parses the template.
|
|
10
|
+
SUPPORTED_EXTENSIONS = [
|
|
11
|
+
"TASK_CHUNKING",
|
|
12
|
+
"REDACTED_ENV_VARS",
|
|
13
|
+
"FEATURE_BUNDLE_1",
|
|
14
|
+
"EXPR",
|
|
15
|
+
"WRAP_ACTIONS",
|
|
16
|
+
]
|
|
8
17
|
|
|
9
18
|
|
|
10
19
|
def add_extensions_argument(run_parser: ArgumentParser):
|
|
11
20
|
run_parser.add_argument(
|
|
12
21
|
"--extensions",
|
|
13
|
-
help=f"A comma-separated list of Open Job Description extension names
|
|
22
|
+
help=f"A comma-separated list of Open Job Description extension names that the CLI will accept from job templates. Templates declare which extensions they use. Defaults to all that are implemented: {','.join(SUPPORTED_EXTENSIONS)}",
|
|
14
23
|
)
|
|
15
24
|
|
|
16
25
|
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import re
|
|
7
|
-
from typing import Optional, Union
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
10
|
from ._validation_utils import get_doc_type
|
|
@@ -15,10 +15,17 @@ from openjd.model import (
|
|
|
15
15
|
Job,
|
|
16
16
|
JobParameterValues,
|
|
17
17
|
JobTemplate,
|
|
18
|
-
|
|
18
|
+
create_job_with_symbol_tables,
|
|
19
19
|
preprocess_job_parameters,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
# Only for annotations: `openjd.expr` is a facade over the native
|
|
24
|
+
# extension, and importing the CLI must not load it. The tables the model
|
|
25
|
+
# hands back are already instances of this type, so nothing here
|
|
26
|
+
# constructs one.
|
|
27
|
+
from openjd.expr import SerializedSymbolTable
|
|
28
|
+
|
|
22
29
|
|
|
23
30
|
def get_params_from_file(parameter_string: str) -> Union[dict, list]:
|
|
24
31
|
"""
|
|
@@ -111,10 +118,18 @@ def job_from_template(
|
|
|
111
118
|
parameter_args: list[str] | None,
|
|
112
119
|
job_template_dir: Path,
|
|
113
120
|
current_working_dir: Path,
|
|
114
|
-
) -> tuple[Job, JobParameterValues]:
|
|
121
|
+
) -> tuple[Job, JobParameterValues, dict[str, "SerializedSymbolTable"]]:
|
|
115
122
|
"""
|
|
116
123
|
Given a decoded Job Template and a user-input parameter dictionary,
|
|
117
|
-
generates a Job object
|
|
124
|
+
generates a Job object, the parameter values for running the job, and the
|
|
125
|
+
per-step resolved symbol tables, keyed by step name.
|
|
126
|
+
|
|
127
|
+
A step's template-scope `let` bindings (RFC 0005 §3.6) are evaluated once
|
|
128
|
+
here, at job creation, and their resolved values reach a session only
|
|
129
|
+
through those tables -- neither the model nor the session re-derives them
|
|
130
|
+
from the source expressions. Sessions for a step must therefore be given
|
|
131
|
+
`step_symbol_tables[step.name]`, or the step's `let` produces no bindings
|
|
132
|
+
at all.
|
|
118
133
|
|
|
119
134
|
Raises: RuntimeError if parameters are an unsupported type or don't correspond to the template
|
|
120
135
|
"""
|
|
@@ -132,11 +147,14 @@ def job_from_template(
|
|
|
132
147
|
raise RuntimeError(str(ve))
|
|
133
148
|
|
|
134
149
|
try:
|
|
135
|
-
|
|
150
|
+
# `create_job_with_symbol_tables` returns the same Job that
|
|
151
|
+
# `create_job` does; it additionally returns the symbol tables that
|
|
152
|
+
# instantiation built instead of discarding them.
|
|
153
|
+
created = create_job_with_symbol_tables(
|
|
136
154
|
job_template=template,
|
|
137
155
|
job_parameter_values=parameter_values,
|
|
138
156
|
environment_templates=environments,
|
|
139
157
|
)
|
|
140
|
-
return (job, parameter_values)
|
|
158
|
+
return (created.job, parameter_values, created.step_symbol_tables)
|
|
141
159
|
except DecodeValidationError as dve:
|
|
142
160
|
raise RuntimeError(f"Could not generate Job from template and parameters: {str(dve)}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Optional
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from openjd.model import (
|
|
@@ -70,15 +70,29 @@ def read_job_template(template_file: Path, *, supported_extensions: list[str]) -
|
|
|
70
70
|
return template
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def read_environment_template(
|
|
73
|
+
def read_environment_template(
|
|
74
|
+
template_file: Path,
|
|
75
|
+
*,
|
|
76
|
+
supported_extensions: Optional[list[str]] = None,
|
|
77
|
+
) -> EnvironmentTemplate:
|
|
74
78
|
"""Open a JSON or YAML-formatted file and attempt to parse it into an EnvironmentTemplate object.
|
|
75
79
|
Raises a RuntimeError if the file doesn't exist or can't be opened, and raises a
|
|
76
80
|
DecodeValidationError if its contents can't be parsed into a valid EnvironmentTemplate.
|
|
81
|
+
|
|
82
|
+
Environment templates may declare extensions just like job templates do
|
|
83
|
+
(e.g. ``WRAP_ACTIONS`` for RFC 0008's wrap hooks). Pass the CLI's allow-list
|
|
84
|
+
via ``supported_extensions`` so the model accepts those declarations;
|
|
85
|
+
omitting the argument preserves the legacy behavior of accepting only the
|
|
86
|
+
default model surface.
|
|
77
87
|
"""
|
|
78
88
|
# Raises RuntimeError
|
|
79
89
|
template_object = read_template(template_file)
|
|
80
90
|
|
|
91
|
+
decode_kwargs: dict[str, list[str]] = {}
|
|
92
|
+
if supported_extensions is not None:
|
|
93
|
+
decode_kwargs["supported_extensions"] = supported_extensions
|
|
94
|
+
|
|
81
95
|
# Raises: DecodeValidationError
|
|
82
|
-
template = decode_environment_template(template=template_object)
|
|
96
|
+
template = decode_environment_template(template=template_object, **decode_kwargs)
|
|
83
97
|
|
|
84
98
|
return template
|
|
@@ -14,7 +14,6 @@ from ._summary import populate_argparser as populate_summary_subparser
|
|
|
14
14
|
from ._run import populate_argparser as populate_run_subparser
|
|
15
15
|
from ._schema import populate_argparser as populate_schema_subparser
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
# Our CLI subcommand construction requires that all leaf subcommands define a default
|
|
19
18
|
# 'func' property which is a Callable[[],None] that implements the subcommand.
|
|
20
19
|
# After parsing, we call that `func` argument of the resulting args object.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from openjd.model import Step, TaskParameterSet
|
|
7
|
+
from openjd.model.v2023_09 import Environment
|
|
8
|
+
from openjd.sessions import Session
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
# Annotations only: `openjd.expr` is a facade over the native extension and
|
|
12
|
+
# importing the CLI must not load it. These tables are produced by
|
|
13
|
+
# `create_job_with_symbol_tables` and forwarded unchanged.
|
|
14
|
+
from openjd.expr import SerializedSymbolTable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnvironmentType(str, Enum):
|
|
18
|
+
"""
|
|
19
|
+
The three different types of environment types that can be entered/exited in a session.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
EXTERNAL = "EXTERNAL"
|
|
23
|
+
JOB = "JOB"
|
|
24
|
+
STEP = "STEP"
|
|
25
|
+
ALL = "ALL"
|
|
26
|
+
|
|
27
|
+
def matches(self, other: "EnvironmentType") -> bool:
|
|
28
|
+
"""Environment types match if they are equal, or one of them is ALL."""
|
|
29
|
+
return self == other or self == EnvironmentType.ALL or other == EnvironmentType.ALL
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SessionAction:
|
|
33
|
+
_session: Session
|
|
34
|
+
duration: float
|
|
35
|
+
|
|
36
|
+
def __init__(self, session: Session):
|
|
37
|
+
self._session = session
|
|
38
|
+
|
|
39
|
+
def run(self):
|
|
40
|
+
"""
|
|
41
|
+
Subclasses of `SessionAction` should have
|
|
42
|
+
custom implementations of this depending on their type.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RunTaskAction(SessionAction):
|
|
47
|
+
_step: Step
|
|
48
|
+
_parameters: TaskParameterSet
|
|
49
|
+
_resolved_symtab: Optional["SerializedSymbolTable"]
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
session: Session,
|
|
54
|
+
step: Step,
|
|
55
|
+
parameters: TaskParameterSet,
|
|
56
|
+
resolved_symtab: Optional["SerializedSymbolTable"] = None,
|
|
57
|
+
):
|
|
58
|
+
super(RunTaskAction, self).__init__(session)
|
|
59
|
+
self._step = step
|
|
60
|
+
self._parameters = parameters
|
|
61
|
+
# The step's create-time resolved symbol table
|
|
62
|
+
# (`create_job_with_symbol_tables().step_symbol_tables[step.name]`).
|
|
63
|
+
# It is the only channel for the step's template-scope `let` values:
|
|
64
|
+
# the model does not merge them into `script.let`, and the session does
|
|
65
|
+
# not re-evaluate the source expressions.
|
|
66
|
+
self._resolved_symtab = resolved_symtab
|
|
67
|
+
|
|
68
|
+
def run(self):
|
|
69
|
+
self._session.run_task(
|
|
70
|
+
step_script=self._step.script,
|
|
71
|
+
task_parameter_values=self._parameters,
|
|
72
|
+
# RFC 0008: the step name feeds the WrappedStep.Name template
|
|
73
|
+
# variable inside an active onWrapTaskRun hook.
|
|
74
|
+
step_name=self._step.name,
|
|
75
|
+
resolved_symtab=self._resolved_symtab,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __str__(self):
|
|
79
|
+
parameters = {name: parameter.value for name, parameter in self._parameters.items()}
|
|
80
|
+
return f"Run Step '{self._step.name}' with Task parameters '{str(parameters)}'"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class EnterEnvironmentAction(SessionAction):
|
|
84
|
+
_environment: Environment
|
|
85
|
+
_id: str
|
|
86
|
+
_resolved_symtab: Optional["SerializedSymbolTable"]
|
|
87
|
+
_step_name: Optional[str]
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
session: Session,
|
|
92
|
+
environment: Environment,
|
|
93
|
+
env_id: str,
|
|
94
|
+
resolved_symtab: Optional["SerializedSymbolTable"] = None,
|
|
95
|
+
step_name: Optional[str] = None,
|
|
96
|
+
):
|
|
97
|
+
super(EnterEnvironmentAction, self).__init__(session)
|
|
98
|
+
self._environment = environment
|
|
99
|
+
self._id = env_id
|
|
100
|
+
# RFC 0005 §3.6: a step's environments are entered with the step's
|
|
101
|
+
# create-time resolved symbol table, which carries the step's
|
|
102
|
+
# template-scope `let` values so their variables/actions can reference
|
|
103
|
+
# them. Only step-environment enters have one; job and external
|
|
104
|
+
# environment enters leave it None.
|
|
105
|
+
self._resolved_symtab = resolved_symtab
|
|
106
|
+
# RFC 0007 §7.3.1 (EXPR): the owning step's name seeds Step.Name for
|
|
107
|
+
# a step environment's `let` bindings, variables, and actions. Only
|
|
108
|
+
# step-environment enters carry a step name; job/external enters
|
|
109
|
+
# leave it None.
|
|
110
|
+
self._step_name = step_name
|
|
111
|
+
|
|
112
|
+
def run(self):
|
|
113
|
+
# `step_name` is omitted rather than passed as None when this enter has
|
|
114
|
+
# no owning step. Passing None would be equivalent today, since
|
|
115
|
+
# enter_environment skips a None `step_name`, but omitting it keeps the
|
|
116
|
+
# call site's intent legible at the boundary and it is what
|
|
117
|
+
# test_localsession_step_env_enter_receives_step_name asserts — that
|
|
118
|
+
# assertion is currently the only check that job and external enters
|
|
119
|
+
# do not seed Step.Name, because a template referencing Step.Name
|
|
120
|
+
# outside a step is rejected by static validation before any session
|
|
121
|
+
# is built, so no loadable template can observe it.
|
|
122
|
+
#
|
|
123
|
+
# `resolved_symtab` is passed unconditionally: None is its documented
|
|
124
|
+
# "no table" value and there is no analogous assertion keyed on its
|
|
125
|
+
# absence.
|
|
126
|
+
optional_kwargs: dict[str, str] = {}
|
|
127
|
+
if self._step_name is not None:
|
|
128
|
+
optional_kwargs["step_name"] = self._step_name
|
|
129
|
+
self._session.enter_environment(
|
|
130
|
+
environment=self._environment,
|
|
131
|
+
identifier=self._id,
|
|
132
|
+
resolved_symtab=self._resolved_symtab,
|
|
133
|
+
**optional_kwargs,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def __str__(self):
|
|
137
|
+
return f"Enter Environment '{self._environment.name}'"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ExitEnvironmentAction(SessionAction):
|
|
141
|
+
_id: str
|
|
142
|
+
_keep_session_running: bool
|
|
143
|
+
_resolved_symtab: Optional["SerializedSymbolTable"]
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
session: Session,
|
|
148
|
+
id: str,
|
|
149
|
+
keep_session_running: bool,
|
|
150
|
+
resolved_symtab: Optional["SerializedSymbolTable"] = None,
|
|
151
|
+
):
|
|
152
|
+
super(ExitEnvironmentAction, self).__init__(session)
|
|
153
|
+
self._id = id
|
|
154
|
+
self._keep_session_running = keep_session_running
|
|
155
|
+
# The same table the environment was entered with, so its onExit
|
|
156
|
+
# resolves in the same scope as its onEnter (what
|
|
157
|
+
# Session.exit_environment documents for this argument).
|
|
158
|
+
#
|
|
159
|
+
# Unlike the worker agent, the CLI holds the table as an object rather
|
|
160
|
+
# than a JSON string served by the service, so there is no parse step
|
|
161
|
+
# here that could fail and no wrapper degrading a parse failure to None
|
|
162
|
+
# to keep teardown unconditional. An environment entered without a
|
|
163
|
+
# table simply exits without one.
|
|
164
|
+
self._resolved_symtab = resolved_symtab
|
|
165
|
+
|
|
166
|
+
def run(self):
|
|
167
|
+
self._session.exit_environment(
|
|
168
|
+
identifier=self._id,
|
|
169
|
+
keep_session_running=self._keep_session_running,
|
|
170
|
+
resolved_symtab=self._resolved_symtab,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def __str__(self):
|
|
174
|
+
return f"Exit Environment '{self._id}'"
|
{openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_session_manager.py
RENAMED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
from queue import Queue
|
|
4
4
|
from threading import Event
|
|
5
5
|
import time
|
|
6
|
-
from typing import Any, Iterable, Optional, Type
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Iterable, Optional, Type
|
|
7
7
|
from types import FrameType, TracebackType
|
|
8
8
|
from signal import signal, SIGINT, SIGTERM, SIG_DFL
|
|
9
9
|
from itertools import islice
|
|
10
10
|
from datetime import datetime, timedelta, timezone
|
|
11
|
+
from math import isfinite
|
|
11
12
|
|
|
12
13
|
from ._actions import (
|
|
13
14
|
EnterEnvironmentAction,
|
|
@@ -38,6 +39,10 @@ from openjd.sessions import (
|
|
|
38
39
|
PathMappingRule,
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
# Annotations only; see the note in _actions.py.
|
|
44
|
+
from openjd.expr import SerializedSymbolTable
|
|
45
|
+
|
|
41
46
|
|
|
42
47
|
class LocalSessionFailed(RuntimeError):
|
|
43
48
|
"""
|
|
@@ -49,6 +54,50 @@ class LocalSessionFailed(RuntimeError):
|
|
|
49
54
|
super().__init__(f"Action failed: {failed_action}")
|
|
50
55
|
|
|
51
56
|
|
|
57
|
+
def _calculate_adaptive_chunk_size(
|
|
58
|
+
*,
|
|
59
|
+
current_chunk_size: int,
|
|
60
|
+
completed_task_count: int,
|
|
61
|
+
completed_task_duration: float,
|
|
62
|
+
target_runtime_seconds: float,
|
|
63
|
+
) -> Optional[int]:
|
|
64
|
+
"""The adaptive-chunking size estimate, or ``None`` to defer adjusting.
|
|
65
|
+
|
|
66
|
+
Implements steps 3-7 of the adaptive chunking algorithm in the Open Job
|
|
67
|
+
Description CLI specification (``specs/cli/run.md`` in openjd-rs): if the
|
|
68
|
+
cumulative duration is zero or non-finite, keep the current chunk size and
|
|
69
|
+
wait for a measurable sample.
|
|
70
|
+
|
|
71
|
+
Deferring matters because a zero ``duration_per_task`` makes the ideal size
|
|
72
|
+
unrepresentable. The two implementations fail differently without this
|
|
73
|
+
guard: the Rust CLI produces ``inf``, which saturates to a huge chunk size,
|
|
74
|
+
while Python raises ``ZeroDivisionError`` out of the run. Neither is
|
|
75
|
+
reachable through a normal ``openjd run`` today -- ``run_task`` always
|
|
76
|
+
spawns and waits on a real subprocess, so a measured chunk duration of
|
|
77
|
+
exactly ``0.0`` would need that round trip to complete inside one
|
|
78
|
+
``perf_counter`` tick (~42 ns here) -- so this is spec conformance and
|
|
79
|
+
hardening rather than a fix for an observed failure.
|
|
80
|
+
|
|
81
|
+
Extracted as a module-level function, mirroring openjd-rs's
|
|
82
|
+
``calculate_adaptive_chunk_size``, so the deferral is unit-testable without
|
|
83
|
+
needing to provoke an unreachable timing condition end to end.
|
|
84
|
+
"""
|
|
85
|
+
if (
|
|
86
|
+
completed_task_count <= 0
|
|
87
|
+
or completed_task_duration <= 0.0
|
|
88
|
+
or not isfinite(completed_task_duration)
|
|
89
|
+
):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
duration_per_task = completed_task_duration / completed_task_count
|
|
93
|
+
adaptive_chunk_size = target_runtime_seconds / duration_per_task
|
|
94
|
+
if completed_task_count < 10 and adaptive_chunk_size > current_chunk_size:
|
|
95
|
+
# When we have data about only a few tasks, gradually blend in the new
|
|
96
|
+
# estimate instead of cutting over immediately.
|
|
97
|
+
adaptive_chunk_size = 0.75 * current_chunk_size + 0.25 * adaptive_chunk_size
|
|
98
|
+
return max(int(adaptive_chunk_size), 1)
|
|
99
|
+
|
|
100
|
+
|
|
52
101
|
class LocalSession:
|
|
53
102
|
"""
|
|
54
103
|
A class to manage a `Session` object from the `sessions` module,
|
|
@@ -73,6 +122,8 @@ class LocalSession:
|
|
|
73
122
|
_path_mapping_rules: Optional[list[PathMappingRule]]
|
|
74
123
|
_environments: Optional[list[Any]]
|
|
75
124
|
_environments_entered: list[tuple[EnvironmentType, str]]
|
|
125
|
+
_step_symbol_tables: dict[str, "SerializedSymbolTable"]
|
|
126
|
+
_entered_env_symtabs: dict[str, "SerializedSymbolTable"]
|
|
76
127
|
_log_handler: LocalSessionLogHandler
|
|
77
128
|
_cleanup_called: bool
|
|
78
129
|
|
|
@@ -82,6 +133,7 @@ class LocalSession:
|
|
|
82
133
|
job: Job,
|
|
83
134
|
job_parameter_values: JobParameterValues,
|
|
84
135
|
session_id: str,
|
|
136
|
+
step_symbol_tables: Optional[dict[str, "SerializedSymbolTable"]] = None,
|
|
85
137
|
timestamp_format: LoggingTimestampFormat = LoggingTimestampFormat.RELATIVE,
|
|
86
138
|
path_mapping_rules: Optional[list[PathMappingRule]] = None,
|
|
87
139
|
environments: Optional[list[Any]] = None,
|
|
@@ -97,11 +149,28 @@ class LocalSession:
|
|
|
97
149
|
self._timestamp_format = timestamp_format
|
|
98
150
|
self._path_mapping_rules = path_mapping_rules
|
|
99
151
|
self._environments = environments
|
|
152
|
+
# The create-time resolved symbol tables from
|
|
153
|
+
# `create_job_with_symbol_tables`, keyed by step name. A step's
|
|
154
|
+
# template-scope `let` (RFC 0005 §3.6) is evaluated once at job
|
|
155
|
+
# creation, and this is the only channel its resolved values have into
|
|
156
|
+
# a session: the model does not fold them into `script.let` and the
|
|
157
|
+
# session does not re-evaluate the source expressions. A caller that
|
|
158
|
+
# omits them gets a session in which every step-level `let` is simply
|
|
159
|
+
# undefined.
|
|
160
|
+
self._step_symbol_tables = step_symbol_tables or {}
|
|
161
|
+
# The table each entered environment was entered with, keyed by
|
|
162
|
+
# environment identifier, so its exit can be given the same one --
|
|
163
|
+
# `Session.exit_environment` documents that as the way an onExit
|
|
164
|
+
# resolves in the same scope as its onEnter. Environments entered
|
|
165
|
+
# without a table are simply absent.
|
|
166
|
+
self._entered_env_symtabs = {}
|
|
100
167
|
|
|
101
168
|
# Create an OpenJD Session
|
|
102
169
|
self._openjd_session = Session(
|
|
103
170
|
session_id=self.session_id,
|
|
104
171
|
job_parameter_values=job_parameter_values,
|
|
172
|
+
# RFC 0007 §7.3.1 (EXPR): seeds the Job.Name template variable.
|
|
173
|
+
job_name=str(job.name),
|
|
105
174
|
path_mapping_rules=self._path_mapping_rules,
|
|
106
175
|
callback=self._action_callback,
|
|
107
176
|
retain_working_dir=retain_working_dir,
|
|
@@ -197,7 +266,14 @@ class LocalSession:
|
|
|
197
266
|
LOG.info("Interruption signal recieved.")
|
|
198
267
|
self.cancel()
|
|
199
268
|
|
|
200
|
-
def run_environment_enters(
|
|
269
|
+
def run_environment_enters(
|
|
270
|
+
self,
|
|
271
|
+
environments: Optional[list[Any]],
|
|
272
|
+
type: EnvironmentType,
|
|
273
|
+
*,
|
|
274
|
+
resolved_symtab: Optional["SerializedSymbolTable"] = None,
|
|
275
|
+
step_name: Optional[str] = None,
|
|
276
|
+
):
|
|
201
277
|
"""Enter one or more environments in the session."""
|
|
202
278
|
if environments is None:
|
|
203
279
|
return
|
|
@@ -211,10 +287,55 @@ class LocalSession:
|
|
|
211
287
|
env_id = f"{type.name} - {env.name}"
|
|
212
288
|
self._action_ended.clear()
|
|
213
289
|
self._current_action = EnterEnvironmentAction(
|
|
214
|
-
session=self._openjd_session,
|
|
290
|
+
session=self._openjd_session,
|
|
291
|
+
environment=env,
|
|
292
|
+
env_id=env_id,
|
|
293
|
+
# RFC 0005 §3.6: a step's environments see the step's resolved
|
|
294
|
+
# template-scope `let` values through its symbol table.
|
|
295
|
+
resolved_symtab=resolved_symtab,
|
|
296
|
+
# RFC 0007 §7.3.1 (EXPR): a step's environments see Step.Name.
|
|
297
|
+
# Only step-environment enters carry a step name.
|
|
298
|
+
step_name=step_name,
|
|
215
299
|
)
|
|
216
300
|
self._environments_entered.append((type, env_id))
|
|
217
|
-
|
|
301
|
+
# Recorded before the enter runs, and deliberately not undone on
|
|
302
|
+
# failure: whether the enter succeeds or not, the exit that follows
|
|
303
|
+
# must be given the table the enter was attempted with.
|
|
304
|
+
if resolved_symtab is not None:
|
|
305
|
+
self._entered_env_symtabs[env_id] = resolved_symtab
|
|
306
|
+
try:
|
|
307
|
+
self._current_action.run()
|
|
308
|
+
except (RuntimeError, ValueError) as exc:
|
|
309
|
+
# Session.enter_environment raises (rather than reporting
|
|
310
|
+
# through the action-status callback) when it rejects the
|
|
311
|
+
# environment up front, before registering it — the RFC 0008
|
|
312
|
+
# "at most one Environment defining wrap hooks" RuntimeError is
|
|
313
|
+
# the trigger that reaches us in practice. Every failure it
|
|
314
|
+
# detects *after* registering goes through
|
|
315
|
+
# _fail_action_before_start() and returns normally instead, so
|
|
316
|
+
# as of openjd-sessions 0.12.0 nothing raises post-registration
|
|
317
|
+
# and the else-branch below is defensive. It is kept because the
|
|
318
|
+
# cost of being wrong is asymmetric: if a future release does
|
|
319
|
+
# raise after registering, dropping the environment from our
|
|
320
|
+
# list would skip its onExit and desynchronize us from the
|
|
321
|
+
# session's LIFO exit-ordering check, masking the original error
|
|
322
|
+
# with "Must exit Environment X first". So: only when the session
|
|
323
|
+
# did NOT register the environment may we drop it from our
|
|
324
|
+
# entered list (cleanup must not try to exit it). If the session
|
|
325
|
+
# did register it, it must stay in our list so cleanup exits it —
|
|
326
|
+
# popping it here would skip its onExit and desynchronize us
|
|
327
|
+
# from the session's LIFO exit ordering check, masking the
|
|
328
|
+
# original error with "Must exit Environment X first".
|
|
329
|
+
if env_id not in self._openjd_session.environments_entered:
|
|
330
|
+
self._environments_entered.pop()
|
|
331
|
+
LOG.info(
|
|
332
|
+
msg=f"Open Job Description CLI: ERROR entering environment '{env.name}': {exc}",
|
|
333
|
+
extra={"session_id": self.session_id},
|
|
334
|
+
)
|
|
335
|
+
self.failed = True
|
|
336
|
+
self._failed_action = self._current_action
|
|
337
|
+
self._current_action = None
|
|
338
|
+
raise LocalSessionFailed(self._failed_action) from exc
|
|
218
339
|
self._action_ended.wait()
|
|
219
340
|
if self.failed:
|
|
220
341
|
self._failed_action = self._current_action
|
|
@@ -238,7 +359,12 @@ class LocalSession:
|
|
|
238
359
|
prev_action_failed = self.failed
|
|
239
360
|
self._action_ended.clear()
|
|
240
361
|
self._current_action = ExitEnvironmentAction(
|
|
241
|
-
session=self._openjd_session,
|
|
362
|
+
session=self._openjd_session,
|
|
363
|
+
id=env_id,
|
|
364
|
+
keep_session_running=keep_session_running,
|
|
365
|
+
# The same table the enter used, so onExit resolves in the same
|
|
366
|
+
# scope as onEnter. `pop` because an environment is exited once.
|
|
367
|
+
resolved_symtab=self._entered_env_symtabs.pop(env_id, None),
|
|
242
368
|
)
|
|
243
369
|
self._current_action.run()
|
|
244
370
|
self._action_ended.wait()
|
|
@@ -258,7 +384,11 @@ class LocalSession:
|
|
|
258
384
|
|
|
259
385
|
self._action_ended.clear()
|
|
260
386
|
self._current_action = RunTaskAction(
|
|
261
|
-
session=self._openjd_session,
|
|
387
|
+
session=self._openjd_session,
|
|
388
|
+
step=step,
|
|
389
|
+
parameters=parameter_set,
|
|
390
|
+
# RFC 0005 §3.6: the step's resolved template-scope `let` values.
|
|
391
|
+
resolved_symtab=self._step_symbol_tables.get(step.name),
|
|
262
392
|
)
|
|
263
393
|
self._current_action.run()
|
|
264
394
|
self._action_ended.wait()
|
|
@@ -300,27 +430,28 @@ class LocalSession:
|
|
|
300
430
|
# Estimate a chunk size based on the statistics, and update the iterator. Note that this
|
|
301
431
|
# logic is very simple, providing a good starting point that behaves reasonably for other implementations
|
|
302
432
|
# to follow.
|
|
303
|
-
|
|
304
|
-
|
|
433
|
+
new_chunk_size = _calculate_adaptive_chunk_size(
|
|
434
|
+
current_chunk_size=task_parameters.chunks_default_task_count, # type: ignore
|
|
435
|
+
completed_task_count=completed_task_count,
|
|
436
|
+
completed_task_duration=completed_task_duration,
|
|
437
|
+
target_runtime_seconds=target_runtime_seconds,
|
|
438
|
+
)
|
|
439
|
+
# `None` means there is no measurable sample yet: keep the current chunk
|
|
440
|
+
# size and wait for one. Deliberately not `continue` -- the maximum-task
|
|
441
|
+
# countdown below must still run for this completed chunk.
|
|
305
442
|
if (
|
|
306
|
-
|
|
307
|
-
and
|
|
443
|
+
new_chunk_size is not None
|
|
444
|
+
and new_chunk_size != task_parameters.chunks_default_task_count
|
|
308
445
|
):
|
|
309
|
-
# When we have data about only a few tasks, gradually blend in the new estimate instead of cutting over immediately
|
|
310
|
-
adaptive_chunk_size = (
|
|
311
|
-
0.75 * task_parameters.chunks_default_task_count + 0.25 * adaptive_chunk_size # type: ignore
|
|
312
|
-
)
|
|
313
|
-
adaptive_chunk_size = max(int(adaptive_chunk_size), 1)
|
|
314
|
-
if adaptive_chunk_size != task_parameters.chunks_default_task_count:
|
|
315
446
|
LOG.info(
|
|
316
447
|
msg=f"Open Job Description CLI: Ran {completed_task_count} tasks in {timedelta(seconds=completed_task_duration)}, average {timedelta(seconds=completed_task_duration / completed_task_count)}",
|
|
317
448
|
extra={"session_id": self.session_id},
|
|
318
449
|
)
|
|
319
450
|
LOG.info(
|
|
320
|
-
msg=f"Open Job Description CLI: Adjusting chunk size from {task_parameters.chunks_default_task_count} to {
|
|
451
|
+
msg=f"Open Job Description CLI: Adjusting chunk size from {task_parameters.chunks_default_task_count} to {new_chunk_size}",
|
|
321
452
|
extra={"session_id": self.session_id},
|
|
322
453
|
)
|
|
323
|
-
task_parameters.chunks_default_task_count =
|
|
454
|
+
task_parameters.chunks_default_task_count = new_chunk_size
|
|
324
455
|
|
|
325
456
|
# If a maximum task count was specified, count them down
|
|
326
457
|
if maximum_tasks and maximum_tasks > 0:
|
|
@@ -348,8 +479,21 @@ class LocalSession:
|
|
|
348
479
|
if task_parameters is None:
|
|
349
480
|
task_parameters = StepParameterSpaceIterator(space=step.parameterSpace)
|
|
350
481
|
|
|
351
|
-
# Enter all the step environments
|
|
352
|
-
|
|
482
|
+
# Enter all the step environments with the step's create-time resolved
|
|
483
|
+
# symbol table, so a step-level `let` (RFC 0005 §3.6) is visible to
|
|
484
|
+
# their variables and actions. The source expressions are not
|
|
485
|
+
# re-evaluated anywhere downstream, so a missing table here means the
|
|
486
|
+
# step's `let` names are undefined rather than merely stale.
|
|
487
|
+
resolved_symtab = self._step_symbol_tables.get(step.name)
|
|
488
|
+
self.run_environment_enters(
|
|
489
|
+
step.stepEnvironments,
|
|
490
|
+
EnvironmentType.STEP,
|
|
491
|
+
resolved_symtab=resolved_symtab,
|
|
492
|
+
# RFC 0007 §7.3.1 (EXPR): Step.Name is available to a step's
|
|
493
|
+
# environments (openjd-rs threads the step's resolved symbol
|
|
494
|
+
# table into enter_environment; this is the CLI counterpart).
|
|
495
|
+
step_name=step.name,
|
|
496
|
+
)
|
|
353
497
|
|
|
354
498
|
try:
|
|
355
499
|
# Run the tasks
|
|
@@ -4,7 +4,7 @@ from argparse import ArgumentParser, Namespace
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import json
|
|
7
|
-
from typing import Iterable, Optional
|
|
7
|
+
from typing import TYPE_CHECKING, Iterable, Optional
|
|
8
8
|
import re
|
|
9
9
|
import logging
|
|
10
10
|
import time
|
|
@@ -40,6 +40,10 @@ from openjd.model import (
|
|
|
40
40
|
)
|
|
41
41
|
from openjd.sessions import PathMappingRule, LOG
|
|
42
42
|
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
# Annotations only; see the note in _local_session/_actions.py.
|
|
45
|
+
from openjd.expr import SerializedSymbolTable
|
|
46
|
+
|
|
43
47
|
|
|
44
48
|
@dataclass
|
|
45
49
|
class OpenJDRunResult(OpenJDCliResult):
|
|
@@ -321,6 +325,7 @@ def _run_local_session(
|
|
|
321
325
|
*,
|
|
322
326
|
job: Job,
|
|
323
327
|
job_parameter_values: JobParameterValues,
|
|
328
|
+
step_symbol_tables: Optional[dict[str, "SerializedSymbolTable"]] = None,
|
|
324
329
|
step_list: list[Step],
|
|
325
330
|
selected_step: Optional[Step],
|
|
326
331
|
timestamp_format: LoggingTimestampFormat,
|
|
@@ -338,6 +343,7 @@ def _run_local_session(
|
|
|
338
343
|
Creates a Session object and listens for log messages to synchronously end the session.
|
|
339
344
|
"""
|
|
340
345
|
|
|
346
|
+
error_message = "Session ended with errors; see Task logs for details"
|
|
341
347
|
try:
|
|
342
348
|
start_seconds = time.perf_counter()
|
|
343
349
|
|
|
@@ -345,6 +351,9 @@ def _run_local_session(
|
|
|
345
351
|
with LocalSession(
|
|
346
352
|
job=job,
|
|
347
353
|
job_parameter_values=job_parameter_values,
|
|
354
|
+
# RFC 0005 §3.6: each step's create-time resolved `let` values. The
|
|
355
|
+
# only channel they have into the session.
|
|
356
|
+
step_symbol_tables=step_symbol_tables,
|
|
348
357
|
timestamp_format=timestamp_format,
|
|
349
358
|
session_id="CLI-session",
|
|
350
359
|
path_mapping_rules=path_mapping_rules,
|
|
@@ -367,6 +376,14 @@ def _run_local_session(
|
|
|
367
376
|
except LocalSessionFailed:
|
|
368
377
|
duration = time.perf_counter() - start_seconds
|
|
369
378
|
session = None
|
|
379
|
+
except (RuntimeError, ValueError) as exc:
|
|
380
|
+
# Exceptions raised by openjd.sessions from within session actions
|
|
381
|
+
# (e.g. the RFC 0008 "at most one wrap environment" RuntimeError)
|
|
382
|
+
# rather than reported through the action-status callback. Report
|
|
383
|
+
# them as a clean error result instead of a raw traceback.
|
|
384
|
+
duration = time.perf_counter() - start_seconds
|
|
385
|
+
session = None
|
|
386
|
+
error_message = f"Session ended with errors: {exc}"
|
|
370
387
|
|
|
371
388
|
preserved_message: str = ""
|
|
372
389
|
if retain_working_dir and session is not None:
|
|
@@ -377,7 +394,7 @@ def _run_local_session(
|
|
|
377
394
|
if session is None or session.failed:
|
|
378
395
|
return OpenJDRunResult(
|
|
379
396
|
status="error",
|
|
380
|
-
message=
|
|
397
|
+
message=error_message + preserved_message,
|
|
381
398
|
job_name=job.name,
|
|
382
399
|
step_name=step_name,
|
|
383
400
|
duration=duration,
|
|
@@ -415,7 +432,11 @@ def do_run(args: Namespace) -> OpenJDCliResult:
|
|
|
415
432
|
filename = Path(env).expanduser()
|
|
416
433
|
try:
|
|
417
434
|
# Raises: RuntimeError, DecodeValidationError
|
|
418
|
-
|
|
435
|
+
# Pass `extensions` so env templates can declare e.g.
|
|
436
|
+
# WRAP_ACTIONS (RFC 0008) — without this they're parsed
|
|
437
|
+
# against the default model surface and reject the
|
|
438
|
+
# extension declaration.
|
|
439
|
+
env_template = read_environment_template(filename, supported_extensions=extensions)
|
|
419
440
|
environments.append(env_template)
|
|
420
441
|
except (RuntimeError, DecodeValidationError) as e:
|
|
421
442
|
return OpenJDCliResult(status="error", message=str(e))
|
|
@@ -451,7 +472,7 @@ def do_run(args: Namespace) -> OpenJDCliResult:
|
|
|
451
472
|
|
|
452
473
|
try:
|
|
453
474
|
# Raises: RuntimeError
|
|
454
|
-
the_job, job_parameter_values = generate_job(
|
|
475
|
+
the_job, job_parameter_values, step_symbol_tables = generate_job(
|
|
455
476
|
args, environments, supported_extensions=extensions
|
|
456
477
|
)
|
|
457
478
|
|
|
@@ -543,14 +564,23 @@ def do_run(args: Namespace) -> OpenJDCliResult:
|
|
|
543
564
|
return OpenJDCliResult(status="error", message=str(rte))
|
|
544
565
|
|
|
545
566
|
# Create a RevisionExtensions object with the default specification version and enabled extensions
|
|
546
|
-
# We use the default v2023_09 since that's what we're currently supporting
|
|
567
|
+
# We use the default v2023_09 since that's what we're currently supporting.
|
|
568
|
+
# Extension behaviors activate only when declared by the templates being
|
|
569
|
+
# run (the job template and any external environment templates) — the
|
|
570
|
+
# CLI's --extensions list is what the CLI *accepts*, not what is enabled.
|
|
571
|
+
declared_extensions: list[str] = list(the_job.extensions or [])
|
|
572
|
+
for env_template in environments or []:
|
|
573
|
+
for ext_name in env_template.extensions or []:
|
|
574
|
+
if ext_name not in declared_extensions:
|
|
575
|
+
declared_extensions.append(ext_name)
|
|
547
576
|
revision_extensions = RevisionExtensions(
|
|
548
|
-
spec_rev=the_job.revision, supported_extensions=
|
|
577
|
+
spec_rev=the_job.revision, supported_extensions=declared_extensions
|
|
549
578
|
)
|
|
550
579
|
|
|
551
580
|
return _run_local_session(
|
|
552
581
|
job=the_job,
|
|
553
582
|
job_parameter_values=job_parameter_values,
|
|
583
|
+
step_symbol_tables=step_symbol_tables,
|
|
554
584
|
step_list=step_list,
|
|
555
585
|
selected_step=selected_step,
|
|
556
586
|
task_parameter_values=task_parameter_values,
|
|
@@ -35,7 +35,7 @@ def do_summary(args: Namespace) -> OpenJDCliResult:
|
|
|
35
35
|
|
|
36
36
|
try:
|
|
37
37
|
# Raises: RuntimeError
|
|
38
|
-
sample_job, _ = generate_job(args, supported_extensions=extensions)
|
|
38
|
+
sample_job, _, _ = generate_job(args, supported_extensions=extensions)
|
|
39
39
|
except RuntimeError as rte:
|
|
40
40
|
return OpenJDCliResult(status="error", message=str(rte))
|
|
41
41
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.7.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 7)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
openjd_cli-0.7.5/_version.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '0.7.5'
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 7, 5)
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = None
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
-
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
from openjd.model import Step, TaskParameterSet
|
|
6
|
-
from openjd.model.v2023_09 import Environment
|
|
7
|
-
from openjd.sessions import Session
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class EnvironmentType(str, Enum):
|
|
11
|
-
"""
|
|
12
|
-
The three different types of environment types that can be entered/exited in a session.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
EXTERNAL = "EXTERNAL"
|
|
16
|
-
JOB = "JOB"
|
|
17
|
-
STEP = "STEP"
|
|
18
|
-
ALL = "ALL"
|
|
19
|
-
|
|
20
|
-
def matches(self, other: "EnvironmentType") -> bool:
|
|
21
|
-
"""Environment types match if they are equal, or one of them is ALL."""
|
|
22
|
-
return self == other or self == EnvironmentType.ALL or other == EnvironmentType.ALL
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SessionAction:
|
|
26
|
-
_session: Session
|
|
27
|
-
duration: float
|
|
28
|
-
|
|
29
|
-
def __init__(self, session: Session):
|
|
30
|
-
self._session = session
|
|
31
|
-
|
|
32
|
-
def run(self):
|
|
33
|
-
"""
|
|
34
|
-
Subclasses of `SessionAction` should have
|
|
35
|
-
custom implementations of this depending on their type.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class RunTaskAction(SessionAction):
|
|
40
|
-
_step: Step
|
|
41
|
-
_parameters: TaskParameterSet
|
|
42
|
-
|
|
43
|
-
def __init__(self, session: Session, step: Step, parameters: TaskParameterSet):
|
|
44
|
-
super(RunTaskAction, self).__init__(session)
|
|
45
|
-
self._step = step
|
|
46
|
-
self._parameters = parameters
|
|
47
|
-
|
|
48
|
-
def run(self):
|
|
49
|
-
self._session.run_task(
|
|
50
|
-
step_script=self._step.script, task_parameter_values=self._parameters
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
def __str__(self):
|
|
54
|
-
parameters = {name: parameter.value for name, parameter in self._parameters.items()}
|
|
55
|
-
return f"Run Step '{self._step.name}' with Task parameters '{str(parameters)}'"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class EnterEnvironmentAction(SessionAction):
|
|
59
|
-
_environment: Environment
|
|
60
|
-
_id: str
|
|
61
|
-
|
|
62
|
-
def __init__(self, session: Session, environment: Environment, env_id: str):
|
|
63
|
-
super(EnterEnvironmentAction, self).__init__(session)
|
|
64
|
-
self._environment = environment
|
|
65
|
-
self._id = env_id
|
|
66
|
-
|
|
67
|
-
def run(self):
|
|
68
|
-
self._session.enter_environment(environment=self._environment, identifier=self._id)
|
|
69
|
-
|
|
70
|
-
def __str__(self):
|
|
71
|
-
return f"Enter Environment '{self._environment.name}'"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class ExitEnvironmentAction(SessionAction):
|
|
75
|
-
_id: str
|
|
76
|
-
_keep_session_running: bool
|
|
77
|
-
|
|
78
|
-
def __init__(self, session: Session, id: str, keep_session_running: bool):
|
|
79
|
-
super(ExitEnvironmentAction, self).__init__(session)
|
|
80
|
-
self._id = id
|
|
81
|
-
self._keep_session_running = keep_session_running
|
|
82
|
-
|
|
83
|
-
def run(self):
|
|
84
|
-
self._session.exit_environment(
|
|
85
|
-
identifier=self._id, keep_session_running=self._keep_session_running
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
def __str__(self):
|
|
89
|
-
return f"Exit Environment '{self._id}'"
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '0.7.5'
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 7, 5)
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|