bladekit 4.5.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.
bladekit/_queue.py ADDED
@@ -0,0 +1,137 @@
1
+ """bladekit.Queue — distributed FIFO queue.
2
+
3
+ Persistent, backed by Postgres on the control plane. Used for producer/consumer
4
+ coordination between functions or between local scripts and remote workers.
5
+
6
+ Protocol (conventions.md §14.5, map-protocol.md is a SEPARATE thing):
7
+ - put(value): append an item to the tail of the queue.
8
+ - get(timeout=0): claim-and-delete the head item atomically
9
+ (FOR UPDATE SKIP LOCKED). Two concurrent get() calls never return the same
10
+ item. Returns None if the queue is empty and timeout=0. Blocks up to timeout
11
+ seconds (long-poll on the server) if timeout > 0.
12
+ - len(): current item count (non-transactional, best-effort).
13
+ - from_name(name): get-or-create by name (global namespace, RBAC planned).
14
+
15
+ Limits (conventions.md §6):
16
+ - Up to 100,000 items per queue.
17
+ - Blocking get: timeout 0–600 s.
18
+ - Item payload: ≤ 1 MiB inline; object store beyond.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any, Optional
24
+
25
+
26
+ class Queue:
27
+ """A distributed FIFO queue persistent in the AI Fabric control plane.
28
+
29
+ Usage::
30
+
31
+ q = bladekit.Queue.from_name("jobs")
32
+ q.put({"url": "https://example.com"})
33
+ item = q.get(timeout=30) # blocks up to 30s
34
+
35
+ Items are serialized with cloudpickle (same format as function arguments).
36
+ The queue is identified by name; ``from_name`` is idempotent (get-or-create).
37
+ """
38
+
39
+ def __init__(self, name: str, client: Any) -> None:
40
+ self._name = name
41
+ self._client = client
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ return self._name
46
+
47
+ def __repr__(self) -> str:
48
+ return f"bladekit.Queue(name={self._name!r})"
49
+
50
+ @classmethod
51
+ def from_name(cls, name: str, *, create_if_missing: bool = False) -> "Queue":
52
+ """Reference a queue by name (get-or-create on first use).
53
+
54
+ No round-trip here: the queue is created lazily by the server on the
55
+ first put/get (openapi.yaml: "get-or-create by name on first use"). Two
56
+ references with the same name address the same queue. ``create_if_missing``
57
+ is accepted for parity with ``Volume.from_name`` (and Modal) but is a
58
+ no-op — queues are always get-or-create on first use.
59
+ """
60
+ _ = create_if_missing
61
+ from ._client import Client
62
+ from .config import load_config
63
+ from . import exceptions as exc_mod
64
+
65
+ cfg = load_config()
66
+ if not cfg.token:
67
+ raise exc_mod.AuthenticationError(
68
+ "UNAUTHENTICATED",
69
+ "no token configured: run `blade token set <token>` or set FABRIC_TOKEN",
70
+ )
71
+ return cls(name, Client(cfg.api_url, cfg.token))
72
+
73
+ @classmethod
74
+ def _from_client(cls, name: str, client: Any) -> "Queue":
75
+ """Create a Queue object backed by an existing client (for tests)."""
76
+ return cls(name, client)
77
+
78
+ # -- operations -------------------------------------------------------
79
+
80
+ def put(self, value: Any) -> None:
81
+ """Append an item to the tail of the queue.
82
+
83
+ The item is serialized with cloudpickle. Raises PayloadTooLargeError
84
+ if the serialized item exceeds 1 MiB (QUEUE_ITEM_TOO_LARGE).
85
+ """
86
+ from ._serialize import dumps_args, make_payload_in
87
+
88
+ raw = dumps_args((value,), {})
89
+ payload_in, object_bytes = make_payload_in(raw)
90
+ if object_bytes is not None:
91
+ decl = self._client.create_payload(
92
+ payload_in["size_bytes"], payload_in["sha256"]
93
+ )
94
+ self._client.upload_to_presigned(
95
+ decl["put_url"], object_bytes, "application/octet-stream"
96
+ )
97
+ payload_in["payload_id"] = decl["payload_id"]
98
+ self._client.queue_put(self._name, payload_in)
99
+
100
+ def get(self, *, timeout: int = 0) -> Optional[Any]:
101
+ """Claim and remove the head item.
102
+
103
+ Returns the item value, or None if the queue is empty and timeout=0.
104
+ With timeout > 0, the call blocks on the server for up to ``timeout``
105
+ seconds (long-poll, max 600 s). Returns None on timeout.
106
+
107
+ Two concurrent get() calls never return the same item.
108
+ """
109
+ from ._serialize import decode_payload_out, loads
110
+
111
+ resp = self._client.queue_get(self._name, timeout=timeout)
112
+ # QueueGetResponse: {item: PayloadOut|null, item_id}. item is the payload
113
+ # directly (not wrapped); null = empty.
114
+ payload = resp.get("item")
115
+ if payload is None:
116
+ return None
117
+ raw = decode_payload_out(payload, fetch=self._client.download_presigned)
118
+ # put() serialized (value,) as args; unwrap the tuple.
119
+ result = loads(raw)
120
+ if isinstance(result, tuple) and len(result) == 2:
121
+ args, kwargs = result
122
+ if isinstance(args, tuple) and len(args) == 1 and not kwargs:
123
+ return args[0]
124
+ return result
125
+
126
+ def len(self) -> int:
127
+ """Return the approximate number of items currently in the queue.
128
+
129
+ The count is non-transactional (another consumer may have claimed items
130
+ between the request and this return value).
131
+ """
132
+ resp = self._client.get_queue(self._name)
133
+ return int(resp.get("len", 0))
134
+
135
+ def delete(self) -> None:
136
+ """Delete the queue and all its items."""
137
+ self._client.delete_queue(self._name)