flowqueue 0.1.0__py3-none-any.whl
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.
- flowqueue/__init__.py +17 -0
- flowqueue/client.py +266 -0
- flowqueue/consumer.py +75 -0
- flowqueue/errors.py +23 -0
- flowqueue/models.py +33 -0
- flowqueue-0.1.0.dist-info/METADATA +92 -0
- flowqueue-0.1.0.dist-info/RECORD +10 -0
- flowqueue-0.1.0.dist-info/WHEEL +5 -0
- flowqueue-0.1.0.dist-info/licenses/LICENSE +21 -0
- flowqueue-0.1.0.dist-info/top_level.txt +1 -0
flowqueue/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""FlowQueue Python SDK — sync client for the FlowQueue message platform."""
|
|
2
|
+
|
|
3
|
+
from .client import FlowQueueClient
|
|
4
|
+
from .consumer import FlowQueueConsumer
|
|
5
|
+
from .errors import ApiError, FlowQueueError
|
|
6
|
+
from .models import Delivery
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"FlowQueueClient",
|
|
12
|
+
"FlowQueueConsumer",
|
|
13
|
+
"Delivery",
|
|
14
|
+
"FlowQueueError",
|
|
15
|
+
"ApiError",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
flowqueue/client.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""FlowQueue synchronous client — full coverage of the FlowQueue HTTP API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import ApiError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _iso(value: Optional[datetime | str]) -> Optional[str]:
|
|
14
|
+
if value is None:
|
|
15
|
+
return None
|
|
16
|
+
return value.isoformat() if isinstance(value, datetime) else value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlowQueueClient:
|
|
20
|
+
"""Authenticated client for a FlowQueue server.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
from flowqueue import FlowQueueClient
|
|
24
|
+
client = FlowQueueClient("https://flowqueue.example.com", "fq_...")
|
|
25
|
+
q = client.create_queue("orders")
|
|
26
|
+
client.publish(q["id"], {"hello": "world"}, idempotency_key="abc")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, base_url: str, api_key: str, *, timeout: float = 30.0) -> None:
|
|
30
|
+
self.base_url = base_url.rstrip("/")
|
|
31
|
+
self._http = httpx.Client(
|
|
32
|
+
base_url=self.base_url,
|
|
33
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# ---- lifecycle -------------------------------------------------------- #
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
self._http.close()
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> "FlowQueueClient":
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *exc: object) -> None:
|
|
45
|
+
self.close()
|
|
46
|
+
|
|
47
|
+
# ---- low-level -------------------------------------------------------- #
|
|
48
|
+
def _request(self, method: str, path: str, **kw) -> Any:
|
|
49
|
+
resp = self._http.request(method, path, **kw)
|
|
50
|
+
if resp.status_code == 204 or not resp.content:
|
|
51
|
+
if 200 <= resp.status_code < 300:
|
|
52
|
+
return None
|
|
53
|
+
try:
|
|
54
|
+
body = resp.json()
|
|
55
|
+
except ValueError:
|
|
56
|
+
body = None
|
|
57
|
+
if not (200 <= resp.status_code < 300):
|
|
58
|
+
code, message = None, resp.text
|
|
59
|
+
if isinstance(body, dict):
|
|
60
|
+
err = body.get("error")
|
|
61
|
+
if isinstance(err, dict):
|
|
62
|
+
code, message = err.get("code"), err.get("message", message)
|
|
63
|
+
elif "detail" in body:
|
|
64
|
+
message = body["detail"]
|
|
65
|
+
raise ApiError(resp.status_code, code, message or "request failed")
|
|
66
|
+
return body
|
|
67
|
+
|
|
68
|
+
_v = "/api/v1"
|
|
69
|
+
|
|
70
|
+
# ---- queues ----------------------------------------------------------- #
|
|
71
|
+
def create_queue(self, name: str, **opts) -> dict:
|
|
72
|
+
"""Create a queue. opts: fifo_enabled, max_retries, retry_delay_seconds,
|
|
73
|
+
visibility_timeout_seconds, retention_seconds, processed_retention_seconds,
|
|
74
|
+
dlq_enabled, metadata."""
|
|
75
|
+
return self._request("POST", f"{self._v}/queues", json={"name": name, **opts})
|
|
76
|
+
|
|
77
|
+
def list_queues(self, archived: bool = False, limit: int = 100, offset: int = 0) -> dict:
|
|
78
|
+
return self._request(
|
|
79
|
+
"GET", f"{self._v}/queues",
|
|
80
|
+
params={"archived": archived, "limit": limit, "offset": offset},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def get_queue(self, queue_id: str) -> dict:
|
|
84
|
+
return self._request("GET", f"{self._v}/queues/{queue_id}")
|
|
85
|
+
|
|
86
|
+
def update_queue(self, queue_id: str, **fields) -> dict:
|
|
87
|
+
return self._request("PATCH", f"{self._v}/queues/{queue_id}", json=fields)
|
|
88
|
+
|
|
89
|
+
def archive_queue(self, queue_id: str) -> dict:
|
|
90
|
+
return self._request("DELETE", f"{self._v}/queues/{queue_id}")
|
|
91
|
+
|
|
92
|
+
def restore_queue(self, queue_id: str) -> dict:
|
|
93
|
+
return self._request("PATCH", f"{self._v}/queues/{queue_id}", json={"is_active": True})
|
|
94
|
+
|
|
95
|
+
def pause_queue(self, queue_id: str) -> dict:
|
|
96
|
+
return self._request("POST", f"{self._v}/queues/{queue_id}/pause")
|
|
97
|
+
|
|
98
|
+
def resume_queue(self, queue_id: str) -> dict:
|
|
99
|
+
return self._request("POST", f"{self._v}/queues/{queue_id}/resume")
|
|
100
|
+
|
|
101
|
+
def queue_stats(self, queue_id: str) -> dict:
|
|
102
|
+
return self._request("GET", f"{self._v}/queues/{queue_id}/stats")
|
|
103
|
+
|
|
104
|
+
def queue_timeseries(self, queue_id: str, minutes: int = 60) -> list:
|
|
105
|
+
return self._request(
|
|
106
|
+
"GET", f"{self._v}/queues/{queue_id}/timeseries", params={"minutes": minutes}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ---- consumers -------------------------------------------------------- #
|
|
110
|
+
def create_consumer(
|
|
111
|
+
self,
|
|
112
|
+
queue_id: str,
|
|
113
|
+
name: str,
|
|
114
|
+
type: str = "http",
|
|
115
|
+
*,
|
|
116
|
+
endpoint_url: str | None = None,
|
|
117
|
+
routing_rules: list | None = None,
|
|
118
|
+
match_mode: str = "any",
|
|
119
|
+
auto_complete: bool = True,
|
|
120
|
+
signing_secret: str | None = None,
|
|
121
|
+
metadata: dict | None = None,
|
|
122
|
+
) -> dict:
|
|
123
|
+
body = {
|
|
124
|
+
"name": name,
|
|
125
|
+
"type": type,
|
|
126
|
+
"endpoint_url": endpoint_url,
|
|
127
|
+
"routing_rules": routing_rules or [],
|
|
128
|
+
"match_mode": match_mode,
|
|
129
|
+
"auto_complete": auto_complete,
|
|
130
|
+
"signing_secret": signing_secret,
|
|
131
|
+
"metadata": metadata or {},
|
|
132
|
+
}
|
|
133
|
+
return self._request("POST", f"{self._v}/queues/{queue_id}/consumers", json=body)
|
|
134
|
+
|
|
135
|
+
def list_consumers(self, queue_id: str, limit: int = 100, offset: int = 0) -> dict:
|
|
136
|
+
return self._request(
|
|
137
|
+
"GET", f"{self._v}/queues/{queue_id}/consumers",
|
|
138
|
+
params={"limit": limit, "offset": offset},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def get_consumer(self, consumer_id: str) -> dict:
|
|
142
|
+
return self._request("GET", f"{self._v}/consumers/{consumer_id}")
|
|
143
|
+
|
|
144
|
+
def update_consumer(self, queue_id: str, consumer_id: str, **fields) -> dict:
|
|
145
|
+
return self._request(
|
|
146
|
+
"PATCH", f"{self._v}/queues/{queue_id}/consumers/{consumer_id}", json=fields
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def deactivate_consumer(self, queue_id: str, consumer_id: str) -> dict:
|
|
150
|
+
return self._request("DELETE", f"{self._v}/queues/{queue_id}/consumers/{consumer_id}")
|
|
151
|
+
|
|
152
|
+
def test_consumer(self, queue_id: str, consumer_id: str) -> dict:
|
|
153
|
+
return self._request("POST", f"{self._v}/queues/{queue_id}/consumers/{consumer_id}/test")
|
|
154
|
+
|
|
155
|
+
# ---- messages (producer) --------------------------------------------- #
|
|
156
|
+
def publish(
|
|
157
|
+
self,
|
|
158
|
+
queue_id: str,
|
|
159
|
+
payload: dict,
|
|
160
|
+
idempotency_key: str | None = None,
|
|
161
|
+
*,
|
|
162
|
+
delay_seconds: int | None = None,
|
|
163
|
+
deliver_at: datetime | str | None = None,
|
|
164
|
+
) -> dict:
|
|
165
|
+
"""Publish a message. Optionally schedule it with delay_seconds or deliver_at."""
|
|
166
|
+
body: dict[str, Any] = {"payload": payload}
|
|
167
|
+
if idempotency_key is not None:
|
|
168
|
+
body["idempotency_key"] = idempotency_key
|
|
169
|
+
if delay_seconds is not None:
|
|
170
|
+
body["delay_seconds"] = delay_seconds
|
|
171
|
+
if deliver_at is not None:
|
|
172
|
+
body["deliver_at"] = _iso(deliver_at)
|
|
173
|
+
return self._request("POST", f"{self._v}/queues/{queue_id}/messages", json=body)
|
|
174
|
+
|
|
175
|
+
def list_messages(self, queue_id: str, limit: int = 50, offset: int = 0) -> dict:
|
|
176
|
+
return self._request(
|
|
177
|
+
"GET", f"{self._v}/queues/{queue_id}/messages",
|
|
178
|
+
params={"limit": limit, "offset": offset},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def get_message(self, queue_id: str, message_id: str) -> dict:
|
|
182
|
+
return self._request("GET", f"{self._v}/queues/{queue_id}/messages/{message_id}")
|
|
183
|
+
|
|
184
|
+
# ---- deliveries (consumer) ------------------------------------------- #
|
|
185
|
+
def poll(self, consumer_id: str) -> dict | None:
|
|
186
|
+
return self._request("POST", f"{self._v}/consumers/{consumer_id}/poll")
|
|
187
|
+
|
|
188
|
+
def ack(self, delivery_id: str) -> dict:
|
|
189
|
+
return self._request("POST", f"{self._v}/deliveries/{delivery_id}/ack")
|
|
190
|
+
|
|
191
|
+
def complete(self, delivery_id: str, remark: str | None = None, metadata: dict | None = None) -> dict:
|
|
192
|
+
return self._request(
|
|
193
|
+
"POST", f"{self._v}/deliveries/{delivery_id}/complete",
|
|
194
|
+
json={"remark": remark, "metadata": metadata or {}},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def fail(self, delivery_id: str, remark: str, metadata: dict | None = None) -> dict:
|
|
198
|
+
return self._request(
|
|
199
|
+
"POST", f"{self._v}/deliveries/{delivery_id}/fail",
|
|
200
|
+
json={"remark": remark, "metadata": metadata or {}},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def add_remark(self, delivery_id: str, remark: str) -> dict:
|
|
204
|
+
return self._request(
|
|
205
|
+
"POST", f"{self._v}/deliveries/{delivery_id}/remark", json={"remark": remark}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def get_delivery(self, delivery_id: str) -> dict:
|
|
209
|
+
return self._request("GET", f"{self._v}/deliveries/{delivery_id}")
|
|
210
|
+
|
|
211
|
+
def delivery_history(self, delivery_id: str) -> list:
|
|
212
|
+
return self._request("GET", f"{self._v}/deliveries/{delivery_id}/history")
|
|
213
|
+
|
|
214
|
+
def list_consumer_deliveries(
|
|
215
|
+
self, consumer_id: str, status: str | None = None, limit: int = 50, offset: int = 0
|
|
216
|
+
) -> dict:
|
|
217
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
218
|
+
if status:
|
|
219
|
+
params["status"] = status
|
|
220
|
+
return self._request(
|
|
221
|
+
"GET", f"{self._v}/consumers/{consumer_id}/deliveries", params=params
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# ---- replay ----------------------------------------------------------- #
|
|
225
|
+
def replay_failed(self, consumer_id: str) -> dict:
|
|
226
|
+
return self._request("POST", f"{self._v}/consumers/{consumer_id}/replay/failed")
|
|
227
|
+
|
|
228
|
+
def replay_range(self, consumer_id: str, from_ts: datetime | str, to_ts: datetime | str) -> dict:
|
|
229
|
+
return self._request(
|
|
230
|
+
"POST", f"{self._v}/consumers/{consumer_id}/replay/range",
|
|
231
|
+
json={"from_ts": _iso(from_ts), "to_ts": _iso(to_ts)},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def replay_selected(self, consumer_id: str, message_ids: list[str]) -> dict:
|
|
235
|
+
return self._request(
|
|
236
|
+
"POST", f"{self._v}/consumers/{consumer_id}/replay/selected",
|
|
237
|
+
json={"message_ids": message_ids},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def replay_backfill(self, consumer_id: str) -> dict:
|
|
241
|
+
return self._request("POST", f"{self._v}/consumers/{consumer_id}/replay/backfill")
|
|
242
|
+
|
|
243
|
+
def get_replay(self, replay_id: str) -> dict:
|
|
244
|
+
return self._request("GET", f"{self._v}/replay/{replay_id}")
|
|
245
|
+
|
|
246
|
+
# ---- dead-letter queue ----------------------------------------------- #
|
|
247
|
+
def dlq_list(self, queue_id: str, limit: int = 100, offset: int = 0) -> dict:
|
|
248
|
+
return self._request(
|
|
249
|
+
"GET", f"{self._v}/queues/{queue_id}/dlq", params={"limit": limit, "offset": offset}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def requeue(self, delivery_id: str) -> dict:
|
|
253
|
+
return self._request("POST", f"{self._v}/deliveries/{delivery_id}/requeue")
|
|
254
|
+
|
|
255
|
+
def discard(self, delivery_id: str) -> dict:
|
|
256
|
+
return self._request("POST", f"{self._v}/deliveries/{delivery_id}/discard")
|
|
257
|
+
|
|
258
|
+
def requeue_all(self, queue_id: str) -> dict:
|
|
259
|
+
return self._request("POST", f"{self._v}/queues/{queue_id}/dlq/requeue")
|
|
260
|
+
|
|
261
|
+
# ---- api keys --------------------------------------------------------- #
|
|
262
|
+
def create_api_key(self, name: str, scopes: list[str] | None = None) -> dict:
|
|
263
|
+
return self._request(
|
|
264
|
+
"POST", f"{self._v}/api-keys",
|
|
265
|
+
json={"name": name, "scopes": scopes or ["publish", "consume"]},
|
|
266
|
+
)
|
flowqueue/consumer.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""High-level pull consumer with an optional run() loop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
from .client import FlowQueueClient
|
|
9
|
+
from .models import Delivery
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlowQueueConsumer:
|
|
13
|
+
"""Pull consumer bound to a single consumer id.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
from flowqueue import FlowQueueClient, FlowQueueConsumer
|
|
17
|
+
client = FlowQueueClient(url, key)
|
|
18
|
+
consumer = FlowQueueConsumer(client, consumer_id)
|
|
19
|
+
|
|
20
|
+
# one-shot
|
|
21
|
+
d = consumer.poll()
|
|
22
|
+
if d:
|
|
23
|
+
consumer.complete(d.id, remark="ok")
|
|
24
|
+
|
|
25
|
+
# or run forever (handler return => complete, raise => fail)
|
|
26
|
+
consumer.run(lambda d: process(d.payload))
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, client: FlowQueueClient, consumer_id: str) -> None:
|
|
30
|
+
self.client = client
|
|
31
|
+
self.consumer_id = consumer_id
|
|
32
|
+
|
|
33
|
+
def poll(self) -> Optional[Delivery]:
|
|
34
|
+
data = self.client.poll(self.consumer_id)
|
|
35
|
+
return Delivery.from_dict(data) if data else None
|
|
36
|
+
|
|
37
|
+
def ack(self, delivery_id: str) -> dict:
|
|
38
|
+
return self.client.ack(delivery_id)
|
|
39
|
+
|
|
40
|
+
def complete(self, delivery_id: str, remark: str | None = None) -> dict:
|
|
41
|
+
return self.client.complete(delivery_id, remark=remark)
|
|
42
|
+
|
|
43
|
+
def fail(self, delivery_id: str, remark: str, metadata: dict | None = None) -> dict:
|
|
44
|
+
return self.client.fail(delivery_id, remark, metadata)
|
|
45
|
+
|
|
46
|
+
def add_remark(self, delivery_id: str, remark: str) -> dict:
|
|
47
|
+
return self.client.add_remark(delivery_id, remark)
|
|
48
|
+
|
|
49
|
+
def run(
|
|
50
|
+
self,
|
|
51
|
+
handler: Callable[[Delivery], None],
|
|
52
|
+
*,
|
|
53
|
+
poll_interval: float = 2.0,
|
|
54
|
+
auto_complete: bool = True,
|
|
55
|
+
max_iterations: int | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Continuously poll and dispatch deliveries to `handler`.
|
|
58
|
+
|
|
59
|
+
On success the delivery is completed (when auto_complete); on exception it is
|
|
60
|
+
failed (triggering retry/DLQ per queue config). Sleeps poll_interval when idle.
|
|
61
|
+
Set max_iterations to bound the loop (useful for tests/cron-style runs).
|
|
62
|
+
"""
|
|
63
|
+
iterations = 0
|
|
64
|
+
while max_iterations is None or iterations < max_iterations:
|
|
65
|
+
iterations += 1
|
|
66
|
+
delivery = self.poll()
|
|
67
|
+
if delivery is None:
|
|
68
|
+
time.sleep(poll_interval)
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
handler(delivery)
|
|
72
|
+
if auto_complete:
|
|
73
|
+
self.complete(delivery.id, remark="ok")
|
|
74
|
+
except Exception as exc: # noqa: BLE001
|
|
75
|
+
self.fail(delivery.id, remark=str(exc)[:500])
|
flowqueue/errors.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlowQueueError(Exception):
|
|
7
|
+
"""Base error for the FlowQueue SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiError(FlowQueueError):
|
|
11
|
+
"""Raised on a non-2xx API response.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
status: HTTP status code.
|
|
15
|
+
code: machine-readable error code from the API envelope (if any).
|
|
16
|
+
message: human-readable message.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, status: int, code: str | None, message: str) -> None:
|
|
20
|
+
self.status = status
|
|
21
|
+
self.code = code
|
|
22
|
+
self.message = message
|
|
23
|
+
super().__init__(f"[{status}] {code or 'error'}: {message}")
|
flowqueue/models.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Lightweight data models returned by the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Delivery:
|
|
11
|
+
"""A delivery claimed by a consumer via poll()."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
message_id: str
|
|
15
|
+
consumer_id: str
|
|
16
|
+
status: str
|
|
17
|
+
attempt_count: int
|
|
18
|
+
payload: dict
|
|
19
|
+
sequence_num: int
|
|
20
|
+
raw: dict[str, Any]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_dict(cls, data: dict) -> "Delivery":
|
|
24
|
+
return cls(
|
|
25
|
+
id=data["id"],
|
|
26
|
+
message_id=data["message_id"],
|
|
27
|
+
consumer_id=data["consumer_id"],
|
|
28
|
+
status=data["status"],
|
|
29
|
+
attempt_count=data["attempt_count"],
|
|
30
|
+
payload=data.get("payload", {}),
|
|
31
|
+
sequence_num=data.get("sequence_num", 0),
|
|
32
|
+
raw=data,
|
|
33
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowqueue
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client SDK for FlowQueue — a cloud-native message processing platform
|
|
5
|
+
Author: FlowQueue
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/your-org/flowqueue
|
|
8
|
+
Project-URL: Repository, https://github.com/your-org/flowqueue
|
|
9
|
+
Project-URL: Documentation, https://github.com/your-org/flowqueue#readme
|
|
10
|
+
Keywords: queue,messaging,webhook,delivery,dlq,flowqueue
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# flowqueue
|
|
27
|
+
|
|
28
|
+
Python client SDK for **FlowQueue** — a cloud-native message processing platform
|
|
29
|
+
(durable queues, per-consumer delivery lifecycle, retries, dead-letter queue,
|
|
30
|
+
scheduled delivery, webhooks with HMAC signing, replay, and a tamper-evident audit
|
|
31
|
+
log).
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install flowqueue
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from flowqueue import FlowQueueClient, FlowQueueConsumer
|
|
41
|
+
|
|
42
|
+
client = FlowQueueClient("https://flowqueue.example.com", "fq_your_api_key")
|
|
43
|
+
|
|
44
|
+
# Create a queue + a pull consumer
|
|
45
|
+
queue = client.create_queue("orders", max_retries=5, dlq_enabled=True)
|
|
46
|
+
consumer = client.create_consumer(queue["id"], "billing", type="http")
|
|
47
|
+
|
|
48
|
+
# Publish (optionally scheduled)
|
|
49
|
+
client.publish(queue["id"], {"order_id": 42}, idempotency_key="order-42")
|
|
50
|
+
client.publish(queue["id"], {"order_id": 43}, delay_seconds=30) # deliver in 30s
|
|
51
|
+
|
|
52
|
+
# Consume one delivery
|
|
53
|
+
c = FlowQueueConsumer(client, consumer["id"])
|
|
54
|
+
d = c.poll()
|
|
55
|
+
if d:
|
|
56
|
+
print(d.payload)
|
|
57
|
+
c.complete(d.id, remark="done")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Run a worker loop
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
def handle(delivery):
|
|
64
|
+
process(delivery.payload) # raise to fail (retry / DLQ), return to complete
|
|
65
|
+
|
|
66
|
+
FlowQueueConsumer(client, consumer_id).run(handle, poll_interval=2.0)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Management, replay, DLQ
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
client.pause_queue(qid); client.resume_queue(qid)
|
|
73
|
+
client.queue_stats(qid)
|
|
74
|
+
client.replay_failed(consumer_id)
|
|
75
|
+
dead = client.dlq_list(qid)
|
|
76
|
+
client.requeue_all(qid) # bulk requeue the dead-letter queue
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API keys & scopes
|
|
80
|
+
|
|
81
|
+
Generate scoped keys in the FlowQueue UI or:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
key = client.create_api_key("ci-publisher", scopes=["publish"])
|
|
85
|
+
print(key["token"]) # shown once
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Errors raise `flowqueue.ApiError(status, code, message)`.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
flowqueue/__init__.py,sha256=EnfxSqLJ1gWkne2ESt6huQJOYHkhK5ocKT1Xa5jXW04,389
|
|
2
|
+
flowqueue/client.py,sha256=fV0eTW6wt4-b4VRwSCRg3Mn_mn0dbhKFh54-AxH2JUY,10823
|
|
3
|
+
flowqueue/consumer.py,sha256=nn-4LUd-I-5xtehyO92yKKote5AuXgMYRLVRTQvwwns,2581
|
|
4
|
+
flowqueue/errors.py,sha256=kj0ASuNpAMQYvcD__WNqeNfRGj_uqnTJU_Gz7aRdOEo,618
|
|
5
|
+
flowqueue/models.py,sha256=2rpvLqXh1Uj2Q7EZYoZROKuC-aWFxvLtwEImpixTrz8,804
|
|
6
|
+
flowqueue-0.1.0.dist-info/licenses/LICENSE,sha256=HY8eppn2qqxGTR4WJwFhdKwTGEJ89QQ7e1-6TpJWMRQ,1066
|
|
7
|
+
flowqueue-0.1.0.dist-info/METADATA,sha256=UV6FluqcLep6-tMEd9sRZGUE9HpVBVdEZ_GEgJDevWo,2673
|
|
8
|
+
flowqueue-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
flowqueue-0.1.0.dist-info/top_level.txt,sha256=Xf0tKH0SZR4P9Kj5BjNPPEt6OXhmV2BqQr6MXEe39o0,10
|
|
10
|
+
flowqueue-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlowQueue
|
|
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 @@
|
|
|
1
|
+
flowqueue
|