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.
Files changed (31) hide show
  1. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/PKG-INFO +3 -3
  2. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/README.md +1 -1
  3. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/worker.py +30 -47
  4. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/pyproject.toml +1 -1
  5. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/__init__.py +31 -1
  6. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/client.py +37 -3
  7. flo_python-0.1.1/src/flo/kv.py +582 -0
  8. flo_python-0.1.1/src/flo/kv_txn.py +340 -0
  9. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/streams.py +116 -5
  10. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/types.py +169 -6
  11. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/wire.py +179 -5
  12. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/worker.py +136 -12
  13. flo_python-0.1.1/tests/test_protocol_version.py +41 -0
  14. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/test_wire.py +81 -0
  15. flo_python-0.1.0.dev3/src/flo/kv.py +0 -257
  16. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/.github/workflows/ci.yml +0 -0
  17. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/.github/workflows/release.yml +0 -0
  18. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/.gitignore +0 -0
  19. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/LICENSE +0 -0
  20. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/basic.py +0 -0
  21. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/stream_worker.py +0 -0
  22. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/examples/test_decorator.py +0 -0
  23. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/actions.py +0 -0
  24. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/exceptions.py +0 -0
  25. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/processing.py +0 -0
  26. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/queue.py +0 -0
  27. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/src/flo/workflows.py +0 -0
  28. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/__init__.py +0 -0
  29. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/conftest.py +0 -0
  30. {flo_python-0.1.0.dev3 → flo_python-0.1.1}/tests/order-workflow.yaml +0 -0
  31. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: flo-python
3
- Version: 0.1.0.dev3
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
@@ -5,7 +5,7 @@ Python SDK for the [Flo](https://github.com/floruntime) distributed systems plat
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- pip install flo
8
+ pip install flo-python
9
9
  ```
10
10
 
11
11
  ## Quick Start
@@ -158,56 +158,39 @@ async def generate_report(ctx: ActionContext) -> bytes:
158
158
 
159
159
 
160
160
  async def main():
161
- # Create and connect client (shared configuration)
162
- client = FloClient(
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
- await client.connect()
168
-
169
- # Create a worker from the client
170
- worker = client.new_action_worker(
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
- # Register action handlers using register_action()
176
- worker.register_action("process-order", process_order)
177
- worker.register_action("send-notification", send_notification)
178
- worker.register_action("generate-report", generate_report)
179
-
180
- # Register using decorator syntax
181
- @worker.action("health-check")
182
- async def health_check(ctx: ActionContext) -> bytes:
183
- """Simple health check action."""
184
- return ctx.to_bytes({
185
- "status": "healthy",
186
- "worker_id": ctx.task_id,
187
- "timestamp": ctx.created_at,
188
- })
189
-
190
- # Handle shutdown signals
191
- stop_event = asyncio.Event()
192
-
193
- def signal_handler():
194
- logger.info("Received shutdown signal")
195
- worker.stop()
196
- stop_event.set()
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flo-python"
7
- version = "0.1.0.dev3"
7
+ version = "0.1.1"
8
8
  description = "Python SDK for the Flo distributed systems platform"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.0"
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
- await worker.start()
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
  )