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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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