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,99 @@
1
+ """
2
+ Building blocks for all schemas
3
+
4
+ All objects for the schema description of function signatures are here
5
+ represented as Pydantic BaseModel classes.
6
+ """
7
+ import warnings
8
+ from enum import Enum
9
+ from typing import Any, Annotated, Type, Mapping
10
+
11
+ from pydantic import ConfigDict, BaseModel, constr, Field, model_validator
12
+
13
+ from pydantic.json_schema import (
14
+ GenerateJsonSchema, DEFAULT_REF_TEMPLATE, JsonSchemaMode, JsonSchemaValue, CoreSchema
15
+ )
16
+
17
+
18
+ # Custom JSON schema generators to create schemas with '$schema' identifier
19
+ # -> copied from: https://github.com/pydantic/pydantic/blob/73373c3e08fe5fe23e4b05f549ea34e0da6a16b7/tests/test_json_schema.py#L3090-L3116
20
+
21
+ class GenerateJsonSchemaWithDialect(GenerateJsonSchema):
22
+ def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue:
23
+ json_schema = super().generate(schema)
24
+ json_schema['$schema'] = self.schema_dialect
25
+ return json_schema
26
+
27
+
28
+ class BaseModelWithSchema(BaseModel):
29
+ @classmethod
30
+ def model_json_schema(
31
+ cls,
32
+ by_alias: bool = True,
33
+ ref_template: str = DEFAULT_REF_TEMPLATE,
34
+ schema_generator: Type[GenerateJsonSchema] = GenerateJsonSchemaWithDialect,
35
+ mode: JsonSchemaMode = 'validation'
36
+ ) -> dict[str, Any]:
37
+ return super().model_json_schema(by_alias, ref_template, schema_generator, mode)
38
+
39
+
40
+ # Models defining the structure of the "manifest" generated by the "signature" command
41
+ class DataslotModel(BaseModel):
42
+ """Description of a single DataSlot"""
43
+ name: str
44
+ title: str
45
+ description: str
46
+ mediatype: str
47
+ metadata: dict
48
+ min_slots: Annotated[int, Field(ge=0, strict=True, serialization_alias='minSlots')]
49
+ max_slots: Annotated[int | None, Field(ge=1, strict=True, serialization_alias='maxSlots')]
50
+
51
+
52
+ class FunctionModel(BaseModel):
53
+ """Structure of a Step-function manifest"""
54
+ version: Annotated[str, Field(min_length=0, max_length=40)]
55
+ function_name: Annotated[str, Field(min_length=1, max_length=100)]
56
+ short_description: str
57
+ long_description: str
58
+ parameters: Any = None # 'DynamicParameters' actually, but embedded at runtime as schema
59
+ returns: Any = None # 'DynamicReturns' actually, but embedded at runtime as schema
60
+ input_dataslots: list[DataslotModel]
61
+ output_dataslots: list[DataslotModel]
62
+ procon_version: str
63
+
64
+
65
+ class DynamicParameters(BaseModelWithSchema):
66
+ """Basis for a dynamically created model of the functions parameters."""
67
+ model_config = ConfigDict(
68
+ arbitrary_types_allowed=True,
69
+ extra="forbid",
70
+ strict=True,
71
+ )
72
+
73
+ @model_validator(mode="before")
74
+ @classmethod
75
+ def _convert_enums(cls, values: Mapping[str, Any]) -> Mapping[str, Any]:
76
+ values = dict(values) # Ensure mutable dict
77
+ for field_name, model_field in cls.model_fields.items():
78
+ field_type = model_field.annotation
79
+ if isinstance(field_type, type) and issubclass(field_type, Enum):
80
+ value = values.get(field_name)
81
+ if isinstance(value, str):
82
+ try:
83
+ values[field_name] = field_type(value)
84
+ except ValueError:
85
+ warnings.warn(
86
+ f"Failed to convert '{value}' to '{field_type.__name__}' for field '{field_name}'."
87
+ )
88
+ return values
89
+
90
+
91
+
92
+
93
+ class DynamicReturns(BaseModelWithSchema):
94
+ """Basis for a dynamically created model of the functions return value."""
95
+ model_config = ConfigDict(
96
+ arbitrary_types_allowed=True,
97
+ extra="ignore",
98
+ strict=True,
99
+ )
@@ -0,0 +1,119 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, TypeVar
3
+
4
+ import pydantic
5
+
6
+ from ..core.exceptions import ProConException, ProConSchemaValidationError
7
+ from ..dataslots import DataSlotDescription, DataslotLayer
8
+ from ..dataslots.metadata import MetadataHandler
9
+ from .introspection import FunctionSchema, StepClassInfo
10
+
11
+
12
+ @dataclass
13
+ class ExecutionContext:
14
+ """Wraps all information to call a function in a Step container"""
15
+
16
+ function_name: str
17
+ parameters: dict[str, Any] = field(default_factory=dict)
18
+ input_dataslots: dict[str, "DataSlotDescription"] = field(default_factory=dict)
19
+ output_dataslots: dict[str, "DataSlotDescription"] = field(default_factory=dict)
20
+ metadata_handler: MetadataHandler | None = None
21
+
22
+
23
+ ExecutionContextType = TypeVar("ExecutionContextType", bound="ExecutionContext")
24
+
25
+
26
+ class Step:
27
+ _signatures: dict[str, FunctionSchema]
28
+ _context: ExecutionContextType | None = None
29
+
30
+ def __init__(self, use_cli=True):
31
+ self._signatures = StepClassInfo(self).get_func_schemas()
32
+
33
+ if use_cli:
34
+ # Do the import here to avoid circular imports
35
+ from pinexq.procon.core.cli import cli
36
+
37
+ cli.main(
38
+ obj=self, # This Step object *self* is available as *obj* attribute in the context of each cli command
39
+ auto_envvar_prefix="PROCON",
40
+ )
41
+
42
+ def _call(self, context: ExecutionContextType) -> Any:
43
+ """Calls a function from this container"""
44
+ try:
45
+ # Check if this class contains the function?
46
+ function = getattr(self, context.function_name)
47
+ # is it one of the exposed functions?
48
+ signature = self._signatures[context.function_name]
49
+ except (KeyError, AttributeError):
50
+ raise ProConException(
51
+ f"No function with such name: '{context.function_name}'"
52
+ )
53
+
54
+ # Connect to the dataslot sources and load the data into parameters
55
+ with DataslotLayer(
56
+ function=function,
57
+ parameters=context.parameters,
58
+ dataslots_in_descr=context.input_dataslots,
59
+ dataslots_out_descr=context.output_dataslots,
60
+ signature=signature,
61
+ metadata_handler=context.metadata_handler,
62
+ ) as ds_handle:
63
+ # Match parameters with the function's signature
64
+ try:
65
+ # The validation may fail when e.g. Enums are used within the
66
+ # parameters. TODO: find a way how to verify nested enums.
67
+ function_parameter_model = signature.get_parameters_model().model_validate(ds_handle.parameters)
68
+ except pydantic.ValidationError as ex:
69
+ raise ProConSchemaValidationError(
70
+ "Parameters don't match the functions signature!"
71
+ ) from ex
72
+
73
+ # Call the function
74
+ self._context = context
75
+ try:
76
+ # Get the parameter model as a dictionary. Unlike .model_dump(), casting
77
+ # it with `dict()` will only convert the root level Basemodel and not
78
+ # any embedded Basemodels.
79
+ function_parameters = dict(function_parameter_model)
80
+
81
+ # The actual function call
82
+ result = ds_handle.function(**function_parameters)
83
+
84
+ # Update parameters to handle "return-by-reference" via Output-Dataslots
85
+ ds_handle.update_parameters(function_parameters)
86
+ # Update return value to handle Return-Dataslots
87
+ ds_handle.update_result(result)
88
+ except ProConException:
89
+ raise
90
+ except Exception as ex:
91
+ raise ProConException(
92
+ f"Exception during execution of '{context.function_name}'!"
93
+ ) from ex
94
+ finally:
95
+ self._context = None
96
+
97
+ # Match the results signature
98
+ # But only if there is no return-Dataslot, in which case there is no return value
99
+ # FixMe: Data in the return.dataslot is actually not validated against the signature!
100
+ if not ds_handle.has_results_data_slot():
101
+ try:
102
+ signature.get_returns_model().model_validate({"value": result})
103
+ except pydantic.ValidationError as ex:
104
+ raise ProConSchemaValidationError(
105
+ "The functions return values doesn't match its signature!"
106
+ ) from ex
107
+
108
+ # The result value or None, if a `return.dataslot` is defined
109
+ func_result = ds_handle.result
110
+
111
+ return func_result
112
+
113
+ @property
114
+ def step_context(self) -> ExecutionContextType | None:
115
+ return self._context
116
+
117
+ @property
118
+ def step_signatures(self) -> dict[str, FunctionSchema]:
119
+ return self._signatures
@@ -0,0 +1,84 @@
1
+ import re
2
+ from typing import Any, Callable
3
+
4
+ from pydantic import Field
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from ..core.types import UNSET, UNSETTYPE
8
+
9
+
10
+ METADATA_ANNOTATION_NAME = "__pxq_metadata__"
11
+
12
+ # Version string regex
13
+ VERSION_PATTERN = r"""
14
+ ^(?: # start of the string
15
+ (?P<version>[0-9]{1,8}(?:\.[0-9]{1,8}){,2}) # version number
16
+ (?:-(?P<postfix>[0-9a-zA-Z_-]+))? # postfix string
17
+ )$ # end of the string
18
+ """
19
+ version_regex = re.compile(VERSION_PATTERN, flags=re.VERBOSE)
20
+
21
+ # Regex pattern for just the postfix substring for pydantic
22
+ POSTFIX_PATTERN = r"^[0-9a-zA-Z_-]*$"
23
+ postfix_regex = re.compile(POSTFIX_PATTERN)
24
+
25
+ # Restrictions
26
+ # <major:int>.<minor:int>.<patch:int>-<string> mit max 40 chars
27
+ # maj/min/patch each <= 8 chars , defaults == 0
28
+
29
+ MAX_VERSION_LENGTH = 40
30
+
31
+
32
+ @dataclass
33
+ class version:
34
+ """Decorator to attach version information to a Step function.
35
+
36
+ Attributes:
37
+ version: ...
38
+
39
+ """
40
+
41
+ version: str | UNSETTYPE = Field(default=UNSET, pattern=version_regex)
42
+ major: int | UNSETTYPE = Field(default=UNSET, ge=0, lt=100_000_000, kw_only=True)
43
+ minor: int | UNSETTYPE = Field(default=UNSET, ge=0, lt=100_000_000, kw_only=True)
44
+ patch: int | UNSETTYPE = Field(default=UNSET, ge=0, lt=100_000_000, kw_only=True)
45
+ postfix: str | UNSETTYPE = Field(default=UNSET, pattern=postfix_regex, kw_only=True)
46
+
47
+
48
+ def __post_init__(self):
49
+ version_was_set = self.version is not UNSET
50
+ any_specifier_was_set = (self.major is not UNSET or self.minor is not UNSET
51
+ or self.patch is not UNSET or self.postfix is not UNSET)
52
+ if not (version_was_set or any_specifier_was_set):
53
+ self.major = 0 # default value when no parameter was given
54
+ elif not (version_was_set ^ any_specifier_was_set):
55
+ raise ValueError("You have to define the version either as a string or explicitly via the "
56
+ "'major', 'minor', 'patch', 'postfix' keyword, but not both!")
57
+
58
+ if not version_was_set:
59
+ self._set_version_from_kwargs()
60
+
61
+ if len(self.version) > MAX_VERSION_LENGTH:
62
+ raise ValueError(f"The version string is too long! "
63
+ f"Max {MAX_VERSION_LENGTH} chars are allowed, but it has {len(self.version)} chars.")
64
+
65
+ def _set_version_from_kwargs(self):
66
+ """Generate the version string from single kwarg variables"""
67
+ # The default string will always be "0.0.0" and all values and postfix are optional
68
+ self.version = (f"{self.major or '0'}.{self.minor or '0'}.{self.patch or '0'}"
69
+ f"{f'-{self.postfix}' if self.postfix else ''}")
70
+
71
+
72
+ def __call__(self, function: Callable[[Any], Any]):
73
+ """Called when used as a decorator to attach metadata to 'func'."""
74
+ metadata = function.__dict__.setdefault(METADATA_ANNOTATION_NAME, {})
75
+ metadata["version"] = self
76
+
77
+ return function
78
+
79
+ def __str__(self) -> str:
80
+ return self.version
81
+
82
+ def get_version_metadata(function: Callable[[Any], Any]) -> version:
83
+ metadata = getattr(function, METADATA_ANNOTATION_NAME, {})
84
+ return metadata.get("version", version())
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.3
2
+ Name: pinexq-procon
3
+ Version: 2.1.0.dev3
4
+ Summary: Framework to create containers for DataCybernetic's PineXQ computing platform
5
+ Author: Sebastian Höfer, Carsten Blank, Mathias Reichardt
6
+ Author-email: Sebastian Höfer <hoefer@data-cybernetics.com>, Carsten Blank <blank@data-cybernetics.com>, Mathias Reichardt <reichardt@data-cybernetics.com>
7
+ License: Copyright (C) data cybernetics ssc GmbH - All Rights Reserved
8
+ <contactus@data-cybernetics.com>, December 2023
9
+ Requires-Dist: aio-pika==9.5.5
10
+ Requires-Dist: click>=8.1.3
11
+ Requires-Dist: docstring-parser==0.*
12
+ Requires-Dist: httpx-caching>=0.1a4
13
+ Requires-Dist: httpx==0.*
14
+ Requires-Dist: pinexq-client>=0.10
15
+ Requires-Dist: pydantic>=2.10.0
16
+ Requires-Dist: pyjwt>=2.10.0
17
+ Requires-Dist: rich>=13.3.2
18
+ Requires-Dist: stamina>=24.2.0
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+
22
+ # PineXQ ProCon Framework
23
+
24
+ Computations in DC-Cloud are done by **Workers** running inside a **ProcessingContainer**, or short "ProCon", which is also the name of the framework.
25
+ ProCon provides an unobtrusive wrapper around function definitions without introducing new semantics, allowing for a clean definition of the computational task, while handling all cloud-related communication and data-management transparently in the background.
26
+ This removes the code and configuration required from the function implementation.
27
+
28
+ ### Installation
29
+
30
+ To install the package use the `pip` command, to either install from a package feed:
31
+ ```
32
+ pip install pinexq-procon
33
+ ```
34
+
35
+
36
+ ### Creating a container
37
+
38
+ To publish a function in a container it has to be a method of a class inheriting from the `Step` class.
39
+
40
+ ```python
41
+ from pinexq.procon.step import Step # import package
42
+
43
+ class MyStepCollection(Step): # define the container class
44
+ def calculate_square(self, x: float) -> float: # define a step function
45
+ """Calculate the square of x
46
+
47
+ :param x: a float number
48
+ :returns: the square of x
49
+ """
50
+ return x ** 2
51
+
52
+ # More step functions can go in the same class
53
+
54
+ if __name__ == '__main__': # add script guard
55
+ MyStepCollection() # run the container - this will spawn the cli
56
+ ```
57
+
58
+ It is mandatory to annotate the types of parameters and return value.
59
+ Docstrings are optional, but highly recommended.
60
+
61
+ The [documentation](doc/ProCon.md) has a detailed section about [implementing processing-steps](doc/Implementing-a-Step.md).
62
+
63
+
64
+ ### Running a Step-function locally
65
+
66
+ The Python file with the container is itself a cli-tool.
67
+ You get a list of all available commands with the `--help` parameter.
68
+
69
+ ```
70
+ python ./my_step_file.py --help
71
+ ```
72
+
73
+ With the `run` option you can call a function in the container directly and the result is written to the console.
74
+
75
+ ```
76
+ python ./my_step_file.py run --function calculate_square --parameters "{'x': 5}"
77
+ 25
78
+ ```
79
+
80
+ You can find full list of available commands in the [cli documentation](doc/Using-Procon-From-Cli.md).
81
+ All possible parameters and environment variables are listed [here](doc/Parameters.md).
82
+
83
+
@@ -0,0 +1,35 @@
1
+ pinexq/procon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pinexq/procon/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pinexq/procon/core/cli.py,sha256=8MPdTi9sCluFn7Y9TfaBKJavTsUmR5SG5Eup1SAMaZM,16602
4
+ pinexq/procon/core/exceptions.py,sha256=0SXvC54-H6E65Fi06mQc6FlTnWB91GHH27ci7ypBYRE,1677
5
+ pinexq/procon/core/helpers.py,sha256=2nnd99o0_oPwAptOF_6LySeeDv8ZPf6EO2R88yUBbuY,1996
6
+ pinexq/procon/core/logconfig.py,sha256=UlIqgzShK3UETmRz2iSDniypQxnIGOuwz-gZetfaths,1503
7
+ pinexq/procon/core/naming.py,sha256=K2g3dFZdz46WevbRpeK-75v6427ph56F43evDARWxHk,916
8
+ pinexq/procon/core/types.py,sha256=w6XmboyUSly4JI-zM-A3Otc-f-F2NrfzGvpC7HLOcwA,226
9
+ pinexq/procon/dataslots/__init__.py,sha256=uiRtBPBbK7thyte7Ch7x-S00LYaX_qJ_ELtPfqaydi4,365
10
+ pinexq/procon/dataslots/abstractionlayer.py,sha256=ll86ONfQNsmbn55IbF4aE9D9jpe32s5L7UmX23JIx-4,9257
11
+ pinexq/procon/dataslots/annotation.py,sha256=O-oAi2vrrwZaAD0tOB4viAqYsGTe_ICftfAa2YH_5nk,16487
12
+ pinexq/procon/dataslots/dataslots.py,sha256=6RQwncq8E_9ILPBhxthnWfy9K6_W8IY6jouTNAZLhXI,14274
13
+ pinexq/procon/dataslots/datatypes.py,sha256=TpbHCAjuFokHUUG8Dweu7RlULmu4QIZYB4lvnk09ROg,1292
14
+ pinexq/procon/dataslots/default_reader_writer.py,sha256=f9edmeaTDO11D5im_cqIqBgFGfqh9Sb5cw6DtxO2KvM,1003
15
+ pinexq/procon/dataslots/filebackend.py,sha256=xGAXwo5XzDtznzoQMLwReKrabNCeRQuYYjHGjWugcyI,3935
16
+ pinexq/procon/dataslots/metadata.py,sha256=Qi5vetHF6_3epubYeoSZ3KFopboxrbjDUSId7SLbrP8,4382
17
+ pinexq/procon/jobmanagement/__init__.py,sha256=W4UC5s70RzL_EwnEuvXsXS7Hph2awsblCQvZ_ROM-DQ,190
18
+ pinexq/procon/jobmanagement/api_helpers.py,sha256=xEtNEOEgQUtIIz6wtK-fiJThfgCIdLBQcKMb9L394VU,10224
19
+ pinexq/procon/remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ pinexq/procon/remote/messages.py,sha256=Mr6rz3VRxRxUM3l2Y0vU7CDmpc9Mi1NuhvEndoZbKEo,6904
21
+ pinexq/procon/remote/rabbitmq.py,sha256=2Kwsct35sN0pameRVCDER3WlPgnCI8YzCq9dlbGQgw8,16070
22
+ pinexq/procon/runtime/__init__.py,sha256=K-pSghTgjHAJJSeJpG8F_sYHDp8j3CL2X6izHS76DT0,119
23
+ pinexq/procon/runtime/foreman.py,sha256=YrKA8u1UieOU1aM22sbTv7PuppwuP8KII7WuDy7aQJc,5022
24
+ pinexq/procon/runtime/job.py,sha256=HDQRGIGj12McUQQ6F8bB2E_Wkuu-M-sf1in1MCxy1OQ,13883
25
+ pinexq/procon/runtime/settings.py,sha256=owSCPBEEpaf7zSDMdgy-jX_ZQ5--A7705nZcsPFx2sU,444
26
+ pinexq/procon/runtime/tool.py,sha256=PJ9Gtz3OuLvtOopPz955qJJvrrRj1ZyXKNYKIPPgQAc,653
27
+ pinexq/procon/runtime/worker.py,sha256=JOYyoZUoIOmVV-sK5fkmSouAiYU4M1pB5F6_XNektlA,17845
28
+ pinexq/procon/step/__init__.py,sha256=QZCtYx2uumBdzfZxeLoVlYqZS6l8nnVewqOtzYucblw,92
29
+ pinexq/procon/step/introspection.py,sha256=OF1hSherQGg9i4-WnyudUvslu0CDaxRzUrV5iKrD6K4,9814
30
+ pinexq/procon/step/schema.py,sha256=TlPjn1ETx3bTTy0Gb_6vU0RZ1QW4OIC6hYmBjWNhxiE,3664
31
+ pinexq/procon/step/step.py,sha256=cmErYmKzEilcMPVmMtSJMY7zGd1rb23x5VTVzmZU_hM,4862
32
+ pinexq/procon/step/versioning.py,sha256=2RzEtz0Y5_yrMFw8hUXVdagLUDgTtD5LqmrZnLogpJc,3304
33
+ pinexq_procon-2.1.0.dev3.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
34
+ pinexq_procon-2.1.0.dev3.dist-info/METADATA,sha256=gWOhHZbbIU97BepESzg4mTA2_GvLHZ__ZbTzYBNl_dk,3038
35
+ pinexq_procon-2.1.0.dev3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.18
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any