openjd-cli 0.7.6__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.
Files changed (33) hide show
  1. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/PKG-INFO +3 -3
  2. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/_version.py +2 -2
  3. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/pyproject.toml +12 -5
  4. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_common/__init__.py +9 -2
  5. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_common/_job_from_template.py +24 -6
  6. openjd_cli-0.7.7/src/openjd/cli/_run/_local_session/_actions.py +174 -0
  7. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_session_manager.py +65 -23
  8. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_run/_run_command.py +11 -2
  9. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_summary/_summary_command.py +1 -1
  10. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_version.py +2 -2
  11. openjd_cli-0.7.6/src/openjd/cli/_run/_local_session/_actions.py +0 -126
  12. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/.gitignore +0 -0
  13. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/LICENSE +0 -0
  14. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/NOTICE +0 -0
  15. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/README.md +0 -0
  16. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/hatch.toml +0 -0
  17. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/hatch_version_hook.py +0 -0
  18. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/__main__.py +0 -0
  19. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/__init__.py +0 -0
  20. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_check/__init__.py +0 -0
  21. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_check/_check_command.py +0 -0
  22. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_common/_extensions.py +0 -0
  23. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_common/_validation_utils.py +0 -0
  24. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_create_argparser.py +0 -0
  25. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_run/__init__.py +0 -0
  26. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_run/_help_formatter.py +0 -0
  27. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/__init__.py +0 -0
  28. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_logs.py +0 -0
  29. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_schema/__init__.py +0 -0
  30. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_schema/_schema_command.py +0 -0
  31. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_summary/__init__.py +0 -0
  32. {openjd_cli-0.7.6 → openjd_cli-0.7.7}/src/openjd/cli/_summary/_summary_output.py +0 -0
  33. {openjd_cli-0.7.6 → 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.6
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.12,>=0.9
26
- Requires-Dist: openjd-sessions<0.11,>=0.10.11
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
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.6'
22
- __version_tuple__ = version_tuple = (0, 7, 6)
21
+ __version__ = version = '0.7.7'
22
+ __version_tuple__ = version_tuple = (0, 7, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -29,11 +29,18 @@ classifiers = [
29
29
  "Intended Audience :: End Users/Desktop"
30
30
  ]
31
31
  dependencies = [
32
- # 0.10.11 is the first release with `step_name` on both Session.run_task and
33
- # Session.enter_environment (RFC 0007/0008). Below it, run_task() raises
34
- # TypeError on every task run, since this CLI passes the keyword unconditionally.
35
- "openjd-sessions >= 0.10.11,< 0.11",
36
- "openjd-model >= 0.9,< 0.12"
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"
37
44
  ]
38
45
 
39
46
  [project.urls]
@@ -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)
@@ -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
- create_job,
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 and the parameter values for running the job.
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
- job = create_job(
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)}")
@@ -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}'"
@@ -3,7 +3,7 @@
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
@@ -39,6 +39,10 @@ from openjd.sessions import (
39
39
  PathMappingRule,
40
40
  )
41
41
 
42
+ if TYPE_CHECKING:
43
+ # Annotations only; see the note in _actions.py.
44
+ from openjd.expr import SerializedSymbolTable
45
+
42
46
 
43
47
  class LocalSessionFailed(RuntimeError):
44
48
  """
@@ -118,6 +122,8 @@ class LocalSession:
118
122
  _path_mapping_rules: Optional[list[PathMappingRule]]
119
123
  _environments: Optional[list[Any]]
120
124
  _environments_entered: list[tuple[EnvironmentType, str]]
125
+ _step_symbol_tables: dict[str, "SerializedSymbolTable"]
126
+ _entered_env_symtabs: dict[str, "SerializedSymbolTable"]
121
127
  _log_handler: LocalSessionLogHandler
122
128
  _cleanup_called: bool
123
129
 
@@ -127,6 +133,7 @@ class LocalSession:
127
133
  job: Job,
128
134
  job_parameter_values: JobParameterValues,
129
135
  session_id: str,
136
+ step_symbol_tables: Optional[dict[str, "SerializedSymbolTable"]] = None,
130
137
  timestamp_format: LoggingTimestampFormat = LoggingTimestampFormat.RELATIVE,
131
138
  path_mapping_rules: Optional[list[PathMappingRule]] = None,
132
139
  environments: Optional[list[Any]] = None,
@@ -142,6 +149,21 @@ class LocalSession:
142
149
  self._timestamp_format = timestamp_format
143
150
  self._path_mapping_rules = path_mapping_rules
144
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 = {}
145
167
 
146
168
  # Create an OpenJD Session
147
169
  self._openjd_session = Session(
@@ -249,7 +271,7 @@ class LocalSession:
249
271
  environments: Optional[list[Any]],
250
272
  type: EnvironmentType,
251
273
  *,
252
- extra_let_bindings: Optional[list[str]] = None,
274
+ resolved_symtab: Optional["SerializedSymbolTable"] = None,
253
275
  step_name: Optional[str] = None,
254
276
  ):
255
277
  """Enter one or more environments in the session."""
@@ -268,27 +290,39 @@ class LocalSession:
268
290
  session=self._openjd_session,
269
291
  environment=env,
270
292
  env_id=env_id,
271
- # RFC 0007: a step's environments see the step-level `let`
272
- # bindings.
273
- extra_let_bindings=extra_let_bindings,
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,
274
296
  # RFC 0007 §7.3.1 (EXPR): a step's environments see Step.Name.
275
297
  # Only step-environment enters carry a step name.
276
298
  step_name=step_name,
277
299
  )
278
300
  self._environments_entered.append((type, env_id))
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
279
306
  try:
280
307
  self._current_action.run()
281
308
  except (RuntimeError, ValueError) as exc:
282
309
  # Session.enter_environment raises (rather than reporting
283
310
  # through the action-status callback) when it rejects the
284
- # environment up front — e.g. the RFC 0008 "at most one wrap
285
- # environment" RuntimeError, or a ValueError from the extra
286
- # `let` bindings — but it can also raise *after* registering
287
- # the environment (e.g. an environment `variables` expression
288
- # that fails to evaluate). Only when the session did NOT
289
- # register the environment may we drop it from our entered
290
- # list (cleanup must not try to exit it). If the session did
291
- # register it, it must stay in our list so cleanup exits it —
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 —
292
326
  # popping it here would skip its onExit and desynchronize us
293
327
  # from the session's LIFO exit ordering check, masking the
294
328
  # original error with "Must exit Environment X first".
@@ -325,7 +359,12 @@ class LocalSession:
325
359
  prev_action_failed = self.failed
326
360
  self._action_ended.clear()
327
361
  self._current_action = ExitEnvironmentAction(
328
- session=self._openjd_session, id=env_id, keep_session_running=keep_session_running
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),
329
368
  )
330
369
  self._current_action.run()
331
370
  self._action_ended.wait()
@@ -345,7 +384,11 @@ class LocalSession:
345
384
 
346
385
  self._action_ended.clear()
347
386
  self._current_action = RunTaskAction(
348
- session=self._openjd_session, step=step, parameters=parameter_set
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),
349
392
  )
350
393
  self._current_action.run()
351
394
  self._action_ended.wait()
@@ -436,17 +479,16 @@ class LocalSession:
436
479
  if task_parameters is None:
437
480
  task_parameters = StepParameterSpaceIterator(space=step.parameterSpace)
438
481
 
439
- # Enter all the step environments. When the step defines step-level
440
- # `let` bindings (RFC 0007), its environments are entered with them so
441
- # their variables and actions can reference them.
442
- # getattr guard: requires an openjd-model with Step.let on the
443
- # instantiated Job (openjd-model PR #318+); collapse to plain
444
- # `step.let` once the version pin floor guarantees it.
445
- step_let_bindings = getattr(step, "let", None)
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)
446
488
  self.run_environment_enters(
447
489
  step.stepEnvironments,
448
490
  EnvironmentType.STEP,
449
- extra_let_bindings=step_let_bindings or None,
491
+ resolved_symtab=resolved_symtab,
450
492
  # RFC 0007 §7.3.1 (EXPR): Step.Name is available to a step's
451
493
  # environments (openjd-rs threads the step's resolved symbol
452
494
  # table into enter_environment; this is the CLI counterpart).
@@ -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,
@@ -346,6 +351,9 @@ def _run_local_session(
346
351
  with LocalSession(
347
352
  job=job,
348
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,
349
357
  timestamp_format=timestamp_format,
350
358
  session_id="CLI-session",
351
359
  path_mapping_rules=path_mapping_rules,
@@ -464,7 +472,7 @@ def do_run(args: Namespace) -> OpenJDCliResult:
464
472
 
465
473
  try:
466
474
  # Raises: RuntimeError
467
- the_job, job_parameter_values = generate_job(
475
+ the_job, job_parameter_values, step_symbol_tables = generate_job(
468
476
  args, environments, supported_extensions=extensions
469
477
  )
470
478
 
@@ -572,6 +580,7 @@ def do_run(args: Namespace) -> OpenJDCliResult:
572
580
  return _run_local_session(
573
581
  job=the_job,
574
582
  job_parameter_values=job_parameter_values,
583
+ step_symbol_tables=step_symbol_tables,
575
584
  step_list=step_list,
576
585
  selected_step=selected_step,
577
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
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.6'
22
- __version_tuple__ = version_tuple = (0, 7, 6)
21
+ __version__ = version = '0.7.7'
22
+ __version_tuple__ = version_tuple = (0, 7, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,126 +0,0 @@
1
- # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
-
3
- from enum import Enum
4
- from typing import Any, 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
-
11
- class EnvironmentType(str, Enum):
12
- """
13
- The three different types of environment types that can be entered/exited in a session.
14
- """
15
-
16
- EXTERNAL = "EXTERNAL"
17
- JOB = "JOB"
18
- STEP = "STEP"
19
- ALL = "ALL"
20
-
21
- def matches(self, other: "EnvironmentType") -> bool:
22
- """Environment types match if they are equal, or one of them is ALL."""
23
- return self == other or self == EnvironmentType.ALL or other == EnvironmentType.ALL
24
-
25
-
26
- class SessionAction:
27
- _session: Session
28
- duration: float
29
-
30
- def __init__(self, session: Session):
31
- self._session = session
32
-
33
- def run(self):
34
- """
35
- Subclasses of `SessionAction` should have
36
- custom implementations of this depending on their type.
37
- """
38
-
39
-
40
- class RunTaskAction(SessionAction):
41
- _step: Step
42
- _parameters: TaskParameterSet
43
-
44
- def __init__(self, session: Session, step: Step, parameters: TaskParameterSet):
45
- super(RunTaskAction, self).__init__(session)
46
- self._step = step
47
- self._parameters = parameters
48
-
49
- def run(self):
50
- self._session.run_task(
51
- step_script=self._step.script,
52
- task_parameter_values=self._parameters,
53
- # RFC 0008: the step name feeds the WrappedStep.Name template
54
- # variable inside an active onWrapTaskRun hook.
55
- step_name=self._step.name,
56
- )
57
-
58
- def __str__(self):
59
- parameters = {name: parameter.value for name, parameter in self._parameters.items()}
60
- return f"Run Step '{self._step.name}' with Task parameters '{str(parameters)}'"
61
-
62
-
63
- class EnterEnvironmentAction(SessionAction):
64
- _environment: Environment
65
- _id: str
66
- _extra_let_bindings: Optional[list[str]]
67
- _step_name: Optional[str]
68
-
69
- def __init__(
70
- self,
71
- session: Session,
72
- environment: Environment,
73
- env_id: str,
74
- extra_let_bindings: Optional[list[str]] = None,
75
- step_name: Optional[str] = None,
76
- ):
77
- super(EnterEnvironmentAction, self).__init__(session)
78
- self._environment = environment
79
- self._id = env_id
80
- # RFC 0007: a step's environments are entered with the step-level
81
- # `let` bindings so their variables/actions can reference them.
82
- self._extra_let_bindings = extra_let_bindings
83
- # RFC 0007 §7.3.1 (EXPR): the owning step's name seeds Step.Name for
84
- # a step environment's `let` bindings, variables, and actions. Only
85
- # step-environment enters carry a step name; job/external enters
86
- # leave it None.
87
- self._step_name = step_name
88
-
89
- def run(self):
90
- # Both keywords are guaranteed by this package's `openjd-sessions`
91
- # floor (>= 0.10.11), so neither is feature-detected. They are still
92
- # only forwarded when they carry something: a step with no `let`
93
- # bindings means "no extra bindings", and job/external environment
94
- # enters have no owning step, so `Step.Name` must stay undefined for
95
- # them rather than being seeded with None.
96
- optional_kwargs: dict[str, Any] = {}
97
- if self._extra_let_bindings:
98
- optional_kwargs["extra_let_bindings"] = self._extra_let_bindings
99
- if self._step_name is not None:
100
- optional_kwargs["step_name"] = self._step_name
101
- self._session.enter_environment(
102
- environment=self._environment,
103
- identifier=self._id,
104
- **optional_kwargs,
105
- )
106
-
107
- def __str__(self):
108
- return f"Enter Environment '{self._environment.name}'"
109
-
110
-
111
- class ExitEnvironmentAction(SessionAction):
112
- _id: str
113
- _keep_session_running: bool
114
-
115
- def __init__(self, session: Session, id: str, keep_session_running: bool):
116
- super(ExitEnvironmentAction, self).__init__(session)
117
- self._id = id
118
- self._keep_session_running = keep_session_running
119
-
120
- def run(self):
121
- self._session.exit_environment(
122
- identifier=self._id, keep_session_running=self._keep_session_running
123
- )
124
-
125
- def __str__(self):
126
- return f"Exit Environment '{self._id}'"
File without changes
File without changes
File without changes
File without changes
File without changes