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.
- fastbpmn/__init__.py +15 -0
- fastbpmn/aetpi/__init__.py +0 -0
- fastbpmn/aetpi/logonly.py +262 -0
- fastbpmn/aetpi/middleware/__init__.py +5 -0
- fastbpmn/aetpi/middleware/variables.py +90 -0
- fastbpmn/aetpi/servers/__init__.py +0 -0
- fastbpmn/aetpi/servers/camunda7/__init__.py +45 -0
- fastbpmn/aetpi/servers/camunda7/decoder.py +214 -0
- fastbpmn/aetpi/servers/camunda7/encoder.py +129 -0
- fastbpmn/aetpi/servers/camunda7/middleware.py +50 -0
- fastbpmn/aetpi/servers/camunda7/producer.py +211 -0
- fastbpmn/aetpi/servers/camunda7/queue.py +34 -0
- fastbpmn/aetpi/servers/camunda7/server.py +309 -0
- fastbpmn/aetpi/servers/camunda7/settings.py +52 -0
- fastbpmn/aetpi/servers/camunda7/worker.py +448 -0
- fastbpmn/aetpi/utils.py +214 -0
- fastbpmn/camunda/__init__.py +37 -0
- fastbpmn/camunda/asynchttp.py +340 -0
- fastbpmn/camunda/config.py +11 -0
- fastbpmn/camunda/datatypes.py +44 -0
- fastbpmn/camunda/errors.py +110 -0
- fastbpmn/camunda/json.py +37 -0
- fastbpmn/camunda/models.py +55 -0
- fastbpmn/camunda/processengine.py +735 -0
- fastbpmn/camunda/utils.py +43 -0
- fastbpmn/context/__init__.py +4 -0
- fastbpmn/context/context.py +99 -0
- fastbpmn/context/io.py +33 -0
- fastbpmn/context/utils.py +32 -0
- fastbpmn/errors/__init__.py +27 -0
- fastbpmn/errors/base.py +42 -0
- fastbpmn/errors/bpmn_error.py +35 -0
- fastbpmn/errors/context.py +36 -0
- fastbpmn/errors/process_engine.py +64 -0
- fastbpmn/errors/task_failure_error.py +47 -0
- fastbpmn/errors/types.py +5 -0
- fastbpmn/fastbpmn.py +347 -0
- fastbpmn/fastbpmn_types.py +35 -0
- fastbpmn/lifespan/__init__.py +209 -0
- fastbpmn/middleware/__init__.py +33 -0
- fastbpmn/middleware/context/__init__.py +31 -0
- fastbpmn/middleware/exceptions/__init__.py +178 -0
- fastbpmn/middleware/exceptions/handler.py +167 -0
- fastbpmn/middleware/exceptions/types.py +10 -0
- fastbpmn/middleware/exceptions/utils.py +73 -0
- fastbpmn/middleware/guard/__init__.py +33 -0
- fastbpmn/middleware/guard/types.py +5 -0
- fastbpmn/middleware/guard/utils.py +69 -0
- fastbpmn/models/__init__.py +43 -0
- fastbpmn/models/base.py +80 -0
- fastbpmn/models/decoder.py +26 -0
- fastbpmn/models/encoder.py +208 -0
- fastbpmn/models/task.py +62 -0
- fastbpmn/models/types/__init__.py +11 -0
- fastbpmn/models/types/camunda7.py +237 -0
- fastbpmn/process.py +124 -0
- fastbpmn/processor/__init__.py +306 -0
- fastbpmn/processor/routing.py +171 -0
- fastbpmn/processor/task.py +78 -0
- fastbpmn/readiness.py +13 -0
- fastbpmn/result.py +162 -0
- fastbpmn/task.py +114 -0
- fastbpmn/task_group.py +93 -0
- fastbpmn/types.py +11 -0
- fastbpmn/utils/__init__.py +0 -0
- fastbpmn/utils/asyncer.py +101 -0
- fastbpmn/utils/asyncio.py +30 -0
- fastbpmn/utils/concurrency.py +38 -0
- fastbpmn/utils/config.py +57 -0
- fastbpmn/utils/func.py +22 -0
- fastbpmn/utils/inspect.py +64 -0
- fastbpmn/utils/names.py +21 -0
- fastbpmn-0.1.0.dist-info/METADATA +491 -0
- fastbpmn-0.1.0.dist-info/RECORD +88 -0
- fastbpmn-0.1.0.dist-info/WHEEL +4 -0
- fastbpmn-0.1.0.dist-info/entry_points.txt +4 -0
- fastbpmn-0.1.0.dist-info/licenses/LICENSE +21 -0
- squirrel/__init__.py +9 -0
- squirrel/__main__.py +3 -0
- squirrel/app.py +7 -0
- squirrel/arguments.py +25 -0
- squirrel/flavours/__init__.py +5 -0
- squirrel/flavours/camunda7/__init__.py +0 -0
- squirrel/flavours/camunda7/entrypoint.py +92 -0
- squirrel/flavours/camunda7/settings.py +15 -0
- squirrel/parser.py +54 -0
- squirrel/run.py +84 -0
- 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,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
|