dex-python-sdk 0.0.2__tar.gz → 0.1.2__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 (155) hide show
  1. dex_python_sdk-0.1.2/PKG-INFO +295 -0
  2. dex_python_sdk-0.1.2/README.md +278 -0
  3. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/__init__.py +30 -3
  4. dex_python_sdk-0.1.2/dex/_async_value_hydrator.py +223 -0
  5. dex_python_sdk-0.1.2/dex/_async_worker_dispatcher.py +155 -0
  6. dex_python_sdk-0.1.2/dex/_async_worker_service.py +65 -0
  7. dex_python_sdk-0.1.2/dex/_grpc_errors.py +145 -0
  8. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_invocation_context.py +3 -1
  9. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_value_mapper.py +56 -0
  10. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_worker_dispatcher.py +68 -39
  11. dex_python_sdk-0.1.2/dex/async_client.py +1084 -0
  12. dex_python_sdk-0.1.2/dex/async_worker.py +189 -0
  13. dex_python_sdk-0.1.2/dex/attribute.py +243 -0
  14. dex_python_sdk-0.1.2/dex/blob_cache.py +145 -0
  15. dex_python_sdk-0.1.2/dex/channel.py +342 -0
  16. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/client.py +392 -24
  17. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/client_options.py +9 -0
  18. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/codec.py +143 -4
  19. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/condition.py +17 -0
  20. dex_python_sdk-0.1.2/dex/context.py +169 -0
  21. dex_python_sdk-0.1.2/dex/dexpb/dex_pb2.py +395 -0
  22. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/dexpb/dex_pb2.pyi +82 -31
  23. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/dexpb/dex_pb2_grpc.py +43 -0
  24. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/flow.py +250 -25
  25. dex_python_sdk-0.1.2/dex/flow_config.py +59 -0
  26. dex_python_sdk-0.1.2/dex/flow_info.py +107 -0
  27. dex_python_sdk-0.1.2/dex/flow_options.py +214 -0
  28. dex_python_sdk-0.1.2/dex/runtime_errors.py +285 -0
  29. dex_python_sdk-0.1.2/dex/step.py +427 -0
  30. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/step_execution.py +36 -0
  31. dex_python_sdk-0.1.2/dex/timer.py +44 -0
  32. dex_python_sdk-0.1.2/dex/wait.py +108 -0
  33. dex_python_sdk-0.1.2/dex/worker.py +214 -0
  34. dex_python_sdk-0.1.2/dex/worker_options.py +52 -0
  35. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/pyproject.toml +12 -3
  36. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/Cargo.lock +299 -0
  37. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/Cargo.toml +8 -0
  38. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/config.rs +31 -0
  39. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/error.rs +11 -0
  40. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/lib.rs +76 -0
  41. dex_python_sdk-0.0.2/PKG-INFO +0 -202
  42. dex_python_sdk-0.0.2/README.md +0 -184
  43. dex_python_sdk-0.0.2/dex/_grpc_errors.py +0 -73
  44. dex_python_sdk-0.0.2/dex/attribute.py +0 -87
  45. dex_python_sdk-0.0.2/dex/blob_cache.py +0 -73
  46. dex_python_sdk-0.0.2/dex/channel.py +0 -165
  47. dex_python_sdk-0.0.2/dex/command_request.py +0 -120
  48. dex_python_sdk-0.0.2/dex/command_results.py +0 -107
  49. dex_python_sdk-0.0.2/dex/communication.py +0 -136
  50. dex_python_sdk-0.0.2/dex/communication_schema.py +0 -54
  51. dex_python_sdk-0.0.2/dex/context.py +0 -86
  52. dex_python_sdk-0.0.2/dex/data_attributes.py +0 -70
  53. dex_python_sdk-0.0.2/dex/dexpb/dex_pb2.py +0 -381
  54. dex_python_sdk-0.0.2/dex/errors.py +0 -109
  55. dex_python_sdk-0.0.2/dex/flow_config.py +0 -29
  56. dex_python_sdk-0.0.2/dex/flow_info.py +0 -51
  57. dex_python_sdk-0.0.2/dex/flow_options.py +0 -122
  58. dex_python_sdk-0.0.2/dex/object_encoder.py +0 -799
  59. dex_python_sdk-0.0.2/dex/persistence.py +0 -89
  60. dex_python_sdk-0.0.2/dex/persistence_options.py +0 -12
  61. dex_python_sdk-0.0.2/dex/persistence_schema.py +0 -51
  62. dex_python_sdk-0.0.2/dex/registry.py +0 -204
  63. dex_python_sdk-0.0.2/dex/reset_workflow_type_and_options.py +0 -67
  64. dex_python_sdk-0.0.2/dex/rpc.py +0 -93
  65. dex_python_sdk-0.0.2/dex/runtime_errors.py +0 -81
  66. dex_python_sdk-0.0.2/dex/search_attributes.py +0 -184
  67. dex_python_sdk-0.0.2/dex/state_decision.py +0 -153
  68. dex_python_sdk-0.0.2/dex/state_execution_locals.py +0 -66
  69. dex_python_sdk-0.0.2/dex/state_movement.py +0 -115
  70. dex_python_sdk-0.0.2/dex/state_schema.py +0 -48
  71. dex_python_sdk-0.0.2/dex/step.py +0 -194
  72. dex_python_sdk-0.0.2/dex/stop_workflow_options.py +0 -18
  73. dex_python_sdk-0.0.2/dex/tests/__init__.py +0 -80
  74. dex_python_sdk-0.0.2/dex/tests/dex-service-env/docker-compose-init.sh +0 -44
  75. dex_python_sdk-0.0.2/dex/tests/dex-service-env/docker-compose.yml +0 -97
  76. dex_python_sdk-0.0.2/dex/tests/dex-service-env/dynamicconfig/README.md +0 -39
  77. dex_python_sdk-0.0.2/dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +0 -9
  78. dex_python_sdk-0.0.2/dex/tests/dex-service-env/dynamicconfig/docker.yaml +0 -2
  79. dex_python_sdk-0.0.2/dex/tests/test_abnormal_exit_workflow.py +0 -43
  80. dex_python_sdk-0.0.2/dex/tests/test_basic_workflow.py +0 -70
  81. dex_python_sdk-0.0.2/dex/tests/test_conditional_complete.py +0 -50
  82. dex_python_sdk-0.0.2/dex/tests/test_describe_workflow.py +0 -40
  83. dex_python_sdk-0.0.2/dex/tests/test_empty_data_decodes_properly.py +0 -74
  84. dex_python_sdk-0.0.2/dex/tests/test_internal_channel.py +0 -28
  85. dex_python_sdk-0.0.2/dex/tests/test_internal_channel_with_no_prefix_channel.py +0 -41
  86. dex_python_sdk-0.0.2/dex/tests/test_persistence_data_attributes.py +0 -62
  87. dex_python_sdk-0.0.2/dex/tests/test_persistence_search_attributes.py +0 -127
  88. dex_python_sdk-0.0.2/dex/tests/test_persistence_state_execution_locals.py +0 -38
  89. dex_python_sdk-0.0.2/dex/tests/test_rpc.py +0 -64
  90. dex_python_sdk-0.0.2/dex/tests/test_rpc_with_memo.py +0 -195
  91. dex_python_sdk-0.0.2/dex/tests/test_rpc_with_memo_duplicate_java_tests.py +0 -117
  92. dex_python_sdk-0.0.2/dex/tests/test_signal.py +0 -51
  93. dex_python_sdk-0.0.2/dex/tests/test_skip_wait_until.py +0 -76
  94. dex_python_sdk-0.0.2/dex/tests/test_state_failure_recovery.py +0 -28
  95. dex_python_sdk-0.0.2/dex/tests/test_timer.py +0 -35
  96. dex_python_sdk-0.0.2/dex/tests/test_wait_for_state_execution_completion.py +0 -53
  97. dex_python_sdk-0.0.2/dex/tests/test_workflow_errors.py +0 -87
  98. dex_python_sdk-0.0.2/dex/tests/test_workflow_state_options.py +0 -118
  99. dex_python_sdk-0.0.2/dex/tests/test_workflow_state_options_override.py +0 -44
  100. dex_python_sdk-0.0.2/dex/tests/worker_server.py +0 -64
  101. dex_python_sdk-0.0.2/dex/tests/workflows/abnormal_exit_workflow.py +0 -42
  102. dex_python_sdk-0.0.2/dex/tests/workflows/basic_workflow.py +0 -62
  103. dex_python_sdk-0.0.2/dex/tests/workflows/conditional_complete_workflow.py +0 -95
  104. dex_python_sdk-0.0.2/dex/tests/workflows/describe_workflow.py +0 -46
  105. dex_python_sdk-0.0.2/dex/tests/workflows/empty_data_workflow.py +0 -45
  106. dex_python_sdk-0.0.2/dex/tests/workflows/internal_channel_workflow.py +0 -129
  107. dex_python_sdk-0.0.2/dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +0 -100
  108. dex_python_sdk-0.0.2/dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +0 -276
  109. dex_python_sdk-0.0.2/dex/tests/workflows/persistence_data_attributes_workflow.py +0 -98
  110. dex_python_sdk-0.0.2/dex/tests/workflows/persistence_search_attributes_workflow.py +0 -159
  111. dex_python_sdk-0.0.2/dex/tests/workflows/persistence_state_execution_local_workflow.py +0 -63
  112. dex_python_sdk-0.0.2/dex/tests/workflows/recovery_workflow.py +0 -82
  113. dex_python_sdk-0.0.2/dex/tests/workflows/rpc_memo_workflow.py +0 -231
  114. dex_python_sdk-0.0.2/dex/tests/workflows/rpc_workflow.py +0 -117
  115. dex_python_sdk-0.0.2/dex/tests/workflows/state_options_override_workflow.py +0 -93
  116. dex_python_sdk-0.0.2/dex/tests/workflows/state_options_workflow.py +0 -84
  117. dex_python_sdk-0.0.2/dex/tests/workflows/timer_workflow.py +0 -46
  118. dex_python_sdk-0.0.2/dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +0 -70
  119. dex_python_sdk-0.0.2/dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +0 -71
  120. dex_python_sdk-0.0.2/dex/tests/workflows/wait_internal_channel_workflow.py +0 -47
  121. dex_python_sdk-0.0.2/dex/tests/workflows/wait_signal_workflow.py +0 -147
  122. dex_python_sdk-0.0.2/dex/timer.py +0 -21
  123. dex_python_sdk-0.0.2/dex/type_store.py +0 -99
  124. dex_python_sdk-0.0.2/dex/unregistered_client.py +0 -585
  125. dex_python_sdk-0.0.2/dex/utils/__init__.py +0 -3
  126. dex_python_sdk-0.0.2/dex/utils/dex_typing.py +0 -25
  127. dex_python_sdk-0.0.2/dex/utils/persistence_utils.py +0 -32
  128. dex_python_sdk-0.0.2/dex/wait.py +0 -49
  129. dex_python_sdk-0.0.2/dex/worker.py +0 -121
  130. dex_python_sdk-0.0.2/dex/worker_options.py +0 -22
  131. dex_python_sdk-0.0.2/dex/worker_service.py +0 -432
  132. dex_python_sdk-0.0.2/dex/workflow.py +0 -79
  133. dex_python_sdk-0.0.2/dex/workflow_context.py +0 -44
  134. dex_python_sdk-0.0.2/dex/workflow_info.py +0 -16
  135. dex_python_sdk-0.0.2/dex/workflow_options.py +0 -74
  136. dex_python_sdk-0.0.2/dex/workflow_state.py +0 -123
  137. dex_python_sdk-0.0.2/dex/workflow_state_options.py +0 -154
  138. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/LEGACY_NOTICES.md +0 -0
  139. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/LICENSE +0 -0
  140. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_native.pyi +0 -0
  141. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_utils.py +0 -0
  142. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_value_hydrator.py +0 -0
  143. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/_worker_service.py +0 -0
  144. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/dexpb/__init__.py +0 -0
  145. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/dex/py.typed +0 -0
  146. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
  147. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
  148. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
  149. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
  150. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
  151. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
  152. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
  153. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
  154. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
  155. {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
@@ -0,0 +1,295 @@
1
+ Metadata-Version: 2.4
2
+ Name: dex-python-sdk
3
+ Version: 0.1.2
4
+ Requires-Dist: grpcio>=1.83.0
5
+ Requires-Dist: grpcio-status>=1.83.0
6
+ Requires-Dist: protobuf>=7.35.1
7
+ License-File: LICENSE
8
+ License-File: LEGACY_NOTICES.md
9
+ Summary: Python SDK for the Dex workflow engine
10
+ Author: Super Durable
11
+ License-Expression: LicenseRef-Super-Durable-1.0
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
14
+ Project-URL: Homepage, https://github.com/superdurable/dex/tree/main/sdk-python
15
+ Project-URL: Repository, https://github.com/superdurable/dex
16
+
17
+
18
+ # Dex SDK for Python
19
+
20
+ Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
21
+
22
+ ## New user contracts
23
+
24
+ The rewrite targets Python 3.11+ and exposes strongly typed workflow contracts
25
+ from `dex`. This phase includes definitions, attributes, channels, waits,
26
+ decisions, codecs, registry validation, synchronous client calls, and synchronous
27
+ worker handlers. Python owns its gRPC Client and Worker transport;
28
+ the shared Rust Core is used only for BlobCache.
29
+
30
+ ```python
31
+ from datetime import timedelta
32
+
33
+ import dex
34
+
35
+ counter = dex.Attribute("counter", int)
36
+ counters_by_region = dex.AttributeMap("counters-by-region", int)
37
+
38
+ class Run(dex.Step[str]):
39
+ def wait_for(
40
+ self, context: dex.Context, input: str
41
+ ) -> dex.Wait:
42
+ return dex.Wait.until(
43
+ dex.Timer.by_duration(timedelta(seconds=1))
44
+ )
45
+
46
+ def execute(
47
+ self, context: dex.Context, input: str
48
+ ) -> dex.StepDecision:
49
+ return dex.graceful_complete(input)
50
+
51
+ class CounterFlow(dex.Flow[str]):
52
+ run = Run()
53
+
54
+ def get_flow_type(self) -> str:
55
+ return "Counter"
56
+
57
+ def get_steps(self) -> dex.StepList[str]:
58
+ return dex.StepList.start_step(self.run)
59
+
60
+ def get_persistence_schema(self) -> dex.PersistenceSchema:
61
+ return dex.PersistenceSchema.of(counter, counters_by_region)
62
+
63
+ @dex.rpc(name="Increment")
64
+ def increment(
65
+ self, context: dex.Context, input: int
66
+ ) -> dex.RPCResult[int]:
67
+ return dex.RPCResult(input + 1)
68
+
69
+ flow = CounterFlow()
70
+ registry = dex.Registry((flow,))
71
+ ```
72
+
73
+ Registry derives codecs from declared Python types and handler annotations.
74
+ Built-in scalar types and dataclasses need no codec arguments. Register an
75
+ explicit codec only for a custom encoding or a type Registry cannot derive.
76
+ `PersistenceSchema.of(...)` accepts attributes and channels together and
77
+ partitions them by definition type.
78
+
79
+ `Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
80
+ Dex Server before opening their listener. Existing indexes return immediately;
81
+ failure or the default two-minute deadline aborts startup. An indexed
82
+ `AttributeMap` must provide one fixed `index_key`.
83
+
84
+ Initial attributes retain their value types without a public wrapper class:
85
+
86
+ ```python
87
+ options = (
88
+ dex.StartFlowOptions()
89
+ .with_attribute(counter, 1)
90
+ .with_attribute(counters_by_region, "us-west", 1)
91
+ )
92
+ ```
93
+
94
+ Opt in when declaring an Attribute or AttributeMap, and select the Store in
95
+ Flow configuration:
96
+
97
+ ```python
98
+ email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
99
+ config = dex.FlowConfig(attribute_store_name="profiles")
100
+ ```
101
+
102
+ The Store is an asynchronous latest-state projection. Deletion writes SQL
103
+ `NULL`, and projection failures do not roll back Flow Attributes. `None`
104
+ preserves the current target; an explicit empty string disables future
105
+ synchronization while retaining protocol presence.
106
+
107
+ ```
108
+ pip install dex-python-sdk==0.1.0
109
+ ```
110
+
111
+ See [samples](../examples/python) for use case examples.
112
+
113
+ ## Requirements
114
+
115
+ - Python 3.11+
116
+ - [Dex server](https://github.com/superdurable/dex#how-to-use)
117
+
118
+ ## Concepts
119
+
120
+ Applications implement two generic interfaces from [`dex`](dex/):
121
+
122
+ - `Flow[START_INPUT]` returns `StepList.start_step(...)`, followed by optional
123
+ `.other_steps(...)`, from one `get_steps()` method. The `StepList` generic
124
+ binds the Flow input to the starting Step input. Use `StepList.empty()` when
125
+ a Flow has no Steps.
126
+ - `Step[INPUT]` implements `execute` and optionally `wait_for`. The default
127
+ Worker path requires synchronous handlers. With `AsyncWorker` and
128
+ `Registry(..., allow_async_handlers=True)`, handlers may be `async def` and
129
+ `await` an `AsyncClient`.
130
+
131
+ `StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
132
+ two handler calls. Timer and channel conditions determine how long a Step waits.
133
+
134
+ `Registry` validates every Flow, Step, RPC signature, durable name, lock, and
135
+ codec before Client or Worker startup. `Client` methods use these typed objects
136
+ instead of raw Flow, Step, or RPC strings.
137
+
138
+ ### Errors
139
+
140
+ Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
141
+ (`get_attribute`, `describe_flow`, `wait_for_flow`, and `reset_flow`) raise
142
+ `FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
143
+ waits, config updates, and continue-as-new triggers raise
144
+ `FlowNotActiveError` when no running Flow can accept the operation.
145
+
146
+ ```python
147
+ try:
148
+ client.publish(flow_id, orders.approved, order_id)
149
+ except dex.FlowNotActiveError:
150
+ # The Flow is missing or already closed.
151
+ pass
152
+ ```
153
+
154
+ Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts
155
+ raise `FlowAlreadyStartedError`, `WorkerInvocationError`,
156
+ `RpcLockConflictError`, and `LongPollTimeoutError`. All service errors retain
157
+ `code`, `sub_status`, `detail`, `operation`, `flow_id`, and the original gRPC
158
+ exception through Python exception chaining. Worker failures also expose
159
+ `worker_code`, `worker_error_type`, and `worker_error_detail`. Registration,
160
+ serialization, and invalid handler returns use `FlowDefinitionError`,
161
+ `ValueMappingError`, and `InvalidStepResultError`.
162
+
163
+ ### Sync vs asyncio
164
+
165
+ - **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
166
+ Worker. Blocking `Client` calls inside `Step.execute` are safe (one pool
167
+ thread is occupied; other RPCs still run).
168
+ - **Asyncio:** `AsyncClient` and `AsyncWorker` use `grpc.aio`. Use
169
+ `Registry(..., allow_async_handlers=True)` when Steps/RPCs are coroutines.
170
+ Inside async `execute`, inject `AsyncClient` — do not call sync `Client` on
171
+ the Worker event loop. Sync `Worker` still rejects coroutine handlers at
172
+ registry construction unless `allow_async_handlers=True` (and even then the
173
+ sync Worker dispatcher rejects awaitable return values).
174
+
175
+ Integration scenarios live under
176
+ [`tests/integ`](tests/integ/README.md). They exercise the same workflows,
177
+ client operations, and assertions as the Java suite against an isolated
178
+ `dexcli dev` environment.
179
+
180
+ ## Implementation status
181
+
182
+ The strongly typed contracts, registry, synchronous Client/Worker, optional
183
+ `AsyncClient`/`AsyncWorker` (`grpc.aio`), and Rust-backed BlobCache are
184
+ implemented. Python owns its gRPC transport; the native bridge is limited to
185
+ the shared BlobCache. Design notes:
186
+ [`docs/design/plan/python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md).
187
+
188
+ ## Running dex-server locally
189
+
190
+ ### Option 1: use docker compose
191
+ See [dex README](https://github.com/superdurable/dex#using-docker-image--docker-compose)
192
+
193
+ ### Option 2: VSCode Dev Container
194
+
195
+ Dev Container is an easy way to get dex-server running locally. Follow these steps to launch a dev container:
196
+ - Install Docker, VSCode, and [VSCode Dev Container plugin](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
197
+ - Open the project in VSCode.
198
+ ```bash
199
+ cd dex-python-sdk
200
+ code .
201
+ ```
202
+ - Launch the Remote-Containers: Reopen in Container command from Command Palette (Ctrl + Shift + P). You can also click in the bottom left corner to access the remote container menu.
203
+ - Once the dev container starts, dex-server will be listening on port 8801.
204
+
205
+ ## How To Contribute
206
+
207
+ This project uses [uv](https://docs.astral.sh/uv/) for Python versions,
208
+ dependencies, virtual environments, locking, building, and publishing.
209
+
210
+ To install requirements:
211
+
212
+ ```bash
213
+ uv sync --locked
214
+ ```
215
+
216
+ Run the complete Python SDK integration suite with an isolated Dex development
217
+ environment:
218
+
219
+ ```bash
220
+ ./run-integration-tests.sh
221
+ ```
222
+
223
+ ### Measure integration coverage
224
+
225
+ Run the same integration suite with Python source coverage:
226
+
227
+ ```bash
228
+ ./run-integration-tests.sh --coverage
229
+ ```
230
+
231
+ Only the integration scenarios contribute execution data, and only production
232
+ Python modules under `dex` are measured. Generated protobuf modules under
233
+ `dex/dexpb` are excluded. The
234
+ terminal report lists uncovered line ranges. The browser report starts at
235
+ `coverage/html/index.html`; `coverage/coverage.xml` and `coverage/lcov.info`
236
+ are also generated.
237
+
238
+ CI uploads LCOV to Codecov with GitHub OIDC under the
239
+ `sdk-python-integration` flag and retains the full report as the
240
+ `sdk-python-integration-coverage` Actions artifact.
241
+
242
+ #### Update IDL
243
+
244
+ Edit [`protos/dex.proto`](../protos/dex.proto). Rename catalog: [`docs/design/idl-renames.md`](../docs/design/idl-renames.md).
245
+
246
+ #### Generate stubs from IDL
247
+
248
+ ```bash
249
+ make -C ../protos proto-python
250
+ ```
251
+
252
+ Checked-in Python stubs land in `dex/dexpb/`.
253
+ #### Linting
254
+
255
+ Validate that every `dex.__all__` class, function, constant, public method,
256
+ argument, return value, dataclass field, enum value, and public instance
257
+ attribute has a Google-style docstring:
258
+
259
+ ```bash
260
+ uv run --frozen python scripts/check_public_docs.py
261
+ ```
262
+
263
+ The checker resolves definitions from the public package export table, so
264
+ private helpers and generated protobuf modules are excluded. Use `help(dex.Client)`
265
+ or IDE hover information to read the same documentation. To run all other
266
+ linting for this project:
267
+
268
+ ```bash
269
+ uv run --frozen pre-commit run --show-diff-on-failure --color=always --all-files
270
+ ```
271
+
272
+ ## Code of Conduct
273
+ This project is governed by the [Contributor Covenant v 1.4.1](CODE_OF_CONDUCT.md). (Review the Code of Conduct and remove this sentence before publishing your project.)
274
+
275
+ ## Publishing to PyPI
276
+
277
+ 1. Optionally run **Publish Python SDK to PyPI** via workflow_dispatch with a version and
278
+ `publish=false` to validate all distributions without uploading.
279
+ 2. Create a GitHub Release with tag `sdk-python/vX.Y.Z` (for example `sdk-python/v0.1.0`).
280
+ CI stamps that version into `pyproject.toml` for the build (same idea as the TypeScript
281
+ SDK release), then builds and smoke-tests Linux x86_64/ARM64, macOS x86_64/ARM64, and
282
+ Windows x86_64 wheels, verifies the source distribution, and publishes with `PYPI_TOKEN`.
283
+ 3. After publishing, bump the committed `pyproject.toml` / docs install line when you want
284
+ the repo tip to reflect the released version.
285
+
286
+ A manual run publishes only from `main`, and only when `publish` is explicitly selected.
287
+ The dispatch `version` input is stamped the same way as a release tag.
288
+
289
+ See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo tag conventions.
290
+
291
+ ## License
292
+
293
+ [Super Durable Source License 1.0](LICENSE), with legacy portions under their
294
+ original terms as described in [LEGACY_NOTICES.md](LEGACY_NOTICES.md).
295
+
@@ -0,0 +1,278 @@
1
+
2
+ # Dex SDK for Python
3
+
4
+ Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
5
+
6
+ ## New user contracts
7
+
8
+ The rewrite targets Python 3.11+ and exposes strongly typed workflow contracts
9
+ from `dex`. This phase includes definitions, attributes, channels, waits,
10
+ decisions, codecs, registry validation, synchronous client calls, and synchronous
11
+ worker handlers. Python owns its gRPC Client and Worker transport;
12
+ the shared Rust Core is used only for BlobCache.
13
+
14
+ ```python
15
+ from datetime import timedelta
16
+
17
+ import dex
18
+
19
+ counter = dex.Attribute("counter", int)
20
+ counters_by_region = dex.AttributeMap("counters-by-region", int)
21
+
22
+ class Run(dex.Step[str]):
23
+ def wait_for(
24
+ self, context: dex.Context, input: str
25
+ ) -> dex.Wait:
26
+ return dex.Wait.until(
27
+ dex.Timer.by_duration(timedelta(seconds=1))
28
+ )
29
+
30
+ def execute(
31
+ self, context: dex.Context, input: str
32
+ ) -> dex.StepDecision:
33
+ return dex.graceful_complete(input)
34
+
35
+ class CounterFlow(dex.Flow[str]):
36
+ run = Run()
37
+
38
+ def get_flow_type(self) -> str:
39
+ return "Counter"
40
+
41
+ def get_steps(self) -> dex.StepList[str]:
42
+ return dex.StepList.start_step(self.run)
43
+
44
+ def get_persistence_schema(self) -> dex.PersistenceSchema:
45
+ return dex.PersistenceSchema.of(counter, counters_by_region)
46
+
47
+ @dex.rpc(name="Increment")
48
+ def increment(
49
+ self, context: dex.Context, input: int
50
+ ) -> dex.RPCResult[int]:
51
+ return dex.RPCResult(input + 1)
52
+
53
+ flow = CounterFlow()
54
+ registry = dex.Registry((flow,))
55
+ ```
56
+
57
+ Registry derives codecs from declared Python types and handler annotations.
58
+ Built-in scalar types and dataclasses need no codec arguments. Register an
59
+ explicit codec only for a custom encoding or a type Registry cannot derive.
60
+ `PersistenceSchema.of(...)` accepts attributes and channels together and
61
+ partitions them by definition type.
62
+
63
+ `Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
64
+ Dex Server before opening their listener. Existing indexes return immediately;
65
+ failure or the default two-minute deadline aborts startup. An indexed
66
+ `AttributeMap` must provide one fixed `index_key`.
67
+
68
+ Initial attributes retain their value types without a public wrapper class:
69
+
70
+ ```python
71
+ options = (
72
+ dex.StartFlowOptions()
73
+ .with_attribute(counter, 1)
74
+ .with_attribute(counters_by_region, "us-west", 1)
75
+ )
76
+ ```
77
+
78
+ Opt in when declaring an Attribute or AttributeMap, and select the Store in
79
+ Flow configuration:
80
+
81
+ ```python
82
+ email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
83
+ config = dex.FlowConfig(attribute_store_name="profiles")
84
+ ```
85
+
86
+ The Store is an asynchronous latest-state projection. Deletion writes SQL
87
+ `NULL`, and projection failures do not roll back Flow Attributes. `None`
88
+ preserves the current target; an explicit empty string disables future
89
+ synchronization while retaining protocol presence.
90
+
91
+ ```
92
+ pip install dex-python-sdk==0.1.0
93
+ ```
94
+
95
+ See [samples](../examples/python) for use case examples.
96
+
97
+ ## Requirements
98
+
99
+ - Python 3.11+
100
+ - [Dex server](https://github.com/superdurable/dex#how-to-use)
101
+
102
+ ## Concepts
103
+
104
+ Applications implement two generic interfaces from [`dex`](dex/):
105
+
106
+ - `Flow[START_INPUT]` returns `StepList.start_step(...)`, followed by optional
107
+ `.other_steps(...)`, from one `get_steps()` method. The `StepList` generic
108
+ binds the Flow input to the starting Step input. Use `StepList.empty()` when
109
+ a Flow has no Steps.
110
+ - `Step[INPUT]` implements `execute` and optionally `wait_for`. The default
111
+ Worker path requires synchronous handlers. With `AsyncWorker` and
112
+ `Registry(..., allow_async_handlers=True)`, handlers may be `async def` and
113
+ `await` an `AsyncClient`.
114
+
115
+ `StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
116
+ two handler calls. Timer and channel conditions determine how long a Step waits.
117
+
118
+ `Registry` validates every Flow, Step, RPC signature, durable name, lock, and
119
+ codec before Client or Worker startup. `Client` methods use these typed objects
120
+ instead of raw Flow, Step, or RPC strings.
121
+
122
+ ### Errors
123
+
124
+ Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
125
+ (`get_attribute`, `describe_flow`, `wait_for_flow`, and `reset_flow`) raise
126
+ `FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
127
+ waits, config updates, and continue-as-new triggers raise
128
+ `FlowNotActiveError` when no running Flow can accept the operation.
129
+
130
+ ```python
131
+ try:
132
+ client.publish(flow_id, orders.approved, order_id)
133
+ except dex.FlowNotActiveError:
134
+ # The Flow is missing or already closed.
135
+ pass
136
+ ```
137
+
138
+ Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts
139
+ raise `FlowAlreadyStartedError`, `WorkerInvocationError`,
140
+ `RpcLockConflictError`, and `LongPollTimeoutError`. All service errors retain
141
+ `code`, `sub_status`, `detail`, `operation`, `flow_id`, and the original gRPC
142
+ exception through Python exception chaining. Worker failures also expose
143
+ `worker_code`, `worker_error_type`, and `worker_error_detail`. Registration,
144
+ serialization, and invalid handler returns use `FlowDefinitionError`,
145
+ `ValueMappingError`, and `InvalidStepResultError`.
146
+
147
+ ### Sync vs asyncio
148
+
149
+ - **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
150
+ Worker. Blocking `Client` calls inside `Step.execute` are safe (one pool
151
+ thread is occupied; other RPCs still run).
152
+ - **Asyncio:** `AsyncClient` and `AsyncWorker` use `grpc.aio`. Use
153
+ `Registry(..., allow_async_handlers=True)` when Steps/RPCs are coroutines.
154
+ Inside async `execute`, inject `AsyncClient` — do not call sync `Client` on
155
+ the Worker event loop. Sync `Worker` still rejects coroutine handlers at
156
+ registry construction unless `allow_async_handlers=True` (and even then the
157
+ sync Worker dispatcher rejects awaitable return values).
158
+
159
+ Integration scenarios live under
160
+ [`tests/integ`](tests/integ/README.md). They exercise the same workflows,
161
+ client operations, and assertions as the Java suite against an isolated
162
+ `dexcli dev` environment.
163
+
164
+ ## Implementation status
165
+
166
+ The strongly typed contracts, registry, synchronous Client/Worker, optional
167
+ `AsyncClient`/`AsyncWorker` (`grpc.aio`), and Rust-backed BlobCache are
168
+ implemented. Python owns its gRPC transport; the native bridge is limited to
169
+ the shared BlobCache. Design notes:
170
+ [`docs/design/plan/python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md).
171
+
172
+ ## Running dex-server locally
173
+
174
+ ### Option 1: use docker compose
175
+ See [dex README](https://github.com/superdurable/dex#using-docker-image--docker-compose)
176
+
177
+ ### Option 2: VSCode Dev Container
178
+
179
+ Dev Container is an easy way to get dex-server running locally. Follow these steps to launch a dev container:
180
+ - Install Docker, VSCode, and [VSCode Dev Container plugin](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
181
+ - Open the project in VSCode.
182
+ ```bash
183
+ cd dex-python-sdk
184
+ code .
185
+ ```
186
+ - Launch the Remote-Containers: Reopen in Container command from Command Palette (Ctrl + Shift + P). You can also click in the bottom left corner to access the remote container menu.
187
+ - Once the dev container starts, dex-server will be listening on port 8801.
188
+
189
+ ## How To Contribute
190
+
191
+ This project uses [uv](https://docs.astral.sh/uv/) for Python versions,
192
+ dependencies, virtual environments, locking, building, and publishing.
193
+
194
+ To install requirements:
195
+
196
+ ```bash
197
+ uv sync --locked
198
+ ```
199
+
200
+ Run the complete Python SDK integration suite with an isolated Dex development
201
+ environment:
202
+
203
+ ```bash
204
+ ./run-integration-tests.sh
205
+ ```
206
+
207
+ ### Measure integration coverage
208
+
209
+ Run the same integration suite with Python source coverage:
210
+
211
+ ```bash
212
+ ./run-integration-tests.sh --coverage
213
+ ```
214
+
215
+ Only the integration scenarios contribute execution data, and only production
216
+ Python modules under `dex` are measured. Generated protobuf modules under
217
+ `dex/dexpb` are excluded. The
218
+ terminal report lists uncovered line ranges. The browser report starts at
219
+ `coverage/html/index.html`; `coverage/coverage.xml` and `coverage/lcov.info`
220
+ are also generated.
221
+
222
+ CI uploads LCOV to Codecov with GitHub OIDC under the
223
+ `sdk-python-integration` flag and retains the full report as the
224
+ `sdk-python-integration-coverage` Actions artifact.
225
+
226
+ #### Update IDL
227
+
228
+ Edit [`protos/dex.proto`](../protos/dex.proto). Rename catalog: [`docs/design/idl-renames.md`](../docs/design/idl-renames.md).
229
+
230
+ #### Generate stubs from IDL
231
+
232
+ ```bash
233
+ make -C ../protos proto-python
234
+ ```
235
+
236
+ Checked-in Python stubs land in `dex/dexpb/`.
237
+ #### Linting
238
+
239
+ Validate that every `dex.__all__` class, function, constant, public method,
240
+ argument, return value, dataclass field, enum value, and public instance
241
+ attribute has a Google-style docstring:
242
+
243
+ ```bash
244
+ uv run --frozen python scripts/check_public_docs.py
245
+ ```
246
+
247
+ The checker resolves definitions from the public package export table, so
248
+ private helpers and generated protobuf modules are excluded. Use `help(dex.Client)`
249
+ or IDE hover information to read the same documentation. To run all other
250
+ linting for this project:
251
+
252
+ ```bash
253
+ uv run --frozen pre-commit run --show-diff-on-failure --color=always --all-files
254
+ ```
255
+
256
+ ## Code of Conduct
257
+ This project is governed by the [Contributor Covenant v 1.4.1](CODE_OF_CONDUCT.md). (Review the Code of Conduct and remove this sentence before publishing your project.)
258
+
259
+ ## Publishing to PyPI
260
+
261
+ 1. Optionally run **Publish Python SDK to PyPI** via workflow_dispatch with a version and
262
+ `publish=false` to validate all distributions without uploading.
263
+ 2. Create a GitHub Release with tag `sdk-python/vX.Y.Z` (for example `sdk-python/v0.1.0`).
264
+ CI stamps that version into `pyproject.toml` for the build (same idea as the TypeScript
265
+ SDK release), then builds and smoke-tests Linux x86_64/ARM64, macOS x86_64/ARM64, and
266
+ Windows x86_64 wheels, verifies the source distribution, and publishes with `PYPI_TOKEN`.
267
+ 3. After publishing, bump the committed `pyproject.toml` / docs install line when you want
268
+ the repo tip to reflect the released version.
269
+
270
+ A manual run publishes only from `main`, and only when `publish` is explicitly selected.
271
+ The dispatch `version` input is stamped the same way as a release tag.
272
+
273
+ See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo tag conventions.
274
+
275
+ ## License
276
+
277
+ [Super Durable Source License 1.0](LICENSE), with legacy portions under their
278
+ original terms as described in [LEGACY_NOTICES.md](LEGACY_NOTICES.md).
@@ -17,6 +17,8 @@ from dex.attribute import (
17
17
  )
18
18
  from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
19
19
  from dex.channel import Channel, ChannelMap
20
+ from dex.async_client import AsyncClient
21
+ from dex.async_worker import AsyncWorker
20
22
  from dex.client import Client
21
23
  from dex.client_options import ClientOptions
22
24
  from dex.codec import (
@@ -35,7 +37,13 @@ from dex.condition import ConditionCombination
35
37
  from dex.context import Context
36
38
  from dex.flow import Flow, PersistenceSchema, Registry, RPCResult, rpc
37
39
  from dex.flow_config import ActiveStepSearchMode, FlowConfig
38
- from dex.flow_info import FlowInfo, FlowStatus, HealthInfo, SearchFlowEntry
40
+ from dex.flow_info import (
41
+ FlowInfo,
42
+ FlowStatus,
43
+ HealthInfo,
44
+ SearchFlowEntry,
45
+ SearchFlowsPage,
46
+ )
39
47
  from dex.flow_options import (
40
48
  IdReusePolicy,
41
49
  ResetFlowOptions,
@@ -45,11 +53,19 @@ from dex.flow_options import (
45
53
  StopType,
46
54
  )
47
55
  from dex.runtime_errors import (
48
- DexException,
56
+ DexServiceError,
49
57
  ErrorSubStatus,
58
+ FlowAlreadyStartedError,
59
+ FlowDefinitionError,
50
60
  FlowErrorType,
61
+ FlowNotActiveError,
62
+ FlowNotFoundError,
51
63
  FlowUncompletedError,
64
+ InvalidStepResultError,
52
65
  LongPollTimeoutError,
66
+ RpcLockConflictError,
67
+ ValueMappingError,
68
+ WorkerInvocationError,
53
69
  )
54
70
  from dex.step import (
55
71
  RetryPolicy,
@@ -85,6 +101,8 @@ __all__ = [
85
101
  "AttributeIndex",
86
102
  "AttributeLock",
87
103
  "AttributeMap",
104
+ "AsyncClient",
105
+ "AsyncWorker",
88
106
  "BlobCache",
89
107
  "BlobCacheConfig",
90
108
  "Channel",
@@ -95,26 +113,33 @@ __all__ = [
95
113
  "CodecRegistry",
96
114
  "ConditionCombination",
97
115
  "Context",
98
- "DexException",
116
+ "DexServiceError",
99
117
  "ErrorSubStatus",
100
118
  "Flow",
119
+ "FlowAlreadyStartedError",
101
120
  "FlowConfig",
121
+ "FlowDefinitionError",
102
122
  "FlowErrorType",
103
123
  "FlowInfo",
124
+ "FlowNotActiveError",
125
+ "FlowNotFoundError",
104
126
  "FlowStatus",
105
127
  "FlowUncompletedError",
106
128
  "HealthInfo",
107
129
  "IdReusePolicy",
130
+ "InvalidStepResultError",
108
131
  "IndexType",
109
132
  "JsonCodec",
110
133
  "LongPollTimeoutError",
111
134
  "PersistenceSchema",
112
135
  "RPCResult",
136
+ "RpcLockConflictError",
113
137
  "Registry",
114
138
  "ResetFlowOptions",
115
139
  "ResetType",
116
140
  "RetryPolicy",
117
141
  "SearchFlowEntry",
142
+ "SearchFlowsPage",
118
143
  "StartFlowOptions",
119
144
  "StepExecutionId",
120
145
  "StepDecision",
@@ -128,10 +153,12 @@ __all__ = [
128
153
  "Timer",
129
154
  "TimerId",
130
155
  "Value",
156
+ "ValueMappingError",
131
157
  "Wait",
132
158
  "WaitForFailurePolicy",
133
159
  "WireKind",
134
160
  "Worker",
161
+ "WorkerInvocationError",
135
162
  "WorkerOptions",
136
163
  "WorkerTarget",
137
164
  "dead_end",