procodile 0.0.7__tar.gz → 0.0.9__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.
- {procodile-0.0.7 → procodile-0.0.9}/PKG-INFO +1 -1
- {procodile-0.0.7 → procodile-0.0.9}/procodile/__init__.py +18 -3
- procodile-0.0.9/procodile/artifacts.py +192 -0
- {procodile-0.0.7 → procodile-0.0.9}/procodile/cli/__init__.py +2 -2
- procodile-0.0.9/procodile/cli/cli.py +259 -0
- {procodile-0.0.7 → procodile-0.0.9}/procodile/job.py +10 -1
- procodile-0.0.9/procodile/process.py +492 -0
- procodile-0.0.9/procodile/registry.py +169 -0
- {procodile-0.0.7 → procodile-0.0.9}/procodile/reporter.py +10 -3
- procodile-0.0.9/procodile/workflow.py +620 -0
- {procodile-0.0.7 → procodile-0.0.9}/pyproject.toml +1 -1
- procodile-0.0.7/procodile/cli/cli.py +0 -202
- procodile-0.0.7/procodile/process.py +0 -268
- procodile-0.0.7/procodile/registry.py +0 -99
- {procodile-0.0.7 → procodile-0.0.9}/.gitignore +0 -0
- {procodile-0.0.7 → procodile-0.0.9}/LICENSE +0 -0
- {procodile-0.0.7 → procodile-0.0.9}/README.md +0 -0
- {procodile-0.0.7 → procodile-0.0.9}/procodile/py.typed +0 -0
|
@@ -4,22 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
from importlib.metadata import version
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
__version__ = version("procodile")
|
|
9
8
|
|
|
10
9
|
from gavicore.util.request import ExecutionRequest
|
|
10
|
+
|
|
11
|
+
from .artifacts import ArtifactRef, ArtifactStore, ExecutionContext
|
|
11
12
|
from .job import Job, JobCancelledException, JobContext
|
|
12
|
-
from .process import Process
|
|
13
|
+
from .process import Process, additional_parameters
|
|
13
14
|
from .registry import ProcessRegistry
|
|
15
|
+
from .workflow import (
|
|
16
|
+
FromMain,
|
|
17
|
+
FromStep,
|
|
18
|
+
Workflow,
|
|
19
|
+
WorkflowStepRegistry,
|
|
20
|
+
)
|
|
14
21
|
|
|
15
22
|
"""Processes development API."""
|
|
16
23
|
|
|
17
24
|
__all__ = [
|
|
25
|
+
"additional_parameters",
|
|
26
|
+
"ArtifactRef",
|
|
27
|
+
"ArtifactStore",
|
|
18
28
|
"__version__",
|
|
19
29
|
"ExecutionRequest",
|
|
30
|
+
"ExecutionContext",
|
|
31
|
+
"FromStep",
|
|
32
|
+
"FromMain",
|
|
20
33
|
"Job",
|
|
21
34
|
"JobContext",
|
|
22
35
|
"JobCancelledException",
|
|
23
|
-
"ProcessRegistry",
|
|
24
36
|
"Process",
|
|
37
|
+
"ProcessRegistry",
|
|
38
|
+
"WorkflowStepRegistry",
|
|
39
|
+
"Workflow",
|
|
25
40
|
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Mapping, TypeAlias
|
|
4
|
+
|
|
5
|
+
NormalizedOutputs: TypeAlias = dict[str, Any]
|
|
6
|
+
"""
|
|
7
|
+
Normalized mapping of output names to resolved values produced by a step
|
|
8
|
+
execution in a workflow.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ArtifactRef:
|
|
14
|
+
"""
|
|
15
|
+
Lightweight reference to an externally stored artifact.
|
|
16
|
+
|
|
17
|
+
An ``ArtifactRef`` is returned when a value is materialized by an
|
|
18
|
+
ArtifactStore and can later be resolved back into the original object.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
path: str
|
|
22
|
+
loader: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ArtifactStore(ABC):
|
|
26
|
+
"""
|
|
27
|
+
Abstract base class for persisting and loading large objects
|
|
28
|
+
(e.g. xarray datasets, large arrays, model artifacts).
|
|
29
|
+
|
|
30
|
+
Concrete implementations define how objects are stored
|
|
31
|
+
(filesystem, object store, etc.).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def is_artifact_big(self, obj: Any) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Determine whether an object should be treated as a 'big object'
|
|
38
|
+
and stored externally instead of being passed as is.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
obj: Object to inspect.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if the object is considered large and should be stored
|
|
45
|
+
via this store, False otherwise.
|
|
46
|
+
"""
|
|
47
|
+
raise NotImplementedError # pragma: no cover
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def save_artifact(self, obj: Any) -> ArtifactRef:
|
|
51
|
+
"""
|
|
52
|
+
Persist a large object and return a reference to it.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
obj: The object to be stored.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ArtifactRef: A reference that can later be used to load the object.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
TypeError: If the object type is not supported by the implementation.
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError # pragma: no cover
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def load_artifact(self, ref: ArtifactRef) -> Any:
|
|
67
|
+
"""
|
|
68
|
+
Load a previously stored object using an ArtifactRef.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
ref: Reference returned by `save`.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The reconstructed object.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If the loader specified in the reference is unknown.
|
|
78
|
+
"""
|
|
79
|
+
raise NotImplementedError # pragma: no cover
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NullArtifactStore(ArtifactStore):
|
|
83
|
+
"""
|
|
84
|
+
No-op artifact store.
|
|
85
|
+
|
|
86
|
+
This store never treats objects as 'big' and does not
|
|
87
|
+
support saving or loading artifacts.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def is_artifact_big(self, obj: Any) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
No object is considered big in the null store.
|
|
93
|
+
"""
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def save_artifact(self, obj: Any):
|
|
97
|
+
"""
|
|
98
|
+
Saving artifacts is not supported in the null store.
|
|
99
|
+
"""
|
|
100
|
+
raise RuntimeError("NullArtifactStore does not support saving artifacts")
|
|
101
|
+
|
|
102
|
+
def load_artifact(self, ref):
|
|
103
|
+
"""
|
|
104
|
+
Loading artifacts is not supported in the null store.
|
|
105
|
+
"""
|
|
106
|
+
raise RuntimeError("NullArtifactStore does not support loading artifacts")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ExecutionContext:
|
|
110
|
+
"""
|
|
111
|
+
Holds execution-time state for a workflow run.
|
|
112
|
+
|
|
113
|
+
The execution context stores resolved outputs from the main step and
|
|
114
|
+
individual steps, manages artifact materialization, and normalizes outputs.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, store: ArtifactStore) -> None:
|
|
118
|
+
self.store = store
|
|
119
|
+
self.main: dict[str, Any] = {}
|
|
120
|
+
self.steps: dict[str, dict[str, Any]] = {}
|
|
121
|
+
|
|
122
|
+
def resolve(self, value: Any) -> Any:
|
|
123
|
+
"""Recursively resolve the inputs."""
|
|
124
|
+
if isinstance(value, ArtifactRef):
|
|
125
|
+
return self.store.load_artifact(value)
|
|
126
|
+
|
|
127
|
+
if isinstance(value, dict):
|
|
128
|
+
return {k: self.resolve(v) for k, v in value.items()}
|
|
129
|
+
|
|
130
|
+
if isinstance(value, tuple):
|
|
131
|
+
return tuple(self.resolve(v) for v in value)
|
|
132
|
+
|
|
133
|
+
if isinstance(value, list):
|
|
134
|
+
return [self.resolve(v) for v in value]
|
|
135
|
+
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
def normalize_outputs(
|
|
139
|
+
self, result: Any, output_spec: Mapping[str, Any] | None, store: ArtifactStore
|
|
140
|
+
) -> NormalizedOutputs:
|
|
141
|
+
"""
|
|
142
|
+
Normalize function outputs into a dict[str, Any].
|
|
143
|
+
|
|
144
|
+
Rules:
|
|
145
|
+
- If output_spec is None → {"return_value": result}
|
|
146
|
+
- Tuple → positional mapping
|
|
147
|
+
- Dict → key mapping
|
|
148
|
+
- Single value → single output
|
|
149
|
+
"""
|
|
150
|
+
if output_spec is None:
|
|
151
|
+
return {"return_value": self.materialize_artifact(result, store)}
|
|
152
|
+
|
|
153
|
+
output_keys = list(output_spec.keys())
|
|
154
|
+
|
|
155
|
+
if len(output_keys) == 1:
|
|
156
|
+
return {output_keys[0]: self.materialize_artifact(result, store)}
|
|
157
|
+
|
|
158
|
+
if isinstance(result, dict):
|
|
159
|
+
missing = set(output_keys) - result.keys()
|
|
160
|
+
if missing:
|
|
161
|
+
raise ValueError(f"Missing outputs in return dict: {missing}")
|
|
162
|
+
return {k: self.materialize_artifact(result[k], store) for k in output_keys}
|
|
163
|
+
|
|
164
|
+
if isinstance(result, tuple):
|
|
165
|
+
if len(result) != len(output_keys):
|
|
166
|
+
raise ValueError("Tuple output length does not match declared outputs")
|
|
167
|
+
return {
|
|
168
|
+
k: self.materialize_artifact(v, store)
|
|
169
|
+
for k, v in zip(output_keys, result, strict=True)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
raise TypeError(
|
|
173
|
+
f"Invalid return type for declared outputs. result: {result}, output_spec: {output_spec}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def materialize_artifact(self, value: Any, store: ArtifactStore) -> Any:
|
|
177
|
+
"""
|
|
178
|
+
Recursively materialize big objects.
|
|
179
|
+
"""
|
|
180
|
+
if store.is_artifact_big(value):
|
|
181
|
+
return store.save_artifact(value)
|
|
182
|
+
|
|
183
|
+
if isinstance(value, dict):
|
|
184
|
+
return {k: self.materialize_artifact(v, store) for k, v in value.items()}
|
|
185
|
+
|
|
186
|
+
if isinstance(value, tuple):
|
|
187
|
+
return tuple(self.materialize_artifact(v, store) for v in value)
|
|
188
|
+
|
|
189
|
+
if isinstance(value, list):
|
|
190
|
+
return [self.materialize_artifact(v, store) for v in value]
|
|
191
|
+
|
|
192
|
+
return value
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Copyright (c) 2025 by the Eozilla team and contributors
|
|
2
|
+
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
|
+
# https://opensource.org/license/apache-2-0.
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any, Callable, Optional, Union
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from gavicore.util.cli.group import AliasedGroup
|
|
12
|
+
from gavicore.util.cli.parameters import (
|
|
13
|
+
DOT_PATH_OPTION,
|
|
14
|
+
PROCESS_ID_ARGUMENT,
|
|
15
|
+
REQUEST_FILE_OPTION,
|
|
16
|
+
REQUEST_INPUT_OPTION,
|
|
17
|
+
REQUEST_SUBSCRIBER_OPTION,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
21
|
+
from procodile import ProcessRegistry
|
|
22
|
+
|
|
23
|
+
PROCESS_REGISTRY_GETTER_KEY = "get_process_registry"
|
|
24
|
+
|
|
25
|
+
DEFAULT_NAME = "procodile"
|
|
26
|
+
|
|
27
|
+
DEFAULT_SUMMARY = """`{name}` is a command-line tool to describe and execute
|
|
28
|
+
one or more registered processes."""
|
|
29
|
+
|
|
30
|
+
DEFAULT_HELP = """{summary}
|
|
31
|
+
|
|
32
|
+
You can use shorter command name aliases, e.g., use command name `ep`
|
|
33
|
+
for `execute-process`, or `lp` for `list-processes`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# noinspection PyShadowingBuiltins
|
|
38
|
+
def new_cli(
|
|
39
|
+
registry: Union[
|
|
40
|
+
str,
|
|
41
|
+
"ProcessRegistry",
|
|
42
|
+
Callable[[], "ProcessRegistry"],
|
|
43
|
+
],
|
|
44
|
+
name: str,
|
|
45
|
+
version: str,
|
|
46
|
+
help: str | None = None,
|
|
47
|
+
summary: str | None = None,
|
|
48
|
+
context: dict[str, Any] | None = None,
|
|
49
|
+
) -> typer.Typer:
|
|
50
|
+
"""
|
|
51
|
+
Get the CLI instance configured to use the process registry
|
|
52
|
+
that is given either by
|
|
53
|
+
|
|
54
|
+
- a reference of the form "path.to.module:attribute",
|
|
55
|
+
- or process registry instance,
|
|
56
|
+
- or as a no-arg process registry getter function.
|
|
57
|
+
|
|
58
|
+
The process registry is usually a singleton in your application.
|
|
59
|
+
|
|
60
|
+
The context object `obj` of the returned CLI object
|
|
61
|
+
will be of type `dict` and will contain
|
|
62
|
+
a process registry getter function using the key
|
|
63
|
+
`get_process_registry`.
|
|
64
|
+
|
|
65
|
+
The function must be called before any CLI command or
|
|
66
|
+
callback has been invoked. Otherwise, the provided
|
|
67
|
+
`get_process_registry` getter will not be recognized and
|
|
68
|
+
all commands that require the process registry will
|
|
69
|
+
fail with an `AssertionError`.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: The name of the CLI application.
|
|
73
|
+
registry: A registry reference string,
|
|
74
|
+
or a registry instance, or a no-arg
|
|
75
|
+
function that returns a registry instance.
|
|
76
|
+
help: Optional CLI application help text. If not provided, the default
|
|
77
|
+
`cuiman` help text will be used.
|
|
78
|
+
summary: A one-sentence human-readable description of the tool that
|
|
79
|
+
will be used by the default help text. Hence, used only,
|
|
80
|
+
if `help` is not provided. Should end with a dot '.'.
|
|
81
|
+
version: Optional version string. If not provided, the
|
|
82
|
+
`cuiman` version will be used.
|
|
83
|
+
context: Additional context values that will be registered with the CLI
|
|
84
|
+
and can be accessed by commands that you add to the
|
|
85
|
+
returned `typer.Typer` instance.
|
|
86
|
+
|
|
87
|
+
Return:
|
|
88
|
+
a `typer.Typer` instance
|
|
89
|
+
"""
|
|
90
|
+
assert bool(registry), "registry argument must be provided"
|
|
91
|
+
assert bool(name), "name argument must be provided"
|
|
92
|
+
assert bool(version), "version argument must be provided"
|
|
93
|
+
|
|
94
|
+
t = typer.Typer(
|
|
95
|
+
cls=AliasedGroup,
|
|
96
|
+
name=name,
|
|
97
|
+
help=(
|
|
98
|
+
help
|
|
99
|
+
or DEFAULT_HELP.format(summary=summary or DEFAULT_SUMMARY.format(name=name))
|
|
100
|
+
),
|
|
101
|
+
add_completion=False,
|
|
102
|
+
invoke_without_command=True,
|
|
103
|
+
context_settings={
|
|
104
|
+
"obj": {
|
|
105
|
+
PROCESS_REGISTRY_GETTER_KEY: _parse_process_registry_getter(registry),
|
|
106
|
+
**(context or {}),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@t.callback()
|
|
112
|
+
def main(
|
|
113
|
+
version_: Annotated[
|
|
114
|
+
bool, typer.Option("--version", help="Show version and exit.")
|
|
115
|
+
] = False,
|
|
116
|
+
):
|
|
117
|
+
if version_:
|
|
118
|
+
from procodile import __version__ as procodile_version
|
|
119
|
+
|
|
120
|
+
typer.echo(f"{version} (procodile {procodile_version})")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
@t.command("execute-process")
|
|
124
|
+
def execute_process(
|
|
125
|
+
ctx: typer.Context,
|
|
126
|
+
process_id: Annotated[Optional[str], PROCESS_ID_ARGUMENT] = None,
|
|
127
|
+
dotpath: Annotated[bool, DOT_PATH_OPTION] = False,
|
|
128
|
+
request_inputs: Annotated[Optional[list[str]], REQUEST_INPUT_OPTION] = None,
|
|
129
|
+
request_subscribers: Annotated[
|
|
130
|
+
Optional[list[str]], REQUEST_SUBSCRIBER_OPTION
|
|
131
|
+
] = None,
|
|
132
|
+
request_file: Annotated[Optional[str], REQUEST_FILE_OPTION] = None,
|
|
133
|
+
):
|
|
134
|
+
"""
|
|
135
|
+
Execute a process.
|
|
136
|
+
|
|
137
|
+
The process request to be submitted may be read from a file given
|
|
138
|
+
by `--request`, or from `stdin`, or from the `process_id` argument
|
|
139
|
+
with zero, one, or more `--input` (or `-i`) options.
|
|
140
|
+
|
|
141
|
+
The `process_id` argument and any given `--input` options will override
|
|
142
|
+
settings with the same name found in the given request file or `stdin`,
|
|
143
|
+
if any.
|
|
144
|
+
"""
|
|
145
|
+
from procodile import ExecutionRequest, Job
|
|
146
|
+
|
|
147
|
+
registry = _get_process_registry(ctx)
|
|
148
|
+
execution_request = ExecutionRequest.create(
|
|
149
|
+
process_id=process_id,
|
|
150
|
+
dotpath=dotpath,
|
|
151
|
+
inputs=request_inputs,
|
|
152
|
+
subscribers=request_subscribers,
|
|
153
|
+
request_path=request_file,
|
|
154
|
+
)
|
|
155
|
+
process_id_ = execution_request.process_id
|
|
156
|
+
process = registry.get(process_id_)
|
|
157
|
+
if process is None:
|
|
158
|
+
raise click.ClickException(f"Process {process_id_!r} not found.")
|
|
159
|
+
|
|
160
|
+
job = Job.create(process, request=execution_request.to_process_request())
|
|
161
|
+
job_results = job.run()
|
|
162
|
+
if job_results is not None:
|
|
163
|
+
typer.echo(job_results.model_dump_json(indent=2))
|
|
164
|
+
else:
|
|
165
|
+
typer.echo(job.job_info.model_dump_json(indent=2))
|
|
166
|
+
|
|
167
|
+
@t.command("list-processes", help="List all processes.")
|
|
168
|
+
def list_processes(ctx: typer.Context):
|
|
169
|
+
registry = _get_process_registry(ctx)
|
|
170
|
+
typer.echo(
|
|
171
|
+
json.dumps(
|
|
172
|
+
{
|
|
173
|
+
k: v.description.model_dump(
|
|
174
|
+
mode="json",
|
|
175
|
+
by_alias=True,
|
|
176
|
+
exclude_none=True,
|
|
177
|
+
exclude_defaults=True,
|
|
178
|
+
exclude_unset=True,
|
|
179
|
+
exclude={"inputs", "outputs"},
|
|
180
|
+
)
|
|
181
|
+
for k, v in registry.items()
|
|
182
|
+
},
|
|
183
|
+
indent=2,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@t.command("get-process", help="Get details of a process.")
|
|
188
|
+
def get_process(
|
|
189
|
+
ctx: typer.Context,
|
|
190
|
+
process_id: Annotated[str, PROCESS_ID_ARGUMENT],
|
|
191
|
+
):
|
|
192
|
+
import json
|
|
193
|
+
|
|
194
|
+
registry = _get_process_registry(ctx)
|
|
195
|
+
process = registry.get(process_id)
|
|
196
|
+
if process is None:
|
|
197
|
+
raise click.ClickException(f"Process {process_id!r} not found.")
|
|
198
|
+
|
|
199
|
+
typer.echo(
|
|
200
|
+
json.dumps(
|
|
201
|
+
process.description.model_dump(
|
|
202
|
+
mode="json",
|
|
203
|
+
by_alias=True,
|
|
204
|
+
exclude_defaults=True,
|
|
205
|
+
exclude_none=True,
|
|
206
|
+
exclude_unset=True,
|
|
207
|
+
),
|
|
208
|
+
indent=2,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return t
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
__all__ = [
|
|
216
|
+
"new_cli",
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _parse_process_registry_getter(
|
|
221
|
+
process_registry: Union[
|
|
222
|
+
str,
|
|
223
|
+
"ProcessRegistry",
|
|
224
|
+
Callable[[], "ProcessRegistry"],
|
|
225
|
+
],
|
|
226
|
+
) -> Callable[[], "ProcessRegistry"]:
|
|
227
|
+
process_registry_getter: Callable
|
|
228
|
+
if isinstance(process_registry, str):
|
|
229
|
+
|
|
230
|
+
def process_registry_getter():
|
|
231
|
+
from gavicore.util.dynimp import import_value
|
|
232
|
+
from procodile import ProcessRegistry
|
|
233
|
+
|
|
234
|
+
return import_value(
|
|
235
|
+
process_registry, name="process registry", type=ProcessRegistry
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return process_registry_getter
|
|
239
|
+
|
|
240
|
+
elif callable(process_registry):
|
|
241
|
+
return process_registry
|
|
242
|
+
else:
|
|
243
|
+
|
|
244
|
+
def process_registry_getter():
|
|
245
|
+
return process_registry
|
|
246
|
+
|
|
247
|
+
return process_registry_getter
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_process_registry(
|
|
251
|
+
ctx: typer.Context,
|
|
252
|
+
) -> "ProcessRegistry":
|
|
253
|
+
from procodile import ProcessRegistry
|
|
254
|
+
|
|
255
|
+
process_registry_getter = ctx.obj.get(PROCESS_REGISTRY_GETTER_KEY)
|
|
256
|
+
assert process_registry_getter is not None and callable(process_registry_getter)
|
|
257
|
+
process_registry = process_registry_getter()
|
|
258
|
+
assert isinstance(process_registry, ProcessRegistry)
|
|
259
|
+
return process_registry
|
|
@@ -71,7 +71,8 @@ class JobContext(ABC):
|
|
|
71
71
|
del frame
|
|
72
72
|
# noinspection PyUnreachableCode
|
|
73
73
|
warnings.warn(
|
|
74
|
-
"cannot determine current job context; using non-functional dummy"
|
|
74
|
+
"cannot determine current job context; using non-functional dummy",
|
|
75
|
+
stacklevel=2,
|
|
75
76
|
)
|
|
76
77
|
return NullJobContext()
|
|
77
78
|
|
|
@@ -141,6 +142,7 @@ class Job(JobContext):
|
|
|
141
142
|
job_id: Optional job identifier.
|
|
142
143
|
If omitted, a unique identifier will be generated (UUID4).
|
|
143
144
|
|
|
145
|
+
|
|
144
146
|
Returns:
|
|
145
147
|
A new job instance.
|
|
146
148
|
|
|
@@ -148,6 +150,7 @@ class Job(JobContext):
|
|
|
148
150
|
pydantic.ValidationError: if an input value is not valid
|
|
149
151
|
with respect to its process input description.
|
|
150
152
|
"""
|
|
153
|
+
|
|
151
154
|
process_desc = process.description
|
|
152
155
|
input_params = request.inputs or {}
|
|
153
156
|
input_default_params = {
|
|
@@ -276,6 +279,12 @@ class Job(JobContext):
|
|
|
276
279
|
def _get_job_results(self, function_result: Any) -> JobResults:
|
|
277
280
|
assert self.job_info.status == JobStatus.successful
|
|
278
281
|
assert self.job_info.processID is not None
|
|
282
|
+
|
|
283
|
+
# Outputs are already validated and normalized by
|
|
284
|
+
# `ExecutionContext.normalize_outputs()`.
|
|
285
|
+
if isinstance(function_result, dict):
|
|
286
|
+
return JobResults(**function_result)
|
|
287
|
+
|
|
279
288
|
outputs = self.process.description.outputs or {}
|
|
280
289
|
output_count = len(outputs)
|
|
281
290
|
return JobResults(
|