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
|
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,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
|
+
)
|