fastbpmn 0.1.0__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.
Files changed (88) hide show
  1. fastbpmn/__init__.py +15 -0
  2. fastbpmn/aetpi/__init__.py +0 -0
  3. fastbpmn/aetpi/logonly.py +262 -0
  4. fastbpmn/aetpi/middleware/__init__.py +5 -0
  5. fastbpmn/aetpi/middleware/variables.py +90 -0
  6. fastbpmn/aetpi/servers/__init__.py +0 -0
  7. fastbpmn/aetpi/servers/camunda7/__init__.py +45 -0
  8. fastbpmn/aetpi/servers/camunda7/decoder.py +214 -0
  9. fastbpmn/aetpi/servers/camunda7/encoder.py +129 -0
  10. fastbpmn/aetpi/servers/camunda7/middleware.py +50 -0
  11. fastbpmn/aetpi/servers/camunda7/producer.py +211 -0
  12. fastbpmn/aetpi/servers/camunda7/queue.py +34 -0
  13. fastbpmn/aetpi/servers/camunda7/server.py +309 -0
  14. fastbpmn/aetpi/servers/camunda7/settings.py +52 -0
  15. fastbpmn/aetpi/servers/camunda7/worker.py +448 -0
  16. fastbpmn/aetpi/utils.py +214 -0
  17. fastbpmn/camunda/__init__.py +37 -0
  18. fastbpmn/camunda/asynchttp.py +340 -0
  19. fastbpmn/camunda/config.py +11 -0
  20. fastbpmn/camunda/datatypes.py +44 -0
  21. fastbpmn/camunda/errors.py +110 -0
  22. fastbpmn/camunda/json.py +37 -0
  23. fastbpmn/camunda/models.py +55 -0
  24. fastbpmn/camunda/processengine.py +735 -0
  25. fastbpmn/camunda/utils.py +43 -0
  26. fastbpmn/context/__init__.py +4 -0
  27. fastbpmn/context/context.py +99 -0
  28. fastbpmn/context/io.py +33 -0
  29. fastbpmn/context/utils.py +32 -0
  30. fastbpmn/errors/__init__.py +27 -0
  31. fastbpmn/errors/base.py +42 -0
  32. fastbpmn/errors/bpmn_error.py +35 -0
  33. fastbpmn/errors/context.py +36 -0
  34. fastbpmn/errors/process_engine.py +64 -0
  35. fastbpmn/errors/task_failure_error.py +47 -0
  36. fastbpmn/errors/types.py +5 -0
  37. fastbpmn/fastbpmn.py +347 -0
  38. fastbpmn/fastbpmn_types.py +35 -0
  39. fastbpmn/lifespan/__init__.py +209 -0
  40. fastbpmn/middleware/__init__.py +33 -0
  41. fastbpmn/middleware/context/__init__.py +31 -0
  42. fastbpmn/middleware/exceptions/__init__.py +178 -0
  43. fastbpmn/middleware/exceptions/handler.py +167 -0
  44. fastbpmn/middleware/exceptions/types.py +10 -0
  45. fastbpmn/middleware/exceptions/utils.py +73 -0
  46. fastbpmn/middleware/guard/__init__.py +33 -0
  47. fastbpmn/middleware/guard/types.py +5 -0
  48. fastbpmn/middleware/guard/utils.py +69 -0
  49. fastbpmn/models/__init__.py +43 -0
  50. fastbpmn/models/base.py +80 -0
  51. fastbpmn/models/decoder.py +26 -0
  52. fastbpmn/models/encoder.py +208 -0
  53. fastbpmn/models/task.py +62 -0
  54. fastbpmn/models/types/__init__.py +11 -0
  55. fastbpmn/models/types/camunda7.py +237 -0
  56. fastbpmn/process.py +124 -0
  57. fastbpmn/processor/__init__.py +306 -0
  58. fastbpmn/processor/routing.py +171 -0
  59. fastbpmn/processor/task.py +78 -0
  60. fastbpmn/readiness.py +13 -0
  61. fastbpmn/result.py +162 -0
  62. fastbpmn/task.py +114 -0
  63. fastbpmn/task_group.py +93 -0
  64. fastbpmn/types.py +11 -0
  65. fastbpmn/utils/__init__.py +0 -0
  66. fastbpmn/utils/asyncer.py +101 -0
  67. fastbpmn/utils/asyncio.py +30 -0
  68. fastbpmn/utils/concurrency.py +38 -0
  69. fastbpmn/utils/config.py +57 -0
  70. fastbpmn/utils/func.py +22 -0
  71. fastbpmn/utils/inspect.py +64 -0
  72. fastbpmn/utils/names.py +21 -0
  73. fastbpmn-0.1.0.dist-info/METADATA +491 -0
  74. fastbpmn-0.1.0.dist-info/RECORD +88 -0
  75. fastbpmn-0.1.0.dist-info/WHEEL +4 -0
  76. fastbpmn-0.1.0.dist-info/entry_points.txt +4 -0
  77. fastbpmn-0.1.0.dist-info/licenses/LICENSE +21 -0
  78. squirrel/__init__.py +9 -0
  79. squirrel/__main__.py +3 -0
  80. squirrel/app.py +7 -0
  81. squirrel/arguments.py +25 -0
  82. squirrel/flavours/__init__.py +5 -0
  83. squirrel/flavours/camunda7/__init__.py +0 -0
  84. squirrel/flavours/camunda7/entrypoint.py +92 -0
  85. squirrel/flavours/camunda7/settings.py +15 -0
  86. squirrel/parser.py +54 -0
  87. squirrel/run.py +84 -0
  88. squirrel/settings.py +23 -0
fastbpmn/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Top-level package for yio-minions."""
2
+
3
+ import importlib.metadata
4
+ from .fastbpmn import FastBPMN
5
+ from .process import Process
6
+ from .task import TaskHandler
7
+ from .task_group import TaskGroup
8
+
9
+ __version__ = importlib.metadata.version("fastbpmn")
10
+ __author__ = """yio gmbh"""
11
+ __email__ = "hello@yio.at"
12
+
13
+ # Later we will include all types and definitions here that are really required to implement external tasks
14
+ # to keep the interface probably as simple as possible.
15
+ __all__ = ["FastBPMN", "Process", "TaskGroup", "TaskHandler"]
File without changes
@@ -0,0 +1,262 @@
1
+ import asyncio
2
+ import logging
3
+ import uuid
4
+ from asyncio import Queue
5
+ from pathlib import Path
6
+ from random import choice
7
+ from typing import TYPE_CHECKING
8
+ from uuid import UUID
9
+
10
+ import pydantic.v1
11
+ from aetpiref.typing import (
12
+ AETPIApplication,
13
+ AETPIReceiveCallable,
14
+ AETPISendCallable,
15
+ ExternalTaskScope,
16
+ TaskScope,
17
+ )
18
+
19
+ from fastbpmn.aetpi import protocol as event
20
+ from fastbpmn.aetpi import utils
21
+
22
+ if TYPE_CHECKING:
23
+ from fastbpmn.context import Context
24
+
25
+ logger = logging.getLogger(__name__)
26
+ # An AETPI protocol implementation that does nothing but log the events
27
+ # and sometimes sleep a little bit
28
+
29
+
30
+ class LogOnlyExternalTaskProcessor:
31
+ """
32
+ External Task Processor for Camunda 7
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ ):
38
+ self._lock_duration = 10000
39
+
40
+ async def _handle_lock_request(
41
+ self, _event: event.ExternalTaskLockRequestEvent, send: AETPISendCallable
42
+ ) -> None:
43
+ """
44
+ Handle the lock request
45
+ """
46
+ if bool(choice([True, True, True, False])):
47
+ await send(utils.create_event_lock_accept(self._lock_duration))
48
+ else:
49
+ await send(
50
+ utils.create_event_lock_reject(
51
+ "CUSTOM", "Flipped the coin ... and I don't want to"
52
+ )
53
+ )
54
+
55
+ async def _handle_execute_request(
56
+ self, _event: event.ExternalTaskExecuteRequestEvent, send: AETPISendCallable
57
+ ) -> None:
58
+ """
59
+ Handle the execute request
60
+ """
61
+ if bool(choice([True, True, True, False])):
62
+ await send(utils.create_event_execute_accept())
63
+ else:
64
+ await send(
65
+ utils.create_event_execute_reject(
66
+ "CUSTOM", "Flipped the coin ... and I don't want to"
67
+ )
68
+ )
69
+
70
+ async def simulate_work(self, _event: event.ExternalTaskExecuteStartEvent):
71
+
72
+ class VarsV1(pydantic.v1.BaseModel):
73
+ x: int
74
+ y: int
75
+ z: int
76
+
77
+ class VarsV2(pydantic.BaseModel):
78
+ x: int
79
+ y: int
80
+ z: int
81
+ sub_guid: UUID = pydantic.Field(default_factory=uuid.uuid4)
82
+
83
+ @pydantic.computed_field
84
+ def path_to_var(self) -> Path:
85
+ return Path(__file__).parent
86
+
87
+ @pydantic.computed_field
88
+ def sum(self) -> int:
89
+ return self.x + self.y + self.z
90
+
91
+ class Test:
92
+ def __init__(self, x: int, y: int, z: int):
93
+ self.x = x
94
+ self.y = y
95
+ self.z = z
96
+
97
+ class OutputModel(pydantic.BaseModel):
98
+ vars_v2: VarsV2
99
+
100
+ guid: UUID = pydantic.Field(default_factory=uuid.uuid4)
101
+
102
+ @pydantic.computed_field
103
+ def sum_v2(self) -> int:
104
+ return self.vars_v2.x + self.vars_v2.y + self.vars_v2.z
105
+
106
+ @pydantic.computed_field
107
+ def path_to_var(self) -> Path:
108
+ return Path(__file__)
109
+
110
+ class InputModel(pydantic.BaseModel):
111
+ x: int
112
+ y: int
113
+ z: int
114
+
115
+ input_model = InputModel(**_event["variables"])
116
+ _ = VarsV1(**input_model.model_dump(mode="python"))
117
+ vars_v2 = VarsV2(**input_model.model_dump(mode="python"))
118
+
119
+ output_model = OutputModel(vars_v2=vars_v2)
120
+
121
+ all_values = output_model.model_dump(mode="python")
122
+ all_values.update(dict(output_model))
123
+
124
+ return output_model.model_dump(mode="python")
125
+
126
+ async def _handle_execute_start_event(
127
+ self,
128
+ _task_scope: TaskScope,
129
+ _event: event.ExternalTaskExecuteStartEvent,
130
+ send: AETPISendCallable,
131
+ ):
132
+ """
133
+ Handle the execute start event
134
+ """
135
+
136
+ match choice([0, 0, 0, 0, 1, 2]):
137
+ case 0:
138
+ logger.info(f"Doing things for {_task_scope} - {_event}")
139
+ await asyncio.sleep((self._lock_duration / 4) / 1000)
140
+
141
+ result = await self.simulate_work(_event)
142
+
143
+ await send(utils.create_event_execute_complete(variables=result))
144
+ case 1:
145
+ await send(
146
+ utils.create_event_execute_failure(
147
+ error_message="I don't like the color of the sky today",
148
+ error_details="The error was random ... please try again later",
149
+ variables={"error": "random", "location": Path(__file__)},
150
+ )
151
+ )
152
+ case _:
153
+ await send(
154
+ utils.create_event_execute_business_error(
155
+ error_code="business_error_101",
156
+ error_message="Processes are here to face with errors as well",
157
+ variables={"error": "business", "location": Path(__file__)},
158
+ )
159
+ )
160
+
161
+ async def __call__(
162
+ self,
163
+ scope: ExternalTaskScope,
164
+ context: "Context",
165
+ receive: AETPIReceiveCallable,
166
+ send: AETPISendCallable,
167
+ ) -> None:
168
+ """
169
+ Process the external task
170
+ """
171
+ # Check if the protocol is supported
172
+ if scope["type"] != "externaltask": # pragma: no cover
173
+ await self.app(scope, context, receive, send)
174
+ return
175
+
176
+ # The tasks properties are crucial so lets extract them now
177
+ task_scope: TaskScope = scope.get("task")
178
+
179
+ while (event := await receive()) is not None:
180
+ # a more sophisticated implementation would use a state machine
181
+ # and check if the event is valid in the current state
182
+ # but for testing thats ok.
183
+ match event:
184
+ case {"type": "externaltask.start"}:
185
+ logger.info(f"External Task started: {task_scope}")
186
+ case {"type": "externaltask.lock.request"}:
187
+ logger.info(f"External Task lock request: {task_scope}")
188
+ await self._handle_lock_request(event, send)
189
+ case {"type": "externaltask.execute.request"}:
190
+ logger.info(f"External Task execute request: {task_scope}")
191
+ await self._handle_execute_request(event, send)
192
+ case {"type": "externaltask.execute.start"}:
193
+ logger.info(f"External Task execute start: {task_scope}")
194
+ await self._handle_execute_start_event(task_scope, event, send)
195
+ case {"type": "externaltask.end"}:
196
+ logger.info(f"External Task end: {task_scope}")
197
+ break
198
+ case _:
199
+ logger.warning(f"Unexpected event: {event}")
200
+
201
+
202
+ async def server(app: AETPIApplication, topic: str):
203
+ send_queue: Queue[event.AETPIReceiveEvent] = Queue()
204
+
205
+ # First indicate the start of an arbitrary external task
206
+ await send_queue.put(utils.create_event_start())
207
+ await send_queue.put(utils.create_event_lock_request())
208
+
209
+ async def receive() -> event.AETPIReceiveEvent:
210
+ return await send_queue.get()
211
+
212
+ # The send callable is actually where get the responses from
213
+
214
+ async def send(event: event.AETPISendEvent) -> None:
215
+
216
+ match event:
217
+ case {"type": "externaltask.lock.accept"}:
218
+ logger.info("<server-receive> Lock accepted")
219
+ logger.info(event)
220
+ await send_queue.put(utils.create_event_execute_request())
221
+ case {"type": "externaltask.execute.accept"}:
222
+ logger.info("<server-receive> Execute accepted")
223
+ logger.info(event)
224
+ await send_queue.put(
225
+ utils.create_event_execute_start({"x": 1, "y": 2, "z": 3})
226
+ )
227
+ case {"type": "externaltask.execute.complete"}:
228
+ logger.info("Execute complete")
229
+ logger.info(event)
230
+ await send_queue.put(utils.create_event_end())
231
+
232
+ case {"type": "externaltask.lock.reject"}:
233
+ logger.info("Lock rejected")
234
+ logger.info(event)
235
+ await send_queue.put(utils.create_event_end())
236
+ case {"type": "externaltask.execute.reject"}:
237
+ logger.info("Execute rejected")
238
+ logger.info(event)
239
+ await send_queue.put(utils.create_event_end())
240
+ case {"type": "externaltask.execute.failure"}:
241
+ logger.info("Execute failure")
242
+ logger.info(event)
243
+ await send_queue.put(utils.create_event_end())
244
+ case {"type": "externaltask.execute.error"}:
245
+ logger.info("Execute business error")
246
+ logger.info(event)
247
+ await send_queue.put(utils.create_event_end())
248
+ case _:
249
+ logger.info(event)
250
+ logger.warning(f"Unexpected event: {event}")
251
+
252
+ await app(
253
+ {
254
+ "type": "externaltask",
255
+ "protocol": "camunda7",
256
+ "aetpi": {"version": "1.0", "spec_version": "1.0"},
257
+ "task": {"id": "12345678-1234-5678-1234-567812345678", "topic": topic},
258
+ },
259
+ None,
260
+ receive,
261
+ send,
262
+ )
@@ -0,0 +1,5 @@
1
+ from .variables import BaseSyncVariableHandlerMiddleware
2
+
3
+ __all__ = [
4
+ "BaseSyncVariableHandlerMiddleware",
5
+ ]
@@ -0,0 +1,90 @@
1
+ """
2
+ Middleware to handle variables in the AETPI
3
+ """
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from aetpiref.typing import (
8
+ AETPIApplication,
9
+ AETPIReceiveCallable,
10
+ AETPIReceiveEvent,
11
+ AETPISendCallable,
12
+ AETPISendEvent,
13
+ ExternalTaskScope,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ pass
18
+
19
+
20
+ class BaseSyncVariableHandlerMiddleware:
21
+ """
22
+ Base class for variable handlers
23
+ """
24
+
25
+ def __init__(self, app: AETPIApplication):
26
+ self.app = app
27
+
28
+ def process_application_variables(
29
+ self, variables: dict[str, Any] | None
30
+ ) -> dict[str, Any] | None:
31
+ """
32
+ Process variables, that are passed from the server to the application
33
+ (from process engine -> to your task handlers)
34
+ """
35
+ return variables
36
+
37
+ def process_server_variables(
38
+ self, variables: dict[str, Any] | None
39
+ ) -> dict[str, Any] | None:
40
+ """
41
+ Process variables, that are passed from the application to the server
42
+ (from your task handlers -> to process engine)
43
+ """
44
+ return variables
45
+
46
+ async def __call__(
47
+ self,
48
+ scope: ExternalTaskScope,
49
+ receive: AETPIReceiveCallable,
50
+ send: AETPISendCallable,
51
+ ) -> None:
52
+ if scope["type"] != "externaltask": # pragma: no cover
53
+ return await self.app(scope, receive, send)
54
+
55
+ # Now lets see interest is only in the variable a like events
56
+ # - exter
57
+
58
+ async def receive_wrapper() -> AETPIReceiveEvent:
59
+
60
+ event = await receive()
61
+
62
+ match event:
63
+ case {"type": "externaltask.execute.start", "variables": variables}:
64
+ variables = self.process_application_variables(variables)
65
+ event["variables"] = variables
66
+
67
+ return event
68
+
69
+ async def send_wrapper(event: AETPISendEvent) -> None:
70
+
71
+ match event:
72
+ case {"type": "externaltask.execute.complete", "variables": variables}:
73
+ variables = self.process_server_variables(variables)
74
+ event["variables"] = variables
75
+ case {
76
+ "type": "externaltask.execute.failure",
77
+ "variables": variables,
78
+ "local_variables": local_variables,
79
+ }:
80
+ variables = self.process_server_variables(variables)
81
+ local_variables = self.process_server_variables(local_variables)
82
+ event["variables"] = variables
83
+ event["local_variables"] = local_variables
84
+ case {"type": "externaltask.execute.error", "variables": variables}:
85
+ variables = self.process_server_variables(variables)
86
+ event["variables"] = variables
87
+
88
+ return await send(event)
89
+
90
+ return await self.app(scope, receive_wrapper, send_wrapper)
File without changes
@@ -0,0 +1,45 @@
1
+ import logging
2
+ import re
3
+
4
+ from aetpiref.typing import AETPIApplication
5
+ from pydantic import HttpUrl
6
+
7
+ from .server import Camunda7Server
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def run(
13
+ app: AETPIApplication,
14
+ *,
15
+ name: str,
16
+ parallelism: int = 10,
17
+ shuffle_pending: bool = True,
18
+ tenant_ids: list[str] | None = None,
19
+ business_key_alike: re.Pattern | None = None,
20
+ camunda_base_url: HttpUrl | str = None,
21
+ camunda_username: str | None = None,
22
+ camunda_password: str | None = None,
23
+ camunda_timeout: float | None = None,
24
+ ):
25
+
26
+ if business_key_alike is not None:
27
+ logger.warning(
28
+ "[DEPRECATED] consider using tenants instead of business_key_alike"
29
+ )
30
+
31
+ Camunda7Server(
32
+ app,
33
+ name=name,
34
+ parallelism=parallelism,
35
+ shuffle_pending=shuffle_pending,
36
+ tenant_ids=tenant_ids,
37
+ business_key_alike=business_key_alike,
38
+ camunda_base_url=camunda_base_url,
39
+ camunda_username=camunda_username,
40
+ camunda_password=camunda_password,
41
+ camunda_timeout=camunda_timeout,
42
+ ).run()
43
+
44
+
45
+ __all__ = ["run"]
@@ -0,0 +1,214 @@
1
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, Union
2
+
3
+ from pydantic import BaseModel, Field, Json, TypeAdapter, WrapValidator, field_validator
4
+ from pydantic_core.core_schema import ValidationInfo
5
+
6
+ from fastbpmn.models.base import FileInfo
7
+
8
+ if TYPE_CHECKING:
9
+ pass
10
+
11
+ BooleanTypeLiteral = Literal["boolean", "Boolean"]
12
+ IntegerTypeLiteral = Literal["integer", "Integer"]
13
+ ShortTypeLiteral = Literal["short", "Short"]
14
+ LongTypeLiteral = Literal["long", "Long"]
15
+ DoubleTypeLiteral = Literal["double", "Double"]
16
+ StringTypeLiteral = Literal["string", "String"]
17
+ JsonTypeLiteral = Literal["json", "Json"]
18
+ NullTypeLiteral = Literal["null", "Null"]
19
+ FileTypeLiteral = Literal["file", "File"]
20
+
21
+
22
+ DecodedPythonTypes = Union[bool, int, float, str, None, Json, FileInfo]
23
+
24
+
25
+ class Camunda7Primitive(BaseModel):
26
+ """
27
+ Handling for primitive types
28
+ """
29
+
30
+ name: str
31
+ value_type: str = Field(..., alias="type")
32
+ value: DecodedPythonTypes
33
+
34
+
35
+ class Camunda7Boolean(Camunda7Primitive):
36
+ """
37
+ Handling for boolean values
38
+ """
39
+
40
+ name: str
41
+ value_type: BooleanTypeLiteral = Field(..., alias="type")
42
+ value: bool
43
+
44
+
45
+ class Camunda7Integer(Camunda7Primitive):
46
+ """
47
+ Handling for integer values
48
+ """
49
+
50
+ name: str
51
+ value_type: IntegerTypeLiteral = Field(..., alias="type")
52
+ value: int
53
+
54
+
55
+ class Camunda7Short(Camunda7Primitive):
56
+ """
57
+ Handling for short values
58
+ """
59
+
60
+ name: str
61
+ value_type: ShortTypeLiteral = Field(..., alias="type")
62
+ value: int
63
+
64
+
65
+ class Camunda7Long(Camunda7Primitive):
66
+ """
67
+ Handling for long values
68
+ """
69
+
70
+ name: str
71
+ value_type: LongTypeLiteral = Field(..., alias="type")
72
+ value: int
73
+
74
+
75
+ class Camunda7Double(Camunda7Primitive):
76
+ """
77
+ Handling for double values
78
+ """
79
+
80
+ name: str
81
+ value_type: DoubleTypeLiteral = Field(..., alias="type")
82
+ value: float
83
+
84
+
85
+ class Camunda7String(Camunda7Primitive):
86
+ """
87
+ Handling for string values
88
+ """
89
+
90
+ name: str
91
+ value_type: StringTypeLiteral = Field(..., alias="type")
92
+ # Check if = None is required as default assignment
93
+ value: str | None
94
+
95
+
96
+ class Camunda7Json(Camunda7Primitive):
97
+ """
98
+ Handling for json values
99
+ """
100
+
101
+ name: str
102
+ value_type: JsonTypeLiteral = Field(..., alias="type")
103
+ value: Json
104
+
105
+
106
+ class Camunda7Null(Camunda7Primitive):
107
+ """
108
+ Handling for null values
109
+ """
110
+
111
+ name: str
112
+ value_type: Literal["null", "Null"] = Field(..., alias="type")
113
+ value: Any = None
114
+
115
+
116
+ class Camunda7FileInfo(Camunda7Primitive):
117
+ """
118
+ Handling for file values
119
+ """
120
+
121
+ name: str
122
+ value_type: Literal["file", "File"] = Field(..., alias="type")
123
+ value: FileInfo = Field(None, alias="valueInfo", validate_default=True)
124
+
125
+ @field_validator("value", mode="before")
126
+ @classmethod
127
+ def validate_value(cls, value: dict[str, Any], info: ValidationInfo):
128
+
129
+ value = value or {}
130
+ variable_name = info.data.get("name")
131
+
132
+ return {
133
+ **value,
134
+ "variable_name": variable_name,
135
+ }
136
+
137
+
138
+ # Annotated Union to parse single camunda7 variables
139
+ # easily
140
+ Camunda7VariableType = Union[
141
+ Camunda7Boolean,
142
+ Camunda7Integer,
143
+ Camunda7Short,
144
+ Camunda7Long,
145
+ Camunda7Double,
146
+ Camunda7String,
147
+ Camunda7Json,
148
+ Camunda7Null,
149
+ Camunda7FileInfo,
150
+ ]
151
+ AnnotatedCamunda7VariableType = Annotated[
152
+ Union[
153
+ Camunda7Boolean,
154
+ Camunda7Integer,
155
+ Camunda7Short,
156
+ Camunda7Long,
157
+ Camunda7Double,
158
+ Camunda7String,
159
+ Camunda7Json,
160
+ Camunda7Null,
161
+ Camunda7FileInfo,
162
+ ],
163
+ Field(discriminator="value_type"),
164
+ ]
165
+
166
+
167
+ # We want / or need ... the variable name and value together
168
+ # so we need to list that somehow ...
169
+ Camunda7Variables = list[AnnotatedCamunda7VariableType]
170
+
171
+
172
+ def preprocess_var_dict(rest_vars: dict[str, Any]) -> list[dict[str, Any]]:
173
+ """
174
+ Create a list that is suitable to be used with provided type adapter
175
+ """
176
+ return [{"name": key, **value} for (key, value) in rest_vars.items()]
177
+
178
+
179
+ # TypeAdapters useful for parsing camunda7 variables
180
+ Camunda7TypeAdapter = TypeAdapter(AnnotatedCamunda7VariableType)
181
+ Camunda7VariableAdapter = TypeAdapter(Camunda7Variables) # Todo: Remove
182
+ Camunda7VariablesAdapter = TypeAdapter(Camunda7Variables)
183
+
184
+ # Conversion layer to more python friendly types
185
+ # for camunda7 variables
186
+ DecodedVariableTypes = Union[bool, int, float, str, dict, None, FileInfo]
187
+
188
+
189
+ def primitive_validator(
190
+ v: Camunda7VariableType, handler, info: ValidationInfo
191
+ ) -> DecodedVariableTypes:
192
+ if isinstance(v, Camunda7VariableType):
193
+ return v.value
194
+ handler(v)
195
+
196
+
197
+ CamundaVariables = dict[
198
+ str, Annotated[DecodedPythonTypes, WrapValidator(primitive_validator)]
199
+ ]
200
+ CamundaVariableAdapter = TypeAdapter(CamundaVariables)
201
+
202
+
203
+ def decode_variables(variables: dict[str, Any]) -> CamundaVariables:
204
+ """
205
+ Read camunda variables from a dict
206
+ """
207
+ preprocessed = preprocess_var_dict(variables)
208
+
209
+ variables = Camunda7VariablesAdapter.validate_python(preprocessed)
210
+ variables_dict = {v.name: v for v in variables}
211
+
212
+ minion_variables = CamundaVariableAdapter.validate_python(variables_dict)
213
+
214
+ return minion_variables