quantumbpm-sdk 0.12.0__tar.gz

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 (113) hide show
  1. quantumbpm_sdk-0.12.0/LICENSE +21 -0
  2. quantumbpm_sdk-0.12.0/PKG-INFO +356 -0
  3. quantumbpm_sdk-0.12.0/README.md +333 -0
  4. quantumbpm_sdk-0.12.0/pyproject.toml +53 -0
  5. quantumbpm_sdk-0.12.0/quantumbpm/__init__.py +37 -0
  6. quantumbpm_sdk-0.12.0/quantumbpm/api/__init__.py +6 -0
  7. quantumbpm_sdk-0.12.0/quantumbpm/api/bpmn_api.py +11753 -0
  8. quantumbpm_sdk-0.12.0/quantumbpm/api/default_api.py +8978 -0
  9. quantumbpm_sdk-0.12.0/quantumbpm/api_client.py +804 -0
  10. quantumbpm_sdk-0.12.0/quantumbpm/api_response.py +21 -0
  11. quantumbpm_sdk-0.12.0/quantumbpm/auth.py +140 -0
  12. quantumbpm_sdk-0.12.0/quantumbpm/bpmn.py +385 -0
  13. quantumbpm_sdk-0.12.0/quantumbpm/client.py +86 -0
  14. quantumbpm_sdk-0.12.0/quantumbpm/configuration.py +596 -0
  15. quantumbpm_sdk-0.12.0/quantumbpm/dmn.py +116 -0
  16. quantumbpm_sdk-0.12.0/quantumbpm/exceptions.py +218 -0
  17. quantumbpm_sdk-0.12.0/quantumbpm/models/__init__.py +98 -0
  18. quantumbpm_sdk-0.12.0/quantumbpm/models/active_scope.py +98 -0
  19. quantumbpm_sdk-0.12.0/quantumbpm/models/batch_evaluate_design_request.py +90 -0
  20. quantumbpm_sdk-0.12.0/quantumbpm/models/batch_evaluation_response.py +96 -0
  21. quantumbpm_sdk-0.12.0/quantumbpm/models/batch_evaluation_response_results_inner.py +105 -0
  22. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_activity_state.py +114 -0
  23. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_external_job_paginated_response.py +102 -0
  24. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_incident.py +114 -0
  25. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_incident_record.py +125 -0
  26. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_incident_record_paginated_response.py +102 -0
  27. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_instance.py +117 -0
  28. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_instance_children_response.py +96 -0
  29. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_instance_paginated_response.py +102 -0
  30. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_instance_state.py +136 -0
  31. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_process_definition.py +100 -0
  32. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_process_summary.py +96 -0
  33. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_process_summary_paginated_response.py +102 -0
  34. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_process_version.py +112 -0
  35. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_process_version_paginated_response.py +102 -0
  36. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_resource.py +104 -0
  37. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_resource_detail.py +114 -0
  38. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_resource_paginated_response.py +102 -0
  39. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_resource_summary.py +100 -0
  40. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_resource_summary_paginated_response.py +102 -0
  41. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_user_task_paginated_response.py +102 -0
  42. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_validate_response.py +105 -0
  43. quantumbpm_sdk-0.12.0/quantumbpm/models/bpmn_validation_issue.py +99 -0
  44. quantumbpm_sdk-0.12.0/quantumbpm/models/complete_bpmn_external_job_request.py +90 -0
  45. quantumbpm_sdk-0.12.0/quantumbpm/models/complete_bpmn_external_jobs_batch_request.py +97 -0
  46. quantumbpm_sdk-0.12.0/quantumbpm/models/complete_bpmn_external_jobs_batch_request_items_inner.py +92 -0
  47. quantumbpm_sdk-0.12.0/quantumbpm/models/correlation_keys.py +177 -0
  48. quantumbpm_sdk-0.12.0/quantumbpm/models/create_bpmn_resource_request.py +90 -0
  49. quantumbpm_sdk-0.12.0/quantumbpm/models/create_definition_request.py +92 -0
  50. quantumbpm_sdk-0.12.0/quantumbpm/models/create_project_request.py +88 -0
  51. quantumbpm_sdk-0.12.0/quantumbpm/models/decision_input_field.py +90 -0
  52. quantumbpm_sdk-0.12.0/quantumbpm/models/decision_summary.py +106 -0
  53. quantumbpm_sdk-0.12.0/quantumbpm/models/definition.py +104 -0
  54. quantumbpm_sdk-0.12.0/quantumbpm/models/element_mapping.py +90 -0
  55. quantumbpm_sdk-0.12.0/quantumbpm/models/error.py +99 -0
  56. quantumbpm_sdk-0.12.0/quantumbpm/models/evaluate_design_request.py +96 -0
  57. quantumbpm_sdk-0.12.0/quantumbpm/models/evaluate_stored_request.py +94 -0
  58. quantumbpm_sdk-0.12.0/quantumbpm/models/evaluation_result.py +137 -0
  59. quantumbpm_sdk-0.12.0/quantumbpm/models/execution.py +107 -0
  60. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job.py +125 -0
  61. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job_active_workers_item.py +90 -0
  62. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job_active_workers_response.py +96 -0
  63. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job_batch_response.py +96 -0
  64. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job_batch_response_results_inner.py +99 -0
  65. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job_queue_depth_item.py +90 -0
  66. quantumbpm_sdk-0.12.0/quantumbpm/models/external_job_queue_depth_response.py +96 -0
  67. quantumbpm_sdk-0.12.0/quantumbpm/models/feel_value.py +200 -0
  68. quantumbpm_sdk-0.12.0/quantumbpm/models/get_bpmn_instance_variables200_response.py +88 -0
  69. quantumbpm_sdk-0.12.0/quantumbpm/models/get_health200_response.py +88 -0
  70. quantumbpm_sdk-0.12.0/quantumbpm/models/get_version200_response.py +92 -0
  71. quantumbpm_sdk-0.12.0/quantumbpm/models/heartbeat_bpmn_external_job_request.py +90 -0
  72. quantumbpm_sdk-0.12.0/quantumbpm/models/hit_rule.py +95 -0
  73. quantumbpm_sdk-0.12.0/quantumbpm/models/migrate_bpmn_instance_request.py +100 -0
  74. quantumbpm_sdk-0.12.0/quantumbpm/models/migration_validation_result.py +90 -0
  75. quantumbpm_sdk-0.12.0/quantumbpm/models/modification_instruction.py +101 -0
  76. quantumbpm_sdk-0.12.0/quantumbpm/models/modify_bpmn_instance_request.py +96 -0
  77. quantumbpm_sdk-0.12.0/quantumbpm/models/paginated_decisions_response.py +102 -0
  78. quantumbpm_sdk-0.12.0/quantumbpm/models/paginated_definitions_response.py +102 -0
  79. quantumbpm_sdk-0.12.0/quantumbpm/models/paginated_executions_response.py +102 -0
  80. quantumbpm_sdk-0.12.0/quantumbpm/models/pagination_metadata.py +94 -0
  81. quantumbpm_sdk-0.12.0/quantumbpm/models/poll_bpmn_job_request.py +96 -0
  82. quantumbpm_sdk-0.12.0/quantumbpm/models/project.py +96 -0
  83. quantumbpm_sdk-0.12.0/quantumbpm/models/publish_bpmn_message_request.py +98 -0
  84. quantumbpm_sdk-0.12.0/quantumbpm/models/publish_bpmn_signal_request.py +92 -0
  85. quantumbpm_sdk-0.12.0/quantumbpm/models/start_bpmn_instance201_response.py +88 -0
  86. quantumbpm_sdk-0.12.0/quantumbpm/models/start_bpmn_instance_request.py +91 -0
  87. quantumbpm_sdk-0.12.0/quantumbpm/models/start_bpmn_test_instance201_response.py +88 -0
  88. quantumbpm_sdk-0.12.0/quantumbpm/models/start_bpmn_test_instance_request.py +90 -0
  89. quantumbpm_sdk-0.12.0/quantumbpm/models/suspend_bpmn_definition_request.py +88 -0
  90. quantumbpm_sdk-0.12.0/quantumbpm/models/suspend_bpmn_instance_request.py +88 -0
  91. quantumbpm_sdk-0.12.0/quantumbpm/models/suspension_entry.py +92 -0
  92. quantumbpm_sdk-0.12.0/quantumbpm/models/throw_bpmn_external_job_error_request.py +90 -0
  93. quantumbpm_sdk-0.12.0/quantumbpm/models/throw_bpmn_external_job_errors_batch_request.py +97 -0
  94. quantumbpm_sdk-0.12.0/quantumbpm/models/throw_bpmn_external_job_errors_batch_request_items_inner.py +94 -0
  95. quantumbpm_sdk-0.12.0/quantumbpm/models/trigger_bpmn_ad_hoc_node_request.py +88 -0
  96. quantumbpm_sdk-0.12.0/quantumbpm/models/update_bpmn_instance_variables_request.py +88 -0
  97. quantumbpm_sdk-0.12.0/quantumbpm/models/update_definition_request.py +92 -0
  98. quantumbpm_sdk-0.12.0/quantumbpm/models/update_user_task_assignment_request.py +97 -0
  99. quantumbpm_sdk-0.12.0/quantumbpm/models/user_task.py +133 -0
  100. quantumbpm_sdk-0.12.0/quantumbpm/models/validate_bpmn_resource_request.py +88 -0
  101. quantumbpm_sdk-0.12.0/quantumbpm/py.typed +0 -0
  102. quantumbpm_sdk-0.12.0/quantumbpm/rest.py +263 -0
  103. quantumbpm_sdk-0.12.0/quantumbpm/variables.py +125 -0
  104. quantumbpm_sdk-0.12.0/quantumbpm/workers.py +406 -0
  105. quantumbpm_sdk-0.12.0/quantumbpm_sdk.egg-info/PKG-INFO +356 -0
  106. quantumbpm_sdk-0.12.0/quantumbpm_sdk.egg-info/SOURCES.txt +112 -0
  107. quantumbpm_sdk-0.12.0/quantumbpm_sdk.egg-info/dependency_links.txt +1 -0
  108. quantumbpm_sdk-0.12.0/quantumbpm_sdk.egg-info/requires.txt +6 -0
  109. quantumbpm_sdk-0.12.0/quantumbpm_sdk.egg-info/top_level.txt +1 -0
  110. quantumbpm_sdk-0.12.0/setup.cfg +7 -0
  111. quantumbpm_sdk-0.12.0/setup.py +37 -0
  112. quantumbpm_sdk-0.12.0/tests/test_smoke.py +39 -0
  113. quantumbpm_sdk-0.12.0/tests/test_workers_clamp.py +48 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QuantumDMN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,356 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantumbpm-sdk
3
+ Version: 0.12.0
4
+ Summary: QuantumBPM Python SDK — DMN evaluation, BPMN orchestration, and external job workers.
5
+ Home-page: https://quantumbpm.com
6
+ Author: QuantumBPM
7
+ Author-email: QuantumBPM <support@quantumbpm.com>
8
+ Project-URL: Repository, https://github.com/QuantumBPM/quantum-python-sdk
9
+ Keywords: quantumbpm,dmn,bpmn,workflow,feel,sdk
10
+ Requires-Python: >= 3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: urllib3<3.0.0,>=2.1.0
14
+ Requires-Dist: python-dateutil>=2.8.2
15
+ Requires-Dist: pydantic>=2
16
+ Requires-Dist: typing-extensions>=4.7.1
17
+ Requires-Dist: PyJWT[crypto]>=2.8.0
18
+ Requires-Dist: requests>=2.31.0
19
+ Dynamic: author
20
+ Dynamic: home-page
21
+ Dynamic: license-file
22
+ Dynamic: requires-python
23
+
24
+ # QuantumBPM Python SDK
25
+
26
+ Official Python SDK for the [QuantumBPM](https://quantumbpm.com) platform — DMN evaluation, BPMN process orchestration, and external job workers.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install quantumbpm-sdk
32
+ ```
33
+
34
+ Python 3.10+. Async-first; built on `asyncio`.
35
+
36
+ ## What's in the box
37
+
38
+ | Module | Purpose |
39
+ | ------------------------- | ----------------------------------------------------------------------------- |
40
+ | `quantumbpm.QuantumBPM` | Top-level client exposing `.dmn`, `.bpmn`, plus `new_worker(...)` |
41
+ | `quantumbpm.auth` | `TokenProvider`, `ZitadelTokenProvider`, `StaticTokenProvider` |
42
+ | `quantumbpm.dmn` | DMN evaluation: stored definitions, ad-hoc XML, batch |
43
+ | `quantumbpm.bpmn` | BPMN resources, instances, messaging, user tasks, processes |
44
+ | `quantumbpm.workers` | External job worker runtime — long-poll, lock heartbeat, dispatch |
45
+ | `quantumbpm.variables` | `Vars` wrapper with typed accessors and FEEL-context conversion |
46
+ | `quantumbpm.api[_client]` | OpenAPI-generated client. Reachable via `client.raw`, never hand-edited |
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ import asyncio
52
+ from quantumbpm import QuantumBPM, Vars, ZitadelTokenProvider
53
+
54
+ async def main():
55
+ provider = ZitadelTokenProvider(
56
+ "./service-account.json", # Zitadel JSON Key file
57
+ "https://auth.quantumbpm.com", # issuer
58
+ "your-zitadel-project-id", # audience scope
59
+ )
60
+
61
+ async with QuantumBPM(
62
+ base_url="https://api.quantumbpm.com",
63
+ project_id="00000000-0000-0000-0000-000000000000",
64
+ token_provider=provider,
65
+ ) as client:
66
+ result = await client.dmn.evaluate(
67
+ "loan-eligibility",
68
+ Vars().set("requestedAmt", 1000).set("creditScore", 720),
69
+ )
70
+ print(result)
71
+
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ The async context manager (`async with`) acquires a fresh bearer token on entry. Skipping the context manager and calling methods directly works too — the SDK refreshes tokens on demand.
76
+
77
+ ## Authentication
78
+
79
+ The `TokenProvider` Protocol returns a bearer token on each request. Two implementations ship out of the box.
80
+
81
+ ### Zitadel service account
82
+
83
+ ```python
84
+ from quantumbpm import ZitadelTokenProvider
85
+
86
+ provider = ZitadelTokenProvider(
87
+ "./service-account.json", # path to JSON Key file
88
+ "https://auth.quantumbpm.com", # issuer URL
89
+ "your-zitadel-project-id", # adds the audience scope
90
+ ssl_ca_cert="/path/to/ca.crt", # optional, for self-signed CAs
91
+ )
92
+ ```
93
+
94
+ The provider caches tokens in-memory until shortly before expiry.
95
+
96
+ ### Static bearer token
97
+
98
+ For Enterprise deployments that issue long-lived API keys, or in tests where a token is acquired out of band:
99
+
100
+ ```python
101
+ from quantumbpm import StaticTokenProvider
102
+
103
+ provider = StaticTokenProvider("eyJhbGciOi...")
104
+ ```
105
+
106
+ ### Bring your own
107
+
108
+ Implement the Protocol:
109
+
110
+ ```python
111
+ from quantumbpm import TokenProvider
112
+
113
+ class MyProvider:
114
+ async def get_token(self) -> str:
115
+ return await fetch_my_token()
116
+
117
+ provider: TokenProvider = MyProvider()
118
+ ```
119
+
120
+ ## DMN evaluation
121
+
122
+ The `client.dmn` sub-client offers four async methods.
123
+
124
+ ### Evaluate a stored definition
125
+
126
+ ```python
127
+ result = await client.dmn.evaluate(
128
+ "loan-eligibility",
129
+ Vars().set("requestedAmt", 5000).set("creditScore", 720),
130
+ )
131
+ ```
132
+
133
+ Returns `dict[str, EvaluationResult]` keyed by decision name. Each result has `value`, `hit_rules`, `error`, and `type`.
134
+
135
+ Pin a version, restrict the evaluated decisions, or attach decision services:
136
+
137
+ ```python
138
+ result = await client.dmn.evaluate(
139
+ "loan-eligibility",
140
+ vars,
141
+ version=3,
142
+ decisions=["eligibility", "rate"],
143
+ )
144
+ ```
145
+
146
+ ### Evaluate by platform UUID
147
+
148
+ When you already hold a database-version pointer:
149
+
150
+ ```python
151
+ result = await client.dmn.evaluate_by_id(definition_uuid, vars)
152
+ ```
153
+
154
+ ### Ad-hoc XML evaluation
155
+
156
+ For "evaluate while editing" flows that don't store the XML:
157
+
158
+ ```python
159
+ result = await client.dmn.evaluate_design(
160
+ dmn_xml,
161
+ vars,
162
+ additional_xmls=[imported_xml1, imported_xml2],
163
+ decisions=["eligibility"],
164
+ )
165
+ ```
166
+
167
+ ### Batch ad-hoc evaluation
168
+
169
+ ```python
170
+ rows = [
171
+ Vars().set("requestedAmt", 1000),
172
+ Vars().set("requestedAmt", 5000),
173
+ Vars().set("requestedAmt", 25000),
174
+ ]
175
+ batch = await client.dmn.evaluate_design_batch(dmn_xml, rows)
176
+ ```
177
+
178
+ ## BPMN processes
179
+
180
+ `client.bpmn` covers the full BPMN runtime surface.
181
+
182
+ ### Deploy and start
183
+
184
+ ```python
185
+ draft = await client.bpmn.create_resource("loan-process", bpmn_xml)
186
+ await client.bpmn.deploy_resource(draft.id)
187
+
188
+ # Re-fetch to get the populated process-definition list.
189
+ deployed = await client.bpmn.get_resource(draft.id)
190
+ process_def = deployed.processes[0]
191
+
192
+ workflow_id = await client.bpmn.start_instance(
193
+ process_def.id,
194
+ Vars().set("applicantID", "u-123").set("requestedAmt", 25000),
195
+ )
196
+ ```
197
+
198
+ ### Inspect runtime state
199
+
200
+ ```python
201
+ state = await client.bpmn.get_instance(workflow_id)
202
+ print(state.status, state.active_scopes)
203
+
204
+ vars = await client.bpmn.get_instance_variables(workflow_id)
205
+
206
+ children = await client.bpmn.get_instance_children(workflow_id)
207
+ ```
208
+
209
+ ### Send messages and signals
210
+
211
+ ```python
212
+ await client.bpmn.publish_message(
213
+ "loan-approved",
214
+ Vars().set("approvedAmt", 24000),
215
+ correlation_keys={"applicantID": "u-123"},
216
+ ttl="PT5M",
217
+ )
218
+
219
+ await client.bpmn.publish_signal("system-maintenance")
220
+ ```
221
+
222
+ ### User tasks
223
+
224
+ ```python
225
+ page = await client.bpmn.list_user_tasks(
226
+ assignee="alice@example.com",
227
+ status="CREATED",
228
+ )
229
+
230
+ await client.bpmn.complete_user_task(execution_key, Vars().set("approved", True))
231
+
232
+ # Or fail with a BPMN error code (matches boundary error events):
233
+ await client.bpmn.throw_user_task_error(execution_key, "REVIEW_REJECTED")
234
+ ```
235
+
236
+ ## External job workers
237
+
238
+ Workers handle service tasks asynchronously. Register a handler per task type with a decorator, then call `run`. The runtime owns long-polling, lock heartbeats, dispatch, and outcome mapping.
239
+
240
+ ### Minimal worker
241
+
242
+ ```python
243
+ import asyncio
244
+ from quantumbpm import QuantumBPM, Vars, BpmnError
245
+ from quantumbpm.workers import Job
246
+
247
+ async def main():
248
+ async with QuantumBPM(...) as client:
249
+ worker = client.new_worker(client_id="billing-svc")
250
+ stop = asyncio.Event()
251
+
252
+ @worker.handler("send-email")
253
+ async def handle(job: Job) -> Vars:
254
+ recipient = job.vars.lookup("recipient")
255
+ subject = job.vars.lookup("subject")
256
+ await emailer.send(recipient, subject)
257
+ return Vars().set("messageID", "msg-123") # → Complete
258
+
259
+ await worker.run(stop) # blocks until stop is set
260
+
261
+ asyncio.run(main())
262
+ ```
263
+
264
+ `run(stop)` resolves when the `asyncio.Event` is set, after in-flight handlers settle.
265
+
266
+ ### Concurrency, polling, and locks
267
+
268
+ ```python
269
+ @worker.handler(
270
+ "send-email",
271
+ max_jobs=10, # up to 10 in flight per task type
272
+ poll_timeout="45s", # long-poll wait
273
+ lock_duration="2m", # exclusive lock per job
274
+ )
275
+ async def handle(job: Job) -> Vars:
276
+ ...
277
+ ```
278
+
279
+ Concurrency is per task type. Different task types run independently. The runtime auto-renews the lock at half the lock-duration interval while the handler runs.
280
+
281
+ ### Throwing typed BPMN errors
282
+
283
+ Raise a `BpmnError` to fail the job with a code that boundary error events on the originating service task can catch:
284
+
285
+ ```python
286
+ @worker.handler("charge-card")
287
+ async def handle(job: Job) -> Vars:
288
+ try:
289
+ tx_id = await charge(job.vars.to_dict())
290
+ return Vars().set("transactionID", tx_id)
291
+ except InsufficientFundsError:
292
+ raise BpmnError("INSUFFICIENT_FUNDS", Vars().set("availableBalance", 12.0))
293
+ ```
294
+
295
+ Any other exception is reported as `WORKER_ERROR`, which the server treats as a retryable failure that decrements the job's retry budget.
296
+
297
+ ### Typed handlers (Pydantic)
298
+
299
+ Type-annotate the `Job` parameter with a Pydantic model — the runtime validates the job's input variables before invoking the handler. The decoded value lands in `job.typed`.
300
+
301
+ ```python
302
+ from pydantic import BaseModel
303
+ from quantumbpm.workers import Job
304
+
305
+ class EmailJob(BaseModel):
306
+ recipient: str
307
+ subject: str
308
+
309
+ @worker.handler("send-email")
310
+ async def handle(job: Job[EmailJob]) -> Vars:
311
+ await emailer.send(job.typed.recipient, job.typed.subject)
312
+ return Vars().set("messageID", "msg-123")
313
+ ```
314
+
315
+ Decode failures become `WORKER_ERROR` automatically, with the validation message in the variables.
316
+
317
+ ## Variables
318
+
319
+ `Vars` is a thin wrapper around `dict[str, Any]` shared by DMN, BPMN, and workers.
320
+
321
+ ### Construction
322
+
323
+ ```python
324
+ v = Vars().set("amount", 100).set("name", "Alice")
325
+ v = Vars.from_dict({"amount": 100, "name": "Alice"})
326
+ ```
327
+
328
+ ### Typed access
329
+
330
+ ```python
331
+ amount = v.get("amount", float)
332
+ flag = v.get("approved", bool)
333
+
334
+ class Loan(BaseModel):
335
+ requestedAmt: float
336
+ approved: bool
337
+
338
+ loan = v.as_type(Loan)
339
+ ```
340
+
341
+ `Vars.get(name, type_)` and `Vars.as_type(type_)` accept Pydantic models, dataclasses, and primitives — Pydantic's `TypeAdapter` does the validation.
342
+
343
+ ## Escape hatch
344
+
345
+ The `client.raw` property exposes the underlying generated `ApiClient` for endpoints not yet wrapped (instance migration, modification, ad-hoc triggers, batch job complete/error, etc.):
346
+
347
+ ```python
348
+ from quantumbpm.api.bpmn_api import BpmnApi
349
+
350
+ api = BpmnApi(client.raw)
351
+ result = api.migrate_bpmn_instance(client.project_id, workflow_id, body)
352
+ ```
353
+
354
+ ## License
355
+
356
+ MIT License — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,333 @@
1
+ # QuantumBPM Python SDK
2
+
3
+ Official Python SDK for the [QuantumBPM](https://quantumbpm.com) platform — DMN evaluation, BPMN process orchestration, and external job workers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install quantumbpm-sdk
9
+ ```
10
+
11
+ Python 3.10+. Async-first; built on `asyncio`.
12
+
13
+ ## What's in the box
14
+
15
+ | Module | Purpose |
16
+ | ------------------------- | ----------------------------------------------------------------------------- |
17
+ | `quantumbpm.QuantumBPM` | Top-level client exposing `.dmn`, `.bpmn`, plus `new_worker(...)` |
18
+ | `quantumbpm.auth` | `TokenProvider`, `ZitadelTokenProvider`, `StaticTokenProvider` |
19
+ | `quantumbpm.dmn` | DMN evaluation: stored definitions, ad-hoc XML, batch |
20
+ | `quantumbpm.bpmn` | BPMN resources, instances, messaging, user tasks, processes |
21
+ | `quantumbpm.workers` | External job worker runtime — long-poll, lock heartbeat, dispatch |
22
+ | `quantumbpm.variables` | `Vars` wrapper with typed accessors and FEEL-context conversion |
23
+ | `quantumbpm.api[_client]` | OpenAPI-generated client. Reachable via `client.raw`, never hand-edited |
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ import asyncio
29
+ from quantumbpm import QuantumBPM, Vars, ZitadelTokenProvider
30
+
31
+ async def main():
32
+ provider = ZitadelTokenProvider(
33
+ "./service-account.json", # Zitadel JSON Key file
34
+ "https://auth.quantumbpm.com", # issuer
35
+ "your-zitadel-project-id", # audience scope
36
+ )
37
+
38
+ async with QuantumBPM(
39
+ base_url="https://api.quantumbpm.com",
40
+ project_id="00000000-0000-0000-0000-000000000000",
41
+ token_provider=provider,
42
+ ) as client:
43
+ result = await client.dmn.evaluate(
44
+ "loan-eligibility",
45
+ Vars().set("requestedAmt", 1000).set("creditScore", 720),
46
+ )
47
+ print(result)
48
+
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ The async context manager (`async with`) acquires a fresh bearer token on entry. Skipping the context manager and calling methods directly works too — the SDK refreshes tokens on demand.
53
+
54
+ ## Authentication
55
+
56
+ The `TokenProvider` Protocol returns a bearer token on each request. Two implementations ship out of the box.
57
+
58
+ ### Zitadel service account
59
+
60
+ ```python
61
+ from quantumbpm import ZitadelTokenProvider
62
+
63
+ provider = ZitadelTokenProvider(
64
+ "./service-account.json", # path to JSON Key file
65
+ "https://auth.quantumbpm.com", # issuer URL
66
+ "your-zitadel-project-id", # adds the audience scope
67
+ ssl_ca_cert="/path/to/ca.crt", # optional, for self-signed CAs
68
+ )
69
+ ```
70
+
71
+ The provider caches tokens in-memory until shortly before expiry.
72
+
73
+ ### Static bearer token
74
+
75
+ For Enterprise deployments that issue long-lived API keys, or in tests where a token is acquired out of band:
76
+
77
+ ```python
78
+ from quantumbpm import StaticTokenProvider
79
+
80
+ provider = StaticTokenProvider("eyJhbGciOi...")
81
+ ```
82
+
83
+ ### Bring your own
84
+
85
+ Implement the Protocol:
86
+
87
+ ```python
88
+ from quantumbpm import TokenProvider
89
+
90
+ class MyProvider:
91
+ async def get_token(self) -> str:
92
+ return await fetch_my_token()
93
+
94
+ provider: TokenProvider = MyProvider()
95
+ ```
96
+
97
+ ## DMN evaluation
98
+
99
+ The `client.dmn` sub-client offers four async methods.
100
+
101
+ ### Evaluate a stored definition
102
+
103
+ ```python
104
+ result = await client.dmn.evaluate(
105
+ "loan-eligibility",
106
+ Vars().set("requestedAmt", 5000).set("creditScore", 720),
107
+ )
108
+ ```
109
+
110
+ Returns `dict[str, EvaluationResult]` keyed by decision name. Each result has `value`, `hit_rules`, `error`, and `type`.
111
+
112
+ Pin a version, restrict the evaluated decisions, or attach decision services:
113
+
114
+ ```python
115
+ result = await client.dmn.evaluate(
116
+ "loan-eligibility",
117
+ vars,
118
+ version=3,
119
+ decisions=["eligibility", "rate"],
120
+ )
121
+ ```
122
+
123
+ ### Evaluate by platform UUID
124
+
125
+ When you already hold a database-version pointer:
126
+
127
+ ```python
128
+ result = await client.dmn.evaluate_by_id(definition_uuid, vars)
129
+ ```
130
+
131
+ ### Ad-hoc XML evaluation
132
+
133
+ For "evaluate while editing" flows that don't store the XML:
134
+
135
+ ```python
136
+ result = await client.dmn.evaluate_design(
137
+ dmn_xml,
138
+ vars,
139
+ additional_xmls=[imported_xml1, imported_xml2],
140
+ decisions=["eligibility"],
141
+ )
142
+ ```
143
+
144
+ ### Batch ad-hoc evaluation
145
+
146
+ ```python
147
+ rows = [
148
+ Vars().set("requestedAmt", 1000),
149
+ Vars().set("requestedAmt", 5000),
150
+ Vars().set("requestedAmt", 25000),
151
+ ]
152
+ batch = await client.dmn.evaluate_design_batch(dmn_xml, rows)
153
+ ```
154
+
155
+ ## BPMN processes
156
+
157
+ `client.bpmn` covers the full BPMN runtime surface.
158
+
159
+ ### Deploy and start
160
+
161
+ ```python
162
+ draft = await client.bpmn.create_resource("loan-process", bpmn_xml)
163
+ await client.bpmn.deploy_resource(draft.id)
164
+
165
+ # Re-fetch to get the populated process-definition list.
166
+ deployed = await client.bpmn.get_resource(draft.id)
167
+ process_def = deployed.processes[0]
168
+
169
+ workflow_id = await client.bpmn.start_instance(
170
+ process_def.id,
171
+ Vars().set("applicantID", "u-123").set("requestedAmt", 25000),
172
+ )
173
+ ```
174
+
175
+ ### Inspect runtime state
176
+
177
+ ```python
178
+ state = await client.bpmn.get_instance(workflow_id)
179
+ print(state.status, state.active_scopes)
180
+
181
+ vars = await client.bpmn.get_instance_variables(workflow_id)
182
+
183
+ children = await client.bpmn.get_instance_children(workflow_id)
184
+ ```
185
+
186
+ ### Send messages and signals
187
+
188
+ ```python
189
+ await client.bpmn.publish_message(
190
+ "loan-approved",
191
+ Vars().set("approvedAmt", 24000),
192
+ correlation_keys={"applicantID": "u-123"},
193
+ ttl="PT5M",
194
+ )
195
+
196
+ await client.bpmn.publish_signal("system-maintenance")
197
+ ```
198
+
199
+ ### User tasks
200
+
201
+ ```python
202
+ page = await client.bpmn.list_user_tasks(
203
+ assignee="alice@example.com",
204
+ status="CREATED",
205
+ )
206
+
207
+ await client.bpmn.complete_user_task(execution_key, Vars().set("approved", True))
208
+
209
+ # Or fail with a BPMN error code (matches boundary error events):
210
+ await client.bpmn.throw_user_task_error(execution_key, "REVIEW_REJECTED")
211
+ ```
212
+
213
+ ## External job workers
214
+
215
+ Workers handle service tasks asynchronously. Register a handler per task type with a decorator, then call `run`. The runtime owns long-polling, lock heartbeats, dispatch, and outcome mapping.
216
+
217
+ ### Minimal worker
218
+
219
+ ```python
220
+ import asyncio
221
+ from quantumbpm import QuantumBPM, Vars, BpmnError
222
+ from quantumbpm.workers import Job
223
+
224
+ async def main():
225
+ async with QuantumBPM(...) as client:
226
+ worker = client.new_worker(client_id="billing-svc")
227
+ stop = asyncio.Event()
228
+
229
+ @worker.handler("send-email")
230
+ async def handle(job: Job) -> Vars:
231
+ recipient = job.vars.lookup("recipient")
232
+ subject = job.vars.lookup("subject")
233
+ await emailer.send(recipient, subject)
234
+ return Vars().set("messageID", "msg-123") # → Complete
235
+
236
+ await worker.run(stop) # blocks until stop is set
237
+
238
+ asyncio.run(main())
239
+ ```
240
+
241
+ `run(stop)` resolves when the `asyncio.Event` is set, after in-flight handlers settle.
242
+
243
+ ### Concurrency, polling, and locks
244
+
245
+ ```python
246
+ @worker.handler(
247
+ "send-email",
248
+ max_jobs=10, # up to 10 in flight per task type
249
+ poll_timeout="45s", # long-poll wait
250
+ lock_duration="2m", # exclusive lock per job
251
+ )
252
+ async def handle(job: Job) -> Vars:
253
+ ...
254
+ ```
255
+
256
+ Concurrency is per task type. Different task types run independently. The runtime auto-renews the lock at half the lock-duration interval while the handler runs.
257
+
258
+ ### Throwing typed BPMN errors
259
+
260
+ Raise a `BpmnError` to fail the job with a code that boundary error events on the originating service task can catch:
261
+
262
+ ```python
263
+ @worker.handler("charge-card")
264
+ async def handle(job: Job) -> Vars:
265
+ try:
266
+ tx_id = await charge(job.vars.to_dict())
267
+ return Vars().set("transactionID", tx_id)
268
+ except InsufficientFundsError:
269
+ raise BpmnError("INSUFFICIENT_FUNDS", Vars().set("availableBalance", 12.0))
270
+ ```
271
+
272
+ Any other exception is reported as `WORKER_ERROR`, which the server treats as a retryable failure that decrements the job's retry budget.
273
+
274
+ ### Typed handlers (Pydantic)
275
+
276
+ Type-annotate the `Job` parameter with a Pydantic model — the runtime validates the job's input variables before invoking the handler. The decoded value lands in `job.typed`.
277
+
278
+ ```python
279
+ from pydantic import BaseModel
280
+ from quantumbpm.workers import Job
281
+
282
+ class EmailJob(BaseModel):
283
+ recipient: str
284
+ subject: str
285
+
286
+ @worker.handler("send-email")
287
+ async def handle(job: Job[EmailJob]) -> Vars:
288
+ await emailer.send(job.typed.recipient, job.typed.subject)
289
+ return Vars().set("messageID", "msg-123")
290
+ ```
291
+
292
+ Decode failures become `WORKER_ERROR` automatically, with the validation message in the variables.
293
+
294
+ ## Variables
295
+
296
+ `Vars` is a thin wrapper around `dict[str, Any]` shared by DMN, BPMN, and workers.
297
+
298
+ ### Construction
299
+
300
+ ```python
301
+ v = Vars().set("amount", 100).set("name", "Alice")
302
+ v = Vars.from_dict({"amount": 100, "name": "Alice"})
303
+ ```
304
+
305
+ ### Typed access
306
+
307
+ ```python
308
+ amount = v.get("amount", float)
309
+ flag = v.get("approved", bool)
310
+
311
+ class Loan(BaseModel):
312
+ requestedAmt: float
313
+ approved: bool
314
+
315
+ loan = v.as_type(Loan)
316
+ ```
317
+
318
+ `Vars.get(name, type_)` and `Vars.as_type(type_)` accept Pydantic models, dataclasses, and primitives — Pydantic's `TypeAdapter` does the validation.
319
+
320
+ ## Escape hatch
321
+
322
+ The `client.raw` property exposes the underlying generated `ApiClient` for endpoints not yet wrapped (instance migration, modification, ad-hoc triggers, batch job complete/error, etc.):
323
+
324
+ ```python
325
+ from quantumbpm.api.bpmn_api import BpmnApi
326
+
327
+ api = BpmnApi(client.raw)
328
+ result = api.migrate_bpmn_instance(client.project_id, workflow_id, body)
329
+ ```
330
+
331
+ ## License
332
+
333
+ MIT License — see [LICENSE](LICENSE) for details.