dockbay 0.0.0__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.
@@ -0,0 +1,8 @@
1
+ .venv
2
+ .pytest_cache
3
+ .ruff_cache
4
+ dist
5
+ *.egg-info
6
+ __pycache__
7
+ .DS_Store
8
+
@@ -0,0 +1,16 @@
1
+ # gitleaks config — runs in CI. Extends the default ruleset.
2
+ #
3
+ # Run manually:
4
+ # gitleaks detect --config .gitleaks.toml --redact
5
+ # gitleaks protect --staged --config .gitleaks.toml
6
+
7
+ [extend]
8
+ useDefault = true
9
+
10
+ [allowlist]
11
+ description = "allowlist — docs and example/template files only"
12
+ paths = [
13
+ '''\.gitleaks\.toml$''',
14
+ '''README\.md$''',
15
+ '''CONTRIBUTING\.md$''',
16
+ ]
dockbay-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: dockbay
3
+ Version: 0.0.0
4
+ Summary: Backend driver substrate for primitive store adapters.
5
+ Project-URL: Homepage, https://github.com/cachetronaut/dockbay
6
+ Project-URL: Repository, https://github.com/cachetronaut/dockbay
7
+ Project-URL: Issues, https://github.com/cachetronaut/dockbay/issues
8
+ License: MIT
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: psycopg-pool>=3.2
11
+ Requires-Dist: psycopg[binary]>=3.2
12
+ Description-Content-Type: text/markdown
13
+
14
+ # dockbay
15
+
16
+ Python implementation of DockBay.
17
+
18
+ For product-level context, shared contracts, and cross-language repository information, see the public repository: https://github.com/cachetronaut/dockbay.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ pip install dockbay
24
+ ```
25
+
26
+ ## Import
27
+
28
+ ```python
29
+ import dockbay
30
+ ```
31
+
32
+ ## Development
33
+
34
+ Run from `py/`:
35
+
36
+ ```sh
37
+ uv sync --dev
38
+ uv run --with ruff ruff check .
39
+ uv run --with ruff ruff format --check .
40
+ uv run --with ty ty check
41
+ uv run --with pytest --with pytest-asyncio python -m pytest
42
+ ```
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,33 @@
1
+ # dockbay
2
+
3
+ Python implementation of DockBay.
4
+
5
+ For product-level context, shared contracts, and cross-language repository information, see the public repository: https://github.com/cachetronaut/dockbay.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install dockbay
11
+ ```
12
+
13
+ ## Import
14
+
15
+ ```python
16
+ import dockbay
17
+ ```
18
+
19
+ ## Development
20
+
21
+ Run from `py/`:
22
+
23
+ ```sh
24
+ uv sync --dev
25
+ uv run --with ruff ruff check .
26
+ uv run --with ruff ruff format --check .
27
+ uv run --with ty ty check
28
+ uv run --with pytest --with pytest-asyncio python -m pytest
29
+ ```
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "dockbay"
3
+ version = "0.0.0"
4
+ description = "Backend driver substrate for primitive store adapters."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ dependencies = ["psycopg[binary]>=3.2", "psycopg-pool>=3.2"]
9
+
10
+ [project.urls]
11
+ Homepage = "https://github.com/cachetronaut/dockbay"
12
+ Repository = "https://github.com/cachetronaut/dockbay"
13
+ Issues = "https://github.com/cachetronaut/dockbay/issues"
14
+
15
+ [build-system]
16
+ requires = ["hatchling"]
17
+ build-backend = "hatchling.build"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/dockbay"]
21
+
22
+ [dependency-groups]
23
+ dev = ["pytest>=8", "ruff>=0.6", "ty>=0.0.1a8"]
24
+
25
+ [tool.ruff]
26
+ line-length = 100
27
+ target-version = "py311"
28
+
29
+ [tool.ruff.lint]
30
+ select = ["E", "F", "I", "UP", "B", "SIM"]
31
+
32
+ [tool.pytest.ini_options]
33
+ pythonpath = ["src"]
34
+ testpaths = ["tests"]
35
+
36
+ [tool.ty.environment]
37
+ extra-paths = ["src"]
@@ -0,0 +1,49 @@
1
+ from .core import (
2
+ ConvexOperationContext,
3
+ ConvexOperationDriver,
4
+ ConvexOperationKind,
5
+ ConvexOperationKindError,
6
+ ConvexStoreOperation,
7
+ InMemoryConvexOperationHost,
8
+ InMemoryStoreDriver,
9
+ JsonValue,
10
+ MigrationSet,
11
+ MissingConvexOperationError,
12
+ PostgresStoreDriver,
13
+ PostgresStoreDriverOptions,
14
+ Row,
15
+ ScanOptions,
16
+ StoreDriver,
17
+ Transaction,
18
+ canonicalize,
19
+ compare_rows,
20
+ create_in_memory_driver,
21
+ create_postgres_driver,
22
+ key_of,
23
+ matches_prefix,
24
+ )
25
+
26
+ __all__ = [
27
+ "InMemoryStoreDriver",
28
+ "ConvexOperationContext",
29
+ "ConvexOperationDriver",
30
+ "ConvexOperationKind",
31
+ "ConvexOperationKindError",
32
+ "ConvexStoreOperation",
33
+ "InMemoryConvexOperationHost",
34
+ "JsonValue",
35
+ "MigrationSet",
36
+ "MissingConvexOperationError",
37
+ "PostgresStoreDriver",
38
+ "PostgresStoreDriverOptions",
39
+ "Row",
40
+ "ScanOptions",
41
+ "StoreDriver",
42
+ "Transaction",
43
+ "canonicalize",
44
+ "compare_rows",
45
+ "create_in_memory_driver",
46
+ "create_postgres_driver",
47
+ "key_of",
48
+ "matches_prefix",
49
+ ]
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import re
6
+ from collections.abc import AsyncIterator, Awaitable, Callable
7
+ from dataclasses import dataclass
8
+ from typing import Any, Literal, Protocol, TypeAlias
9
+
10
+ from psycopg import AsyncConnection, sql
11
+ from psycopg.types.json import Jsonb
12
+ from psycopg_pool import AsyncConnectionPool
13
+
14
+ Row = dict[str, Any]
15
+ JsonValue: TypeAlias = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
16
+ ConvexOperationKind: TypeAlias = Literal["mutation", "query"]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ScanOptions:
21
+ after: Row | None = None
22
+ limit: int | None = None
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MigrationSet:
27
+ backend: str
28
+ statements: list[str]
29
+
30
+
31
+ class Transaction(Protocol):
32
+ async def upsert(self, table: str, key: Row, row: Row) -> None: ...
33
+ async def get(self, table: str, key: Row) -> Row | None: ...
34
+ def scan(
35
+ self, table: str, prefix: Row, opts: ScanOptions | None = None
36
+ ) -> AsyncIterator[Row]: ...
37
+ async def compare_and_apply(
38
+ self, table: str, key: Row, expect: Any, next_value: Any
39
+ ) -> bool: ...
40
+
41
+
42
+ class StoreDriver(Protocol):
43
+ backend: str
44
+
45
+ async def transaction(self, work: Callable[[Transaction], Awaitable[Any]]) -> Any: ...
46
+ async def close(self) -> None: ...
47
+
48
+
49
+ class ConvexOperationDriver(Protocol):
50
+ async def call(self, operation: str, input_value: JsonValue) -> JsonValue: ...
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class ConvexOperationContext:
55
+ kind: ConvexOperationKind
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ConvexStoreOperation:
60
+ name: str
61
+ kind: ConvexOperationKind
62
+ run: Callable[[ConvexOperationContext, JsonValue], Awaitable[JsonValue]]
63
+
64
+
65
+ class MissingConvexOperationError(Exception):
66
+ def __init__(self, operation: str) -> None:
67
+ super().__init__(f"Unknown Convex store operation: {operation}")
68
+ self.operation = operation
69
+
70
+
71
+ class ConvexOperationKindError(Exception):
72
+ def __init__(
73
+ self, operation: str, expected: ConvexOperationKind, actual: ConvexOperationKind
74
+ ) -> None:
75
+ super().__init__(f"Convex store operation {operation} is {actual}; expected {expected}")
76
+ self.operation = operation
77
+ self.expected = expected
78
+ self.actual = actual
79
+
80
+
81
+ class InMemoryConvexOperationHost:
82
+ def __init__(self, operations: list[ConvexStoreOperation] | None = None) -> None:
83
+ self._operations: dict[str, ConvexStoreOperation] = {}
84
+ for operation in operations or []:
85
+ self.register(operation)
86
+
87
+ def register(self, operation: ConvexStoreOperation) -> None:
88
+ if operation.name in self._operations:
89
+ raise ValueError(f"Duplicate Convex store operation: {operation.name}")
90
+ self._operations[operation.name] = operation
91
+
92
+ async def call(self, operation: str, input_value: JsonValue) -> JsonValue:
93
+ handler = self._operation(operation)
94
+ return await handler.run(ConvexOperationContext(kind=handler.kind), input_value)
95
+
96
+ async def call_mutation(self, operation: str, input_value: JsonValue) -> JsonValue:
97
+ return await self._call_kind(operation, input_value, "mutation")
98
+
99
+ async def call_query(self, operation: str, input_value: JsonValue) -> JsonValue:
100
+ return await self._call_kind(operation, input_value, "query")
101
+
102
+ def create_driver(self) -> ConvexOperationDriver:
103
+ return _HostedConvexOperationDriver(self)
104
+
105
+ async def _call_kind(
106
+ self, operation: str, input_value: JsonValue, expected: ConvexOperationKind
107
+ ) -> JsonValue:
108
+ handler = self._operation(operation)
109
+ if handler.kind != expected:
110
+ raise ConvexOperationKindError(operation, expected, handler.kind)
111
+ return await self.call(operation, input_value)
112
+
113
+ def _operation(self, operation: str) -> ConvexStoreOperation:
114
+ handler = self._operations.get(operation)
115
+ if handler is None:
116
+ raise MissingConvexOperationError(operation)
117
+ return handler
118
+
119
+
120
+ class _HostedConvexOperationDriver:
121
+ def __init__(self, host: InMemoryConvexOperationHost) -> None:
122
+ self._host = host
123
+
124
+ async def call(self, operation: str, input_value: JsonValue) -> JsonValue:
125
+ return await self._host.call(operation, input_value)
126
+
127
+
128
+ def canonicalize(value: object) -> str:
129
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), default=_json_default)
130
+
131
+
132
+ def key_of(key: Row) -> str:
133
+ return canonicalize(key)
134
+
135
+
136
+ def matches_prefix(key: Row, prefix: Row) -> bool:
137
+ for name, expected in prefix.items():
138
+ if canonicalize(key.get(name)) != canonicalize(expected):
139
+ return False
140
+ return True
141
+
142
+
143
+ def compare_rows(left: Row, right: Row) -> int:
144
+ left_key = key_of(left)
145
+ right_key = key_of(right)
146
+ if left_key < right_key:
147
+ return -1
148
+ if left_key > right_key:
149
+ return 1
150
+ return 0
151
+
152
+
153
+ class InMemoryStoreDriver:
154
+ backend = "memory"
155
+
156
+ def __init__(self) -> None:
157
+ self._tables: dict[str, dict[str, _Entry]] = {}
158
+ self._lock = asyncio.Lock()
159
+ self._closed = False
160
+
161
+ async def transaction(self, work: Callable[[Transaction], Awaitable[Any]]) -> Any:
162
+ if self._closed:
163
+ raise RuntimeError("Store driver is closed")
164
+ async with self._lock:
165
+ return await work(_InMemoryTransaction(self._tables))
166
+
167
+ async def close(self) -> None:
168
+ self._closed = True
169
+
170
+
171
+ def create_in_memory_driver() -> InMemoryStoreDriver:
172
+ return InMemoryStoreDriver()
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class PostgresStoreDriverOptions:
177
+ table: str = "store_driver_rows"
178
+
179
+
180
+ class PostgresStoreDriver:
181
+ backend = "postgres"
182
+
183
+ def __init__(
184
+ self, pool: AsyncConnectionPool, options: PostgresStoreDriverOptions | None = None
185
+ ):
186
+ self._pool = pool
187
+ self._table = (options or PostgresStoreDriverOptions()).table
188
+ if not _IDENTIFIER.match(self._table):
189
+ raise ValueError(f"Invalid Postgres store-driver table: {self._table}")
190
+ self._ready = False
191
+
192
+ async def transaction(self, work: Callable[[Transaction], Awaitable[Any]]) -> Any:
193
+ await self._ensure_ready()
194
+ async with self._pool.connection() as conn, conn.transaction():
195
+ return await work(_PostgresTransaction(conn, self._table_sql()))
196
+
197
+ async def close(self) -> None:
198
+ await self._pool.close()
199
+
200
+ def _table_sql(self) -> sql.Identifier:
201
+ return sql.Identifier(self._table)
202
+
203
+ async def _ensure_ready(self) -> None:
204
+ if self._ready:
205
+ return
206
+ async with self._pool.connection() as conn:
207
+ await conn.execute(
208
+ sql.SQL(
209
+ "CREATE TABLE IF NOT EXISTS {table} ("
210
+ " table_name text NOT NULL,"
211
+ " key_json text NOT NULL,"
212
+ " key jsonb NOT NULL,"
213
+ " row jsonb NOT NULL,"
214
+ " value jsonb NOT NULL,"
215
+ " PRIMARY KEY (table_name, key_json)"
216
+ ")"
217
+ ).format(table=self._table_sql())
218
+ )
219
+ self._ready = True
220
+
221
+
222
+ def create_postgres_driver(
223
+ pool: AsyncConnectionPool, options: PostgresStoreDriverOptions | None = None
224
+ ) -> PostgresStoreDriver:
225
+ return PostgresStoreDriver(pool, options)
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class _Entry:
230
+ key: Row
231
+ row: Row
232
+ value: Any
233
+
234
+
235
+ class _InMemoryTransaction:
236
+ def __init__(self, tables: dict[str, dict[str, _Entry]]) -> None:
237
+ self._tables = tables
238
+
239
+ async def upsert(self, table: str, key: Row, row: Row) -> None:
240
+ self._table(table)[key_of(key)] = _Entry(key=key, row=row, value=row)
241
+
242
+ async def get(self, table: str, key: Row) -> Row | None:
243
+ entry = self._table(table).get(key_of(key))
244
+ return entry.row if entry is not None else None
245
+
246
+ async def scan(
247
+ self, table: str, prefix: Row, opts: ScanOptions | None = None
248
+ ) -> AsyncIterator[Row]:
249
+ options = opts or ScanOptions()
250
+ rows = sorted(
251
+ [entry for entry in self._table(table).values() if matches_prefix(entry.key, prefix)],
252
+ key=lambda entry: key_of(entry.key),
253
+ )
254
+ emitted = 0
255
+ for entry in rows:
256
+ if options.after is not None and compare_rows(entry.key, options.after) <= 0:
257
+ continue
258
+ if options.limit is not None and emitted >= options.limit:
259
+ break
260
+ emitted += 1
261
+ yield entry.row
262
+
263
+ async def compare_and_apply(self, table: str, key: Row, expect: Any, next_value: Any) -> bool:
264
+ rows = self._table(table)
265
+ entry_id = key_of(key)
266
+ current = rows.get(entry_id)
267
+ current_value = current.value if current is not None else None
268
+ if key_of({"value": current_value}) != key_of({"value": expect}):
269
+ return False
270
+ row = next_value if isinstance(next_value, dict) else {"value": next_value}
271
+ rows[entry_id] = _Entry(key=key, row=row, value=next_value)
272
+ return True
273
+
274
+ def _table(self, name: str) -> dict[str, _Entry]:
275
+ if name not in self._tables:
276
+ self._tables[name] = {}
277
+ return self._tables[name]
278
+
279
+
280
+ class _PostgresTransaction:
281
+ def __init__(self, conn: AsyncConnection[Any], table_sql: sql.Identifier) -> None:
282
+ self._conn = conn
283
+ self._table_sql = table_sql
284
+
285
+ async def upsert(self, table: str, key: Row, row: Row) -> None:
286
+ key_json = key_of(key)
287
+ statement = sql.SQL(
288
+ "INSERT INTO {table} (table_name, key_json, key, row, value) "
289
+ "VALUES (%s, %s, %s, %s, %s) "
290
+ "ON CONFLICT (table_name, key_json) DO UPDATE SET "
291
+ "key = EXCLUDED.key, row = EXCLUDED.row"
292
+ ).format(table=self._table_sql)
293
+ await self._conn.execute(statement, (table, key_json, Jsonb(key), Jsonb(row), Jsonb(row)))
294
+
295
+ async def get(self, table: str, key: Row) -> Row | None:
296
+ statement = sql.SQL(
297
+ "SELECT row FROM {table} WHERE table_name = %s AND key_json = %s"
298
+ ).format(table=self._table_sql)
299
+ cursor = await self._conn.execute(statement, (table, key_of(key)))
300
+ result = await cursor.fetchone()
301
+ return None if result is None else result[0]
302
+
303
+ async def scan(
304
+ self, table: str, prefix: Row, opts: ScanOptions | None = None
305
+ ) -> AsyncIterator[Row]:
306
+ options = opts or ScanOptions()
307
+ statement = sql.SQL("SELECT key, row FROM {table} WHERE table_name = %s").format(
308
+ table=self._table_sql
309
+ )
310
+ cursor = await self._conn.execute(statement, (table,))
311
+ rows = sorted(
312
+ [
313
+ {"key": key, "row": row}
314
+ for key, row in await cursor.fetchall()
315
+ if matches_prefix(key, prefix)
316
+ ],
317
+ key=lambda entry: key_of(entry["key"]),
318
+ )
319
+ emitted = 0
320
+ for entry in rows:
321
+ if options.after is not None and compare_rows(entry["key"], options.after) <= 0:
322
+ continue
323
+ if options.limit is not None and emitted >= options.limit:
324
+ break
325
+ emitted += 1
326
+ yield entry["row"]
327
+
328
+ async def compare_and_apply(self, table: str, key: Row, expect: Any, next_value: Any) -> bool:
329
+ row = next_value if isinstance(next_value, dict) else {"value": next_value}
330
+ key_json = key_of(key)
331
+ if expect is None:
332
+ statement = sql.SQL(
333
+ "INSERT INTO {table} (table_name, key_json, key, row, value) "
334
+ "VALUES (%s, %s, %s, %s, %s) ON CONFLICT (table_name, key_json) DO NOTHING"
335
+ ).format(table=self._table_sql)
336
+ cursor = await self._conn.execute(
337
+ statement, (table, key_json, Jsonb(key), Jsonb(row), Jsonb(row))
338
+ )
339
+ return cursor.rowcount == 1
340
+ statement = sql.SQL(
341
+ "UPDATE {table} SET row = %s, value = %s "
342
+ "WHERE table_name = %s AND key_json = %s AND row = %s"
343
+ ).format(table=self._table_sql)
344
+ cursor = await self._conn.execute(
345
+ statement, (Jsonb(row), Jsonb(row), table, key_json, Jsonb(expect))
346
+ )
347
+ return cursor.rowcount == 1
348
+
349
+
350
+ def _json_default(value: object) -> object:
351
+ if hasattr(value, "__dict__"):
352
+ return {key: item for key, item in vars(value).items() if item is not None}
353
+ raise TypeError(f"Cannot serialize {type(value)!r}")
354
+
355
+
356
+ _IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from dockbay import (
8
+ ConvexOperationKindError,
9
+ ConvexStoreOperation,
10
+ InMemoryConvexOperationHost,
11
+ MissingConvexOperationError,
12
+ )
13
+
14
+
15
+ def test_dispatches_named_mutation_operation() -> None:
16
+ asyncio.run(_assert_dispatches_named_mutation_operation())
17
+
18
+
19
+ async def _assert_dispatches_named_mutation_operation() -> None:
20
+ async def run(ctx, input_value):
21
+ assert ctx.kind == "mutation"
22
+ assert isinstance(input_value, dict)
23
+ return {"ok": True, "reserved": input_value["amount"]}
24
+
25
+ driver = InMemoryConvexOperationHost(
26
+ [ConvexStoreOperation(name="budget.tryReserve", kind="mutation", run=run)]
27
+ ).create_driver()
28
+
29
+ assert await driver.call("budget.tryReserve", {"amount": 2}) == {
30
+ "ok": True,
31
+ "reserved": 2,
32
+ }
33
+
34
+
35
+ def test_dispatches_queries_separately_from_mutations() -> None:
36
+ asyncio.run(_assert_dispatches_queries_separately_from_mutations())
37
+
38
+
39
+ async def _assert_dispatches_queries_separately_from_mutations() -> None:
40
+ async def run(ctx, input_value):
41
+ assert ctx.kind == "query"
42
+ assert isinstance(input_value, dict)
43
+ return {"revoked": input_value["jti"] == "jti_1"}
44
+
45
+ host = InMemoryConvexOperationHost(
46
+ [ConvexStoreOperation(name="revocation.isRevoked", kind="query", run=run)]
47
+ )
48
+
49
+ assert await host.call_query("revocation.isRevoked", {"jti": "jti_1"}) == {"revoked": True}
50
+ with pytest.raises(ConvexOperationKindError):
51
+ await host.call_mutation("revocation.isRevoked", {"jti": "jti_1"})
52
+
53
+
54
+ def test_fails_clearly_when_operation_is_missing() -> None:
55
+ asyncio.run(_assert_fails_clearly_when_operation_is_missing())
56
+
57
+
58
+ async def _assert_fails_clearly_when_operation_is_missing() -> None:
59
+ driver = InMemoryConvexOperationHost().create_driver()
60
+
61
+ with pytest.raises(MissingConvexOperationError):
62
+ await driver.call("budget.tryReserve", {"amount": 1})
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from dockbay import canonicalize, create_in_memory_driver, matches_prefix
6
+
7
+
8
+ def test_canonicalize_and_prefix_matching() -> None:
9
+ assert canonicalize({"b": 2, "a": 1}) == '{"a":1,"b":2}'
10
+ assert matches_prefix({"runId": "run_1", "seq": 2}, {"runId": "run_1"})
11
+ assert not matches_prefix({"runId": "run_2", "seq": 2}, {"runId": "run_1"})
12
+
13
+
14
+ def test_upserts_idempotently_and_gets_rows() -> None:
15
+ asyncio.run(_assert_upserts_idempotently_and_gets_rows())
16
+
17
+
18
+ async def _assert_upserts_idempotently_and_gets_rows() -> None:
19
+ driver = create_in_memory_driver()
20
+
21
+ async def work(txn) -> None:
22
+ await txn.upsert("events", {"runId": "run_1", "seq": 1}, {"type": "stage"})
23
+ await txn.upsert("events", {"runId": "run_1", "seq": 1}, {"type": "stage"})
24
+ assert await txn.get("events", {"runId": "run_1", "seq": 1}) == {"type": "stage"}
25
+
26
+ await driver.transaction(work)
27
+
28
+
29
+ def test_scans_rows_in_key_order_by_prefix() -> None:
30
+ asyncio.run(_assert_scans_rows_in_key_order_by_prefix())
31
+
32
+
33
+ async def _assert_scans_rows_in_key_order_by_prefix() -> None:
34
+ driver = create_in_memory_driver()
35
+
36
+ async def work(txn) -> None:
37
+ await txn.upsert("events", {"runId": "run_1", "seq": 2}, {"seq": 2})
38
+ await txn.upsert("events", {"runId": "run_2", "seq": 1}, {"seq": 1})
39
+ await txn.upsert("events", {"runId": "run_1", "seq": 1}, {"seq": 1})
40
+ rows = []
41
+ async for row in txn.scan("events", {"runId": "run_1"}):
42
+ rows.append(row)
43
+ assert rows == [{"seq": 1}, {"seq": 2}]
44
+
45
+ await driver.transaction(work)
46
+
47
+
48
+ def test_compare_and_apply_admits_one_winner_under_contention() -> None:
49
+ asyncio.run(_assert_compare_and_apply_admits_one_winner_under_contention())
50
+
51
+
52
+ async def _assert_compare_and_apply_admits_one_winner_under_contention() -> None:
53
+ driver = create_in_memory_driver()
54
+
55
+ async def seed(txn) -> bool:
56
+ return await txn.compare_and_apply("locks", {"id": "budget"}, None, {"owner": "seed"})
57
+
58
+ await driver.transaction(seed)
59
+
60
+ async def attempt(index: int) -> bool:
61
+ async def work(txn) -> bool:
62
+ return await txn.compare_and_apply(
63
+ "locks", {"id": "budget"}, {"owner": "seed"}, {"owner": index}
64
+ )
65
+
66
+ return await driver.transaction(work)
67
+
68
+ results = await asyncio.gather(*(attempt(index) for index in range(10)))
69
+
70
+ assert len([result for result in results if result]) == 1
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import uuid
6
+ from typing import cast
7
+
8
+ import pytest
9
+ from psycopg import AsyncConnection, sql
10
+ from psycopg_pool import AsyncConnectionPool
11
+
12
+ from dockbay import PostgresStoreDriverOptions, create_postgres_driver
13
+
14
+ if not os.environ.get("DOCKBAY_TEST_POSTGRES_URL"):
15
+ pytest.skip(
16
+ "set DOCKBAY_TEST_POSTGRES_URL to run the Postgres driver suite",
17
+ allow_module_level=True,
18
+ )
19
+
20
+ POSTGRES_URL = cast(str, os.environ.get("DOCKBAY_TEST_POSTGRES_URL"))
21
+
22
+
23
+ def test_postgres_driver_upserts_scans_and_admits_one_cas_winner() -> None:
24
+ table = f"store_driver_test_{uuid.uuid4().hex}"
25
+
26
+ async def scenario() -> None:
27
+ pool = AsyncConnectionPool(POSTGRES_URL, open=False)
28
+ await pool.open()
29
+ driver = create_postgres_driver(pool, PostgresStoreDriverOptions(table=table))
30
+ try:
31
+ await driver.transaction(
32
+ lambda txn: txn.upsert("events", {"runId": "run_1", "seq": 2}, {"seq": 2})
33
+ )
34
+ await driver.transaction(
35
+ lambda txn: txn.upsert("events", {"runId": "run_1", "seq": 1}, {"seq": 1})
36
+ )
37
+
38
+ async def scan() -> list[dict[str, object]]:
39
+ rows: list[dict[str, object]] = []
40
+
41
+ async def work(txn):
42
+ async for row in txn.scan("events", {"runId": "run_1"}):
43
+ rows.append(row)
44
+
45
+ await driver.transaction(work)
46
+ return rows
47
+
48
+ assert await scan() == [{"seq": 1}, {"seq": 2}]
49
+ await driver.transaction(
50
+ lambda txn: txn.compare_and_apply(
51
+ "locks", {"id": "budget"}, None, {"owner": "seed"}
52
+ )
53
+ )
54
+ results = await asyncio.gather(
55
+ *(
56
+ driver.transaction(
57
+ lambda txn, index=index: txn.compare_and_apply(
58
+ "locks", {"id": "budget"}, {"owner": "seed"}, {"owner": index}
59
+ )
60
+ )
61
+ for index in range(10)
62
+ )
63
+ )
64
+ assert len([result for result in results if result]) == 1
65
+ finally:
66
+ async with await AsyncConnection.connect(POSTGRES_URL) as conn:
67
+ await conn.execute(
68
+ sql.SQL("DROP TABLE IF EXISTS {table}").format(table=sql.Identifier(table))
69
+ )
70
+ await conn.commit()
71
+ await driver.close()
72
+
73
+ asyncio.run(scenario())
dockbay-0.0.0/uv.lock ADDED
@@ -0,0 +1,242 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "iniconfig"
16
+ version = "2.3.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "packaging"
25
+ version = "26.2"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "pluggy"
34
+ version = "1.6.0"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "psycopg"
43
+ version = "3.3.4"
44
+ source = { registry = "https://pypi.org/simple" }
45
+ dependencies = [
46
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
47
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
48
+ ]
49
+ sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
50
+ wheels = [
51
+ { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
52
+ ]
53
+
54
+ [package.optional-dependencies]
55
+ binary = [
56
+ { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
57
+ ]
58
+
59
+ [[package]]
60
+ name = "psycopg-binary"
61
+ version = "3.3.4"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ wheels = [
64
+ { url = "https://files.pythonhosted.org/packages/b6/82/df3312c0ca083d5b43b352f27d4dd8b1e614bd334473074715d9e0000da4/psycopg_binary-3.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da", size = 4609813, upload-time = "2026-05-01T23:26:30.612Z" },
65
+ { url = "https://files.pythonhosted.org/packages/1f/b5/d74d542458d3e8ac0571d8a88f57ca369999b9a82f4fa528052d0d7d3e4c/psycopg_binary-3.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e", size = 4676799, upload-time = "2026-05-01T23:26:38.475Z" },
66
+ { url = "https://files.pythonhosted.org/packages/09/67/06bab9c60671999f4c6ceff1b334f3ac1f9fc5789eb467c714623ea21de9/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992", size = 5497050, upload-time = "2026-05-01T23:26:47.061Z" },
67
+ { url = "https://files.pythonhosted.org/packages/72/9b/023433e2b20f970de1e22d29132a95281277646da0b2e2879dd4ee94b8c1/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4", size = 5172428, upload-time = "2026-05-01T23:26:56.708Z" },
68
+ { url = "https://files.pythonhosted.org/packages/08/cd/ae16da8fde228a38b2fe9269bbc13cf89e0186173f2265600f02d6a71e64/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e", size = 6762746, upload-time = "2026-05-01T23:27:07.023Z" },
69
+ { url = "https://files.pythonhosted.org/packages/4f/81/0ba09fa5f5f88779093a2541a8e02489825721f258ab88058b11d68b3eb5/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097", size = 5006033, upload-time = "2026-05-01T23:27:12.221Z" },
70
+ { url = "https://files.pythonhosted.org/packages/73/6a/629136040cc3497adb442a305710b5913f2a754d4630fc3d3717c4c0df65/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95", size = 4534175, upload-time = "2026-05-01T23:27:18.248Z" },
71
+ { url = "https://files.pythonhosted.org/packages/7c/32/1027f843c6dc2d5d51960ee62cc0c2cf755a4c39455aff1371173edbef7d/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839", size = 4224203, upload-time = "2026-05-01T23:27:24.3Z" },
72
+ { url = "https://files.pythonhosted.org/packages/0b/e1/380a724d9093c74adb14d4fce920ea8327838abb61f760b1448586b14a8e/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007", size = 3954509, upload-time = "2026-05-01T23:27:30.815Z" },
73
+ { url = "https://files.pythonhosted.org/packages/db/cd/895893ae575a09c97ccfd5def070d88993d955ef34df45a881fd5ff506d6/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c", size = 4259551, upload-time = "2026-05-01T23:27:38.828Z" },
74
+ { url = "https://files.pythonhosted.org/packages/dd/c6/2330a20794e37a3ec609ef2fd8522919ec7a4395a1abf979a8e2d1775cd5/psycopg_binary-3.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d", size = 3572054, upload-time = "2026-05-01T23:27:45.455Z" },
75
+ { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" },
76
+ { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" },
77
+ { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" },
78
+ { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" },
79
+ { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" },
80
+ { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" },
81
+ { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" },
82
+ { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" },
83
+ { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" },
84
+ { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" },
85
+ { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" },
86
+ { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
87
+ { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
88
+ { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
89
+ { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
90
+ { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
91
+ { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
92
+ { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
93
+ { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
94
+ { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
95
+ { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
96
+ { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
97
+ { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
98
+ { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
99
+ { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
100
+ { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
101
+ { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
102
+ { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
103
+ { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
104
+ { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
105
+ { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
106
+ { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
107
+ { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
108
+ ]
109
+
110
+ [[package]]
111
+ name = "psycopg-pool"
112
+ version = "3.3.1"
113
+ source = { registry = "https://pypi.org/simple" }
114
+ dependencies = [
115
+ { name = "typing-extensions" },
116
+ ]
117
+ sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" }
118
+ wheels = [
119
+ { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" },
120
+ ]
121
+
122
+ [[package]]
123
+ name = "pygments"
124
+ version = "2.20.0"
125
+ source = { registry = "https://pypi.org/simple" }
126
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
127
+ wheels = [
128
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
129
+ ]
130
+
131
+ [[package]]
132
+ name = "pytest"
133
+ version = "9.0.3"
134
+ source = { registry = "https://pypi.org/simple" }
135
+ dependencies = [
136
+ { name = "colorama", marker = "sys_platform == 'win32'" },
137
+ { name = "iniconfig" },
138
+ { name = "packaging" },
139
+ { name = "pluggy" },
140
+ { name = "pygments" },
141
+ ]
142
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
143
+ wheels = [
144
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
145
+ ]
146
+
147
+ [[package]]
148
+ name = "ruff"
149
+ version = "0.15.16"
150
+ source = { registry = "https://pypi.org/simple" }
151
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
152
+ wheels = [
153
+ { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
154
+ { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
155
+ { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
156
+ { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
157
+ { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
158
+ { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
159
+ { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
160
+ { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
161
+ { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
162
+ { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
163
+ { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
164
+ { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
165
+ { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
166
+ { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
167
+ { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
168
+ { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
169
+ { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
170
+ ]
171
+
172
+ [[package]]
173
+ name = "dockbay"
174
+ version = "0.0.0"
175
+ source = { editable = "." }
176
+ dependencies = [
177
+ { name = "psycopg", extra = ["binary"] },
178
+ { name = "psycopg-pool" },
179
+ ]
180
+
181
+ [package.dev-dependencies]
182
+ dev = [
183
+ { name = "pytest" },
184
+ { name = "ruff" },
185
+ { name = "ty" },
186
+ ]
187
+
188
+ [package.metadata]
189
+ requires-dist = [
190
+ { name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
191
+ { name = "psycopg-pool", specifier = ">=3.2" },
192
+ ]
193
+
194
+ [package.metadata.requires-dev]
195
+ dev = [
196
+ { name = "pytest", specifier = ">=8" },
197
+ { name = "ruff", specifier = ">=0.6" },
198
+ { name = "ty", specifier = ">=0.0.1a8" },
199
+ ]
200
+
201
+ [[package]]
202
+ name = "ty"
203
+ version = "0.0.43"
204
+ source = { registry = "https://pypi.org/simple" }
205
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
206
+ wheels = [
207
+ { url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
208
+ { url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
209
+ { url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
210
+ { url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
211
+ { url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
212
+ { url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
213
+ { url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
214
+ { url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
215
+ { url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
216
+ { url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
217
+ { url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
218
+ { url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
219
+ { url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
220
+ { url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
221
+ { url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
222
+ { url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
223
+ { url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
224
+ ]
225
+
226
+ [[package]]
227
+ name = "typing-extensions"
228
+ version = "4.15.0"
229
+ source = { registry = "https://pypi.org/simple" }
230
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
231
+ wheels = [
232
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
233
+ ]
234
+
235
+ [[package]]
236
+ name = "tzdata"
237
+ version = "2026.2"
238
+ source = { registry = "https://pypi.org/simple" }
239
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
240
+ wheels = [
241
+ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
242
+ ]