quantumbpm-sdk 1.0.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.
- quantumbpm_sdk-1.0.0/LICENSE +21 -0
- quantumbpm_sdk-1.0.0/PKG-INFO +356 -0
- quantumbpm_sdk-1.0.0/README.md +333 -0
- quantumbpm_sdk-1.0.0/pyproject.toml +49 -0
- quantumbpm_sdk-1.0.0/quantumbpm/__init__.py +37 -0
- quantumbpm_sdk-1.0.0/quantumbpm/api/__init__.py +6 -0
- quantumbpm_sdk-1.0.0/quantumbpm/api/bpmn_api.py +10202 -0
- quantumbpm_sdk-1.0.0/quantumbpm/api/default_api.py +8909 -0
- quantumbpm_sdk-1.0.0/quantumbpm/api_client.py +804 -0
- quantumbpm_sdk-1.0.0/quantumbpm/api_response.py +21 -0
- quantumbpm_sdk-1.0.0/quantumbpm/auth.py +140 -0
- quantumbpm_sdk-1.0.0/quantumbpm/bpmn.py +378 -0
- quantumbpm_sdk-1.0.0/quantumbpm/client.py +86 -0
- quantumbpm_sdk-1.0.0/quantumbpm/configuration.py +596 -0
- quantumbpm_sdk-1.0.0/quantumbpm/dmn.py +116 -0
- quantumbpm_sdk-1.0.0/quantumbpm/exceptions.py +218 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/__init__.py +94 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/active_scope.py +98 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/batch_evaluate_design_request.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/batch_evaluation_response.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/batch_evaluation_response_results_inner.py +105 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_activity_state.py +114 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_external_job_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_incident.py +114 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_instance.py +109 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_instance_children_response.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_instance_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_instance_state.py +131 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_process_definition.py +100 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_process_summary.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_process_summary_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_process_version.py +106 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_process_version_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_resource.py +104 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_resource_detail.py +114 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_resource_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_resource_summary.py +100 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_resource_summary_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_user_task_paginated_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_validate_response.py +105 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/bpmn_validation_issue.py +99 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/complete_bpmn_external_job_request.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/complete_bpmn_external_jobs_batch_request.py +97 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/complete_bpmn_external_jobs_batch_request_items_inner.py +92 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/correlation_keys.py +177 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/create_bpmn_resource_request.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/create_definition_request.py +92 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/create_project_request.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/decision_input_field.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/decision_summary.py +106 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/definition.py +104 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/element_mapping.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/error.py +99 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/evaluate_design_request.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/evaluate_stored_request.py +94 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/evaluation_result.py +137 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/execution.py +107 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job.py +125 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job_active_workers_item.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job_active_workers_response.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job_batch_response.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job_batch_response_results_inner.py +99 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job_queue_depth_item.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/external_job_queue_depth_response.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/feel_value.py +200 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/get_bpmn_instance_variables200_response.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/get_health200_response.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/get_version200_response.py +92 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/heartbeat_bpmn_external_job_request.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/hit_rule.py +95 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/migrate_bpmn_instance_request.py +100 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/migration_validation_result.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/modification_instruction.py +101 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/modify_bpmn_instance_request.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/paginated_decisions_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/paginated_definitions_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/paginated_executions_response.py +102 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/pagination_metadata.py +94 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/poll_bpmn_job_request.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/project.py +96 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/publish_bpmn_message_request.py +98 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/publish_bpmn_signal_request.py +92 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/start_bpmn_instance201_response.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/start_bpmn_instance_request.py +91 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/start_bpmn_test_instance201_response.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/start_bpmn_test_instance_request.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/throw_bpmn_external_job_error_request.py +90 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/throw_bpmn_external_job_errors_batch_request.py +97 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/throw_bpmn_external_job_errors_batch_request_items_inner.py +94 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/trigger_bpmn_ad_hoc_node_request.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/update_bpmn_instance_variables_request.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/update_definition_request.py +92 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/update_user_task_assignment_request.py +97 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/user_task.py +133 -0
- quantumbpm_sdk-1.0.0/quantumbpm/models/validate_bpmn_resource_request.py +88 -0
- quantumbpm_sdk-1.0.0/quantumbpm/py.typed +0 -0
- quantumbpm_sdk-1.0.0/quantumbpm/rest.py +263 -0
- quantumbpm_sdk-1.0.0/quantumbpm/variables.py +125 -0
- quantumbpm_sdk-1.0.0/quantumbpm/workers.py +377 -0
- quantumbpm_sdk-1.0.0/quantumbpm_sdk.egg-info/PKG-INFO +356 -0
- quantumbpm_sdk-1.0.0/quantumbpm_sdk.egg-info/SOURCES.txt +106 -0
- quantumbpm_sdk-1.0.0/quantumbpm_sdk.egg-info/dependency_links.txt +1 -0
- quantumbpm_sdk-1.0.0/quantumbpm_sdk.egg-info/requires.txt +6 -0
- quantumbpm_sdk-1.0.0/quantumbpm_sdk.egg-info/top_level.txt +1 -0
- quantumbpm_sdk-1.0.0/setup.cfg +7 -0
- quantumbpm_sdk-1.0.0/setup.py +37 -0
- quantumbpm_sdk-1.0.0/tests/test_smoke.py +39 -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: 1.0.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.
|