queues-sdk 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.
- queues_sdk/__init__.py +28 -0
- queues_sdk/_version.py +1 -0
- queues_sdk/core.py +224 -0
- queues_sdk/errors.py +2 -0
- queues_sdk/py.typed +0 -0
- queues_sdk/watermarks.py +19 -0
- queues_sdk-0.1.0.dist-info/METADATA +105 -0
- queues_sdk-0.1.0.dist-info/RECORD +10 -0
- queues_sdk-0.1.0.dist-info/WHEEL +4 -0
- queues_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
queues_sdk/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""queues_sdk - zero-loss async queue SDK.
|
|
2
|
+
|
|
3
|
+
One class does the work::
|
|
4
|
+
|
|
5
|
+
from queues_sdk import Queues
|
|
6
|
+
|
|
7
|
+
q = Queues() # maxsize=1024*512, thresholds built-in
|
|
8
|
+
await q.publish(message) # unnamed: memory path, never dropped
|
|
9
|
+
await q.publish(message, name="audit") # named: pressure-gated spill
|
|
10
|
+
message = await q.consume() # backpressure-aware receive
|
|
11
|
+
message = await q.consume(name="audit") # auto-drains disk spill first
|
|
12
|
+
|
|
13
|
+
Pressure semantics (memory/disk watermarks, 0-100):
|
|
14
|
+
|
|
15
|
+
- disk hard line -> named messages soak in RAM, spill resumes when disk recovers
|
|
16
|
+
- memory soft line -> named messages spill to disk as batch-*.jsonl, FIFO drain
|
|
17
|
+
- queue full -> publish awaits space instead of raising; queue-head never evicted
|
|
18
|
+
|
|
19
|
+
Zero-loss promise: messages are only lost if memory AND disk are exhausted.
|
|
20
|
+
Thresholds are constructor arguments, never config files. Import name is
|
|
21
|
+
``queues_sdk``; the PyPI distribution is ``queues-sdk``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ._version import __version__
|
|
25
|
+
from .core import Queues
|
|
26
|
+
from .errors import QueuesError
|
|
27
|
+
|
|
28
|
+
__all__ = ["Queues", "QueuesError", "__version__"]
|
queues_sdk/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
queues_sdk/core.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import sys, os, time, asyncio, uuid, traceback, threading, collections, json, shutil, datetime, pathlib
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from queues_sdk.watermarks import default_watermarks
|
|
5
|
+
from queues_sdk.errors import QueuesError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Queues:
|
|
13
|
+
def __init__(self, maxsize: int = 1024*512, memory_max: int = 99, disk_max: int = 99, spill_path: str = "~/.local/queues_sdk/spill/", spill_batch: int = 1024, watermarks: Callable[[], tuple[int, int]] | None = None):
|
|
14
|
+
self.pending = dict()
|
|
15
|
+
self.draining = dict()
|
|
16
|
+
self.checked = set()
|
|
17
|
+
self.pressure_memory_max = int(memory_max)
|
|
18
|
+
self.pressure_disk_max = int(disk_max)
|
|
19
|
+
self.pressure_spill_path = str(spill_path)
|
|
20
|
+
self.pressure_spill_batch = int(spill_batch)
|
|
21
|
+
self._watermarks = watermarks if watermarks is not None else default_watermarks
|
|
22
|
+
self.db = collections.deque(maxlen=int(maxsize))
|
|
23
|
+
self.db_append = self.db.append
|
|
24
|
+
self.db_popleft = self.db.popleft
|
|
25
|
+
self.event = asyncio.Event()
|
|
26
|
+
self.waiters = collections.deque()
|
|
27
|
+
self.waiters_append = self.waiters.append
|
|
28
|
+
self.waiters_popleft = self.waiters.popleft
|
|
29
|
+
self.space_waiters = collections.deque()
|
|
30
|
+
self.space_waiters_append = self.space_waiters.append
|
|
31
|
+
self.space_waiters_popleft = self.space_waiters.popleft
|
|
32
|
+
self.almightydb_service_logger_warning = asyncio.Queue(maxsize=1024*4)
|
|
33
|
+
self.almightydb_service_logger_info = asyncio.Queue(maxsize=1024*4)
|
|
34
|
+
self.almightydb_service_logger_debug = asyncio.Queue(maxsize=1024*4)
|
|
35
|
+
self.almightydb_service_logger_error = asyncio.Queue(maxsize=1024*4)
|
|
36
|
+
self.almightydb_service_logger_store = asyncio.Queue(maxsize=1024*4)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def state(self) -> str:
|
|
43
|
+
try:
|
|
44
|
+
# 磁盘过硬线 → hard;内存过硬线 → soft;否则 normal。
|
|
45
|
+
memory, disk = self._watermarks()
|
|
46
|
+
if int(disk) >= int(self.pressure_disk_max): return "hard"
|
|
47
|
+
if int(memory) >= int(self.pressure_memory_max): return "soft"
|
|
48
|
+
return "normal"
|
|
49
|
+
|
|
50
|
+
except BaseException: return "normal"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def gate(self, name: str | None, message) -> str:
|
|
57
|
+
try:
|
|
58
|
+
# 写侧统一入口:'spill' 压力缓冲(硬线滞留内存不写盘/软线溢出落盘)/ 'pass' 内存路径。
|
|
59
|
+
memory, disk = self._watermarks()
|
|
60
|
+
hard = int(disk) >= int(self.pressure_disk_max)
|
|
61
|
+
if name is None: return "pass"
|
|
62
|
+
if name not in self.checked:
|
|
63
|
+
self.checked.add(name)
|
|
64
|
+
directory = self._spill_directory(name)
|
|
65
|
+
if directory.is_dir() and any(directory.glob("*.jsonl")): self.draining[name] = True
|
|
66
|
+
if hard or int(memory) >= int(self.pressure_memory_max) or self.draining.get(name):
|
|
67
|
+
lines = self.pending.setdefault(name, list())
|
|
68
|
+
lines.append(json.dumps(message, ensure_ascii=False, default=str))
|
|
69
|
+
# 攒批到界且磁盘未硬线时落盘一个 batch-*.jsonl,行缓冲随即清空。
|
|
70
|
+
if len(lines) >= int(self.pressure_spill_batch) and not hard:
|
|
71
|
+
directory = self._spill_directory(name)
|
|
72
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
path = directory / "batch-{}.jsonl".format(time.monotonic_ns())
|
|
74
|
+
with open(path, "w", encoding="utf-8") as handle: handle.write("\n".join(lines) + "\n")
|
|
75
|
+
lines.clear()
|
|
76
|
+
return "spill"
|
|
77
|
+
return "pass"
|
|
78
|
+
|
|
79
|
+
except BaseException as error: await self.__log_error("gate", error); return "pass"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _spill_directory(self, name: str):
|
|
86
|
+
return pathlib.Path(self.pressure_spill_path).expanduser() / str(name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def drain(self, name: str, target, limit: int | None = None) -> int:
|
|
93
|
+
try:
|
|
94
|
+
# 有界回灌:盘上旧 batch 文件在前 FIFO,pending 就地落成最新文件在后,顺序自然正确。
|
|
95
|
+
limit = int(self.pressure_spill_batch) if limit is None else int(limit)
|
|
96
|
+
pulled = int(0); lines = self.pending.get(name); directory = self._spill_directory(name)
|
|
97
|
+
if lines:
|
|
98
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
path = directory / "batch-{}.jsonl".format(time.monotonic_ns())
|
|
100
|
+
with open(path, "w", encoding="utf-8") as handle: handle.write("\n".join(lines) + "\n")
|
|
101
|
+
lines.clear()
|
|
102
|
+
if directory.is_dir():
|
|
103
|
+
for path in sorted(directory.glob("*.jsonl")):
|
|
104
|
+
if pulled >= limit: break
|
|
105
|
+
try:
|
|
106
|
+
with open(path, encoding="utf-8") as handle: batch = [line.strip() for line in handle if line.strip()]
|
|
107
|
+
target.extend(json.loads(line) for line in batch); pulled = pulled + len(batch)
|
|
108
|
+
path.unlink()
|
|
109
|
+
except BaseException:
|
|
110
|
+
# 毒文件改名 .bad 隔离,不阻塞 FIFO。
|
|
111
|
+
os.replace(path, directory / (path.name + ".bad"))
|
|
112
|
+
self.checked.add(name)
|
|
113
|
+
if self.pending.get(name) or (directory.is_dir() and any(directory.glob("*.jsonl"))): self.draining[name] = True
|
|
114
|
+
else: self.draining.pop(name, None)
|
|
115
|
+
return pulled
|
|
116
|
+
|
|
117
|
+
except BaseException as error: await self.__log_error("drain", error); return int(0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def discard(self, name: str) -> int:
|
|
124
|
+
try:
|
|
125
|
+
# 清盘并整盘对账:丢弃条数 = pending 行数 + 盘上 batch 行数。
|
|
126
|
+
dropped = len(self.pending.pop(name, list()))
|
|
127
|
+
self.draining.pop(name, None)
|
|
128
|
+
self.checked.discard(name)
|
|
129
|
+
directory = self._spill_directory(name)
|
|
130
|
+
if directory.is_dir():
|
|
131
|
+
for path in directory.glob("*.jsonl"):
|
|
132
|
+
with open(path, encoding="utf-8") as handle: dropped = dropped + len([line for line in handle if line.strip()])
|
|
133
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
134
|
+
return int(dropped)
|
|
135
|
+
|
|
136
|
+
except BaseException as error: await self.__log_error("discard", error); return int(0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def publish(self, message, name: str | None = None):
|
|
143
|
+
try:
|
|
144
|
+
# 未命名消息直走内存路径(零依赖快路径);命名消息过压力门。
|
|
145
|
+
action = "pass" if name is None else await self.gate(name, message)
|
|
146
|
+
if action == "spill": return None
|
|
147
|
+
if self.waiters and not self.waiters[0].done(): self.waiters_popleft().set_result(message); return None
|
|
148
|
+
if self.waiters: self.waiters_popleft()
|
|
149
|
+
# 容量满 → 背压等待消费腾位,不拒绝不挤队首(零丢失)。
|
|
150
|
+
while self.db.maxlen is not None and len(self.db) >= self.db.maxlen:
|
|
151
|
+
producer = asyncio.get_running_loop().create_future()
|
|
152
|
+
self.space_waiters_append(producer)
|
|
153
|
+
try:
|
|
154
|
+
await producer
|
|
155
|
+
except asyncio.CancelledError:
|
|
156
|
+
if producer in self.space_waiters: self.space_waiters.remove(producer)
|
|
157
|
+
raise
|
|
158
|
+
self.db_append(message)
|
|
159
|
+
|
|
160
|
+
except BaseException as error:
|
|
161
|
+
# 取消必须上抛:吸收会让 producer 误以为消息已入队(丢失语义)。
|
|
162
|
+
if isinstance(error, asyncio.CancelledError): raise
|
|
163
|
+
await self.__log_error("publish", error); return None; return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def consume(self, name: str | None = None):
|
|
170
|
+
try:
|
|
171
|
+
if len(self.db) > 0:
|
|
172
|
+
message = self.db_popleft()
|
|
173
|
+
if self.space_waiters and self.db.maxlen is not None and len(self.db) < self.db.maxlen: self.space_waiters_popleft().set_result(None)
|
|
174
|
+
return message
|
|
175
|
+
if name and (self.draining.get(name) or self.pending.get(name)): await self.drain(name, self.db)
|
|
176
|
+
if len(self.db) > 0: return self.db_popleft()
|
|
177
|
+
waiter = asyncio.get_running_loop().create_future()
|
|
178
|
+
self.waiters_append(waiter)
|
|
179
|
+
# 取消时清理等待者并上抛(吸收取消会让消费端拿到 None 混淆空值语义)。
|
|
180
|
+
try:
|
|
181
|
+
message = await waiter
|
|
182
|
+
except asyncio.CancelledError:
|
|
183
|
+
if waiter in self.waiters: self.waiters.remove(waiter)
|
|
184
|
+
raise
|
|
185
|
+
return message
|
|
186
|
+
|
|
187
|
+
except BaseException as error:
|
|
188
|
+
# 取消必须上抛:吸收会让消费循环把 None 当消息、teardown 永久等待。
|
|
189
|
+
if isinstance(error, asyncio.CancelledError): raise
|
|
190
|
+
await self.__log_error("consume", error); return None; return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def main(self):
|
|
197
|
+
try:
|
|
198
|
+
pass
|
|
199
|
+
except BaseException as error: await self.__log_error("main", error)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def __log_error(self, method: str, error: BaseException):
|
|
206
|
+
try:
|
|
207
|
+
import logging
|
|
208
|
+
frames = traceback.extract_tb(error.__traceback__)
|
|
209
|
+
frame = frames[-1] if frames else None
|
|
210
|
+
detail = {
|
|
211
|
+
"error_id": uuid.uuid4().hex,
|
|
212
|
+
"error_time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
213
|
+
"error_method": "{}.{}".format(type(self).__name__, method),
|
|
214
|
+
"error_type": type(error).__name__,
|
|
215
|
+
"error_value": str(error),
|
|
216
|
+
"error_file": os.path.abspath(frame.filename) if frame else "unknown",
|
|
217
|
+
"error_line": frame.lineno if frame else 0,
|
|
218
|
+
"error_pid": os.getpid(),
|
|
219
|
+
"error_thread": threading.current_thread().name,
|
|
220
|
+
"error_traceback": "".join(traceback.format_exception(type(error), error, error.__traceback__)),
|
|
221
|
+
}
|
|
222
|
+
logging.getLogger("queues_sdk").error("[queues_sdk error] [{}]".format(detail))
|
|
223
|
+
|
|
224
|
+
except BaseException: pass
|
queues_sdk/errors.py
ADDED
queues_sdk/py.typed
ADDED
|
File without changes
|
queues_sdk/watermarks.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def default_watermarks() -> tuple[int, int]:
|
|
8
|
+
try:
|
|
9
|
+
memory_total = int(0); memory_available = int(0)
|
|
10
|
+
with open("/proc/meminfo", encoding="utf-8") as handle:
|
|
11
|
+
for line in handle:
|
|
12
|
+
if line.startswith("MemTotal:"): memory_total = int(line.split()[1])
|
|
13
|
+
elif line.startswith("MemAvailable:"): memory_available = int(line.split()[1])
|
|
14
|
+
usage = os.statvfs("/")
|
|
15
|
+
disk_percent = int(100 - usage.f_bavail * 100 / max(int(usage.f_blocks), 1))
|
|
16
|
+
memory_percent = int(100 - memory_available * 100 / max(memory_total, 1))
|
|
17
|
+
return (int(memory_percent), int(disk_percent))
|
|
18
|
+
|
|
19
|
+
except OSError: return (int(0), int(0))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: queues-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-loss async queue SDK: direct waiter handoff, backpressure waiting, pressure-gated spill to disk with FIFO drain. No network, no dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/scapking/queues-sdk
|
|
6
|
+
Project-URL: Source, https://github.com/scapking/queues-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/scapking/queues-sdk/issues
|
|
8
|
+
Author: queues-sdk contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: asyncio,backpressure,pressure,queue,sdk,spill,zero-loss
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
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 :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# queues-sdk
|
|
29
|
+
|
|
30
|
+
**Zero-loss async queue SDK for Python.** Direct waiter handoff, backpressure waiting, pressure-gated spill to disk with FIFO drain. No network, no dependencies, stdlib only.
|
|
31
|
+
|
|
32
|
+
Import name is `queues_sdk`; the PyPI distribution is `queues-sdk`.
|
|
33
|
+
|
|
34
|
+
## Why
|
|
35
|
+
|
|
36
|
+
Plain `asyncio.Queue` either blocks forever when full or drops messages if you cap it. Real pipelines need a third behaviour: **never lose, wait when possible, spill to disk when memory is tight, and soak in RAM when disk is tight.** queues-sdk implements exactly that on top of a lock-free deque + future handoff design.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install queues-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from queues_sdk import Queues
|
|
49
|
+
|
|
50
|
+
q = Queues() # maxsize=1024*512, thresholds built-in
|
|
51
|
+
|
|
52
|
+
async def main():
|
|
53
|
+
await q.publish({"event": "signup"}) # unnamed: memory path
|
|
54
|
+
message = await q.consume() # backpressure-aware receive
|
|
55
|
+
|
|
56
|
+
await q.publish({"event": "audit"}, name="audit") # named: pressure-gated
|
|
57
|
+
message = await q.consume(name="audit") # auto-drains disk spill
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Zero-loss semantics
|
|
63
|
+
|
|
64
|
+
| Condition | Behaviour | Lost messages |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| normal watermarks, queue not full | lock-free deque append / future handoff | none |
|
|
67
|
+
| queue full | publish **awaits space** (backpressure); queue-head never evicted | none |
|
|
68
|
+
| memory soft line (>= memory_max) | named messages spill to disk as `batch-*.jsonl`, FIFO drain on consume | none |
|
|
69
|
+
| disk hard line (>= disk_max) | named messages soak in RAM, spill resumes when disk recovers | none |
|
|
70
|
+
| memory AND disk exhausted | no retreat | only here |
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
- `Queues(maxsize=1024*512, memory_max=99, disk_max=99, spill_path="~/.local/queues_sdk/spill/", spill_batch=1024, watermarks=None)`
|
|
75
|
+
- `await publish(message, name=None) -> None` — unnamed skips the pressure gate entirely (hot path)
|
|
76
|
+
- `await consume(name=None) -> Any` — auto-drains the named spill buffer before waiting
|
|
77
|
+
- `await drain(name, target_list, limit=None) -> int` — bounded FIFO refill from disk
|
|
78
|
+
- `await discard(name) -> int` — wipe a spill buffer, returns dropped-message count
|
|
79
|
+
- `state() -> "normal" | "soft" | "hard"` — current pressure mode
|
|
80
|
+
- Five pre-built `asyncio.Queue` log lanes (`almightydb_service_logger_warning/info/debug/error/store`, each 1024*4) ship inside every instance
|
|
81
|
+
|
|
82
|
+
Watermarks are injected: pass `watermarks=lambda: (memory_pct, disk_pct)`; the default provider reads `/proc/meminfo` + `statvfs("/")` on Linux and reports `(0, 0)` elsewhere.
|
|
83
|
+
|
|
84
|
+
## Performance
|
|
85
|
+
|
|
86
|
+
Measured on CPython 3.14 free-threaded, Linux, single process:
|
|
87
|
+
|
|
88
|
+
| Path | Throughput |
|
|
89
|
+
|---|---|
|
|
90
|
+
| publish (unnamed, normal) | ~820k ops/s |
|
|
91
|
+
| publish+consume pair (end-to-end TPS) | ~72k pairs/s |
|
|
92
|
+
| named spill (soft line) | ~140k ops/s |
|
|
93
|
+
| drain refill | ~356k ops/s |
|
|
94
|
+
|
|
95
|
+
Spill writes are batched (1024 lines per file): expect one ~1-4 ms disk pause per batch under the soft line.
|
|
96
|
+
|
|
97
|
+
## Docs
|
|
98
|
+
|
|
99
|
+
- [Getting started](docs/01-getting-started.md)
|
|
100
|
+
- [API reference](docs/02-api-reference.md)
|
|
101
|
+
- [Performance & latency profile](docs/03-performance.md)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
queues_sdk/__init__.py,sha256=k4y3HTy2XYnpJmBMjtX8zTzMelxndaDoaNn5QJm6Vtg,1145
|
|
2
|
+
queues_sdk/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
3
|
+
queues_sdk/core.py,sha256=JiaqzN0J7oNC-L2Vdvxr-0JAU49PUdvies1Isyg2mTY,9948
|
|
4
|
+
queues_sdk/errors.py,sha256=cS9NuGP79AMLBvEvn0pZl6Nb5II39GYwYL5yAFmHc60,138
|
|
5
|
+
queues_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
queues_sdk/watermarks.py,sha256=0JVd8aJIYUtk57eeQeFJxMxygyNnx-8Pe_e1RQ7GY_s,702
|
|
7
|
+
queues_sdk-0.1.0.dist-info/METADATA,sha256=um8H_2wad38-oESpru-HxCoovEc6VwvYGVAgHrNe3Ug,4374
|
|
8
|
+
queues_sdk-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
9
|
+
queues_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=IX60b71kGaHVYXcr6DRaALn1phbboivn1leualFq7ZE,1080
|
|
10
|
+
queues_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 queues-sdk contributors
|
|
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.
|