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.
- dockbay-0.0.0/.gitignore +8 -0
- dockbay-0.0.0/.gitleaks.toml +16 -0
- dockbay-0.0.0/PKG-INFO +46 -0
- dockbay-0.0.0/README.md +33 -0
- dockbay-0.0.0/pyproject.toml +37 -0
- dockbay-0.0.0/src/dockbay/__init__.py +49 -0
- dockbay-0.0.0/src/dockbay/core.py +356 -0
- dockbay-0.0.0/tests/test_convex.py +62 -0
- dockbay-0.0.0/tests/test_dockbay.py +70 -0
- dockbay-0.0.0/tests/test_postgres.py +73 -0
- dockbay-0.0.0/uv.lock +242 -0
dockbay-0.0.0/.gitignore
ADDED
|
@@ -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
|
dockbay-0.0.0/README.md
ADDED
|
@@ -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
|
+
]
|