pinexq-procon 2.1.0.dev3__py3-none-any.whl
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.
- pinexq/procon/__init__.py +0 -0
- pinexq/procon/core/__init__.py +0 -0
- pinexq/procon/core/cli.py +442 -0
- pinexq/procon/core/exceptions.py +64 -0
- pinexq/procon/core/helpers.py +61 -0
- pinexq/procon/core/logconfig.py +48 -0
- pinexq/procon/core/naming.py +36 -0
- pinexq/procon/core/types.py +15 -0
- pinexq/procon/dataslots/__init__.py +19 -0
- pinexq/procon/dataslots/abstractionlayer.py +215 -0
- pinexq/procon/dataslots/annotation.py +389 -0
- pinexq/procon/dataslots/dataslots.py +369 -0
- pinexq/procon/dataslots/datatypes.py +50 -0
- pinexq/procon/dataslots/default_reader_writer.py +26 -0
- pinexq/procon/dataslots/filebackend.py +126 -0
- pinexq/procon/dataslots/metadata.py +137 -0
- pinexq/procon/jobmanagement/__init__.py +9 -0
- pinexq/procon/jobmanagement/api_helpers.py +287 -0
- pinexq/procon/remote/__init__.py +0 -0
- pinexq/procon/remote/messages.py +250 -0
- pinexq/procon/remote/rabbitmq.py +420 -0
- pinexq/procon/runtime/__init__.py +3 -0
- pinexq/procon/runtime/foreman.py +128 -0
- pinexq/procon/runtime/job.py +384 -0
- pinexq/procon/runtime/settings.py +12 -0
- pinexq/procon/runtime/tool.py +16 -0
- pinexq/procon/runtime/worker.py +437 -0
- pinexq/procon/step/__init__.py +3 -0
- pinexq/procon/step/introspection.py +234 -0
- pinexq/procon/step/schema.py +99 -0
- pinexq/procon/step/step.py +119 -0
- pinexq/procon/step/versioning.py +84 -0
- pinexq_procon-2.1.0.dev3.dist-info/METADATA +83 -0
- pinexq_procon-2.1.0.dev3.dist-info/RECORD +35 -0
- pinexq_procon-2.1.0.dev3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import typing
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, Final, NewType, TypeVar
|
|
4
|
+
|
|
5
|
+
from ..core.exceptions import ProConDataslotError
|
|
6
|
+
from .annotation import RETURN_SLOT_NAME, dataslot
|
|
7
|
+
from .dataslots import DataSlot, DataSlotDescription, SlotType
|
|
8
|
+
from .metadata import MetadataHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..step.introspection import FunctionSchema
|
|
13
|
+
|
|
14
|
+
R = TypeVar('R')
|
|
15
|
+
P = NewType('P', dict[str, Any])
|
|
16
|
+
F = NewType('F', Callable[[P], R])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# fmt: off
|
|
20
|
+
class NotSetYetType(enum.IntEnum):
|
|
21
|
+
token = 0
|
|
22
|
+
NOTSET: Final = NotSetYetType.token # noqa: E305
|
|
23
|
+
# fmt: on
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DataslotLayer:
|
|
27
|
+
"""Abstraction layer that handles reading and writing from/to storage locations
|
|
28
|
+
and presents them as regular parameters to the wrapped function."""
|
|
29
|
+
|
|
30
|
+
_function: F
|
|
31
|
+
_regular_parameters: P | NotSetYetType = NOTSET
|
|
32
|
+
_dataslot_parameters: P | NotSetYetType = NOTSET
|
|
33
|
+
_result: R | NotSetYetType = NOTSET
|
|
34
|
+
_signature: "FunctionSchema"
|
|
35
|
+
_dataslots_in: dict[str, DataSlot]
|
|
36
|
+
_dataslots_out: dict[str, DataSlot]
|
|
37
|
+
_sig_by_alias: dict[str, dataslot]
|
|
38
|
+
_result_slot: DataSlot | None
|
|
39
|
+
_metadata_handler: MetadataHandler | None
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
function: F,
|
|
44
|
+
parameters: P,
|
|
45
|
+
signature: "FunctionSchema",
|
|
46
|
+
dataslots_in_descr: dict[str, DataSlotDescription],
|
|
47
|
+
dataslots_out_descr: dict[str, DataSlotDescription],
|
|
48
|
+
metadata_handler: MetadataHandler | None = None
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
function: A function (potentially) with dataslots for parameters
|
|
54
|
+
or return value.
|
|
55
|
+
parameters: Dictionary with the parameters the function will be
|
|
56
|
+
called with.
|
|
57
|
+
dataslots_in_descr: Dictionary with `DataSlotDescription` objects
|
|
58
|
+
for each input dataslot parameter.
|
|
59
|
+
dataslots_out_descr: Dictionary with `DataSlotDescription` objects
|
|
60
|
+
for all output dataslots. The return value of the function is
|
|
61
|
+
internally defined by the name '__returns__'.
|
|
62
|
+
signature: A `FunctionSchema` object created during the function's
|
|
63
|
+
introspection (optional). If not provided, the introspection will
|
|
64
|
+
be called on the function internally.
|
|
65
|
+
metadata_handler: An optional `MetadataHandler` object uses to access
|
|
66
|
+
metadata from the currently used backend.
|
|
67
|
+
"""
|
|
68
|
+
self._function = function
|
|
69
|
+
self._regular_parameters = parameters or {} # avoid parameters being None
|
|
70
|
+
self._metadata_handler = metadata_handler
|
|
71
|
+
|
|
72
|
+
self._signature = signature
|
|
73
|
+
# Dataslots are presented to the outside by their alias name
|
|
74
|
+
self._sig_by_alias = {(s.alias or s.name): s for n, s in self._signature.dataslots.items()}
|
|
75
|
+
|
|
76
|
+
self._dataslots_in = self._init_dataslots(dataslots_in_descr)
|
|
77
|
+
|
|
78
|
+
# filter out special output slot `__returns__` (if present) since it isn't part of the function's parameters
|
|
79
|
+
output_slots = self._init_dataslots(dataslots_out_descr)
|
|
80
|
+
self._result_slot = None
|
|
81
|
+
for alias_name, ds in output_slots.items():
|
|
82
|
+
if ds.annotation.name == RETURN_SLOT_NAME:
|
|
83
|
+
self._result_slot = ds
|
|
84
|
+
output_slots.pop(alias_name)
|
|
85
|
+
break
|
|
86
|
+
self._dataslots_out = output_slots
|
|
87
|
+
|
|
88
|
+
def _init_dataslots(self, dataslot_description: dict[str, DataSlotDescription]) -> dict[str, DataSlot]:
|
|
89
|
+
"""Creates the actual `DataSlot` object and match parameter alias names with
|
|
90
|
+
dataslots from the signature."""
|
|
91
|
+
slots = {}
|
|
92
|
+
for name, descr in dataslot_description.items():
|
|
93
|
+
try:
|
|
94
|
+
slots[name] = DataSlot(
|
|
95
|
+
description=descr,
|
|
96
|
+
annotation=self._sig_by_alias[name],
|
|
97
|
+
metadata_handler=self._metadata_handler,
|
|
98
|
+
)
|
|
99
|
+
except KeyError as ex:
|
|
100
|
+
raise ProConDataslotError(
|
|
101
|
+
f"Dataslot '{name}' not found in function '{self._signature.name}'. "
|
|
102
|
+
f"Possibly a typo or the Dataslot is renamed by an alias? "
|
|
103
|
+
f"(available dataslots: {list(self._sig_by_alias.keys())})") from ex
|
|
104
|
+
return slots
|
|
105
|
+
|
|
106
|
+
def __enter__(self) -> "DataslotLayer":
|
|
107
|
+
"""Sync all files in the dataslots; i.e. if not local, get them"""
|
|
108
|
+
self._resolve_parameters()
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def _resolve_parameters(self):
|
|
112
|
+
"""Resolve all implicit dataslots.
|
|
113
|
+
If a dataslot is NOT defined as type `Dataslot`, open it and deserialize the data if necessary"""
|
|
114
|
+
self._dataslot_parameters = typing.cast(P, {})
|
|
115
|
+
for n, ds in (self._dataslots_in | self._dataslots_out).items():
|
|
116
|
+
param_name = self._sig_by_alias[n].name
|
|
117
|
+
self._dataslot_parameters[param_name] = self._extract_parameter(ds)
|
|
118
|
+
|
|
119
|
+
self._check_for_disjoint_names()
|
|
120
|
+
|
|
121
|
+
def _extract_parameter(self, ds: DataSlot) -> Any:
|
|
122
|
+
# Function parameter is explicitly annotated with ":DataSlot"
|
|
123
|
+
if ds.annotation.dtype is DataSlot:
|
|
124
|
+
return ds
|
|
125
|
+
elif ds.annotation.slot_type == SlotType.INPUT:
|
|
126
|
+
return ds.read_data_from_slots()
|
|
127
|
+
elif ds.annotation.slot_type == SlotType.OUTPUT:
|
|
128
|
+
return self._init_output_dataslot_parameter(ds)
|
|
129
|
+
else:
|
|
130
|
+
raise Exception("Tried to init a return dataslot as parameter.")
|
|
131
|
+
|
|
132
|
+
def _check_for_disjoint_names(self):
|
|
133
|
+
dataslot_names = set(self._dataslot_parameters.keys())
|
|
134
|
+
parameter_names = set(self._regular_parameters.keys())
|
|
135
|
+
if not dataslot_names.isdisjoint(parameter_names):
|
|
136
|
+
raise ProConDataslotError("Name collision between dataslot- and parameter-names!"
|
|
137
|
+
"The following names appear in both: "
|
|
138
|
+
f"{', '.join(dataslot_names & parameter_names)}")
|
|
139
|
+
|
|
140
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
141
|
+
"""Sync all files in the dataslots; i.e. if not local, push them to their destination"""
|
|
142
|
+
# Skip syncing return-Dataslots when there was an Exception, as the function did not return anything.
|
|
143
|
+
# Sync all other Dataslots, even in case of an error.
|
|
144
|
+
if exc_type is None:
|
|
145
|
+
self._resolve_result()
|
|
146
|
+
# self._resolve_result(include_result_slot=exc_type is None)
|
|
147
|
+
|
|
148
|
+
def _resolve_result(self, include_result_slot: bool = True):
|
|
149
|
+
"""Resolve the result value to a dataslot; i.e. serializing and saving
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
include_result_slot: Write data for return-Dataslots (default: True)
|
|
153
|
+
"""
|
|
154
|
+
if self._result_slot and include_result_slot:
|
|
155
|
+
self._result_slot.write_data_to_slots(self._result)
|
|
156
|
+
|
|
157
|
+
for dataslot_name, ds in self._dataslots_out.items():
|
|
158
|
+
if ds.annotation.dtype is not DataSlot:
|
|
159
|
+
ds.write_data_to_slots(self._dataslot_parameters[dataslot_name])
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def parameters(self) -> P | NotSetYetType:
|
|
163
|
+
"""The given parameters and the resolved data from all input dataslots combined."""
|
|
164
|
+
if self._dataslot_parameters is NOTSET:
|
|
165
|
+
return NOTSET
|
|
166
|
+
return typing.cast(
|
|
167
|
+
P, self._regular_parameters | self._dataslot_parameters
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# @property
|
|
171
|
+
# def function(self) -> F:
|
|
172
|
+
# return self._function
|
|
173
|
+
|
|
174
|
+
def call_function(self) -> R:
|
|
175
|
+
"""Call the function and return the *raw parameters*."""
|
|
176
|
+
self._result = self._function(
|
|
177
|
+
**self.parameters
|
|
178
|
+
)
|
|
179
|
+
return self._result
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def result(self) -> R | NotSetYetType:
|
|
183
|
+
"""The return value of the function call or None if it was written to an output dataslot"""
|
|
184
|
+
return self._result if not self._result_slot else None
|
|
185
|
+
|
|
186
|
+
def has_results_data_slot(self) -> bool:
|
|
187
|
+
"""Return True if the function has a `return.dataslot` annotation."""
|
|
188
|
+
return self._result_slot is not None
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _init_output_dataslot_parameter(ds) -> Any:
|
|
192
|
+
"""Initialize a dataslot with an empty instance of the annotated type.
|
|
193
|
+
This requires, that the type's constructor can be called without parameter.
|
|
194
|
+
E.g. for `list` calling `list()` will return and empty list."""
|
|
195
|
+
try:
|
|
196
|
+
return ds.annotation.dtype()
|
|
197
|
+
except Exception as ex:
|
|
198
|
+
raise ProConDataslotError(f"Can not create default instance of {ds.annotation.dtype.__name__}"
|
|
199
|
+
f" for output dataslot {ds.annotation.name}."
|
|
200
|
+
f" Type must be creatable without parameters.") from ex
|
|
201
|
+
|
|
202
|
+
def update_parameters(self, new: dict[str, Any]) -> None:
|
|
203
|
+
"""Update the internal state of the parameters. Needed to update parameters that use
|
|
204
|
+
"return by reference", i.e. Output-Dataslots, after calling the function."""
|
|
205
|
+
for name, value in new.items():
|
|
206
|
+
if name in self._dataslot_parameters:
|
|
207
|
+
self._dataslot_parameters[name] = value
|
|
208
|
+
|
|
209
|
+
def update_result(self, new: Any) -> None:
|
|
210
|
+
"""Set the return value of the function. Needed to funnel the return value to a Return-DataSlot."""
|
|
211
|
+
self._result = new
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def function(self) -> F:
|
|
215
|
+
return self._function
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import IO, Any, Callable, Literal, TypeAlias
|
|
5
|
+
|
|
6
|
+
from ..core.types import UNSET, UNSETTYPE
|
|
7
|
+
from ..dataslots.datatypes import MediaTypes, SlotType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
DATASLOT_ANNOTATION_NAME = "__dataslots__"
|
|
13
|
+
RETURN_SLOT_NAME = "__returns__"
|
|
14
|
+
|
|
15
|
+
FileMode: TypeAlias = Literal["r", "rb", "w", "wb"]
|
|
16
|
+
ReaderType: TypeAlias = Callable[[IO], Any]
|
|
17
|
+
WriterType: TypeAlias = Callable[[IO, Any], None]
|
|
18
|
+
CollectionReaderType: TypeAlias = Callable[[list[IO]], Any]
|
|
19
|
+
CollectionWriterType: TypeAlias = Callable[[[IO], Any], None]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def media_type_2_file_mode(media_type: MediaTypes, is_write=False) -> FileMode:
|
|
23
|
+
access_mode = "w" if is_write else "r"
|
|
24
|
+
mapping = {
|
|
25
|
+
MediaTypes.OCTETSTREAM: "b",
|
|
26
|
+
MediaTypes.JSON: "",
|
|
27
|
+
MediaTypes.FORMDATA: "",
|
|
28
|
+
MediaTypes.SIREN: "",
|
|
29
|
+
MediaTypes.XML: "",
|
|
30
|
+
MediaTypes.ZIP: "b",
|
|
31
|
+
MediaTypes.PDF: "b",
|
|
32
|
+
MediaTypes.TEXT: "",
|
|
33
|
+
MediaTypes.HTML: "",
|
|
34
|
+
MediaTypes.CSV: "",
|
|
35
|
+
MediaTypes.SVG: "",
|
|
36
|
+
MediaTypes.PNG: "b",
|
|
37
|
+
MediaTypes.JPEG: "b",
|
|
38
|
+
MediaTypes.BMP: "b",
|
|
39
|
+
MediaTypes.WORKFLOW_DEFINITION: "",
|
|
40
|
+
MediaTypes.WORKFLOW_REPORT: "",
|
|
41
|
+
}
|
|
42
|
+
# if we do not know the mediatype we assume binary
|
|
43
|
+
binary_suffix = mapping.get(media_type, None)
|
|
44
|
+
if binary_suffix is None:
|
|
45
|
+
binary_suffix = "b"
|
|
46
|
+
log.warning(
|
|
47
|
+
f"Unknown media type {media_type} will assume binary format. "
|
|
48
|
+
f"If this does not fit, please set the mode on the dataslot explicitly."
|
|
49
|
+
)
|
|
50
|
+
return access_mode + binary_suffix
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class dataslot:
|
|
55
|
+
"""Decorator to attach metadata for a dataslot to a function.
|
|
56
|
+
You can use the general Dataslot decorator `@dataslot` to parametrize all possible options
|
|
57
|
+
manually or use the specialized constructors, which offer default settings and only the
|
|
58
|
+
necessary options for every dataslot type.
|
|
59
|
+
|
|
60
|
+
The available constructors are:
|
|
61
|
+
- `dataslot.input`
|
|
62
|
+
- `dataslot.output`
|
|
63
|
+
- `dataslot.input_collection`
|
|
64
|
+
- `dataslot.output_collection`
|
|
65
|
+
- `dataslot.returns`
|
|
66
|
+
- `dataslot.returns_collection`
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
name: The name has to match the functions parameter that will be used as dataslot.
|
|
70
|
+
alias: The name for this dataslot presented in the manifest and expected in a job offer.
|
|
71
|
+
title: Brief description of the dataslot.
|
|
72
|
+
description: More detailed description of the dataslot.
|
|
73
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
74
|
+
reader: A callable accepting a file object as parameter and returning the datatype
|
|
75
|
+
expected by the functions' parameter.
|
|
76
|
+
writer: A callable accepting a file object and data to be written as a parameter.
|
|
77
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
78
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
79
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
80
|
+
collection: Whether the dataslot accepts more than one file as slots. (default: False)
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
alias: str | None = field(default=None, kw_only=True)
|
|
85
|
+
slot_type: SlotType = field(default=SlotType.INPUT, kw_only=True)
|
|
86
|
+
title: str = field(default_factory=str, kw_only=True)
|
|
87
|
+
description: str = field(default_factory=str, kw_only=True)
|
|
88
|
+
media_type: str = field(default=MediaTypes.OCTETSTREAM, kw_only=True)
|
|
89
|
+
reader: ReaderType | None = field(default=None, kw_only=True)
|
|
90
|
+
writer: WriterType | None = field(default=None, kw_only=True)
|
|
91
|
+
collection_reader: CollectionReaderType | None = field(default=None, kw_only=True)
|
|
92
|
+
collection_writer: CollectionWriterType | None = field(default=None, kw_only=True)
|
|
93
|
+
mode: FileMode = field(default="r", kw_only=True)
|
|
94
|
+
dtype: type | None = field(default=None, kw_only=True)
|
|
95
|
+
collection: bool = field(default=False, kw_only=True)
|
|
96
|
+
min_slots: int | UNSETTYPE = field(default=UNSET, kw_only=True)
|
|
97
|
+
max_slots: int | None | UNSETTYPE = field(default=UNSET, kw_only=True)
|
|
98
|
+
|
|
99
|
+
def __post_init__(self):
|
|
100
|
+
if self.min_slots is UNSET:
|
|
101
|
+
self.min_slots = 1
|
|
102
|
+
if self.max_slots is UNSET:
|
|
103
|
+
self.max_slots = self.min_slots
|
|
104
|
+
if (self.max_slots is not None) and (self.min_slots > self.max_slots):
|
|
105
|
+
raise ValueError("Dataslot attribute 'min_slots' has to smaller than 'max_slots'!")
|
|
106
|
+
|
|
107
|
+
if (self.collection_reader or self.collection_writer) and not self.collection:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"'collection_reader' and '*_writer' can only be used for collection-DataSlots"
|
|
110
|
+
" (when the parameter 'collection=True')!"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def input(
|
|
115
|
+
cls,
|
|
116
|
+
name: str,
|
|
117
|
+
title: str = "",
|
|
118
|
+
description: str = "",
|
|
119
|
+
media_type: MediaTypes = MediaTypes.OCTETSTREAM,
|
|
120
|
+
mode: FileMode | None = None,
|
|
121
|
+
alias: str | None = None,
|
|
122
|
+
reader: ReaderType | None = None,
|
|
123
|
+
dtype: type | None = None,
|
|
124
|
+
):
|
|
125
|
+
"""Define the parameter `name` as input dataslot.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
name: The name has to match the functions parameter that will be used as dataslot.
|
|
129
|
+
alias: The name for this dataslot presented in the manifest and expected in a job offer.
|
|
130
|
+
title: Brief description of the dataslot.
|
|
131
|
+
description: More detailed description of the dataslot.
|
|
132
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
133
|
+
reader: A callable accepting a file object as parameter and returning the datatype
|
|
134
|
+
expected by the functions' parameter.
|
|
135
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
136
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
137
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
138
|
+
"""
|
|
139
|
+
# The typechecker will complain that `_mode` could be None in the return statement below, which can't be.
|
|
140
|
+
_mode = media_type_2_file_mode(media_type, is_write=False) if mode is None else mode
|
|
141
|
+
# Using the following if/else clause it's recognized correctly, even though it's functionally the same.
|
|
142
|
+
# if mode is None:
|
|
143
|
+
# _mode = media_type_2_file_mode(media_type, is_write=False)
|
|
144
|
+
# else:
|
|
145
|
+
# _mode = mode
|
|
146
|
+
return cls(
|
|
147
|
+
name,
|
|
148
|
+
slot_type=SlotType.INPUT,
|
|
149
|
+
title=title,
|
|
150
|
+
description=description,
|
|
151
|
+
media_type=media_type,
|
|
152
|
+
mode=_mode,
|
|
153
|
+
alias=alias,
|
|
154
|
+
reader=reader,
|
|
155
|
+
dtype=dtype,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def input_collection(
|
|
160
|
+
cls,
|
|
161
|
+
name: str,
|
|
162
|
+
alias: str | None = None,
|
|
163
|
+
title: str = "",
|
|
164
|
+
description: str = "",
|
|
165
|
+
media_type: MediaTypes = MediaTypes.OCTETSTREAM,
|
|
166
|
+
mode: FileMode | None = None,
|
|
167
|
+
collection_reader: CollectionReaderType | None = None,
|
|
168
|
+
dtype: type | None = None,
|
|
169
|
+
min_slots: int | UNSETTYPE = UNSET,
|
|
170
|
+
max_slots: int | None | UNSETTYPE = UNSET,
|
|
171
|
+
):
|
|
172
|
+
"""Define the input dataslot as collection of files
|
|
173
|
+
|
|
174
|
+
Attributes:
|
|
175
|
+
name: The name has to match the functions parameter that will be used as dataslot.
|
|
176
|
+
alias: The name for this dataslot presented in the manifest and expected in a job offer.
|
|
177
|
+
title: Brief description of the dataslot.
|
|
178
|
+
description: More detailed description of the dataslot.
|
|
179
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
180
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
181
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
182
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
183
|
+
"""
|
|
184
|
+
_mode = media_type_2_file_mode(media_type, is_write=False) if mode is None else mode
|
|
185
|
+
return cls(
|
|
186
|
+
name,
|
|
187
|
+
slot_type=SlotType.INPUT,
|
|
188
|
+
collection=True,
|
|
189
|
+
title=title,
|
|
190
|
+
description=description,
|
|
191
|
+
media_type=media_type,
|
|
192
|
+
mode=_mode, # see comment in input()
|
|
193
|
+
alias=alias,
|
|
194
|
+
collection_reader=collection_reader,
|
|
195
|
+
dtype=dtype,
|
|
196
|
+
min_slots=min_slots,
|
|
197
|
+
max_slots=max_slots,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def output(
|
|
202
|
+
cls,
|
|
203
|
+
name: str,
|
|
204
|
+
title: str = "",
|
|
205
|
+
description: str = "",
|
|
206
|
+
media_type: MediaTypes = MediaTypes.OCTETSTREAM,
|
|
207
|
+
mode: FileMode | None = None,
|
|
208
|
+
alias: str | None = None,
|
|
209
|
+
writer: WriterType | None = None,
|
|
210
|
+
dtype: type | None = None,
|
|
211
|
+
):
|
|
212
|
+
"""Define the parameter `name` as output dataslot.
|
|
213
|
+
|
|
214
|
+
Attributes:
|
|
215
|
+
name: The name has to match the functions parameter that will be used as dataslot.
|
|
216
|
+
alias: The name for this dataslot presented in the manifest and expected in a job offer.
|
|
217
|
+
title: Brief description of the dataslot.
|
|
218
|
+
description: More detailed description of the dataslot.
|
|
219
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
220
|
+
writer: A callable accepting a file object and data to be written as a parameter.
|
|
221
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
222
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
223
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
224
|
+
"""
|
|
225
|
+
_mode = media_type_2_file_mode(media_type, is_write=True) if mode is None else mode
|
|
226
|
+
return cls(
|
|
227
|
+
name,
|
|
228
|
+
slot_type=SlotType.OUTPUT,
|
|
229
|
+
title=title,
|
|
230
|
+
description=description,
|
|
231
|
+
media_type=media_type,
|
|
232
|
+
mode=_mode, # see comment in input()
|
|
233
|
+
alias=alias,
|
|
234
|
+
writer=writer,
|
|
235
|
+
dtype=dtype,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def output_collection(
|
|
240
|
+
cls,
|
|
241
|
+
name: str,
|
|
242
|
+
title: str = "",
|
|
243
|
+
description: str = "",
|
|
244
|
+
media_type: MediaTypes = MediaTypes.OCTETSTREAM,
|
|
245
|
+
mode: FileMode | None = None,
|
|
246
|
+
alias: str | None = None,
|
|
247
|
+
collection_writer: CollectionWriterType | None = None,
|
|
248
|
+
dtype: type | None = None,
|
|
249
|
+
min_slots: int | UNSETTYPE = UNSET,
|
|
250
|
+
max_slots: int | None | UNSETTYPE = UNSET,
|
|
251
|
+
):
|
|
252
|
+
"""Define the output dataslot as collection of files
|
|
253
|
+
|
|
254
|
+
Attributes:
|
|
255
|
+
name: The name has to match the functions parameter that will be used as dataslot.
|
|
256
|
+
alias: The name for this dataslot presented in the manifest and expected in a job offer.
|
|
257
|
+
title: Brief description of the dataslot.
|
|
258
|
+
description: More detailed description of the dataslot.
|
|
259
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
260
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
261
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
262
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
263
|
+
"""
|
|
264
|
+
_mode = media_type_2_file_mode(media_type, is_write=True) if mode is None else mode
|
|
265
|
+
return cls(
|
|
266
|
+
name,
|
|
267
|
+
slot_type=SlotType.OUTPUT,
|
|
268
|
+
collection=True,
|
|
269
|
+
title=title,
|
|
270
|
+
description=description,
|
|
271
|
+
media_type=media_type,
|
|
272
|
+
mode=_mode, # see comment in input()
|
|
273
|
+
alias=alias,
|
|
274
|
+
collection_writer=collection_writer,
|
|
275
|
+
dtype=dtype,
|
|
276
|
+
min_slots=min_slots,
|
|
277
|
+
max_slots=max_slots,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def returns(
|
|
282
|
+
cls,
|
|
283
|
+
title: str = "",
|
|
284
|
+
description: str = "",
|
|
285
|
+
media_type: MediaTypes = MediaTypes.OCTETSTREAM,
|
|
286
|
+
mode: FileMode | None = None,
|
|
287
|
+
writer: WriterType | None = None,
|
|
288
|
+
dtype: type | None = None,
|
|
289
|
+
):
|
|
290
|
+
"""Define the return value of the function to be written to a dataslot.
|
|
291
|
+
|
|
292
|
+
Attributes:
|
|
293
|
+
title: Brief description of the dataslot.
|
|
294
|
+
description: More detailed description of the dataslot.
|
|
295
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
296
|
+
writer: A callable accepting a file object and data to be written as a parameter.
|
|
297
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
298
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
299
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
300
|
+
"""
|
|
301
|
+
_mode = media_type_2_file_mode(media_type, is_write=True) if mode is None else mode
|
|
302
|
+
return cls(
|
|
303
|
+
RETURN_SLOT_NAME,
|
|
304
|
+
slot_type=SlotType.RETURN,
|
|
305
|
+
collection=False,
|
|
306
|
+
title=title,
|
|
307
|
+
description=description,
|
|
308
|
+
media_type=media_type,
|
|
309
|
+
mode=_mode, # see comment in input()
|
|
310
|
+
writer=writer,
|
|
311
|
+
dtype=dtype,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def returns_collection(
|
|
316
|
+
cls,
|
|
317
|
+
title: str = "",
|
|
318
|
+
description: str = "",
|
|
319
|
+
media_type: MediaTypes = MediaTypes.OCTETSTREAM,
|
|
320
|
+
mode: FileMode | None = None,
|
|
321
|
+
collection_writer: CollectionWriterType | None = None,
|
|
322
|
+
dtype: type | None = None,
|
|
323
|
+
min_slots: int | UNSETTYPE = UNSET,
|
|
324
|
+
max_slots: int | None | UNSETTYPE = UNSET,
|
|
325
|
+
):
|
|
326
|
+
"""Define the return value of the function will be written to a return dataslot.
|
|
327
|
+
|
|
328
|
+
Attributes:
|
|
329
|
+
title: Brief description of the dataslot.
|
|
330
|
+
description: More detailed description of the dataslot.
|
|
331
|
+
media_type: The media type as `MediaTypes` Enum.
|
|
332
|
+
mode: File mode of the IO object representing the dataslot. Usually set by the constructor
|
|
333
|
+
to a reasonable value ('r' for inputs, 'w' for outputs, 'b' for binary).
|
|
334
|
+
dtype: The annotated type of implicitly defined dataslots. Set by the introspection internally.
|
|
335
|
+
"""
|
|
336
|
+
_mode = media_type_2_file_mode(media_type, is_write=True) if mode is None else mode
|
|
337
|
+
return cls(
|
|
338
|
+
RETURN_SLOT_NAME,
|
|
339
|
+
slot_type=SlotType.RETURN,
|
|
340
|
+
collection=True,
|
|
341
|
+
title=title,
|
|
342
|
+
description=description,
|
|
343
|
+
mode=_mode, # see comment in input()
|
|
344
|
+
media_type=media_type,
|
|
345
|
+
collection_writer=collection_writer,
|
|
346
|
+
dtype=dtype,
|
|
347
|
+
min_slots=min_slots,
|
|
348
|
+
max_slots=max_slots,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# @classmethod
|
|
352
|
+
# def watch(cls, name: str, **kwargs):
|
|
353
|
+
# """Watch the local filesystem and upload written files to this dataslot."""
|
|
354
|
+
# raise NotImplementedError()
|
|
355
|
+
|
|
356
|
+
def __call__(self, function: Callable[[Any], Any]):
|
|
357
|
+
"""Called when used as a decorator to attach metadata to 'func'."""
|
|
358
|
+
metadata = function.__dict__.setdefault(DATASLOT_ANNOTATION_NAME, {})
|
|
359
|
+
if self.name in metadata:
|
|
360
|
+
raise NameError(f"A dataslot with the name '{self.name}' is already defined on '{function.__name__}()'!")
|
|
361
|
+
if (self.name == RETURN_SLOT_NAME) and (self.slot_type is not SlotType.RETURN):
|
|
362
|
+
raise NameError(f"Invalid dataslot name.The name '{RETURN_SLOT_NAME}' is reserved for internal use.")
|
|
363
|
+
metadata[self.name] = self
|
|
364
|
+
|
|
365
|
+
return function
|
|
366
|
+
|
|
367
|
+
def update(self, d: "dataslot"):
|
|
368
|
+
"""Update this dataslot with information from another dataslot"""
|
|
369
|
+
if not isinstance(d, self.__class__):
|
|
370
|
+
raise TypeError("Can only update with dataslots of the same type!")
|
|
371
|
+
|
|
372
|
+
fields_with_default_value = {
|
|
373
|
+
f.name #
|
|
374
|
+
for f in dataclasses.fields(d)
|
|
375
|
+
if getattr(d, f.name) == f.default
|
|
376
|
+
}
|
|
377
|
+
set_fields = {
|
|
378
|
+
name: value #
|
|
379
|
+
for name, value in dataclasses.asdict(d).items()
|
|
380
|
+
if name not in fields_with_default_value
|
|
381
|
+
}
|
|
382
|
+
return dataclasses.replace(self, **set_fields)
|
|
383
|
+
|
|
384
|
+
def __or__(self, other):
|
|
385
|
+
return self.update(other)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def get_dataslot_metadata(function: Callable[[Any], Any]) -> dict[str, dataslot]:
|
|
389
|
+
return getattr(function, DATASLOT_ANNOTATION_NAME, {})
|