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.
File without changes
File without changes
@@ -0,0 +1,442 @@
1
+ """
2
+ This is the CLI interface for the module.
3
+ It provides the entry points and top-level implementation for all commands.
4
+
5
+ The available commands are:
6
+ <your_script.py>
7
+ - list: List the available functions
8
+ - run: Execute a function locally
9
+ - signature: Show a functions signature
10
+ - remote: Connect to job-management remotely
11
+ - register: Register a functions signature via the API
12
+ """
13
+
14
+ import ast
15
+ import json
16
+ import logging
17
+ import uuid
18
+ from typing import Any
19
+
20
+ import click
21
+ import rich
22
+ from pydantic import BaseModel
23
+
24
+ from ..dataslots import create_dataslot_description
25
+ from ..step import Step
26
+ from ..step.step import ExecutionContext
27
+ from .exceptions import ProConUnknownFunctionError
28
+ from .helpers import log_version_info, remove_environment_variables
29
+ from .logconfig import configure_logging
30
+ from .naming import is_valid_version_string
31
+
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ @click.group()
37
+ @click.pass_context
38
+ def cli(ctx):
39
+ """CLI interface for the processing container."""
40
+ # The Step object is provided as *obj* when invoking *cli* in Step.__init__()
41
+ # and available in the context *ctx* to all subcommands.
42
+ step: Step = ctx.obj # noqa: F841 # here it is just for demonstration
43
+
44
+
45
+ @cli.command(name="list") # avoid shadowing builtin 'list'
46
+ @click.option("-j", "--json", "json_", is_flag=True, help="Output in JSON format")
47
+ @click.option("-y", "--pretty", is_flag=True, help="Prettier output with color and comments")
48
+ @click.pass_context
49
+ def list_(ctx, json_: bool, pretty: bool):
50
+ """List all available functions in this container."""
51
+ step: Step = ctx.obj
52
+ # noinspection PyProtectedMember
53
+ funcs = step._signatures.items()
54
+
55
+ if json_:
56
+ func_names = [name for name, _ in funcs]
57
+ print(json.dumps(func_names))
58
+ return
59
+
60
+ if pretty:
61
+ rich.print("\nAvailable functions:")
62
+ for name, schema in funcs:
63
+ short_doc = f"# {schema.signature['short_description']}"
64
+ rich.print(f"[green]{name:<16}[/green] {short_doc}")
65
+ else:
66
+ for name, _ in funcs:
67
+ rich.print(f"{name}")
68
+
69
+
70
+ @cli.command()
71
+ @click.option("-f", "--function", type=str, required=True, help="The function to be called in this container.")
72
+ @click.option("-p", "--parameters", help="Parameters formatted as Python dictionary")
73
+ @click.option("-di", "--input-dataslots", help="URIs for input dataslots formatted as Python dictionary")
74
+ @click.option("-do", "--output-dataslots", help="URIs for output dataslots formatted as Python dictionary")
75
+ @click.option(
76
+ "-i",
77
+ "--input-file",
78
+ type=click.Path(exists=True, dir_okay=False),
79
+ help="Json-formatted file with function parameters",
80
+ )
81
+ @click.option("-o", "--output-file", type=click.Path(exists=False, dir_okay=False), help="Output file (Json-formatted)")
82
+ @click.option("-j", "--json", "as_json", is_flag=True, help="Output as JSON schema")
83
+ @click.option("-y", "--pretty", is_flag=True, help="Format output indentations and color")
84
+ @click.option("-d", "--debug", is_flag=True, help="enable more verbose debug output")
85
+ @click.option("-r", "--richformat", is_flag=True, help="enable rich formatting of log output")
86
+ @click.pass_context
87
+ def run(
88
+ ctx,
89
+ function: str,
90
+ parameters: str,
91
+ input_dataslots: str,
92
+ output_dataslots: str,
93
+ input_file: click.Path,
94
+ output_file: click.Path,
95
+ as_json: bool,
96
+ pretty: bool,
97
+ debug: bool = False,
98
+ richformat: bool = False,
99
+ ):
100
+ """Execute a function locally.
101
+
102
+ You can provide the PARAMETERS for the FUNCTION as a Python dictionary:
103
+ e.g. python <this_script> -f <function_name> -p "{'a':1, 'b':'Hello'}"
104
+
105
+ Dataslots can be defined the same way as parameters for local testing:
106
+ e.g. python <this_script> -f <function_name> -di "{'slot_name':'/usr/test/data.txt'}"
107
+
108
+ Alternatively you can read the parameters from a Json-formatted INPUT-FILE.
109
+
110
+ The result is written to the console or to a Json-formatted OUTPUT-FILE.
111
+ """
112
+ from pinexq.procon.dataslots.metadata import LocalFileMetadataStore
113
+
114
+ configure_logging(debug=debug, rich=richformat)
115
+ log_version_info(log=True)
116
+
117
+ step: Step = ctx.obj
118
+
119
+ # noinspection PyProtectedMember
120
+ if function not in step._signatures:
121
+ exit_unknown_function(step, function)
122
+
123
+ if parameters and input_file:
124
+ raise ValueError("Parameters provided by commandline and file. Use either of those options.")
125
+
126
+ func_params = {} # Default - a function might not need parameters
127
+ if parameters:
128
+ func_params = params_from_python_expr(parameters)
129
+ if input_file:
130
+ func_params = params_from_json_file(str(input_file))
131
+ input_dataslot_params = {}
132
+ output_dataslot_params = {}
133
+ if input_dataslots:
134
+ raw_dataslot_parameters = params_from_python_expr(input_dataslots)
135
+ input_dataslot_params = create_dataslot_description(raw_dataslot_parameters)
136
+ if output_dataslots:
137
+ raw_dataslot_parameters = params_from_python_expr(output_dataslots)
138
+ output_dataslot_params = create_dataslot_description(raw_dataslot_parameters)
139
+
140
+ metadata_handler = LocalFileMetadataStore()
141
+
142
+ context = ExecutionContext(
143
+ function_name=function,
144
+ parameters=func_params,
145
+ input_dataslots=input_dataslot_params,
146
+ output_dataslots=output_dataslot_params,
147
+ metadata_handler=metadata_handler,
148
+ )
149
+ # noinspection PyProtectedMember
150
+ result = step._call(context)
151
+
152
+ print_to_console(result, as_json, pretty)
153
+
154
+ if output_file:
155
+ write_to_json_file(str(output_file), result)
156
+
157
+
158
+ @cli.command()
159
+ @click.option("-j", "--json", "as_json", is_flag=True, help="Output as JSON schema")
160
+ @click.option("-y", "--pretty", is_flag=True, help="Format JSON with indentations and color")
161
+ @click.option("-f", "--function", type=str, help="The function to be inspected in this container.")
162
+ @click.pass_context
163
+ def signature(ctx, function: str, as_json: bool, pretty: bool):
164
+ """Output a functions signature"""
165
+ step: Step = ctx.obj
166
+
167
+ # noinspection PyProtectedMember
168
+ if function not in step._signatures:
169
+ exit_unknown_function(step, function)
170
+
171
+ # noinspection PyProtectedMember
172
+ schema = step._signatures[function]
173
+ model = schema.get_function_model()
174
+ if as_json:
175
+ if pretty:
176
+ rich.print_json(model.model_dump_json(by_alias=True))
177
+ else:
178
+ print(model.model_dump_json(by_alias=True))
179
+ else:
180
+ rich.print(model)
181
+
182
+
183
+ def exit_unknown_function(step: Step, func_name: str):
184
+ """Raises a custom `click` exception, in case an unknown Step-function name was provided."""
185
+ # noinspection PyProtectedMember
186
+ available_funcs = ", ".join((f"'{func}'" for func in step._signatures.keys()))
187
+ raise click.BadParameter(
188
+ f"No definition found for: '{func_name}'! Available functions: {available_funcs}", param_hint="function"
189
+ )
190
+
191
+
192
+ def print_to_console(result: object, as_json: bool, pretty: bool):
193
+ """Prints the Repr of a given object to StdOut. Optionally output the JSON representation
194
+ of the object and/or use `rich` for a nicer output.
195
+
196
+ Args:
197
+ result: The object whose __repr__ to print.
198
+ as_json: Format the output as Json if True.
199
+ pretty: Use `rich` for printing, if True.
200
+ """
201
+ if as_json:
202
+ result = result.model_dump_json() if isinstance(result, BaseModel) else result
203
+ output = json.dumps(result)
204
+ else:
205
+ output = result
206
+
207
+ if pretty:
208
+ if as_json:
209
+ rich.print_json(output)
210
+ else:
211
+ rich.print(output)
212
+ else:
213
+ print(output)
214
+
215
+
216
+ def params_from_python_expr(expr: str) -> dict[str, Any]:
217
+ """Parses a string of a Python expression into its object representation."""
218
+ params = ast.literal_eval(expr)
219
+ if not isinstance(params, dict):
220
+ raise ValueError("Parameters are not formatted as a Python dictionary!")
221
+ return params
222
+
223
+
224
+ def params_from_json_file(filename: str) -> dict:
225
+ """Loads data from a Json file into a dictionary."""
226
+ with open(filename) as f:
227
+ params = json.load(f)
228
+ if not isinstance(params, dict):
229
+ raise ValueError("Parameter file is not formatted as a Python dictionary!")
230
+ return params
231
+
232
+
233
+ def write_to_json_file(filename: str, data: object):
234
+ """Writes the Json representation of an object into a Json file."""
235
+ with open(str(filename), "w") as f:
236
+ data = data.model_dump_json() if isinstance(data, BaseModel) else data
237
+ return json.dump(data, f)
238
+
239
+
240
+ @cli.command()
241
+ @click.option("--host", type=str, required=True, help="RabbitMQ host")
242
+ @click.option("--port", type=int, default=5671, required=True, help="RabbitMQ port")
243
+ @click.option("--apikey", type=str, help="RabbitMQ API-Key (used instead of user/pw)")
244
+ @click.option("--user", type=str, help="RabbitMQ user name")
245
+ @click.option("--password", type=str, help="RabbitMQ password")
246
+ @click.option("--exchange", type=str, default="processing-topic-exchange", help="RabbitMQ exchange name")
247
+ @click.option("--vhost", type=str, default="/", help="RabbitMQ virtual host")
248
+ @click.option("-d", "--debug", is_flag=True, help="enable more verbose debug output")
249
+ @click.option("-r", "--richformat", is_flag=True, help="enable rich formatting of log output")
250
+ @click.option("-f", "--function", type=str, multiple=True, help="The function(s) to be exposed to the job-management.")
251
+ @click.option("-af", "--allfunctions", is_flag=True, help="Expose all available functions")
252
+ @click.option(
253
+ "-v", "--version", type=str, default="0", help="The version of all function(s) registered by this container (deprecated)"
254
+ )
255
+ @click.option("--workerid", type=str, default=uuid.uuid4(), help="Set a worker id (default: random uuid)")
256
+ @click.option("--contextid", type=str, required=True, help="The context-id this worker will be running in")
257
+ @click.option("--idle-timeout", type=int, default=0, help="Idle timeout in seconds")
258
+ @click.pass_context
259
+ def remote(
260
+ ctx,
261
+ function: list[str],
262
+ allfunctions: bool,
263
+ version: str,
264
+ workerid: str,
265
+ contextid: str,
266
+ host: str,
267
+ port: int,
268
+ apikey: str = "",
269
+ user: str = "",
270
+ password: str = "",
271
+ exchange: str = "processing-topic-exchange",
272
+ vhost: str = "/",
273
+ idle_timeout: int = 0,
274
+ debug: bool = False,
275
+ richformat: bool = False,
276
+ ):
277
+ """
278
+ Connect to job-management remotely [Only available with the "remote" module]
279
+
280
+ You can provide the --function/-f parameter multiple times or use --allfunctions/-af to
281
+ expose multiple/all functions.
282
+ The same function can be available in different versions. If a container is not responding
283
+ to Job offers for a function there might be a mismatch between exposed and requested version
284
+ of a function. You can use the '--allversions' parameter to respond to requests of any version,
285
+ but this is recommended only for development uses.
286
+ """
287
+ # import here to avoid circular imports
288
+ from pinexq.procon.runtime.worker import ProConWorker
289
+ from pinexq.procon.runtime.foreman import ProConForeman
290
+
291
+ # Get Step function from CLI-context
292
+ step: Step = ctx.obj
293
+
294
+ configure_logging(debug=debug, rich=richformat)
295
+ log_version_info(log=True)
296
+
297
+ # Validate and check input parameters
298
+ if not exchange:
299
+ raise ValueError("Exchange for RabbitMQ not set!")
300
+
301
+ if not vhost:
302
+ raise ValueError("Virtual host for RabbitMQ not set!")
303
+
304
+ if function and allfunctions:
305
+ raise ValueError("Provide either the --function or --allfunctions parameter, but not booth!")
306
+
307
+ if version != "0":
308
+ logger.warning("The 'VERSION' CLI parameter is deprecated!"
309
+ " Please use explicit versioning with the @version function decorator.")
310
+
311
+ # Check that either an API-key xor the user/pw for RabbitMQ was given
312
+ if not (apikey or (user and password)):
313
+ raise ValueError("Provide either username and password or the API-key for RabbitMQ access!")
314
+ elif apikey and (user or password):
315
+ raise ValueError("Provide either username and password or the API-key for RabbitMQ access, but not both!")
316
+
317
+ # If there is an API-key, split it into 3 parts
318
+ # we only use the "context-id" as username and the whole "apikey" as password
319
+ if apikey:
320
+ try:
321
+ _, user, _ = apikey.split("_")
322
+ password = apikey
323
+ except ValueError as ex:
324
+ raise ValueError("Unrecognized API-key format! Expected: '<namespace>_<context-id>_<key>'")
325
+
326
+ remove_environment_variables(
327
+ include=["PROCON_REMOTE_*", "KUBERNETES_*"]
328
+ )
329
+
330
+ try:
331
+ worker = ProConWorker(
332
+ step=step,
333
+ function_names=["*"] if allfunctions else function,
334
+ rmq_parameters={
335
+ "host": host,
336
+ "port": port,
337
+ "login": user,
338
+ "password": password,
339
+ "exchange": exchange,
340
+ "vhost": vhost,
341
+ },
342
+ worker_id=workerid,
343
+ context_id=contextid,
344
+ idle_timeout=idle_timeout,
345
+ )
346
+ except ProConUnknownFunctionError as ex:
347
+ exit_unknown_function(step=step, func_name=ex.function_name)
348
+
349
+ # noinspection PyUnboundLocalVariable
350
+ ProConForeman(worker, debug=False)
351
+
352
+
353
+ @cli.command()
354
+ @click.option("-f", "--function", type=str, required=True, help="The function's name in this container.")
355
+ @click.option(
356
+ "-n", "--name", type=str, help="Name under which the function is registered. Defaults to the function name."
357
+ )
358
+ @click.option("--api-url", type=str, required=True, help="URL to the JMA's API entry point.")
359
+ @click.option("--api-key", type=str, default=None, help="API-key to connect to the API.")
360
+ @click.option("--user-id", type=str, default=None, help="User-id to connect to the API, if not using an API-key")
361
+ @click.option("--user-group", type=str, default=None, help="User group under which to connect, implies user-id access.")
362
+ @click.option("-t", "--tag", type=str, multiple=True, default=[], help="Optional tags for the processing step.")
363
+ @click.option("-v", "--version", type=str, help="Custom value for the version.")
364
+ @click.pass_context
365
+ def register(
366
+ ctx,
367
+ function: str,
368
+ name: str,
369
+ api_url: str,
370
+ api_key: str,
371
+ user_id: str,
372
+ user_group: str,
373
+ tag: list[str],
374
+ version: str,
375
+ ):
376
+ """Register a functions signature at the JobManagement via the API"""
377
+ from httpx import Client
378
+ from pinexq_client.job_management import ProcessingStep
379
+ import pinexq_client
380
+
381
+ if not is_valid_version_string(version):
382
+ raise ValueError(f"Given version: '{version}' is not valid. Allowed: a-Z 0-9 and '_' '-' '.'")
383
+
384
+ step: Step = ctx.obj
385
+
386
+ # noinspection PyProtectedMember
387
+ if function not in step._signatures:
388
+ exit_unknown_function(step, function)
389
+
390
+ # If no name to register is provided, default to the function name
391
+ processing_step_name = name or function
392
+
393
+ # Create the function manifest
394
+ # noinspection PyProtectedMember
395
+ schema = step._signatures[function]
396
+ model = schema.get_function_model()
397
+ manifest_dict = model.model_dump(by_alias=True)
398
+
399
+ # rich.print_json(manifest_dict)
400
+
401
+ # Connect to the JMA with a httpx client
402
+ if user_id and api_key:
403
+ raise click.BadParameter("The parameters --api-key and --user-id can not be provided at the same time.")
404
+
405
+ if api_key:
406
+ if user_group:
407
+ raise click.BadParameter("Parameter --api-key does not allow --user-group")
408
+ headers = {"x-api-key": api_key}
409
+ elif user_id:
410
+ if user_group:
411
+ headers = {"x-user-id": user_id, "x-user-groups": user_group}
412
+ else:
413
+ headers = {"x-user-id": user_id}
414
+ else:
415
+ raise click.BadParameter("Either --api-key or --user-id must be provided.")
416
+
417
+ client_instance = Client(
418
+ base_url=api_url,
419
+ headers=headers,
420
+ )
421
+
422
+ # Register the manifest using the API client
423
+ try:
424
+ processing_step = (
425
+ ProcessingStep(client_instance)
426
+ .create( # 'title' and 'function_name' are the same (for now?)
427
+ title=processing_step_name, function_name=processing_step_name, version=version
428
+ )
429
+ .upload_configuration(manifest_dict)
430
+ )
431
+ # If the user supplied tags, add them
432
+ if tag:
433
+ processing_step.set_tags(tag)
434
+
435
+ # Print a compact error message instead of a stacktrace
436
+ except pinexq_client.core.exceptions.ApiException as exc:
437
+ details = exc.problem_details
438
+ print(f"Error [{details.status}]: {details.title}\n-> {details.detail}")
439
+
440
+ client_instance.close()
441
+
442
+
@@ -0,0 +1,64 @@
1
+ """
2
+ Module-specific exceptions
3
+
4
+ Exceptions for all defined error cases and wrapper for ProblemJSON-formatted
5
+ error reporting.
6
+ """
7
+
8
+
9
+ class ProConException(Exception):
10
+ """Parent class for all module-specific Exceptions
11
+
12
+ Attributes:
13
+ user_message: Custom message visible to the end user.
14
+
15
+ """
16
+ user_message: str | None = None
17
+
18
+ def __init__(self, *args, user_message: str = "", **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ self.user_message = user_message
21
+
22
+
23
+ class ProConUnknownFunctionError(ProConException):
24
+ """A function of the requested name could not be found
25
+
26
+ Attributes:
27
+ function_name: Name of the *unknown* function.
28
+ """
29
+ function_name: str
30
+
31
+ def __init__(self, *args, func_name: str = "", **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+ self.function_name = func_name
34
+
35
+
36
+ class ProConMessageRejected(ProConException):
37
+ """Invalid or malformed message received"""
38
+
39
+
40
+ class ProConBadMessage(ProConException):
41
+ """Received message can not be processed"""
42
+
43
+
44
+ class ProConSchemaValidationError(ProConException):
45
+ """The data does not match the function annotation"""
46
+
47
+
48
+ class ProConSchemaError(ProConException):
49
+ """Function schema can not be generated """
50
+
51
+
52
+ class ProConDataslotError(ProConException):
53
+ """There's a problem with the dataslots """
54
+
55
+
56
+ class ProConWorkerNotAvailable(ProConException):
57
+ """Already processing or no resources available to process a job"""
58
+
59
+
60
+ class ProConJobExecutionError(ProConException):
61
+ """Custom exception raised to stop the execution."""
62
+
63
+ class ProConShutdown(ProConException):
64
+ """Custom exception that triggers a clean shutdown of ProCon."""
@@ -0,0 +1,61 @@
1
+ import importlib.metadata
2
+ import platform
3
+ from fnmatch import fnmatch
4
+ from os import environ
5
+ from typing import Sequence
6
+
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def log_version_info(log: bool = False) -> None:
13
+ """Log info about the platform and installed packages.
14
+
15
+ Args:
16
+ log: If true, the info is sent to a logger, otherwise it's just printed. (default: False)
17
+ """
18
+
19
+ output = print
20
+ if log:
21
+ output = logger.info
22
+
23
+ message = [
24
+ "Platform and package information:",
25
+ f"OS: {platform.platform()}",
26
+ f"Python: {platform.python_implementation()} {platform.python_version()}",
27
+ ]
28
+
29
+ # Collection version info for installed packages. (The list are package names, not the import names)
30
+ packages = ("pinexq-procon", "pinexq-client")
31
+ for p in packages:
32
+ message.append(f"{p}: {importlib.metadata.version(p)}")
33
+
34
+ output("\n".join(message))
35
+
36
+
37
+ def remove_environment_variables(include: Sequence[str]|None=None, exclude: Sequence[str]|None=None) -> None:
38
+ """Removes environment variables from the current environment.
39
+
40
+ Args:
41
+ include: A list of pattern strings that may include wildcards *.
42
+ If an environment variable matches any of these, it will be marked for removal. (default: ["*"])
43
+ exclude: A list of pattern strings that may include wildcards *.
44
+ If an environment variable matches any of these, it will be exempt from removal,
45
+ even though it might match an include pattern.
46
+ """
47
+ if include is None:
48
+ include = ["*"]
49
+ if exclude is None:
50
+ exclude = []
51
+
52
+ vars_to_remove = [
53
+ name
54
+ for name, value in environ.items()
55
+ if any((fnmatch(name, incl_pattern) for incl_pattern in include)) and \
56
+ not any((fnmatch(name, excl_pattern) for excl_pattern in exclude))
57
+ ]
58
+ for name in vars_to_remove:
59
+ del environ[name]
60
+
61
+ logger.debug(f"Removed environment variables: {vars_to_remove}")
@@ -0,0 +1,48 @@
1
+ import logging
2
+ from logging import DEBUG, INFO, WARNING
3
+
4
+ from rich.logging import RichHandler
5
+
6
+
7
+ def configure_logging(debug: bool = False, rich: bool = False):
8
+ # Define log-levels for external (and internal) packages we depend on
9
+ if debug:
10
+ log_levels = {
11
+ "aiormq": INFO,
12
+ "aio_pika": INFO,
13
+ "procon": DEBUG,
14
+ }
15
+ else:
16
+ log_levels = {
17
+ "aiormq": WARNING,
18
+ "aio_pika": WARNING,
19
+ "procon": INFO,
20
+ }
21
+
22
+ config_options = {}
23
+ if rich:
24
+ import click, aiormq, aio_pika, asyncio
25
+ # use rich for colored, more structured formatting
26
+ handlers = [
27
+ RichHandler(
28
+ rich_tracebacks=True,
29
+ # tracebacks_show_locals=True,
30
+ # log_time_format="%d-%m-%y %H:%M:%S.%f",
31
+ # omit_repeated_times=False,
32
+ # enable_link_path=False,
33
+ # show_path=False,
34
+ # show_level=False
35
+ tracebacks_suppress=[click, aiormq, aio_pika, asyncio]
36
+ )
37
+ ]
38
+ config_options["handlers"] = handlers
39
+ else:
40
+ # use plain formatting
41
+ config_options["format"] = "%(asctime)s %(levelname)s %(message)s"
42
+ config_options["datefmt"] = "%d-%m-%y %H:%M:%S"
43
+
44
+ config_options["level"] = "INFO"
45
+ logging.basicConfig(**config_options)
46
+
47
+ for logger, level in log_levels.items():
48
+ logging.getLogger(logger).setLevel(level)
@@ -0,0 +1,36 @@
1
+ import string
2
+
3
+
4
+ ALLOWED_VERSION_CHARS = set(string.ascii_letters + string.digits + '-_.')
5
+
6
+ def is_valid_version_string(to_check: str) -> bool:
7
+ """
8
+ Checks if the non-empty input string contains ONLY allowed characters.
9
+ Allowed characters are: ASCII letters, numbers, '-', '_', and '.'.
10
+
11
+ Args:
12
+ to_check: The string to validate.
13
+
14
+ Returns:
15
+ True if all characters in the string are allowed, False otherwise.
16
+ """
17
+
18
+ if not to_check:
19
+ return False
20
+
21
+ for char in to_check:
22
+ if char not in ALLOWED_VERSION_CHARS:
23
+ return False
24
+ return True
25
+
26
+ def escape_version_string(string_to_escape: str) -> str:
27
+ """
28
+ Replaces all occurrences of '.' with '|' in the given string.
29
+
30
+ Args:
31
+ string_to_escape: The string to modify.
32
+
33
+ Returns:
34
+ A new string with all dots replaced by pipes.
35
+ """
36
+ return string_to_escape.replace('.', '/')
@@ -0,0 +1,15 @@
1
+ import enum
2
+ from typing import Final
3
+
4
+
5
+ class UNSETTYPE(enum.IntEnum):
6
+ token = 0
7
+
8
+ def __repr__(self):
9
+ return "NOTSET"
10
+
11
+ def __bool__(self):
12
+ return False
13
+
14
+
15
+ UNSET: Final = UNSETTYPE.token # noqa: E305
@@ -0,0 +1,19 @@
1
+ # ruff: noqa: F401
2
+ from .abstractionlayer import DataslotLayer
3
+ from .annotation import dataslot
4
+ from .dataslots import (
5
+ DataSlot,
6
+ Slot,
7
+ create_dataslot_description,
8
+ )
9
+ from .metadata import (
10
+ json_to_metadata,
11
+ metadata_to_json
12
+ )
13
+ from .datatypes import (
14
+ Metadata,
15
+ SlotType,
16
+ SlotDescription,
17
+ DataSlotDescription,
18
+ MediaTypes
19
+ )