iron-sql 0.4.2__tar.gz → 0.4.3__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.2 → iron_sql-0.4.3}/PKG-INFO +4 -2
- {iron_sql-0.4.2 → iron_sql-0.4.3}/README.md +3 -1
- {iron_sql-0.4.2 → iron_sql-0.4.3}/pyproject.toml +1 -1
- {iron_sql-0.4.2 → iron_sql-0.4.3}/src/iron_sql/codegen/generator.py +41 -23
- {iron_sql-0.4.2 → iron_sql-0.4.3}/src/iron_sql/runtime.py +34 -9
- {iron_sql-0.4.2 → iron_sql-0.4.3}/LICENSE +0 -0
- {iron_sql-0.4.2 → iron_sql-0.4.3}/src/iron_sql/__init__.py +0 -0
- {iron_sql-0.4.2 → iron_sql-0.4.3}/src/iron_sql/codegen/__init__.py +0 -0
- {iron_sql-0.4.2 → iron_sql-0.4.3}/src/iron_sql/codegen/sqlc.py +0 -0
- {iron_sql-0.4.2 → iron_sql-0.4.3}/src/iron_sql/codegen/util.py +0 -0
- {iron_sql-0.4.2 → iron_sql-0.4.3}/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.3
|
|
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
|
|
@@ -46,6 +46,7 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
46
46
|
- **Query discovery.** `generate_sql_package` scans your codebase for calls like `<package>_sql("SELECT ...")`, runs `sqlc` for type analysis, and emits a typed module.
|
|
47
47
|
- **Strong typing.** Generated dataclasses and method signatures flow through your IDE and type checker.
|
|
48
48
|
- **Async runtime.** Built on `psycopg` v3 with pooled connections, context-based connection reuse, and transaction helpers.
|
|
49
|
+
- **Streaming.** `query_stream()` uses server-side cursors for memory-efficient iteration over large result sets.
|
|
49
50
|
- **Safe by default.** Helper methods enforce expected row counts instead of returning silent `None`.
|
|
50
51
|
|
|
51
52
|
## Package Layout
|
|
@@ -98,7 +99,8 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
98
99
|
- `ConnectionPool` opens lazily and reopens after `close()`, with `ContextVar`-based connection reuse for nested contexts.
|
|
99
100
|
- `*_listen_session()` uses a dedicated pooled connection and doesn't reuse `ContextVar` transaction connections.
|
|
100
101
|
- `query_single_row()` raises `NoRowsError`; `query_optional_row()` returns `None`. Both raise `TooManyRowsError` on 2+ rows.
|
|
101
|
-
-
|
|
102
|
+
- `query_stream()` returns an async context manager yielding an `AsyncIterator`; uses server-side cursors with automatic transaction management.
|
|
103
|
+
- JSONB params are sent with `psycopg.types.json.Jsonb`; JSON with `psycopg.types.json.Json`. Scalar row factories validate types at runtime.
|
|
102
104
|
- `json_validated` decorator applies Pydantic model validation to dataclass fields on construction.
|
|
103
105
|
|
|
104
106
|
## Example
|
|
@@ -20,6 +20,7 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
20
20
|
- **Query discovery.** `generate_sql_package` scans your codebase for calls like `<package>_sql("SELECT ...")`, runs `sqlc` for type analysis, and emits a typed module.
|
|
21
21
|
- **Strong typing.** Generated dataclasses and method signatures flow through your IDE and type checker.
|
|
22
22
|
- **Async runtime.** Built on `psycopg` v3 with pooled connections, context-based connection reuse, and transaction helpers.
|
|
23
|
+
- **Streaming.** `query_stream()` uses server-side cursors for memory-efficient iteration over large result sets.
|
|
23
24
|
- **Safe by default.** Helper methods enforce expected row counts instead of returning silent `None`.
|
|
24
25
|
|
|
25
26
|
## Package Layout
|
|
@@ -72,7 +73,8 @@ The `sqlc` binary is bundled automatically via the `sqlc` Python package.
|
|
|
72
73
|
- `ConnectionPool` opens lazily and reopens after `close()`, with `ContextVar`-based connection reuse for nested contexts.
|
|
73
74
|
- `*_listen_session()` uses a dedicated pooled connection and doesn't reuse `ContextVar` transaction connections.
|
|
74
75
|
- `query_single_row()` raises `NoRowsError`; `query_optional_row()` returns `None`. Both raise `TooManyRowsError` on 2+ rows.
|
|
75
|
-
-
|
|
76
|
+
- `query_stream()` returns an async context manager yielding an `AsyncIterator`; uses server-side cursors with automatic transaction management.
|
|
77
|
+
- JSONB params are sent with `psycopg.types.json.Jsonb`; JSON with `psycopg.types.json.Json`. Scalar row factories validate types at runtime.
|
|
76
78
|
- `json_validated` decorator applies Pydantic model validation to dataclass fields on construction.
|
|
77
79
|
|
|
78
80
|
## Example
|
|
@@ -55,9 +55,9 @@ class ParamSpec:
|
|
|
55
55
|
|
|
56
56
|
match self.db_type:
|
|
57
57
|
case "json":
|
|
58
|
-
expr = f"
|
|
58
|
+
expr = f"psycopg.types.json.Json({self.name})"
|
|
59
59
|
case "jsonb":
|
|
60
|
-
expr = f"
|
|
60
|
+
expr = f"psycopg.types.json.Jsonb({self.name})"
|
|
61
61
|
case _:
|
|
62
62
|
return self.name
|
|
63
63
|
|
|
@@ -312,7 +312,6 @@ def generate_sql_package( # noqa: PLR0913, PLR0914
|
|
|
312
312
|
render_query_class(
|
|
313
313
|
q.name,
|
|
314
314
|
q.text,
|
|
315
|
-
package_name,
|
|
316
315
|
[
|
|
317
316
|
resolver.param_spec(
|
|
318
317
|
p.column,
|
|
@@ -385,16 +384,20 @@ import uuid
|
|
|
385
384
|
from collections.abc import AsyncGenerator
|
|
386
385
|
from collections.abc import AsyncIterator
|
|
387
386
|
from collections.abc import Sequence
|
|
387
|
+
from contextlib import AbstractAsyncContextManager
|
|
388
388
|
from contextlib import asynccontextmanager
|
|
389
389
|
from contextvars import ContextVar
|
|
390
390
|
from dataclasses import dataclass
|
|
391
391
|
from enum import StrEnum
|
|
392
|
+
from typing import ClassVar
|
|
392
393
|
from typing import Literal
|
|
393
394
|
from typing import overload
|
|
394
395
|
|
|
395
396
|
import psycopg
|
|
397
|
+
import psycopg.abc
|
|
396
398
|
import psycopg.rows
|
|
397
|
-
|
|
399
|
+
import psycopg.sql
|
|
400
|
+
import psycopg.types.json
|
|
398
401
|
|
|
399
402
|
from iron_sql import runtime
|
|
400
403
|
|
|
@@ -447,8 +450,28 @@ async def {package_name}_notify(channel: str, payload: str = "") -> None:
|
|
|
447
450
|
{"\n\n\n".join(entities)}
|
|
448
451
|
|
|
449
452
|
|
|
450
|
-
class Query:
|
|
451
|
-
|
|
453
|
+
class Query[T]:
|
|
454
|
+
_stmt: ClassVar[psycopg.sql.SQL]
|
|
455
|
+
_row_factory: psycopg.rows.BaseRowFactory[T]
|
|
456
|
+
|
|
457
|
+
@asynccontextmanager
|
|
458
|
+
async def _client_cursor(self, params: psycopg.abc.Params | None):
|
|
459
|
+
async with (
|
|
460
|
+
{package_name}_connection() as conn,
|
|
461
|
+
psycopg.AsyncRawCursor(conn, row_factory=self._row_factory) as cur,
|
|
462
|
+
):
|
|
463
|
+
await cur.execute(self._stmt, params)
|
|
464
|
+
yield cur
|
|
465
|
+
|
|
466
|
+
@asynccontextmanager
|
|
467
|
+
async def _server_cursor(self, params: psycopg.abc.Params | None):
|
|
468
|
+
async with (
|
|
469
|
+
{package_name}_connection() as conn,
|
|
470
|
+
runtime.ensure_transaction(conn),
|
|
471
|
+
psycopg.AsyncRawServerCursor(conn, row_factory=self._row_factory, name=runtime.next_cursor_name()) as cur,
|
|
472
|
+
):
|
|
473
|
+
await cur.execute(self._stmt, params)
|
|
474
|
+
yield cur
|
|
452
475
|
|
|
453
476
|
|
|
454
477
|
{"\n\n\n".join(query_classes)}
|
|
@@ -470,7 +493,7 @@ def {sql_fn_name}(stmt: str, row_type: str | None = None) -> Query:
|
|
|
470
493
|
msg = f"Unknown statement: {{stmt!r}}"
|
|
471
494
|
raise KeyError(msg)
|
|
472
495
|
|
|
473
|
-
""".strip()
|
|
496
|
+
""".strip() # noqa: E501
|
|
474
497
|
|
|
475
498
|
|
|
476
499
|
def render_enum_class(
|
|
@@ -536,7 +559,6 @@ def deduplicate_params(params: list[ParamSpec]) -> list[ParamSpec]:
|
|
|
536
559
|
def render_query_class(
|
|
537
560
|
query_name: str,
|
|
538
561
|
stmt: str,
|
|
539
|
-
package_name: str,
|
|
540
562
|
query_params: list[ParamSpec],
|
|
541
563
|
result: str,
|
|
542
564
|
columns_num: int,
|
|
@@ -582,39 +604,35 @@ def render_query_class(
|
|
|
582
604
|
methods = f"""
|
|
583
605
|
|
|
584
606
|
async def query_all_rows({", ".join(query_fn_params)}) -> list[{result}]:
|
|
585
|
-
async with self.
|
|
607
|
+
async with self._client_cursor({params_arg}) as cur:
|
|
586
608
|
return await cur.fetchall()
|
|
587
609
|
|
|
588
610
|
async def query_single_row({", ".join(query_fn_params)}) -> {result}:
|
|
589
|
-
async with self.
|
|
611
|
+
async with self._client_cursor({params_arg}) as cur:
|
|
590
612
|
return runtime.get_one_row(await cur.fetchmany(2))
|
|
591
613
|
|
|
592
614
|
async def query_optional_row({", ".join(query_fn_params)}) -> {base_result} | None:
|
|
593
|
-
async with self.
|
|
615
|
+
async with self._client_cursor({params_arg}) as cur:
|
|
594
616
|
return runtime.get_one_row_or_none(await cur.fetchmany(2))
|
|
595
617
|
|
|
596
|
-
|
|
618
|
+
def query_stream({", ".join(query_fn_params)}) -> AbstractAsyncContextManager[AsyncIterator[{result}]]:
|
|
619
|
+
return self._server_cursor({params_arg})
|
|
620
|
+
|
|
621
|
+
""".strip() # noqa: E501
|
|
597
622
|
else:
|
|
598
623
|
methods = f"""
|
|
599
624
|
|
|
600
625
|
async def execute({", ".join(query_fn_params)}) -> None:
|
|
601
|
-
async with self.
|
|
626
|
+
async with self._client_cursor({params_arg}):
|
|
602
627
|
pass
|
|
603
628
|
|
|
604
629
|
""".strip()
|
|
605
630
|
|
|
606
631
|
return f"""
|
|
607
632
|
|
|
608
|
-
class {query_name}(Query):
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
stmt = {stmt!r}
|
|
612
|
-
async with (
|
|
613
|
-
{package_name}_connection() as conn,
|
|
614
|
-
psycopg.AsyncRawCursor(conn, row_factory={row_factory}) as cur,
|
|
615
|
-
):
|
|
616
|
-
await cur.execute(stmt, params)
|
|
617
|
-
yield cur
|
|
633
|
+
class {query_name}(Query[{result}]):
|
|
634
|
+
_stmt = psycopg.sql.SQL({stmt!r})
|
|
635
|
+
_row_factory = staticmethod({row_factory})
|
|
618
636
|
|
|
619
637
|
{indent_block(methods, " ")}
|
|
620
638
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
+
import itertools
|
|
2
3
|
import types
|
|
3
4
|
from collections.abc import AsyncGenerator
|
|
4
5
|
from collections.abc import AsyncIterator
|
|
@@ -14,9 +15,9 @@ from typing import overload
|
|
|
14
15
|
|
|
15
16
|
import psycopg
|
|
16
17
|
import psycopg.rows
|
|
18
|
+
import psycopg.sql
|
|
19
|
+
import psycopg.types.json
|
|
17
20
|
import psycopg_pool
|
|
18
|
-
from psycopg import sql
|
|
19
|
-
from psycopg.types import json as pgjson
|
|
20
21
|
from pydantic import TypeAdapter
|
|
21
22
|
|
|
22
23
|
_adapter_cache: dict[object, TypeAdapter[object]] = {}
|
|
@@ -63,21 +64,25 @@ async def listen(
|
|
|
63
64
|
async def notify(conn: psycopg.AsyncConnection, channel: str, payload: str) -> None:
|
|
64
65
|
_validate_channel(channel)
|
|
65
66
|
await conn.execute(
|
|
66
|
-
sql.SQL("NOTIFY {}, {}").format(
|
|
67
|
-
sql.Identifier(channel),
|
|
68
|
-
sql.Literal(payload),
|
|
67
|
+
psycopg.sql.SQL("NOTIFY {}, {}").format(
|
|
68
|
+
psycopg.sql.Identifier(channel),
|
|
69
|
+
psycopg.sql.Literal(payload),
|
|
69
70
|
)
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
async def execute_listen(conn: psycopg.AsyncConnection, channel: str) -> None:
|
|
74
75
|
_validate_channel(channel)
|
|
75
|
-
await conn.execute(
|
|
76
|
+
await conn.execute(
|
|
77
|
+
psycopg.sql.SQL("LISTEN {}").format(psycopg.sql.Identifier(channel))
|
|
78
|
+
)
|
|
76
79
|
|
|
77
80
|
|
|
78
81
|
async def execute_unlisten(conn: psycopg.AsyncConnection, channel: str) -> None:
|
|
79
82
|
_validate_channel(channel)
|
|
80
|
-
await conn.execute(
|
|
83
|
+
await conn.execute(
|
|
84
|
+
psycopg.sql.SQL("UNLISTEN {}").format(psycopg.sql.Identifier(channel))
|
|
85
|
+
)
|
|
81
86
|
|
|
82
87
|
|
|
83
88
|
async def _has_active_listen_subscriptions(conn: psycopg.AsyncConnection) -> bool:
|
|
@@ -96,6 +101,26 @@ def _validate_channel(name: str) -> None:
|
|
|
96
101
|
raise ValueError(msg)
|
|
97
102
|
|
|
98
103
|
|
|
104
|
+
_cursor_seq = itertools.count()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def next_cursor_name() -> str:
|
|
108
|
+
return f"_c{next(_cursor_seq)}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@asynccontextmanager
|
|
112
|
+
async def ensure_transaction(conn: psycopg.AsyncConnection) -> AsyncIterator[None]:
|
|
113
|
+
match conn.info.transaction_status:
|
|
114
|
+
case psycopg.pq.TransactionStatus.IDLE:
|
|
115
|
+
async with conn.transaction():
|
|
116
|
+
yield
|
|
117
|
+
case psycopg.pq.TransactionStatus.INTRANS:
|
|
118
|
+
yield
|
|
119
|
+
case status:
|
|
120
|
+
msg = f"Cannot use server-side cursor: connection is in {status.name} state"
|
|
121
|
+
raise psycopg.InterfaceError(msg)
|
|
122
|
+
|
|
123
|
+
|
|
99
124
|
class ConnectionPool:
|
|
100
125
|
def __init__(
|
|
101
126
|
self,
|
|
@@ -196,9 +221,9 @@ def serialize_json_param(typ: object, value: object, db_type: str) -> object:
|
|
|
196
221
|
adapter = get_adapter(typ)
|
|
197
222
|
match db_type:
|
|
198
223
|
case "json":
|
|
199
|
-
return
|
|
224
|
+
return psycopg.types.json.Json(adapter.dump_python(value, mode="json"))
|
|
200
225
|
case "jsonb":
|
|
201
|
-
return
|
|
226
|
+
return psycopg.types.json.Jsonb(adapter.dump_python(value, mode="json"))
|
|
202
227
|
case _:
|
|
203
228
|
return adapter.dump_json(value).decode()
|
|
204
229
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|