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.
@@ -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, {})