flo-python 0.1.0.dev3__tar.gz → 0.1.1__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.
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/PKG-INFO +3 -3
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/README.md +1 -1
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/worker.py +30 -47
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/pyproject.toml +1 -1
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/__init__.py +31 -1
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/client.py +37 -3
- flo_python-0.1.1/src/flo/kv.py +582 -0
- flo_python-0.1.1/src/flo/kv_txn.py +340 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/streams.py +116 -5
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/types.py +169 -6
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/wire.py +179 -5
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/worker.py +136 -12
- flo_python-0.1.1/tests/test_protocol_version.py +41 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/test_wire.py +81 -0
- flo_python-0.1.0.dev3/src/flo/kv.py +0 -257
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/.github/workflows/ci.yml +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/.github/workflows/release.yml +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/.gitignore +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/LICENSE +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/basic.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/stream_worker.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/test_decorator.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/actions.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/exceptions.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/processing.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/queue.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/workflows.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/__init__.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/conftest.py +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/order-workflow.yaml +0 -0
- {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/test_workflows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: flo-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Python SDK for the Flo distributed systems platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/floruntime/flo-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/floruntime/flo-python#readme
|
|
@@ -35,7 +35,7 @@ Python SDK for the [Flo](https://github.com/floruntime) distributed systems plat
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pip install flo
|
|
38
|
+
pip install flo-python
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Quick Start
|
|
@@ -158,56 +158,39 @@ async def generate_report(ctx: ActionContext) -> bytes:
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
async def main():
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
os.getenv("FLO_ENDPOINT", "localhost:3000"),
|
|
161
|
+
async with FloClient(
|
|
162
|
+
os.getenv("FLO_ENDPOINT", "localhost:4455"),
|
|
164
163
|
namespace=os.getenv("FLO_NAMESPACE", "myapp"),
|
|
165
164
|
debug=os.getenv("FLO_DEBUG", "").lower() in ("1", "true"),
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
concurrency=5,
|
|
172
|
-
action_timeout=300, # 5 minutes
|
|
173
|
-
)
|
|
165
|
+
) as client:
|
|
166
|
+
worker = client.new_action_worker(
|
|
167
|
+
concurrency=5,
|
|
168
|
+
action_timeout=300, # 5 minutes
|
|
169
|
+
)
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
loop = asyncio.get_running_loop()
|
|
199
|
-
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
200
|
-
loop.add_signal_handler(sig, signal_handler)
|
|
201
|
-
|
|
202
|
-
# Start worker (blocks until stopped)
|
|
203
|
-
logger.info("Starting worker...")
|
|
204
|
-
try:
|
|
205
|
-
await worker.start()
|
|
206
|
-
except KeyboardInterrupt:
|
|
207
|
-
logger.info("Interrupted")
|
|
208
|
-
finally:
|
|
209
|
-
await worker.close()
|
|
210
|
-
await client.close()
|
|
171
|
+
# Register action handlers
|
|
172
|
+
worker.register_action("process-order", process_order)
|
|
173
|
+
worker.register_action("send-notification", send_notification)
|
|
174
|
+
worker.register_action("generate-report", generate_report)
|
|
175
|
+
|
|
176
|
+
# Register using decorator syntax
|
|
177
|
+
@worker.action("health-check")
|
|
178
|
+
async def health_check(ctx: ActionContext) -> bytes:
|
|
179
|
+
return ctx.to_bytes({"status": "healthy", "worker_id": ctx.task_id})
|
|
180
|
+
|
|
181
|
+
# Handle shutdown signals
|
|
182
|
+
def signal_handler():
|
|
183
|
+
logger.info("Received shutdown signal")
|
|
184
|
+
worker.stop()
|
|
185
|
+
|
|
186
|
+
loop = asyncio.get_running_loop()
|
|
187
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
188
|
+
loop.add_signal_handler(sig, signal_handler)
|
|
189
|
+
|
|
190
|
+
# Start worker (blocks until stopped)
|
|
191
|
+
logger.info("Starting worker...")
|
|
192
|
+
async with worker:
|
|
193
|
+
await worker.start()
|
|
211
194
|
logger.info("Worker shutdown complete")
|
|
212
195
|
|
|
213
196
|
|
|
@@ -60,6 +60,7 @@ from .exceptions import (
|
|
|
60
60
|
ValueTooLargeError,
|
|
61
61
|
is_connection_error,
|
|
62
62
|
)
|
|
63
|
+
from .kv_txn import Transaction, TxnFinishedError, TxnUnsupportedOpError
|
|
63
64
|
from .processing import ProcessingOperations
|
|
64
65
|
from .types import (
|
|
65
66
|
AckOptions,
|
|
@@ -79,14 +80,25 @@ from .types import (
|
|
|
79
80
|
DlqListOptions,
|
|
80
81
|
DlqRequeueOptions,
|
|
81
82
|
EnqueueOptions,
|
|
83
|
+
GetJsonResult,
|
|
82
84
|
GetOptions,
|
|
85
|
+
GetResult,
|
|
83
86
|
HistoryOptions,
|
|
87
|
+
KVBeginResult,
|
|
88
|
+
KVCommitResult,
|
|
84
89
|
KVEntry,
|
|
90
|
+
KVExistsOptions,
|
|
91
|
+
KVIncrOptions,
|
|
92
|
+
KVJsonOptions,
|
|
93
|
+
KVMGetOptions,
|
|
94
|
+
KVTouchOptions,
|
|
85
95
|
Message,
|
|
96
|
+
MGetEntry,
|
|
86
97
|
NackOptions,
|
|
87
98
|
OpCode,
|
|
88
99
|
OptionTag,
|
|
89
100
|
PeekOptions,
|
|
101
|
+
PendingEntry,
|
|
90
102
|
ProcessingCancelOptions,
|
|
91
103
|
ProcessingListEntry,
|
|
92
104
|
ProcessingListOptions,
|
|
@@ -100,12 +112,14 @@ from .types import (
|
|
|
100
112
|
ProcessingSyncOptions,
|
|
101
113
|
ProcessingSyncResult,
|
|
102
114
|
PutOptions,
|
|
115
|
+
PutResult,
|
|
103
116
|
ScanOptions,
|
|
104
117
|
ScanResult,
|
|
105
118
|
StatusCode,
|
|
106
119
|
StorageTier,
|
|
107
120
|
StreamAppendOptions,
|
|
108
121
|
StreamAppendResult,
|
|
122
|
+
StreamClaimResult,
|
|
109
123
|
StreamGroupAckOptions,
|
|
110
124
|
StreamGroupJoinOptions,
|
|
111
125
|
StreamGroupNackOptions,
|
|
@@ -156,9 +170,23 @@ from .worker import (
|
|
|
156
170
|
)
|
|
157
171
|
from .workflows import WorkflowOperations
|
|
158
172
|
|
|
159
|
-
__version__ = "0.1.
|
|
173
|
+
__version__ = "0.1.1"
|
|
160
174
|
|
|
161
175
|
__all__ = [
|
|
176
|
+
"Transaction",
|
|
177
|
+
"TxnFinishedError",
|
|
178
|
+
"TxnUnsupportedOpError",
|
|
179
|
+
"KVBeginResult",
|
|
180
|
+
"KVCommitResult",
|
|
181
|
+
"KVExistsOptions",
|
|
182
|
+
"KVIncrOptions",
|
|
183
|
+
"KVJsonOptions",
|
|
184
|
+
"KVMGetOptions",
|
|
185
|
+
"KVTouchOptions",
|
|
186
|
+
"GetResult",
|
|
187
|
+
"GetJsonResult",
|
|
188
|
+
"MGetEntry",
|
|
189
|
+
"PutResult",
|
|
162
190
|
# Client
|
|
163
191
|
"FloClient",
|
|
164
192
|
# High-level Worker API
|
|
@@ -229,6 +257,8 @@ __all__ = [
|
|
|
229
257
|
"StreamRecord",
|
|
230
258
|
"StreamAppendResult",
|
|
231
259
|
"StreamReadResult",
|
|
260
|
+
"StreamClaimResult",
|
|
261
|
+
"PendingEntry",
|
|
232
262
|
"StreamInfo",
|
|
233
263
|
# Stream options
|
|
234
264
|
"StreamAppendOptions",
|
|
@@ -83,7 +83,27 @@ class FloClient:
|
|
|
83
83
|
|
|
84
84
|
@staticmethod
|
|
85
85
|
def _parse_endpoint(endpoint: str) -> tuple[str, int]:
|
|
86
|
-
"""Parse endpoint string into host and port.
|
|
86
|
+
"""Parse endpoint string into host and port.
|
|
87
|
+
|
|
88
|
+
Accepts an optional URI scheme: ``flo://`` (plaintext) is stripped and
|
|
89
|
+
bare ``host:port`` remains valid. ``flos://`` (TLS) is reserved but not
|
|
90
|
+
yet implemented, and any other scheme is rejected so it fails here
|
|
91
|
+
rather than as a confusing DNS error at connect time.
|
|
92
|
+
"""
|
|
93
|
+
if "://" in endpoint:
|
|
94
|
+
scheme, _, rest = endpoint.partition("://")
|
|
95
|
+
scheme = scheme.lower()
|
|
96
|
+
if scheme == "flo":
|
|
97
|
+
# Drop scheme and any trailing path/query (reserved for future use).
|
|
98
|
+
endpoint = rest.split("/", 1)[0]
|
|
99
|
+
elif scheme == "flos":
|
|
100
|
+
raise InvalidEndpointError(f"flos:// (TLS) is not yet supported: {endpoint}")
|
|
101
|
+
else:
|
|
102
|
+
raise InvalidEndpointError(
|
|
103
|
+
f"Unsupported scheme {scheme}:// (expected flo:// or bare "
|
|
104
|
+
f"host:port): {endpoint}"
|
|
105
|
+
)
|
|
106
|
+
|
|
87
107
|
parts = endpoint.rsplit(":", 1)
|
|
88
108
|
if len(parts) != 2:
|
|
89
109
|
raise InvalidEndpointError(f"Invalid endpoint format: {endpoint} (expected host:port)")
|
|
@@ -372,11 +392,13 @@ class FloClient:
|
|
|
372
392
|
Returns:
|
|
373
393
|
A new ActionWorker instance ready to register actions and start.
|
|
374
394
|
|
|
375
|
-
Example
|
|
395
|
+
Example::
|
|
396
|
+
|
|
376
397
|
async with FloClient("localhost:3000", namespace="myapp") as client:
|
|
377
398
|
worker = client.new_action_worker(concurrency=5)
|
|
378
399
|
worker.register_action("process-order", process_order)
|
|
379
|
-
|
|
400
|
+
async with worker:
|
|
401
|
+
await worker.start()
|
|
380
402
|
"""
|
|
381
403
|
from .worker import ActionWorker
|
|
382
404
|
|
|
@@ -400,6 +422,8 @@ class FloClient:
|
|
|
400
422
|
batch_size: int = 10,
|
|
401
423
|
block_ms: int = 30000,
|
|
402
424
|
message_timeout: float = 300.0,
|
|
425
|
+
redeliver_pending_on_reconnect: bool = True,
|
|
426
|
+
claim_min_idle_ms: int = 0,
|
|
403
427
|
) -> "StreamWorker":
|
|
404
428
|
"""Create a new StreamWorker from this client.
|
|
405
429
|
|
|
@@ -417,6 +441,14 @@ class FloClient:
|
|
|
417
441
|
batch_size: Number of records to fetch per poll.
|
|
418
442
|
block_ms: Timeout for blocking read in milliseconds.
|
|
419
443
|
message_timeout: Timeout for record handlers in seconds.
|
|
444
|
+
redeliver_pending_on_reconnect: Drain this consumer's pending
|
|
445
|
+
(delivered-but-unacked) entries via group_claim after a
|
|
446
|
+
reconnect, before resuming normal reads. Defaults to True
|
|
447
|
+
(required for at-least-once across reconnects). Set False for
|
|
448
|
+
at-most-once.
|
|
449
|
+
claim_min_idle_ms: Minimum idle time (ms) before the reconnect drain
|
|
450
|
+
claims an entry. 0 drains everything this consumer owns; > 0
|
|
451
|
+
also steals entries abandoned by dead consumers.
|
|
420
452
|
|
|
421
453
|
Returns:
|
|
422
454
|
A new StreamWorker instance ready to start.
|
|
@@ -444,4 +476,6 @@ class FloClient:
|
|
|
444
476
|
batch_size=batch_size,
|
|
445
477
|
block_ms=block_ms,
|
|
446
478
|
message_timeout=message_timeout,
|
|
479
|
+
redeliver_pending_on_reconnect=redeliver_pending_on_reconnect,
|
|
480
|
+
claim_min_idle_ms=claim_min_idle_ms,
|
|
447
481
|
)
|