asyncpg-typed 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,827 @@
1
+ """
2
+ Type-safe queries for asyncpg.
3
+
4
+ :see: https://github.com/hunyadi/asyncpg_typed
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __author__ = "Levente Hunyadi"
9
+ __copyright__ = "Copyright 2025, Levente Hunyadi"
10
+ __license__ = "MIT"
11
+ __maintainer__ = "Levente Hunyadi"
12
+ __status__ = "Production"
13
+
14
+ import sys
15
+ import typing
16
+ from abc import abstractmethod
17
+ from collections.abc import Iterable, Sequence
18
+ from datetime import date, datetime, time
19
+ from decimal import Decimal
20
+ from functools import reduce
21
+ from io import StringIO
22
+ from types import UnionType
23
+ from typing import Any, Generic, TypeAlias, TypeVar, Union, get_args, get_origin, overload
24
+ from uuid import UUID
25
+
26
+ import asyncpg
27
+ from asyncpg.prepared_stmt import PreparedStatement
28
+
29
+ if sys.version_info < (3, 11):
30
+ from typing_extensions import TypeVarTuple, Unpack
31
+ else:
32
+ from typing import TypeVarTuple, Unpack
33
+
34
+ # list of supported data types
35
+ DATA_TYPES: list[type[Any]] = [bool, int, float, Decimal, date, time, datetime, str, bytes, UUID]
36
+
37
+ # maximum number of inbound query parameters
38
+ NUM_ARGS = 8
39
+
40
+
41
+ def is_union_type(tp: Any) -> bool:
42
+ """
43
+ Returns `True` if `tp` is a union type such as `A | B` or `Union[A, B]`.
44
+ """
45
+
46
+ origin = get_origin(tp)
47
+ return origin is Union or origin is UnionType
48
+
49
+
50
+ def is_optional_type(tp: Any) -> bool:
51
+ """
52
+ Returns `True` if `tp` is an optional type such as `T | None`, `Optional[T]` or `Union[T, None]`.
53
+ """
54
+
55
+ return is_union_type(tp) and any(a is type(None) for a in get_args(tp))
56
+
57
+
58
+ def is_standard_type(tp: Any) -> bool:
59
+ """
60
+ Returns `True` if the type represents a built-in or a well-known standard type.
61
+ """
62
+
63
+ return tp.__module__ == "builtins" or tp.__module__ == UnionType.__module__
64
+
65
+
66
+ def make_union_type(tpl: list[Any]) -> UnionType:
67
+ """
68
+ Creates a `UnionType` (a.k.a. `A | B | C`) dynamically at run time.
69
+ """
70
+
71
+ if len(tpl) < 2:
72
+ raise ValueError("expected: at least two types to make a `UnionType`")
73
+
74
+ return reduce(lambda a, b: a | b, tpl)
75
+
76
+
77
+ def get_required_type(tp: Any) -> Any:
78
+ """
79
+ Removes `None` from an optional type (i.e. a union type that has `None` as a member).
80
+ """
81
+
82
+ if not is_optional_type(tp):
83
+ return tp
84
+
85
+ tpl = [a for a in get_args(tp) if a is not type(None)]
86
+ if len(tpl) > 1:
87
+ return make_union_type(tpl)
88
+ elif len(tpl) > 0:
89
+ return tpl[0]
90
+ else:
91
+ return type(None)
92
+
93
+
94
+ # maps PostgreSQL internal type names to Python types
95
+ _name_to_type: dict[str, Any] = {
96
+ "bool": bool,
97
+ "int2": int,
98
+ "int4": int,
99
+ "int8": int,
100
+ "float4": float,
101
+ "float8": float,
102
+ "numeric": Decimal,
103
+ "date": date,
104
+ "time": time,
105
+ "timetz": time,
106
+ "timestamp": datetime,
107
+ "timestamptz": datetime,
108
+ "bpchar": str,
109
+ "varchar": str,
110
+ "text": str,
111
+ "bytea": bytes,
112
+ "json": str,
113
+ "jsonb": str,
114
+ "uuid": UUID,
115
+ }
116
+
117
+
118
+ def check_data_type(name: str, data_type: type[Any]) -> bool:
119
+ """
120
+ Verifies if the Python target type can represent the PostgreSQL source type.
121
+ """
122
+
123
+ expected_type = _name_to_type.get(name)
124
+ required_type = get_required_type(data_type)
125
+
126
+ if expected_type is not None:
127
+ return expected_type == required_type
128
+ if is_standard_type(required_type):
129
+ return False
130
+
131
+ # user-defined type registered with `conn.set_type_codec()`
132
+ return True
133
+
134
+
135
+ class _SQLPlaceholder:
136
+ ordinal: int
137
+ data_type: type[Any]
138
+
139
+ def __init__(self, ordinal: int, data_type: type[Any]) -> None:
140
+ self.ordinal = ordinal
141
+ self.data_type = data_type
142
+
143
+ def __repr__(self) -> str:
144
+ return f"{self.__class__.__name__}({self.ordinal}, {self.data_type!r})"
145
+
146
+
147
+ class _SQLObject:
148
+ """
149
+ Associates input and output type information with a SQL statement.
150
+ """
151
+
152
+ parameter_data_types: tuple[_SQLPlaceholder, ...]
153
+ resultset_data_types: tuple[type[Any], ...]
154
+ required: int
155
+
156
+ def __init__(
157
+ self,
158
+ *,
159
+ args: type[Any] | None = None,
160
+ resultset: type[Any] | None = None,
161
+ ) -> None:
162
+ if args is not None:
163
+ if get_origin(args) is tuple:
164
+ self.parameter_data_types = tuple(_SQLPlaceholder(ordinal, arg) for ordinal, arg in enumerate(get_args(args), start=1))
165
+ else:
166
+ self.parameter_data_types = (_SQLPlaceholder(1, args),)
167
+ else:
168
+ self.parameter_data_types = ()
169
+
170
+ if resultset is not None:
171
+ if get_origin(resultset) is tuple:
172
+ self.resultset_data_types = get_args(resultset)
173
+ else:
174
+ self.resultset_data_types = (resultset,)
175
+ else:
176
+ self.resultset_data_types = ()
177
+
178
+ # create a bit-field of required types (1: required; 0: optional)
179
+ required = 0
180
+ for index, data_type in enumerate(self.resultset_data_types):
181
+ required |= (not is_optional_type(data_type)) << index
182
+ self.required = required
183
+
184
+ def _raise_required_is_none(self, row: tuple[Any, ...], row_index: int | None = None) -> None:
185
+ """
186
+ Raises an error with the index of the first column value that is of a required type but has been assigned a value of `None`.
187
+ """
188
+
189
+ for col_index in range(len(row)):
190
+ if (self.required >> col_index & 1) and row[col_index] is None:
191
+ if row_index is not None:
192
+ row_col_spec = f"row #{row_index} and column #{col_index}"
193
+ else:
194
+ row_col_spec = f"column #{col_index}"
195
+ raise TypeError(f"expected: {self.resultset_data_types[col_index]} in {row_col_spec}; got: NULL")
196
+
197
+ def check_rows(self, rows: list[tuple[Any, ...]]) -> None:
198
+ """
199
+ Verifies if declared types match actual value types in a resultset.
200
+ """
201
+
202
+ if not rows:
203
+ return
204
+
205
+ required = self.required
206
+ if not required:
207
+ return
208
+
209
+ match len(rows[0]):
210
+ case 1:
211
+ for r, row in enumerate(rows):
212
+ if required & (row[0] is None):
213
+ self._raise_required_is_none(row, r)
214
+ case 2:
215
+ for r, row in enumerate(rows):
216
+ a, b = row
217
+ if required & ((a is None) | (b is None) << 1):
218
+ self._raise_required_is_none(row, r)
219
+ case 3:
220
+ for r, row in enumerate(rows):
221
+ a, b, c = row
222
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2):
223
+ self._raise_required_is_none(row, r)
224
+ case 4:
225
+ for r, row in enumerate(rows):
226
+ a, b, c, d = row
227
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3):
228
+ self._raise_required_is_none(row, r)
229
+ case 5:
230
+ for r, row in enumerate(rows):
231
+ a, b, c, d, e = row
232
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4):
233
+ self._raise_required_is_none(row, r)
234
+ case 6:
235
+ for r, row in enumerate(rows):
236
+ a, b, c, d, e, f = row
237
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4 | (f is None) << 5):
238
+ self._raise_required_is_none(row, r)
239
+ case 7:
240
+ for r, row in enumerate(rows):
241
+ a, b, c, d, e, f, g = row
242
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4 | (f is None) << 5 | (g is None) << 6):
243
+ self._raise_required_is_none(row, r)
244
+ case 8:
245
+ for r, row in enumerate(rows):
246
+ a, b, c, d, e, f, g, h = row
247
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4 | (f is None) << 5 | (g is None) << 6 | (h is None) << 7):
248
+ self._raise_required_is_none(row, r)
249
+ case _:
250
+ for r, row in enumerate(rows):
251
+ self._raise_required_is_none(row, r)
252
+
253
+ def check_row(self, row: tuple[Any, ...]) -> None:
254
+ """
255
+ Verifies if declared types match actual value types in a single row.
256
+ """
257
+
258
+ required = self.required
259
+ if not required:
260
+ return
261
+
262
+ match len(row):
263
+ case 1:
264
+ if required & (row[0] is None):
265
+ self._raise_required_is_none(row)
266
+ case 2:
267
+ a, b = row
268
+ if required & ((a is None) | (b is None) << 1):
269
+ self._raise_required_is_none(row)
270
+ case 3:
271
+ a, b, c = row
272
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2):
273
+ self._raise_required_is_none(row)
274
+ case 4:
275
+ a, b, c, d = row
276
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3):
277
+ self._raise_required_is_none(row)
278
+ case 5:
279
+ a, b, c, d, e = row
280
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4):
281
+ self._raise_required_is_none(row)
282
+ case 6:
283
+ a, b, c, d, e, f = row
284
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4 | (f is None) << 5):
285
+ self._raise_required_is_none(row)
286
+ case 7:
287
+ a, b, c, d, e, f, g = row
288
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4 | (f is None) << 5 | (g is None) << 6):
289
+ self._raise_required_is_none(row)
290
+ case 8:
291
+ a, b, c, d, e, f, g, h = row
292
+ if required & ((a is None) | (b is None) << 1 | (c is None) << 2 | (d is None) << 3 | (e is None) << 4 | (f is None) << 5 | (g is None) << 6 | (h is None) << 7):
293
+ self._raise_required_is_none(row)
294
+ case _:
295
+ self._raise_required_is_none(row)
296
+
297
+ def check_value(self, value: Any) -> None:
298
+ """
299
+ Verifies if the declared type matches the actual value type.
300
+ """
301
+
302
+ if self.required and value is None:
303
+ raise TypeError(f"expected: {self.resultset_data_types[0]}; got: NULL")
304
+
305
+ @abstractmethod
306
+ def query(self) -> str:
307
+ """
308
+ Returns a SQL query string with PostgreSQL ordinal placeholders.
309
+ """
310
+ ...
311
+
312
+ def __repr__(self) -> str:
313
+ return f"{self.__class__.__name__}({self.query()!r})"
314
+
315
+ def __str__(self) -> str:
316
+ return self.query()
317
+
318
+
319
+ if sys.version_info >= (3, 14):
320
+ from string.templatelib import Interpolation, Template # type: ignore[import-not-found]
321
+
322
+ SQLExpression: TypeAlias = Template | str
323
+
324
+ class _SQLTemplate(_SQLObject):
325
+ """
326
+ A SQL query specified with the Python t-string syntax.
327
+ """
328
+
329
+ strings: tuple[str, ...]
330
+ placeholders: tuple[_SQLPlaceholder, ...]
331
+
332
+ def __init__(
333
+ self,
334
+ template: Template,
335
+ *,
336
+ args: type[Any] | None = None,
337
+ resultset: type[Any] | None = None,
338
+ ) -> None:
339
+ super().__init__(args=args, resultset=resultset)
340
+
341
+ for ip in template.interpolations:
342
+ if ip.conversion is not None:
343
+ raise TypeError(f"interpolation `{ip.expression}` expected to apply no conversion")
344
+ if ip.format_spec:
345
+ raise TypeError(f"interpolation `{ip.expression}` expected to apply no format spec")
346
+ if not isinstance(ip.value, int):
347
+ raise TypeError(f"interpolation `{ip.expression}` expected to evaluate to an integer")
348
+
349
+ self.strings = template.strings
350
+
351
+ if args is not None:
352
+
353
+ def _to_placeholder(ip: Interpolation) -> _SQLPlaceholder:
354
+ ordinal = int(ip.value)
355
+ if not (0 < ordinal <= len(self.parameter_data_types)):
356
+ raise IndexError(f"interpolation `{ip.expression}` is an ordinal out of range; expected: 0 < value <= {len(self.parameter_data_types)}")
357
+ return self.parameter_data_types[int(ip.value) - 1]
358
+
359
+ self.placeholders = tuple(_to_placeholder(ip) for ip in template.interpolations)
360
+ else:
361
+ self.placeholders = ()
362
+
363
+ def query(self) -> str:
364
+ buf = StringIO()
365
+ for s, p in zip(self.strings[:-1], self.placeholders, strict=True):
366
+ buf.write(s)
367
+ buf.write(f"${p.ordinal}")
368
+ buf.write(self.strings[-1])
369
+ return buf.getvalue()
370
+
371
+ else:
372
+ SQLExpression = str
373
+
374
+
375
+ class _SQLString(_SQLObject):
376
+ """
377
+ A SQL query specified as a plain string (e.g. f-string).
378
+ """
379
+
380
+ sql: str
381
+
382
+ def __init__(
383
+ self,
384
+ sql: str,
385
+ *,
386
+ args: type[Any] | None = None,
387
+ resultset: type[Any] | None = None,
388
+ ) -> None:
389
+ super().__init__(args=args, resultset=resultset)
390
+ self.sql = sql
391
+
392
+ def query(self) -> str:
393
+ return self.sql
394
+
395
+
396
+ class _SQL:
397
+ """
398
+ Represents a SQL statement with associated type information.
399
+ """
400
+
401
+
402
+ Connection: TypeAlias = asyncpg.Connection | asyncpg.pool.PoolConnectionProxy
403
+
404
+
405
+ class _SQLImpl(_SQL):
406
+ """
407
+ Forwards input data to an `asyncpg.PreparedStatement`, and validates output data (if necessary).
408
+ """
409
+
410
+ sql: _SQLObject
411
+
412
+ def __init__(self, sql: _SQLObject) -> None:
413
+ self.sql = sql
414
+
415
+ def __str__(self) -> str:
416
+ return str(self.sql)
417
+
418
+ def __repr__(self) -> str:
419
+ return repr(self.sql)
420
+
421
+ async def _prepare(self, connection: Connection) -> PreparedStatement:
422
+ stmt = await connection.prepare(self.sql.query())
423
+
424
+ for attr, data_type in zip(stmt.get_attributes(), self.sql.resultset_data_types, strict=True):
425
+ if not check_data_type(attr.type.name, data_type):
426
+ raise TypeError(f"expected: {data_type} in column `{attr.name}`; got: `{attr.type.kind}` of `{attr.type.name}`")
427
+
428
+ return stmt
429
+
430
+ async def execute(self, connection: asyncpg.Connection, *args: Any) -> None:
431
+ await connection.execute(self.sql.query(), *args)
432
+
433
+ async def executemany(self, connection: asyncpg.Connection, args: Iterable[Sequence[Any]]) -> None:
434
+ stmt = await self._prepare(connection)
435
+ await stmt.executemany(args)
436
+
437
+ async def fetch(self, connection: asyncpg.Connection, *args: Any) -> list[tuple[Any, ...]]:
438
+ stmt = await self._prepare(connection)
439
+ rows = await stmt.fetch(*args)
440
+ resultset = [tuple(value for value in row) for row in rows]
441
+ self.sql.check_rows(resultset)
442
+ return resultset
443
+
444
+ async def fetchmany(self, connection: asyncpg.Connection, args: Iterable[Sequence[Any]]) -> list[tuple[Any, ...]]:
445
+ stmt = await self._prepare(connection)
446
+ rows = await stmt.fetchmany(args) # type: ignore[arg-type, call-arg] # pyright: ignore[reportCallIssue]
447
+ rows = typing.cast(list[asyncpg.Record], rows)
448
+ resultset = [tuple(value for value in row) for row in rows]
449
+ self.sql.check_rows(resultset)
450
+ return resultset
451
+
452
+ async def fetchrow(self, connection: asyncpg.Connection, *args: Any) -> tuple[Any, ...] | None:
453
+ stmt = await self._prepare(connection)
454
+ row = await stmt.fetchrow(*args)
455
+ if row is None:
456
+ return None
457
+ resultset = tuple(value for value in row)
458
+ self.sql.check_row(resultset)
459
+ return resultset
460
+
461
+ async def fetchval(self, connection: asyncpg.Connection, *args: Any) -> Any:
462
+ stmt = await self._prepare(connection)
463
+ value = await stmt.fetchval(*args)
464
+ self.sql.check_value(value)
465
+ return value
466
+
467
+
468
+ ### START OF AUTO-GENERATED BLOCK ###
469
+
470
+ PS = TypeVar("PS", bool, bool | None, int, int | None, float, float | None, Decimal, Decimal | None, date, date | None, time, time | None, datetime, datetime | None, str, str | None, bytes, bytes | None, UUID, UUID | None)
471
+ P1 = TypeVar("P1")
472
+ P2 = TypeVar("P2")
473
+ P3 = TypeVar("P3")
474
+ P4 = TypeVar("P4")
475
+ P5 = TypeVar("P5")
476
+ P6 = TypeVar("P6")
477
+ P7 = TypeVar("P7")
478
+ P8 = TypeVar("P8")
479
+ RS = TypeVar("RS", bool, bool | None, int, int | None, float, float | None, Decimal, Decimal | None, date, date | None, time, time | None, datetime, datetime | None, str, str | None, bytes, bytes | None, UUID, UUID | None)
480
+ R1 = TypeVar("R1")
481
+ R2 = TypeVar("R2")
482
+ RX = TypeVarTuple("RX")
483
+
484
+
485
+ class SQL_P0(_SQL):
486
+ @abstractmethod
487
+ async def execute(self, connection: Connection) -> None: ...
488
+
489
+
490
+ class SQL_P0_RS(Generic[R1], SQL_P0):
491
+ @abstractmethod
492
+ async def fetch(self, connection: Connection) -> list[tuple[R1]]: ...
493
+ @abstractmethod
494
+ async def fetchrow(self, connection: Connection) -> tuple[R1] | None: ...
495
+ @abstractmethod
496
+ async def fetchval(self, connection: Connection) -> R1: ...
497
+
498
+
499
+ class SQL_P0_RX(Generic[R1, R2, Unpack[RX]], SQL_P0):
500
+ @abstractmethod
501
+ async def fetch(self, connection: Connection) -> list[tuple[R1, R2, Unpack[RX]]]: ...
502
+ @abstractmethod
503
+ async def fetchrow(self, connection: Connection) -> tuple[R1, R2, Unpack[RX]] | None: ...
504
+
505
+
506
+ class SQL_P1(Generic[P1], _SQL):
507
+ @abstractmethod
508
+ async def execute(self, connection: Connection, arg1: P1) -> None: ...
509
+ @abstractmethod
510
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1]]) -> None: ...
511
+
512
+
513
+ class SQL_P1_RS(Generic[P1, R1], SQL_P1[P1]):
514
+ @abstractmethod
515
+ async def fetch(self, connection: Connection, arg1: P1) -> list[tuple[R1]]: ...
516
+ @abstractmethod
517
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1]]) -> list[tuple[R1]]: ...
518
+ @abstractmethod
519
+ async def fetchrow(self, connection: Connection, arg1: P1) -> tuple[R1] | None: ...
520
+ @abstractmethod
521
+ async def fetchval(self, connection: Connection, arg1: P1) -> R1: ...
522
+
523
+
524
+ class SQL_P1_RX(Generic[P1, R1, R2, Unpack[RX]], SQL_P1[P1]):
525
+ @abstractmethod
526
+ async def fetch(self, connection: Connection, arg1: P1) -> list[tuple[R1, R2, Unpack[RX]]]: ...
527
+ @abstractmethod
528
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
529
+ @abstractmethod
530
+ async def fetchrow(self, connection: Connection, arg1: P1) -> tuple[R1, R2, Unpack[RX]] | None: ...
531
+
532
+
533
+ class SQL_P2(Generic[P1, P2], _SQL):
534
+ @abstractmethod
535
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2) -> None: ...
536
+ @abstractmethod
537
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2]]) -> None: ...
538
+
539
+
540
+ class SQL_P2_RS(Generic[P1, P2, R1], SQL_P2[P1, P2]):
541
+ @abstractmethod
542
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2) -> list[tuple[R1]]: ...
543
+ @abstractmethod
544
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2]]) -> list[tuple[R1]]: ...
545
+ @abstractmethod
546
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2) -> tuple[R1] | None: ...
547
+ @abstractmethod
548
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2) -> R1: ...
549
+
550
+
551
+ class SQL_P2_RX(Generic[P1, P2, R1, R2, Unpack[RX]], SQL_P2[P1, P2]):
552
+ @abstractmethod
553
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2) -> list[tuple[R1, R2, Unpack[RX]]]: ...
554
+ @abstractmethod
555
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
556
+ @abstractmethod
557
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2) -> tuple[R1, R2, Unpack[RX]] | None: ...
558
+
559
+
560
+ class SQL_P3(Generic[P1, P2, P3], _SQL):
561
+ @abstractmethod
562
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3) -> None: ...
563
+ @abstractmethod
564
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3]]) -> None: ...
565
+
566
+
567
+ class SQL_P3_RS(Generic[P1, P2, P3, R1], SQL_P3[P1, P2, P3]):
568
+ @abstractmethod
569
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3) -> list[tuple[R1]]: ...
570
+ @abstractmethod
571
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3]]) -> list[tuple[R1]]: ...
572
+ @abstractmethod
573
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3) -> tuple[R1] | None: ...
574
+ @abstractmethod
575
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3) -> R1: ...
576
+
577
+
578
+ class SQL_P3_RX(Generic[P1, P2, P3, R1, R2, Unpack[RX]], SQL_P3[P1, P2, P3]):
579
+ @abstractmethod
580
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3) -> list[tuple[R1, R2, Unpack[RX]]]: ...
581
+ @abstractmethod
582
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
583
+ @abstractmethod
584
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3) -> tuple[R1, R2, Unpack[RX]] | None: ...
585
+
586
+
587
+ class SQL_P4(Generic[P1, P2, P3, P4], _SQL):
588
+ @abstractmethod
589
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4) -> None: ...
590
+ @abstractmethod
591
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4]]) -> None: ...
592
+
593
+
594
+ class SQL_P4_RS(Generic[P1, P2, P3, P4, R1], SQL_P4[P1, P2, P3, P4]):
595
+ @abstractmethod
596
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4) -> list[tuple[R1]]: ...
597
+ @abstractmethod
598
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4]]) -> list[tuple[R1]]: ...
599
+ @abstractmethod
600
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4) -> tuple[R1] | None: ...
601
+ @abstractmethod
602
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4) -> R1: ...
603
+
604
+
605
+ class SQL_P4_RX(Generic[P1, P2, P3, P4, R1, R2, Unpack[RX]], SQL_P4[P1, P2, P3, P4]):
606
+ @abstractmethod
607
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4) -> list[tuple[R1, R2, Unpack[RX]]]: ...
608
+ @abstractmethod
609
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
610
+ @abstractmethod
611
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4) -> tuple[R1, R2, Unpack[RX]] | None: ...
612
+
613
+
614
+ class SQL_P5(Generic[P1, P2, P3, P4, P5], _SQL):
615
+ @abstractmethod
616
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5) -> None: ...
617
+ @abstractmethod
618
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5]]) -> None: ...
619
+
620
+
621
+ class SQL_P5_RS(Generic[P1, P2, P3, P4, P5, R1], SQL_P5[P1, P2, P3, P4, P5]):
622
+ @abstractmethod
623
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5) -> list[tuple[R1]]: ...
624
+ @abstractmethod
625
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5]]) -> list[tuple[R1]]: ...
626
+ @abstractmethod
627
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5) -> tuple[R1] | None: ...
628
+ @abstractmethod
629
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5) -> R1: ...
630
+
631
+
632
+ class SQL_P5_RX(Generic[P1, P2, P3, P4, P5, R1, R2, Unpack[RX]], SQL_P5[P1, P2, P3, P4, P5]):
633
+ @abstractmethod
634
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5) -> list[tuple[R1, R2, Unpack[RX]]]: ...
635
+ @abstractmethod
636
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
637
+ @abstractmethod
638
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5) -> tuple[R1, R2, Unpack[RX]] | None: ...
639
+
640
+
641
+ class SQL_P6(Generic[P1, P2, P3, P4, P5, P6], _SQL):
642
+ @abstractmethod
643
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6) -> None: ...
644
+ @abstractmethod
645
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6]]) -> None: ...
646
+
647
+
648
+ class SQL_P6_RS(Generic[P1, P2, P3, P4, P5, P6, R1], SQL_P6[P1, P2, P3, P4, P5, P6]):
649
+ @abstractmethod
650
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6) -> list[tuple[R1]]: ...
651
+ @abstractmethod
652
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6]]) -> list[tuple[R1]]: ...
653
+ @abstractmethod
654
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6) -> tuple[R1] | None: ...
655
+ @abstractmethod
656
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6) -> R1: ...
657
+
658
+
659
+ class SQL_P6_RX(Generic[P1, P2, P3, P4, P5, P6, R1, R2, Unpack[RX]], SQL_P6[P1, P2, P3, P4, P5, P6]):
660
+ @abstractmethod
661
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6) -> list[tuple[R1, R2, Unpack[RX]]]: ...
662
+ @abstractmethod
663
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
664
+ @abstractmethod
665
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6) -> tuple[R1, R2, Unpack[RX]] | None: ...
666
+
667
+
668
+ class SQL_P7(Generic[P1, P2, P3, P4, P5, P6, P7], _SQL):
669
+ @abstractmethod
670
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7) -> None: ...
671
+ @abstractmethod
672
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6, P7]]) -> None: ...
673
+
674
+
675
+ class SQL_P7_RS(Generic[P1, P2, P3, P4, P5, P6, P7, R1], SQL_P7[P1, P2, P3, P4, P5, P6, P7]):
676
+ @abstractmethod
677
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7) -> list[tuple[R1]]: ...
678
+ @abstractmethod
679
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6, P7]]) -> list[tuple[R1]]: ...
680
+ @abstractmethod
681
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7) -> tuple[R1] | None: ...
682
+ @abstractmethod
683
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7) -> R1: ...
684
+
685
+
686
+ class SQL_P7_RX(Generic[P1, P2, P3, P4, P5, P6, P7, R1, R2, Unpack[RX]], SQL_P7[P1, P2, P3, P4, P5, P6, P7]):
687
+ @abstractmethod
688
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7) -> list[tuple[R1, R2, Unpack[RX]]]: ...
689
+ @abstractmethod
690
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6, P7]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
691
+ @abstractmethod
692
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7) -> tuple[R1, R2, Unpack[RX]] | None: ...
693
+
694
+
695
+ class SQL_P8(Generic[P1, P2, P3, P4, P5, P6, P7, P8], _SQL):
696
+ @abstractmethod
697
+ async def execute(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8) -> None: ...
698
+ @abstractmethod
699
+ async def executemany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6, P7, P8]]) -> None: ...
700
+
701
+
702
+ class SQL_P8_RS(Generic[P1, P2, P3, P4, P5, P6, P7, P8, R1], SQL_P8[P1, P2, P3, P4, P5, P6, P7, P8]):
703
+ @abstractmethod
704
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8) -> list[tuple[R1]]: ...
705
+ @abstractmethod
706
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6, P7, P8]]) -> list[tuple[R1]]: ...
707
+ @abstractmethod
708
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8) -> tuple[R1] | None: ...
709
+ @abstractmethod
710
+ async def fetchval(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8) -> R1: ...
711
+
712
+
713
+ class SQL_P8_RX(Generic[P1, P2, P3, P4, P5, P6, P7, P8, R1, R2, Unpack[RX]], SQL_P8[P1, P2, P3, P4, P5, P6, P7, P8]):
714
+ @abstractmethod
715
+ async def fetch(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8) -> list[tuple[R1, R2, Unpack[RX]]]: ...
716
+ @abstractmethod
717
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[P1, P2, P3, P4, P5, P6, P7, P8]]) -> list[tuple[R1, R2, Unpack[RX]]]: ...
718
+ @abstractmethod
719
+ async def fetchrow(self, connection: Connection, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8) -> tuple[R1, R2, Unpack[RX]] | None: ...
720
+
721
+
722
+ @overload
723
+ def sql(stmt: SQLExpression) -> SQL_P0: ...
724
+ @overload
725
+ def sql(stmt: SQLExpression, *, resultset: type[RS]) -> SQL_P0_RS[RS]: ...
726
+ @overload
727
+ def sql(stmt: SQLExpression, *, resultset: type[tuple[R1]]) -> SQL_P0_RS[R1]: ...
728
+ @overload
729
+ def sql(stmt: SQLExpression, *, resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P0_RX[R1, R2, Unpack[RX]]: ...
730
+ @overload
731
+ def sql(stmt: SQLExpression, *, args: type[PS]) -> SQL_P1[PS]: ...
732
+ @overload
733
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1]]) -> SQL_P1[P1]: ...
734
+ @overload
735
+ def sql(stmt: SQLExpression, *, args: type[PS], resultset: type[RS]) -> SQL_P1_RS[PS, RS]: ...
736
+ @overload
737
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1]], resultset: type[tuple[R1]]) -> SQL_P1_RS[P1, R1]: ...
738
+ @overload
739
+ def sql(stmt: SQLExpression, *, args: type[PS], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P1_RX[PS, R1, R2, Unpack[RX]]: ...
740
+ @overload
741
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P1_RX[P1, R1, R2, Unpack[RX]]: ...
742
+ @overload
743
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2]]) -> SQL_P2[P1, P2]: ...
744
+ @overload
745
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2]], resultset: type[RS]) -> SQL_P2_RS[P1, P2, RS]: ...
746
+ @overload
747
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2]], resultset: type[tuple[R1]]) -> SQL_P2_RS[P1, P2, R1]: ...
748
+ @overload
749
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P2_RX[P1, P2, R1, R2, Unpack[RX]]: ...
750
+ @overload
751
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3]]) -> SQL_P3[P1, P2, P3]: ...
752
+ @overload
753
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3]], resultset: type[RS]) -> SQL_P3_RS[P1, P2, P3, RS]: ...
754
+ @overload
755
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3]], resultset: type[tuple[R1]]) -> SQL_P3_RS[P1, P2, P3, R1]: ...
756
+ @overload
757
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P3_RX[P1, P2, P3, R1, R2, Unpack[RX]]: ...
758
+ @overload
759
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4]]) -> SQL_P4[P1, P2, P3, P4]: ...
760
+ @overload
761
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4]], resultset: type[RS]) -> SQL_P4_RS[P1, P2, P3, P4, RS]: ...
762
+ @overload
763
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4]], resultset: type[tuple[R1]]) -> SQL_P4_RS[P1, P2, P3, P4, R1]: ...
764
+ @overload
765
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P4_RX[P1, P2, P3, P4, R1, R2, Unpack[RX]]: ...
766
+ @overload
767
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5]]) -> SQL_P5[P1, P2, P3, P4, P5]: ...
768
+ @overload
769
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5]], resultset: type[RS]) -> SQL_P5_RS[P1, P2, P3, P4, P5, RS]: ...
770
+ @overload
771
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5]], resultset: type[tuple[R1]]) -> SQL_P5_RS[P1, P2, P3, P4, P5, R1]: ...
772
+ @overload
773
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P5_RX[P1, P2, P3, P4, P5, R1, R2, Unpack[RX]]: ...
774
+ @overload
775
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6]]) -> SQL_P6[P1, P2, P3, P4, P5, P6]: ...
776
+ @overload
777
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6]], resultset: type[RS]) -> SQL_P6_RS[P1, P2, P3, P4, P5, P6, RS]: ...
778
+ @overload
779
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6]], resultset: type[tuple[R1]]) -> SQL_P6_RS[P1, P2, P3, P4, P5, P6, R1]: ...
780
+ @overload
781
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P6_RX[P1, P2, P3, P4, P5, P6, R1, R2, Unpack[RX]]: ...
782
+ @overload
783
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7]]) -> SQL_P7[P1, P2, P3, P4, P5, P6, P7]: ...
784
+ @overload
785
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7]], resultset: type[RS]) -> SQL_P7_RS[P1, P2, P3, P4, P5, P6, P7, RS]: ...
786
+ @overload
787
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7]], resultset: type[tuple[R1]]) -> SQL_P7_RS[P1, P2, P3, P4, P5, P6, P7, R1]: ...
788
+ @overload
789
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P7_RX[P1, P2, P3, P4, P5, P6, P7, R1, R2, Unpack[RX]]: ...
790
+ @overload
791
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7, P8]]) -> SQL_P8[P1, P2, P3, P4, P5, P6, P7, P8]: ...
792
+ @overload
793
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7, P8]], resultset: type[RS]) -> SQL_P8_RS[P1, P2, P3, P4, P5, P6, P7, P8, RS]: ...
794
+ @overload
795
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7, P8]], resultset: type[tuple[R1]]) -> SQL_P8_RS[P1, P2, P3, P4, P5, P6, P7, P8, R1]: ...
796
+ @overload
797
+ def sql(stmt: SQLExpression, *, args: type[tuple[P1, P2, P3, P4, P5, P6, P7, P8]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_P8_RX[P1, P2, P3, P4, P5, P6, P7, P8, R1, R2, Unpack[RX]]: ...
798
+
799
+
800
+ ### END OF AUTO-GENERATED BLOCK ###
801
+
802
+
803
+ def sql(
804
+ stmt: SQLExpression,
805
+ *,
806
+ args: type[Any] | None = None,
807
+ resultset: type[Any] | None = None,
808
+ ) -> _SQL:
809
+ """
810
+ Creates a SQL statement with associated type information.
811
+
812
+ :param stmt: SQL statement as a string or template.
813
+ :param args: Type signature for input parameters. Use the type for a single parameter (e.g. `int`) or `tuple[...]` for multiple parameters.
814
+ :param resultset: Type signature for output data. Use the type for a single parameter (e.g. `int`) or `tuple[...]` for multiple parameters.
815
+ """
816
+
817
+ if sys.version_info >= (3, 14):
818
+ obj: _SQLObject
819
+ match stmt:
820
+ case Template():
821
+ obj = _SQLTemplate(stmt, args=args, resultset=resultset)
822
+ case str():
823
+ obj = _SQLString(stmt, args=args, resultset=resultset)
824
+ else:
825
+ obj = _SQLString(stmt, args=args, resultset=resultset)
826
+
827
+ return _SQLImpl(obj)
asyncpg_typed/py.typed ADDED
File without changes
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: asyncpg_typed
3
+ Version: 0.1.0
4
+ Summary: Type-safe queries for asyncpg
5
+ Author-email: Levente Hunyadi <hunyadi@gmail.com>
6
+ Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/hunyadi/asyncpg_typed
9
+ Project-URL: Source, https://github.com/hunyadi/asyncpg_typed
10
+ Keywords: asyncpg,typed,database-client
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: SQL
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Utilities
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: asyncpg>=0.31
29
+ Requires-Dist: typing-extensions>=4.15; python_version < "3.11"
30
+ Provides-Extra: vector
31
+ Requires-Dist: asyncpg-vector>=0.1; extra == "vector"
32
+ Provides-Extra: dev
33
+ Requires-Dist: asyncpg-stubs>=0.31; extra == "dev"
34
+ Requires-Dist: build>=1.3; extra == "dev"
35
+ Requires-Dist: mypy>=1.19; extra == "dev"
36
+ Requires-Dist: ruff>=0.14; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # Type-safe queries for asyncpg
40
+
41
+ [asyncpg](https://magicstack.github.io/asyncpg/current/) is a high-performance database client to connect to a PostgreSQL server, and execute SQL statements using the async/await paradigm in Python. The library exposes a `Connection` object, which has methods like `execute` and `fetch` that run SQL queries against the database. Unfortunately, these methods take the query as a plain `str`, arguments as `object`, and the resultset is exposed as a `Record`, which is a `tuple`/`dict` hybrid whose `get` and indexer have a return type of `Any`. There is no mechanism to check compatibility of input or output arguments, even if their types are preliminarily known.
42
+
43
+ This Python library provides "compile-time" validation for SQL queries that linters and type checkers can enforce. By creating a generic `SQL` object and associating input and output type information with the query, the signatures of `execute` and `fetch` reveal the exact expected and returned types.
44
+
45
+
46
+ ## Motivating example
47
+
48
+ ```python
49
+ # create a typed object, setting expected and returned types
50
+ select_where_sql = sql(
51
+ """--sql
52
+ SELECT boolean_value, integer_value, string_value
53
+ FROM sample_data
54
+ WHERE boolean_value = $1 AND integer_value > $2
55
+ ORDER BY integer_value;
56
+ """,
57
+ args=tuple[bool, int],
58
+ resultset=tuple[bool, int, str | None],
59
+ )
60
+
61
+ conn = await asyncpg.connect(host="localhost", port=5432, user="postgres", password="postgres")
62
+ try:
63
+ # ✅ Valid signature
64
+ rows = await select_where_sql.fetch(conn, False, 2)
65
+
66
+ # ✅ Type of "rows" is "list[tuple[bool, int, str | None]]"
67
+ reveal_type(rows)
68
+
69
+ # ⚠️ Argument missing for parameter "arg2"
70
+ rows = await select_where_sql.fetch(conn, False)
71
+
72
+ # ⚠️ Argument of type "float" cannot be assigned to parameter "arg2" of type "int" in function "fetch"; "float" is not assignable to "int"
73
+ rows = await select_where_sql.fetch(conn, False, 3.14)
74
+
75
+ finally:
76
+ await conn.close()
77
+ ```
78
+
79
+
80
+ ## Syntax
81
+
82
+ ### Creating a SQL object
83
+
84
+ Instantiate a SQL object with the `sql` function:
85
+
86
+ ```python
87
+ def sql(
88
+ stmt: str | string.templatelib.Template,
89
+ *,
90
+ args: None | type[P1] | type[tuple[P1, P2]] | type[tuple[P1, P2, P3]] | ... = None,
91
+ resultset: None | type[R1] | type[tuple[R1, R2]] | type[tuple[R1, R2, R3]] | ... = None
92
+ ) -> _SQL: ...
93
+ ```
94
+
95
+ The parameter `stmt` represents a SQL expression, either as a string (including an *f-string*) or a template (i.e. a *t-string*).
96
+
97
+ If the expression is a string, it can have PostgreSQL parameter placeholders such as `$1`, `$2` or `$3`:
98
+
99
+ ```python
100
+ f"INSERT INTO table_name (col_1, col_2, col_3) VALUES ($1, $2, $3);"
101
+ ```
102
+
103
+ If the expression is a *t-string*, it can have replacement fields that evaluate to integers:
104
+
105
+ ```python
106
+ t"INSERT INTO table_name (col_1, col_2, col_3) VALUES ({1}, {2}, {3});"
107
+ ```
108
+
109
+ The parameters `args` and `resultset` take a series type `P` or `R`, which may be any of the following:
110
+
111
+ * (required) simple type
112
+ * optional simple type (`T | None`)
113
+ * `tuple` of several (required or optional) simple types.
114
+
115
+ Simple types include:
116
+
117
+ * `bool`
118
+ * `int`
119
+ * `float`
120
+ * `decimal.Decimal`
121
+ * `datetime.date`
122
+ * `datetime.time`
123
+ * `datetime.datetime`
124
+ * `str`
125
+ * `bytes`
126
+ * `uuid.UUID`
127
+
128
+ Types are grouped together with `tuple`:
129
+
130
+ ```python
131
+ tuple[bool, int, str | None]
132
+ ```
133
+
134
+ Passing a simple type directly (e.g. `type[T]`) is for convenience, and is equivalent to passing a one-element tuple of the same simple type (i.e. `type[tuple[T]]`).
135
+
136
+ The number of types in `args` must correspond to the number of query parameters. (This is validated on calling `sql(...)` for the *t-string* syntax.) The number of types in `resultset` must correspond to the number of columns returned by the query.
137
+
138
+ Both `args` and `resultset` types must be compatible with their corresponding PostgreSQL query parameter types and resultset column types, respectively. The following table shows the mapping between PostgreSQL and Python types.
139
+
140
+ | PostgreSQL type | Python type |
141
+ | ----------------- | ------------------ |
142
+ | `bool` | `bool` |
143
+ | `smallint` | `int` |
144
+ | `integer` | `int` |
145
+ | `bigint` | `int` |
146
+ | `real`/`float4` | `float` |
147
+ | `double`/`float8` | `float` |
148
+ | `decimal` | `Decimal` |
149
+ | `numeric` | `Decimal` |
150
+ | `date` | `date` |
151
+ | `time` | `time` (naive) |
152
+ | `timetz` | `time` (tz) |
153
+ | `timestamp` | `datetime` (naive) |
154
+ | `timestamptz` | `datetime` (tz) |
155
+ | `char(N)` | `str` |
156
+ | `varchar(N)` | `str` |
157
+ | `text` | `str` |
158
+ | `bytea` | `bytes` |
159
+ | `json` | `str` |
160
+ | `jsonb` | `str` |
161
+ | `uuid` | `UUID` |
162
+
163
+ ### Using a SQL object
164
+
165
+ The function `sql` returns an object that derives from the base class `_SQL` and is specific to the number and types of parameters passed in `args` and `resultset`.
166
+
167
+ The following functions are available on SQL objects:
168
+
169
+ ```python
170
+ async def execute(self, connection: Connection, *args: *P) -> None: ...
171
+ async def executemany(self, connection: Connection, args: Iterable[tuple[*P]]) -> None: ...
172
+ async def fetch(self, connection: Connection, *args: *P) -> list[tuple[*R]]: ...
173
+ async def fetchmany(self, connection: Connection, args: Iterable[tuple[*P]]) -> list[tuple[*R]]: ...
174
+ async def fetchrow(self, connection: Connection, *args: *P) -> tuple[*R] | None: ...
175
+ async def fetchval(self, connection: Connection, *args: *P) -> R1: ...
176
+ ```
177
+
178
+ `Connection` may be an `asyncpg.Connection` or an `asyncpg.pool.PoolConnectionProxy` acquired from a connection pool.
179
+
180
+ `*P` and `*R` denote several types (a type pack) corresponding to those listed in `args` and `resultset`, respectively.
181
+
182
+ Only those functions are prompted on code completion that make sense in the context of the given number of input and output arguments. Specifically, `fetchval` is available only for a single type passed to `resultset`, and `executemany` and `fetchmany` are available only if the query takes (one or more) parameters.
183
+
184
+
185
+ ## Run-time behavior
186
+
187
+ When a call such as `sql.executemany(conn, records)` or `sql.fetch(conn, param1, param2)` is made on a `SQL` object at run time, the library invokes `connection.prepare(sql)` to create a `PreparedStatement` and compares the actual statement signature against the expected Python types. Unfortunately, PostgreSQL doesn't propagate nullability via prepared statements: resultset types that are declared as required (e.g. `T` as opposed to `T | None`) are validated at run time.
@@ -0,0 +1,8 @@
1
+ asyncpg_typed/__init__.py,sha256=6F8tV2H1ayXFptYmsXLEi3puqKT-U904qamONeCJXUA,36489
2
+ asyncpg_typed/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ asyncpg_typed-0.1.0.dist-info/licenses/LICENSE,sha256=rx4jD36wX8TyLZaR2HEOJ6TphFPjKUqoCSSYWzwWNRk,1093
4
+ asyncpg_typed-0.1.0.dist-info/METADATA,sha256=ti6ld6HyUOodNUCmbNru0xiUu5mNHM_Z2TiDvQm4CNA,8429
5
+ asyncpg_typed-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ asyncpg_typed-0.1.0.dist-info/top_level.txt,sha256=T0X1nWnXRTi5a5oTErGy572ORDbM9UV9wfhRXWLsaoY,14
7
+ asyncpg_typed-0.1.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
8
+ asyncpg_typed-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Levente Hunyadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ asyncpg_typed
@@ -0,0 +1 @@
1
+