iron-sql 0.4.0__tar.gz → 0.4.2__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.
- {iron_sql-0.4.0 → iron_sql-0.4.2}/PKG-INFO +4 -2
- {iron_sql-0.4.0 → iron_sql-0.4.2}/README.md +2 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/pyproject.toml +3 -4
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/codegen/generator.py +15 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/runtime.py +66 -5
- {iron_sql-0.4.0 → iron_sql-0.4.2}/LICENSE +0 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/__init__.py +0 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/codegen/__init__.py +0 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/codegen/sqlc.py +0 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/codegen/util.py +0 -0
- {iron_sql-0.4.0 → iron_sql-0.4.2}/src/iron_sql/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iron-sql
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: iron_sql generates typed async PostgreSQL clients and runtime helpers from schemas and SQL queries
|
|
5
5
|
Keywords: postgresql,sql,sqlc,psycopg,codegen,async
|
|
6
6
|
Author: Ilia Ablamonov
|
|
@@ -16,7 +16,7 @@ Requires-Dist: psycopg>=3.3.2
|
|
|
16
16
|
Requires-Dist: psycopg-pool>=3.3.0
|
|
17
17
|
Requires-Dist: pydantic>=2.12.4
|
|
18
18
|
Requires-Dist: inflection>=0.5.1 ; extra == 'codegen'
|
|
19
|
-
Requires-Dist: sqlc>=1.30.0 ; extra == 'codegen'
|
|
19
|
+
Requires-Dist: sqlc>=1.30.0.post18 ; extra == 'codegen'
|
|
20
20
|
Requires-Python: >=3.13
|
|
21
21
|
Project-URL: Homepage, https://github.com/Flamefork/iron_sql
|
|
22
22
|
Project-URL: Repository, https://github.com/Flamefork/iron_sql.git
|
|
@@ -81,6 +81,7 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
81
81
|
This writes `myapp/db/mydb.py` containing:
|
|
82
82
|
- a connection pool singleton,
|
|
83
83
|
- `*_connection()` and `*_transaction()` context managers,
|
|
84
|
+
- `*_listen_session(channel)` and `*_notify(channel, payload="")` helpers,
|
|
84
85
|
- dataclasses for multi-column results (deduplicated by table),
|
|
85
86
|
- `StrEnum` classes for PostgreSQL enums,
|
|
86
87
|
- a query class per statement with typed methods,
|
|
@@ -95,6 +96,7 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
95
96
|
|
|
96
97
|
## Runtime Highlights
|
|
97
98
|
- `ConnectionPool` opens lazily and reopens after `close()`, with `ContextVar`-based connection reuse for nested contexts.
|
|
99
|
+
- `*_listen_session()` uses a dedicated pooled connection and doesn't reuse `ContextVar` transaction connections.
|
|
98
100
|
- `query_single_row()` raises `NoRowsError`; `query_optional_row()` returns `None`. Both raise `TooManyRowsError` on 2+ rows.
|
|
99
101
|
- JSONB params are sent with `pgjson.Jsonb`; JSON with `pgjson.Json`. Scalar row factories validate types at runtime.
|
|
100
102
|
- `json_validated` decorator applies Pydantic model validation to dataclass fields on construction.
|
|
@@ -55,6 +55,7 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
55
55
|
This writes `myapp/db/mydb.py` containing:
|
|
56
56
|
- a connection pool singleton,
|
|
57
57
|
- `*_connection()` and `*_transaction()` context managers,
|
|
58
|
+
- `*_listen_session(channel)` and `*_notify(channel, payload="")` helpers,
|
|
58
59
|
- dataclasses for multi-column results (deduplicated by table),
|
|
59
60
|
- `StrEnum` classes for PostgreSQL enums,
|
|
60
61
|
- a query class per statement with typed methods,
|
|
@@ -69,6 +70,7 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
69
70
|
|
|
70
71
|
## Runtime Highlights
|
|
71
72
|
- `ConnectionPool` opens lazily and reopens after `close()`, with `ContextVar`-based connection reuse for nested contexts.
|
|
73
|
+
- `*_listen_session()` uses a dedicated pooled connection and doesn't reuse `ContextVar` transaction connections.
|
|
72
74
|
- `query_single_row()` raises `NoRowsError`; `query_optional_row()` returns `None`. Both raise `TooManyRowsError` on 2+ rows.
|
|
73
75
|
- JSONB params are sent with `pgjson.Jsonb`; JSON with `pgjson.Json`. Scalar row factories validate types at runtime.
|
|
74
76
|
- `json_validated` decorator applies Pydantic model validation to dataclass fields on construction.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "iron-sql"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
|
|
5
5
|
description = "iron_sql generates typed async PostgreSQL clients and runtime helpers from schemas and SQL queries"
|
|
6
6
|
readme = "README.md"
|
|
@@ -26,7 +26,7 @@ dependencies = [
|
|
|
26
26
|
[project.optional-dependencies]
|
|
27
27
|
codegen = [
|
|
28
28
|
"inflection>=0.5.1",
|
|
29
|
-
"sqlc>=1.30.0",
|
|
29
|
+
"sqlc>=1.30.0.post18",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
@@ -52,7 +52,6 @@ dev = [
|
|
|
52
52
|
]
|
|
53
53
|
|
|
54
54
|
[tool.pyright]
|
|
55
|
-
ignore = ["example"]
|
|
56
55
|
typeCheckingMode = "strict"
|
|
57
56
|
reportUnknownArgumentType = "none"
|
|
58
57
|
reportUnknownLambdaType = "none"
|
|
@@ -71,7 +70,6 @@ reportImplicitRelativeImport = true
|
|
|
71
70
|
|
|
72
71
|
[tool.ruff]
|
|
73
72
|
target-version = "py313"
|
|
74
|
-
exclude = ["example"]
|
|
75
73
|
|
|
76
74
|
[tool.ruff.format]
|
|
77
75
|
preview = true
|
|
@@ -100,6 +98,7 @@ ignore = [
|
|
|
100
98
|
|
|
101
99
|
[tool.ruff.lint.per-file-ignores]
|
|
102
100
|
"test_*.py" = ["A002", "PLR2004", "S", "FBT"]
|
|
101
|
+
"example/*.py" = ["E501", "T201", "PGH004"]
|
|
103
102
|
|
|
104
103
|
[tool.ruff.lint.isort]
|
|
105
104
|
force-single-line = true
|
|
@@ -382,6 +382,7 @@ import datetime
|
|
|
382
382
|
import decimal
|
|
383
383
|
import ipaddress
|
|
384
384
|
import uuid
|
|
385
|
+
from collections.abc import AsyncGenerator
|
|
385
386
|
from collections.abc import AsyncIterator
|
|
386
387
|
from collections.abc import Sequence
|
|
387
388
|
from contextlib import asynccontextmanager
|
|
@@ -426,6 +427,20 @@ async def {package_name}_transaction() -> AsyncIterator[None]:
|
|
|
426
427
|
yield
|
|
427
428
|
|
|
428
429
|
|
|
430
|
+
@asynccontextmanager
|
|
431
|
+
async def {package_name}_listen_session(
|
|
432
|
+
channel: str,
|
|
433
|
+
) -> AsyncIterator[AsyncGenerator[str]]:
|
|
434
|
+
async with {package_name.upper()}_POOL.connection() as conn:
|
|
435
|
+
async with runtime.listen(conn, channel) as payloads:
|
|
436
|
+
yield payloads
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
async def {package_name}_notify(channel: str, payload: str = "") -> None:
|
|
440
|
+
async with {package_name}_connection() as conn:
|
|
441
|
+
await runtime.notify(conn, channel, payload)
|
|
442
|
+
|
|
443
|
+
|
|
429
444
|
{"\n\n\n".join(enums)}
|
|
430
445
|
|
|
431
446
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import types
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
2
4
|
from collections.abc import AsyncIterator
|
|
3
5
|
from collections.abc import Callable
|
|
4
6
|
from collections.abc import Sequence
|
|
@@ -13,6 +15,7 @@ from typing import overload
|
|
|
13
15
|
import psycopg
|
|
14
16
|
import psycopg.rows
|
|
15
17
|
import psycopg_pool
|
|
18
|
+
from psycopg import sql
|
|
16
19
|
from psycopg.types import json as pgjson
|
|
17
20
|
from pydantic import TypeAdapter
|
|
18
21
|
|
|
@@ -20,11 +23,9 @@ _adapter_cache: dict[object, TypeAdapter[object]] = {}
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def get_adapter(typ: object) -> TypeAdapter[object]:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
_adapter_cache[typ] = adapter
|
|
27
|
-
return adapter
|
|
26
|
+
if typ not in _adapter_cache:
|
|
27
|
+
_adapter_cache[typ] = TypeAdapter(typ)
|
|
28
|
+
return _adapter_cache[typ]
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class NoRowsError(Exception):
|
|
@@ -35,6 +36,66 @@ class TooManyRowsError(Exception):
|
|
|
35
36
|
pass
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
@asynccontextmanager
|
|
40
|
+
async def listen(
|
|
41
|
+
conn: psycopg.AsyncConnection, channel: str
|
|
42
|
+
) -> AsyncIterator[AsyncGenerator[str]]:
|
|
43
|
+
_validate_channel(channel)
|
|
44
|
+
if await _has_active_listen_subscriptions(conn):
|
|
45
|
+
msg = "listen() requires a connection without active LISTEN subscriptions"
|
|
46
|
+
raise RuntimeError(msg)
|
|
47
|
+
await execute_listen(conn, channel)
|
|
48
|
+
|
|
49
|
+
async def _payloads() -> AsyncGenerator[str]:
|
|
50
|
+
async for notify_msg in conn.notifies():
|
|
51
|
+
yield notify_msg.payload
|
|
52
|
+
|
|
53
|
+
gen = _payloads()
|
|
54
|
+
try:
|
|
55
|
+
yield gen
|
|
56
|
+
finally:
|
|
57
|
+
with contextlib.suppress(psycopg.OperationalError, psycopg.InterfaceError):
|
|
58
|
+
await gen.aclose()
|
|
59
|
+
with contextlib.suppress(psycopg.OperationalError, psycopg.InterfaceError):
|
|
60
|
+
await execute_unlisten(conn, channel)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def notify(conn: psycopg.AsyncConnection, channel: str, payload: str) -> None:
|
|
64
|
+
_validate_channel(channel)
|
|
65
|
+
await conn.execute(
|
|
66
|
+
sql.SQL("NOTIFY {}, {}").format(
|
|
67
|
+
sql.Identifier(channel),
|
|
68
|
+
sql.Literal(payload),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def execute_listen(conn: psycopg.AsyncConnection, channel: str) -> None:
|
|
74
|
+
_validate_channel(channel)
|
|
75
|
+
await conn.execute(sql.SQL("LISTEN {}").format(sql.Identifier(channel)))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def execute_unlisten(conn: psycopg.AsyncConnection, channel: str) -> None:
|
|
79
|
+
_validate_channel(channel)
|
|
80
|
+
await conn.execute(sql.SQL("UNLISTEN {}").format(sql.Identifier(channel)))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _has_active_listen_subscriptions(conn: psycopg.AsyncConnection) -> bool:
|
|
84
|
+
async with conn.cursor() as cur:
|
|
85
|
+
await cur.execute("SELECT EXISTS (SELECT FROM pg_listening_channels())")
|
|
86
|
+
row = await cur.fetchone()
|
|
87
|
+
if row is None:
|
|
88
|
+
msg = "Expected a single boolean row from active LISTEN check"
|
|
89
|
+
raise RuntimeError(msg)
|
|
90
|
+
return bool(row[0])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate_channel(name: str) -> None:
|
|
94
|
+
if not name:
|
|
95
|
+
msg = "Channel name must not be empty"
|
|
96
|
+
raise ValueError(msg)
|
|
97
|
+
|
|
98
|
+
|
|
38
99
|
class ConnectionPool:
|
|
39
100
|
def __init__(
|
|
40
101
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|