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.
Files changed (35) hide show
  1. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/PKG-INFO +3 -3
  2. openjd_cli-0.7.7/_version.py +24 -0
  3. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/hatch_version_hook.py +0 -1
  4. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/pyproject.toml +12 -2
  5. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/__main__.py +0 -1
  6. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_check/_check_command.py +4 -1
  7. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/__init__.py +9 -2
  8. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/_extensions.py +12 -3
  9. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/_job_from_template.py +24 -6
  10. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_common/_validation_utils.py +17 -3
  11. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_create_argparser.py +0 -1
  12. openjd_cli-0.7.7/src/openjd/cli/_run/_local_session/_actions.py +174 -0
  13. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_session_manager.py +164 -20
  14. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_run_command.py +36 -6
  15. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_summary/_summary_command.py +1 -1
  16. openjd_cli-0.7.7/src/openjd/cli/_version.py +24 -0
  17. openjd_cli-0.7.5/_version.py +0 -34
  18. openjd_cli-0.7.5/src/openjd/cli/_run/_local_session/_actions.py +0 -89
  19. openjd_cli-0.7.5/src/openjd/cli/_version.py +0 -34
  20. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/.gitignore +0 -0
  21. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/LICENSE +0 -0
  22. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/NOTICE +0 -0
  23. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/README.md +0 -0
  24. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/hatch.toml +0 -0
  25. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/__init__.py +0 -0
  26. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_check/__init__.py +0 -0
  27. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/__init__.py +0 -0
  28. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_help_formatter.py +0 -0
  29. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/__init__.py +0 -0
  30. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_run/_local_session/_logs.py +0 -0
  31. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_schema/__init__.py +0 -0
  32. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_schema/_schema_command.py +0 -0
  33. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_summary/__init__.py +0 -0
  34. {openjd_cli-0.7.5 → openjd_cli-0.7.7}/src/openjd/cli/_summary/_summary_output.py +0 -0
  35. {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.5
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.10,>=0.9
26
- Requires-Dist: openjd-sessions<0.11,>=0.10.7
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
- "openjd-sessions >= 0.10.7,< 0.11",
33
- "openjd-model >= 0.9,< 0.10"
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]
@@ -2,5 +2,4 @@
2
2
 
3
3
  from .cli._create_argparser import main
4
4
 
5
-
6
5
  main()
@@ -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
- decode_environment_template(template=template_object)
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
- SUPPORTED_EXTENSIONS = ["TASK_CHUNKING", "REDACTED_ENV_VARS", "FEATURE_BUNDLE_1"]
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 to enable. Defaults to all that are implemented: {','.join(SUPPORTED_EXTENSIONS)}",
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
- 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)}")
@@ -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(template_file: Path) -> EnvironmentTemplate:
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}'"
@@ -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(self, environments: Optional[list[Any]], type: EnvironmentType):
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, environment=env, env_id=env_id
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
- self._current_action.run()
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, 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),
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, 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),
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
- duration_per_task = completed_task_duration / completed_task_count
304
- adaptive_chunk_size = target_runtime_seconds / duration_per_task
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
- completed_task_count < 10
307
- and adaptive_chunk_size > task_parameters.chunks_default_task_count # type: ignore
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 {adaptive_chunk_size}",
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 = adaptive_chunk_size
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
- self.run_environment_enters(step.stepEnvironments, EnvironmentType.STEP)
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="Session ended with errors; see Task logs for details" + preserved_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
- env_template = read_environment_template(filename)
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=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
@@ -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