flo-python 0.1.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.
- flo_python-0.1.0/.github/workflows/ci.yml +64 -0
- flo_python-0.1.0/.github/workflows/release.yml +40 -0
- flo_python-0.1.0/PKG-INFO +561 -0
- flo_python-0.1.0/README.md +532 -0
- flo_python-0.1.0/examples/basic.py +137 -0
- flo_python-0.1.0/examples/worker.py +209 -0
- flo_python-0.1.0/pyproject.toml +61 -0
- flo_python-0.1.0/src/flo/__init__.py +214 -0
- flo_python-0.1.0/src/flo/actions.py +397 -0
- flo_python-0.1.0/src/flo/client.py +284 -0
- flo_python-0.1.0/src/flo/exceptions.py +224 -0
- flo_python-0.1.0/src/flo/kv.py +257 -0
- flo_python-0.1.0/src/flo/queue.py +376 -0
- flo_python-0.1.0/src/flo/streams.py +379 -0
- flo_python-0.1.0/src/flo/types.py +804 -0
- flo_python-0.1.0/src/flo/wire.py +926 -0
- flo_python-0.1.0/src/flo/worker.py +421 -0
- flo_python-0.1.0/tests/__init__.py +1 -0
- flo_python-0.1.0/tests/test_wire.py +389 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Install tools
|
|
19
|
+
run: pip install ruff mypy
|
|
20
|
+
- name: Ruff check
|
|
21
|
+
run: ruff check src/
|
|
22
|
+
- name: Ruff format check
|
|
23
|
+
run: ruff format --check src/
|
|
24
|
+
- name: Type check
|
|
25
|
+
run: |
|
|
26
|
+
pip install -e .
|
|
27
|
+
mypy src/flo/
|
|
28
|
+
|
|
29
|
+
test:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
strategy:
|
|
32
|
+
matrix:
|
|
33
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: ${{ matrix.python-version }}
|
|
39
|
+
- name: Install
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
- name: Run tests
|
|
44
|
+
run: pytest -v
|
|
45
|
+
|
|
46
|
+
build:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
needs: [lint, test]
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
- uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: "3.12"
|
|
54
|
+
- name: Build
|
|
55
|
+
run: |
|
|
56
|
+
pip install build
|
|
57
|
+
python -m build
|
|
58
|
+
- name: Upload artifacts
|
|
59
|
+
uses: actions/upload-artifact@v4
|
|
60
|
+
with:
|
|
61
|
+
name: dist
|
|
62
|
+
path: dist/
|
|
63
|
+
|
|
64
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
ci:
|
|
13
|
+
uses: ./.github/workflows/ci.yml
|
|
14
|
+
|
|
15
|
+
publish:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
needs: ci
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
- name: Build
|
|
26
|
+
run: |
|
|
27
|
+
pip install build twine
|
|
28
|
+
python -m build
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
env:
|
|
31
|
+
TWINE_USERNAME: __token__
|
|
32
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
33
|
+
run: python -m twine upload dist/*
|
|
34
|
+
- name: Create GitHub Release
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
36
|
+
with:
|
|
37
|
+
generate_release_notes: true
|
|
38
|
+
files: dist/*
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flo-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Flo distributed systems platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/floruntime/flo-python
|
|
6
|
+
Project-URL: Documentation, https://github.com/floruntime/flo-python#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/floruntime/flo-python
|
|
8
|
+
Author: Flo Team
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: async,distributed-systems,flo,kv-store,queue
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# flo-python
|
|
31
|
+
|
|
32
|
+
Python SDK for the [Flo](https://github.com/floruntime) distributed systems platform.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install flo
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from flo import FloClient
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
# Connect to Flo server
|
|
48
|
+
async with FloClient("localhost:9000") as client:
|
|
49
|
+
# KV operations
|
|
50
|
+
await client.kv.put("user:123", b"John Doe")
|
|
51
|
+
value = await client.kv.get("user:123")
|
|
52
|
+
print(f"Got: {value.decode()}")
|
|
53
|
+
|
|
54
|
+
# Queue operations
|
|
55
|
+
seq = await client.queue.enqueue("tasks", b'{"task": "process"}')
|
|
56
|
+
print(f"Enqueued message with seq: {seq}")
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **KV Store**: Versioned key-value storage with MVCC, TTL, and optimistic locking
|
|
64
|
+
- **Queues**: Priority-based task queues with visibility timeout and dead letter queues
|
|
65
|
+
- **Streams**: Append-only logs with consumer groups for distributed processing
|
|
66
|
+
- **Actions**: Registered tasks with configurable timeouts, retries, and idempotency
|
|
67
|
+
- **Workers**: Distributed task execution with lease management and heartbeats
|
|
68
|
+
- **Async/await**: Native asyncio support for high-performance applications
|
|
69
|
+
- **Type hints**: Full type annotations for IDE support
|
|
70
|
+
|
|
71
|
+
## KV Operations
|
|
72
|
+
|
|
73
|
+
### Get
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from flo import GetOptions
|
|
77
|
+
|
|
78
|
+
# Simple get
|
|
79
|
+
value = await client.kv.get("key")
|
|
80
|
+
if value is not None:
|
|
81
|
+
print(value.decode())
|
|
82
|
+
|
|
83
|
+
# Blocking get - wait up to 5 seconds for value to appear
|
|
84
|
+
value = await client.kv.get("key", GetOptions(block_ms=5000))
|
|
85
|
+
|
|
86
|
+
# Blocking get - wait indefinitely (0 = infinite)
|
|
87
|
+
value = await client.kv.get("key", GetOptions(block_ms=0))
|
|
88
|
+
|
|
89
|
+
# Get with namespace override
|
|
90
|
+
value = await client.kv.get("key", GetOptions(namespace="other"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Put
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from flo import PutOptions
|
|
97
|
+
|
|
98
|
+
# Simple put
|
|
99
|
+
await client.kv.put("key", b"value")
|
|
100
|
+
|
|
101
|
+
# Put with TTL (expires in 1 hour)
|
|
102
|
+
await client.kv.put("session:abc", b"data", PutOptions(ttl_seconds=3600))
|
|
103
|
+
|
|
104
|
+
# Put with CAS (optimistic locking)
|
|
105
|
+
await client.kv.put("counter", b"2", PutOptions(cas_version=1))
|
|
106
|
+
|
|
107
|
+
# Put only if key doesn't exist
|
|
108
|
+
await client.kv.put("key", b"value", PutOptions(if_not_exists=True))
|
|
109
|
+
|
|
110
|
+
# Put only if key exists
|
|
111
|
+
await client.kv.put("key", b"value", PutOptions(if_exists=True))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Delete
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
await client.kv.delete("key")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Scan
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from flo import ScanOptions
|
|
124
|
+
|
|
125
|
+
# Scan all keys with prefix
|
|
126
|
+
result = await client.kv.scan("user:")
|
|
127
|
+
for entry in result.entries:
|
|
128
|
+
print(f"{entry.key.decode()}: {entry.value.decode()}")
|
|
129
|
+
|
|
130
|
+
# Paginated scan
|
|
131
|
+
result = await client.kv.scan("user:", ScanOptions(limit=100))
|
|
132
|
+
while result.has_more:
|
|
133
|
+
# Process entries...
|
|
134
|
+
result = await client.kv.scan("user:", ScanOptions(cursor=result.cursor, limit=100))
|
|
135
|
+
|
|
136
|
+
# Keys only (more efficient when you don't need values)
|
|
137
|
+
result = await client.kv.scan("user:", ScanOptions(keys_only=True))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### History
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from flo import HistoryOptions
|
|
144
|
+
|
|
145
|
+
# Get version history for a key
|
|
146
|
+
history = await client.kv.history("user:123", HistoryOptions(limit=10))
|
|
147
|
+
for entry in history:
|
|
148
|
+
print(f"v{entry.version} at {entry.timestamp}: {entry.value.decode()}")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Queue Operations
|
|
152
|
+
|
|
153
|
+
### Enqueue
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from flo import EnqueueOptions
|
|
157
|
+
|
|
158
|
+
# Simple enqueue
|
|
159
|
+
seq = await client.queue.enqueue("tasks", b'{"task": "process"}')
|
|
160
|
+
|
|
161
|
+
# With priority (higher = more urgent, 0-255)
|
|
162
|
+
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(priority=10))
|
|
163
|
+
|
|
164
|
+
# With delay (message invisible for 60 seconds)
|
|
165
|
+
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(delay_ms=60000))
|
|
166
|
+
|
|
167
|
+
# With deduplication key
|
|
168
|
+
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(dedup_key="task-123"))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Dequeue
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from flo import DequeueOptions
|
|
175
|
+
|
|
176
|
+
# Dequeue up to 10 messages
|
|
177
|
+
result = await client.queue.dequeue("tasks", 10)
|
|
178
|
+
for msg in result.messages:
|
|
179
|
+
print(f"Processing message {msg.seq}: {msg.payload}")
|
|
180
|
+
|
|
181
|
+
# Long polling (wait up to 30s for messages)
|
|
182
|
+
result = await client.queue.dequeue("tasks", 10, DequeueOptions(block_ms=30000))
|
|
183
|
+
|
|
184
|
+
# Custom visibility timeout (message invisible for 60s)
|
|
185
|
+
result = await client.queue.dequeue("tasks", 10, DequeueOptions(visibility_timeout_ms=60000))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Ack/Nack
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from flo import NackOptions
|
|
192
|
+
|
|
193
|
+
result = await client.queue.dequeue("tasks", 10)
|
|
194
|
+
for msg in result.messages:
|
|
195
|
+
try:
|
|
196
|
+
process(msg.payload)
|
|
197
|
+
# Acknowledge successful processing
|
|
198
|
+
await client.queue.ack("tasks", [msg.seq])
|
|
199
|
+
except Exception:
|
|
200
|
+
# Retry the message
|
|
201
|
+
await client.queue.nack("tasks", [msg.seq])
|
|
202
|
+
# Or send to DLQ
|
|
203
|
+
await client.queue.nack("tasks", [msg.seq], NackOptions(to_dlq=True))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Dead Letter Queue
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from flo import DlqListOptions
|
|
210
|
+
|
|
211
|
+
# List DLQ messages
|
|
212
|
+
result = await client.queue.dlq_list("tasks", DlqListOptions(limit=100))
|
|
213
|
+
for msg in result.messages:
|
|
214
|
+
print(f"Failed message {msg.seq}: {msg.payload}")
|
|
215
|
+
|
|
216
|
+
# Requeue DLQ messages
|
|
217
|
+
seqs = [msg.seq for msg in result.messages]
|
|
218
|
+
await client.queue.dlq_requeue("tasks", seqs)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Peek
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# Peek at messages without creating leases (no visibility timeout)
|
|
225
|
+
result = await client.queue.peek("tasks", 5)
|
|
226
|
+
for msg in result.messages:
|
|
227
|
+
print(f"Message {msg.seq}: {msg.payload}")
|
|
228
|
+
# Messages remain visible to other consumers
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Touch (Lease Renewal)
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Renew lease on messages during long-running processing
|
|
235
|
+
result = await client.queue.dequeue("tasks", 1)
|
|
236
|
+
msg = result.messages[0]
|
|
237
|
+
|
|
238
|
+
# Long running task - periodically touch to prevent visibility timeout
|
|
239
|
+
for chunk in process_in_chunks(msg.payload):
|
|
240
|
+
await process_chunk(chunk)
|
|
241
|
+
await client.queue.touch("tasks", [msg.seq]) # Renew lease
|
|
242
|
+
|
|
243
|
+
await client.queue.ack("tasks", [msg.seq])
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Stream Operations
|
|
247
|
+
|
|
248
|
+
Streams are append-only logs ideal for event sourcing, activity feeds, and real-time data pipelines.
|
|
249
|
+
|
|
250
|
+
### Append
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from flo import StreamAppendOptions
|
|
254
|
+
|
|
255
|
+
# Simple append
|
|
256
|
+
result = await client.stream.append("events", b'{"event": "click", "user": "123"}')
|
|
257
|
+
print(f"Appended at offset {result.offset}, timestamp {result.timestamp}")
|
|
258
|
+
|
|
259
|
+
# Append with partition key (for ordered processing within partition)
|
|
260
|
+
result = await client.stream.append(
|
|
261
|
+
"events", payload,
|
|
262
|
+
StreamAppendOptions(partition_key="user-123")
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Append with client-provided timestamp
|
|
266
|
+
result = await client.stream.append(
|
|
267
|
+
"events", payload,
|
|
268
|
+
StreamAppendOptions(timestamp=1699999999000)
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Read
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from flo import StreamReadOptions, StreamStartMode
|
|
276
|
+
|
|
277
|
+
# Read from beginning (default)
|
|
278
|
+
result = await client.stream.read("events")
|
|
279
|
+
for record in result.records:
|
|
280
|
+
print(f"offset={record.offset} ts={record.timestamp}: {record.payload}")
|
|
281
|
+
|
|
282
|
+
# Read from specific offset
|
|
283
|
+
result = await client.stream.read(
|
|
284
|
+
"events",
|
|
285
|
+
StreamReadOptions(start_mode=StreamStartMode.OFFSET, offset=100, count=10)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Read from tail (latest records)
|
|
289
|
+
result = await client.stream.read(
|
|
290
|
+
"events",
|
|
291
|
+
StreamReadOptions(start_mode=StreamStartMode.TAIL, count=10)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Read from timestamp
|
|
295
|
+
result = await client.stream.read(
|
|
296
|
+
"events",
|
|
297
|
+
StreamReadOptions(start_mode=StreamStartMode.TIMESTAMP, timestamp=1699999999000)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Blocking read (long polling) - wait up to 30s for new records
|
|
301
|
+
result = await client.stream.read(
|
|
302
|
+
"events",
|
|
303
|
+
StreamReadOptions(offset=100, block_ms=30000)
|
|
304
|
+
)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Consumer Groups
|
|
308
|
+
|
|
309
|
+
Consumer groups allow multiple consumers to process a stream in parallel, with each record delivered to only one consumer.
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from flo import StreamGroupReadOptions
|
|
313
|
+
|
|
314
|
+
# Join a consumer group
|
|
315
|
+
await client.stream.group_join("events", "processors", "worker-1")
|
|
316
|
+
|
|
317
|
+
# Read from consumer group
|
|
318
|
+
result = await client.stream.group_read("events", "processors", "worker-1")
|
|
319
|
+
for record in result.records:
|
|
320
|
+
try:
|
|
321
|
+
process(record.payload)
|
|
322
|
+
# Acknowledge successful processing
|
|
323
|
+
await client.stream.group_ack("events", "processors", [record.offset])
|
|
324
|
+
except Exception:
|
|
325
|
+
# Record will be redelivered to another consumer
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
# With blocking (long polling)
|
|
329
|
+
result = await client.stream.group_read(
|
|
330
|
+
"events", "processors", "worker-1",
|
|
331
|
+
StreamGroupReadOptions(count=10, block_ms=30000)
|
|
332
|
+
)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Action Operations
|
|
336
|
+
|
|
337
|
+
Actions are registered tasks that can be invoked and executed by workers.
|
|
338
|
+
|
|
339
|
+
### Register an Action
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from flo import ActionRegisterOptions, ActionType
|
|
343
|
+
|
|
344
|
+
# Register a user-based action (executed by external workers)
|
|
345
|
+
await client.action.register(
|
|
346
|
+
"process-image",
|
|
347
|
+
ActionType.USER,
|
|
348
|
+
ActionRegisterOptions(timeout_ms=60000, max_retries=3)
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Invoke an Action
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
from flo import ActionInvokeOptions
|
|
356
|
+
|
|
357
|
+
# Invoke an action
|
|
358
|
+
result = await client.action.invoke(
|
|
359
|
+
"process-image",
|
|
360
|
+
b'{"image_url": "https://example.com/image.jpg"}',
|
|
361
|
+
ActionInvokeOptions(priority=10)
|
|
362
|
+
)
|
|
363
|
+
print(f"Run ID: {result.run_id}")
|
|
364
|
+
|
|
365
|
+
# Invoke with idempotency key (prevents duplicate runs)
|
|
366
|
+
result = await client.action.invoke(
|
|
367
|
+
"process-image",
|
|
368
|
+
payload,
|
|
369
|
+
ActionInvokeOptions(idempotency_key="order-123-image")
|
|
370
|
+
)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Check Action Status
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
status = await client.action.status(result.run_id)
|
|
377
|
+
print(f"Status: {status.status}")
|
|
378
|
+
if status.result:
|
|
379
|
+
print(f"Result: {status.result}")
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### List and Delete Actions
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
from flo import ActionListOptions
|
|
386
|
+
|
|
387
|
+
# List all actions
|
|
388
|
+
actions = await client.action.list()
|
|
389
|
+
|
|
390
|
+
# List with prefix filter
|
|
391
|
+
actions = await client.action.list(ActionListOptions(prefix="process-"))
|
|
392
|
+
|
|
393
|
+
# Delete an action
|
|
394
|
+
await client.action.delete("process-image")
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Worker Operations
|
|
398
|
+
|
|
399
|
+
Workers execute tasks for registered actions.
|
|
400
|
+
|
|
401
|
+
### Register a Worker
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
# Register a worker that handles specific task types
|
|
405
|
+
await client.worker.register(
|
|
406
|
+
"worker-1",
|
|
407
|
+
["process-image", "resize-image"]
|
|
408
|
+
)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Await and Process Tasks
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
from flo import WorkerAwaitOptions
|
|
415
|
+
|
|
416
|
+
# Wait for a task (blocking)
|
|
417
|
+
result = await client.worker.await_task(
|
|
418
|
+
"worker-1",
|
|
419
|
+
["process-image", "resize-image"],
|
|
420
|
+
WorkerAwaitOptions(block_ms=30000) # Wait up to 30s
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if result.task:
|
|
424
|
+
task = result.task
|
|
425
|
+
print(f"Got task: {task.task_id} for action: {task.action_name}")
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
# Process the task
|
|
429
|
+
output = process(task.input)
|
|
430
|
+
|
|
431
|
+
# Complete successfully
|
|
432
|
+
await client.worker.complete("worker-1", task.task_id, output)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
# Fail the task (will be retried)
|
|
435
|
+
await client.worker.fail("worker-1", task.task_id, str(e))
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Extend Task Lease
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
from flo import WorkerTouchOptions
|
|
442
|
+
|
|
443
|
+
# For long-running tasks, extend the lease periodically
|
|
444
|
+
await client.worker.touch(
|
|
445
|
+
"worker-1",
|
|
446
|
+
task.task_id,
|
|
447
|
+
WorkerTouchOptions(extend_ms=30000)
|
|
448
|
+
)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Worker Lifecycle Example
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
import asyncio
|
|
455
|
+
from flo import FloClient, WorkerAwaitOptions, WorkerFailOptions
|
|
456
|
+
|
|
457
|
+
async def run_worker():
|
|
458
|
+
async with FloClient("localhost:9000") as client:
|
|
459
|
+
worker_id = "worker-1"
|
|
460
|
+
task_types = ["process-image"]
|
|
461
|
+
|
|
462
|
+
# Register the worker
|
|
463
|
+
await client.worker.register(worker_id, task_types)
|
|
464
|
+
|
|
465
|
+
while True:
|
|
466
|
+
# Wait for tasks
|
|
467
|
+
result = await client.worker.await_task(
|
|
468
|
+
worker_id,
|
|
469
|
+
task_types,
|
|
470
|
+
WorkerAwaitOptions(block_ms=30000)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if not result.task:
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
task = result.task
|
|
477
|
+
try:
|
|
478
|
+
# Process task
|
|
479
|
+
output = await process_image(task.input)
|
|
480
|
+
await client.worker.complete(worker_id, task.task_id, output)
|
|
481
|
+
except Exception as e:
|
|
482
|
+
await client.worker.fail(
|
|
483
|
+
worker_id,
|
|
484
|
+
task.task_id,
|
|
485
|
+
str(e),
|
|
486
|
+
WorkerFailOptions(retry=True)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
asyncio.run(run_worker())
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Configuration
|
|
493
|
+
|
|
494
|
+
### Client Options
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
client = FloClient(
|
|
498
|
+
"localhost:9000",
|
|
499
|
+
namespace="myapp", # Default namespace for all operations
|
|
500
|
+
timeout_ms=5000, # Connection and operation timeout
|
|
501
|
+
debug=True, # Enable debug logging
|
|
502
|
+
)
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Namespaces
|
|
506
|
+
|
|
507
|
+
Each operation can override the default namespace:
|
|
508
|
+
|
|
509
|
+
```python
|
|
510
|
+
from flo import GetOptions, PutOptions, EnqueueOptions
|
|
511
|
+
|
|
512
|
+
# Use client's default namespace
|
|
513
|
+
await client.kv.put("key", b"value")
|
|
514
|
+
|
|
515
|
+
# Override namespace for this operation
|
|
516
|
+
await client.kv.put("key", b"value", PutOptions(namespace="other"))
|
|
517
|
+
await client.kv.get("key", GetOptions(namespace="other"))
|
|
518
|
+
await client.queue.enqueue("tasks", payload, EnqueueOptions(namespace="other"))
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Error Handling
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from flo import (
|
|
525
|
+
FloError,
|
|
526
|
+
NotFoundError,
|
|
527
|
+
ConflictError,
|
|
528
|
+
ConnectionFailedError,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
await client.kv.put("key", b"new", PutOptions(cas_version=1))
|
|
533
|
+
except ConflictError:
|
|
534
|
+
print("CAS version mismatch - value was modified")
|
|
535
|
+
except ConnectionFailedError:
|
|
536
|
+
print("Failed to connect to server")
|
|
537
|
+
except FloError as e:
|
|
538
|
+
print(f"Flo error: {e}")
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Error Types
|
|
542
|
+
|
|
543
|
+
| Error | Description |
|
|
544
|
+
|-------|-------------|
|
|
545
|
+
| `NotFoundError` | Key or resource not found |
|
|
546
|
+
| `BadRequestError` | Invalid request parameters |
|
|
547
|
+
| `ConflictError` | CAS version mismatch |
|
|
548
|
+
| `UnauthorizedError` | Authentication failed |
|
|
549
|
+
| `OverloadedError` | Server is overloaded |
|
|
550
|
+
| `InternalServerError` | Server internal error |
|
|
551
|
+
| `ConnectionFailedError` | Failed to connect |
|
|
552
|
+
| `InvalidChecksumError` | Response CRC32 mismatch |
|
|
553
|
+
|
|
554
|
+
## Requirements
|
|
555
|
+
|
|
556
|
+
- Python 3.10+
|
|
557
|
+
- asyncio
|
|
558
|
+
|
|
559
|
+
## License
|
|
560
|
+
|
|
561
|
+
MIT
|