asyncpg-typed 0.1.3__py3-none-any.whl → 0.1.4__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.
- asyncpg_typed/__init__.py +265 -60
- {asyncpg_typed-0.1.3.dist-info → asyncpg_typed-0.1.4.dist-info}/METADATA +47 -18
- asyncpg_typed-0.1.4.dist-info/RECORD +8 -0
- {asyncpg_typed-0.1.3.dist-info → asyncpg_typed-0.1.4.dist-info}/licenses/LICENSE +1 -1
- asyncpg_typed-0.1.3.dist-info/RECORD +0 -8
- {asyncpg_typed-0.1.3.dist-info → asyncpg_typed-0.1.4.dist-info}/WHEEL +0 -0
- {asyncpg_typed-0.1.3.dist-info → asyncpg_typed-0.1.4.dist-info}/top_level.txt +0 -0
- {asyncpg_typed-0.1.3.dist-info → asyncpg_typed-0.1.4.dist-info}/zip-safe +0 -0
asyncpg_typed/__init__.py
CHANGED
|
@@ -4,9 +4,9 @@ Type-safe queries for asyncpg.
|
|
|
4
4
|
:see: https://github.com/hunyadi/asyncpg_typed
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.1.
|
|
7
|
+
__version__ = "0.1.4"
|
|
8
8
|
__author__ = "Levente Hunyadi"
|
|
9
|
-
__copyright__ = "Copyright 2025, Levente Hunyadi"
|
|
9
|
+
__copyright__ = "Copyright 2025-2026, Levente Hunyadi"
|
|
10
10
|
__license__ = "MIT"
|
|
11
11
|
__maintainer__ = "Levente Hunyadi"
|
|
12
12
|
__status__ = "Production"
|
|
@@ -43,10 +43,18 @@ TargetType: TypeAlias = type[Any] | UnionType
|
|
|
43
43
|
Connection: TypeAlias = asyncpg.Connection | asyncpg.pool.PoolConnectionProxy
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class CountMismatchError(TypeError):
|
|
47
|
+
"Raised when a prepared statement takes or returns a different number of parameters or columns than declared in Python."
|
|
48
|
+
|
|
49
|
+
|
|
46
50
|
class TypeMismatchError(TypeError):
|
|
47
51
|
"Raised when a prepared statement takes or returns a PostgreSQL type incompatible with the declared Python type."
|
|
48
52
|
|
|
49
53
|
|
|
54
|
+
class NameMismatchError(TypeError):
|
|
55
|
+
"Raised when the name of a result-set column differs from what is declared in Python."
|
|
56
|
+
|
|
57
|
+
|
|
50
58
|
class EnumMismatchError(TypeError):
|
|
51
59
|
"Raised when a prepared statement takes or returns a PostgreSQL enum type whose permitted set of values differs from what is declared in Python."
|
|
52
60
|
|
|
@@ -333,15 +341,29 @@ class _TypeVerifier:
|
|
|
333
341
|
if is_enum_type(data_type):
|
|
334
342
|
if pg_type.name not in ["bpchar", "varchar", "text"]:
|
|
335
343
|
raise TypeMismatchError(f"expected: Python enum type `{type_to_str(data_type)}` for {pg_name}; got: PostgreSQL type `{pg_type.kind}` of `{pg_type.name}` instead of `char`, `varchar` or `text`")
|
|
344
|
+
elif pg_type.kind == "array" and get_origin(data_type) is list:
|
|
345
|
+
if not pg_type.name.endswith("[]"):
|
|
346
|
+
raise TypeMismatchError(f"expected: Python list type `{type_to_str(data_type)}` for {pg_name}; got: PostgreSQL type `{pg_type.kind}` of `{pg_type.name}` instead of array")
|
|
347
|
+
|
|
348
|
+
expected_types = _NAME_TO_TYPE.get(pg_type.name[:-2])
|
|
349
|
+
if expected_types is None:
|
|
350
|
+
raise TypeMismatchError(f"expected: Python list type `{type_to_str(data_type)}` for {pg_name}; got: unrecognized PostgreSQL type `{pg_type.kind}` of `{pg_type.name}`")
|
|
351
|
+
elif get_args(data_type)[0] not in expected_types:
|
|
352
|
+
if len(expected_types) == 1:
|
|
353
|
+
target = f"the Python type `{type_to_str(expected_types[0])}`"
|
|
354
|
+
else:
|
|
355
|
+
target = f"one of the Python types {', '.join(f'`{type_to_str(tp)}`' for tp in expected_types)}"
|
|
356
|
+
raise TypeMismatchError(f"expected: Python list type `{type_to_str(data_type)}` for {pg_name}; got: incompatible PostgreSQL type `{pg_type.kind}` of `{pg_type.name}` whose elements convert to {target}")
|
|
336
357
|
else:
|
|
337
358
|
expected_types = _NAME_TO_TYPE.get(pg_type.name)
|
|
338
359
|
if expected_types is None:
|
|
339
360
|
raise TypeMismatchError(f"expected: Python type `{type_to_str(data_type)}` for {pg_name}; got: unrecognized PostgreSQL type `{pg_type.kind}` of `{pg_type.name}`")
|
|
340
361
|
elif data_type not in expected_types:
|
|
341
|
-
|
|
342
|
-
f"
|
|
343
|
-
|
|
344
|
-
|
|
362
|
+
if len(expected_types) == 1:
|
|
363
|
+
target = f"the Python type `{type_to_str(expected_types[0])}`"
|
|
364
|
+
else:
|
|
365
|
+
target = f"one of the Python types {', '.join(f'`{type_to_str(tp)}`' for tp in expected_types)}"
|
|
366
|
+
raise TypeMismatchError(f"expected: Python type `{type_to_str(data_type)}` for {pg_name}; got: incompatible PostgreSQL type `{pg_type.kind}` of `{pg_type.name}` which converts to {target}")
|
|
345
367
|
elif pg_type.kind == "composite": # PostgreSQL composite types
|
|
346
368
|
# user-defined composite types registered with `conn.set_type_codec()` typically using `format="tuple"`
|
|
347
369
|
pass
|
|
@@ -356,7 +378,7 @@ class _TypeVerifier:
|
|
|
356
378
|
|
|
357
379
|
|
|
358
380
|
@dataclass(frozen=True)
|
|
359
|
-
class
|
|
381
|
+
class _Placeholder:
|
|
360
382
|
ordinal: int
|
|
361
383
|
data_type: TargetType
|
|
362
384
|
|
|
@@ -364,13 +386,33 @@ class _SQLPlaceholder:
|
|
|
364
386
|
return f"{self.__class__.__name__}({self.ordinal}, {self.data_type!r})"
|
|
365
387
|
|
|
366
388
|
|
|
389
|
+
class _ResultsetWrapper:
|
|
390
|
+
"Wraps result-set rows into a tuple or named tuple."
|
|
391
|
+
|
|
392
|
+
init: Callable[..., tuple[Any, ...]] | None
|
|
393
|
+
iterable: Callable[[Iterable[Any]], tuple[Any, ...]]
|
|
394
|
+
|
|
395
|
+
def __init__(self, init: Callable[..., tuple[Any, ...]] | None, iterable: Callable[[Iterable[Any]], tuple[Any, ...]]) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Initializes a result-set wrapper.
|
|
398
|
+
|
|
399
|
+
:param init: Initializer function that takes as many arguments as columns in the result-set.
|
|
400
|
+
:param iterable: Initializer function that takes an iterable over columns of a result-set row.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
self.init = init
|
|
404
|
+
self.iterable = iterable
|
|
405
|
+
|
|
406
|
+
|
|
367
407
|
class _SQLObject:
|
|
368
408
|
"""
|
|
369
409
|
Associates input and output type information with a SQL statement.
|
|
370
410
|
"""
|
|
371
411
|
|
|
372
|
-
_parameter_data_types: tuple[
|
|
412
|
+
_parameter_data_types: tuple[_Placeholder, ...]
|
|
373
413
|
_resultset_data_types: tuple[TargetType, ...]
|
|
414
|
+
_resultset_column_names: tuple[str, ...] | None
|
|
415
|
+
_resultset_wrapper: _ResultsetWrapper
|
|
374
416
|
_parameter_cast: int
|
|
375
417
|
_parameter_converters: tuple[Callable[[Any], Any], ...]
|
|
376
418
|
_required: int
|
|
@@ -378,20 +420,35 @@ class _SQLObject:
|
|
|
378
420
|
_resultset_converters: tuple[Callable[[Any], Any], ...]
|
|
379
421
|
|
|
380
422
|
@property
|
|
381
|
-
def parameter_data_types(self) -> tuple[
|
|
423
|
+
def parameter_data_types(self) -> tuple[_Placeholder, ...]:
|
|
424
|
+
"Expected inbound parameter data types."
|
|
425
|
+
|
|
382
426
|
return self._parameter_data_types
|
|
383
427
|
|
|
384
428
|
@property
|
|
385
429
|
def resultset_data_types(self) -> tuple[TargetType, ...]:
|
|
430
|
+
"Expected column data types in the result-set."
|
|
431
|
+
|
|
386
432
|
return self._resultset_data_types
|
|
387
433
|
|
|
434
|
+
@property
|
|
435
|
+
def resultset_column_names(self) -> tuple[str, ...] | None:
|
|
436
|
+
"Expected column names in the result-set."
|
|
437
|
+
|
|
438
|
+
return self._resultset_column_names
|
|
439
|
+
|
|
388
440
|
def __init__(
|
|
389
441
|
self,
|
|
390
|
-
|
|
391
|
-
|
|
442
|
+
*,
|
|
443
|
+
args: tuple[TargetType, ...],
|
|
444
|
+
resultset: tuple[TargetType, ...],
|
|
445
|
+
names: tuple[str, ...] | None,
|
|
446
|
+
wrapper: _ResultsetWrapper,
|
|
392
447
|
) -> None:
|
|
393
|
-
self._parameter_data_types = tuple(
|
|
394
|
-
self._resultset_data_types = tuple(get_required_type(data_type) for data_type in
|
|
448
|
+
self._parameter_data_types = tuple(_Placeholder(ordinal, get_required_type(arg)) for ordinal, arg in enumerate(args, start=1))
|
|
449
|
+
self._resultset_data_types = tuple(get_required_type(data_type) for data_type in resultset)
|
|
450
|
+
self._resultset_column_names = names
|
|
451
|
+
self._resultset_wrapper = wrapper
|
|
395
452
|
|
|
396
453
|
# create a bit-field of types that require cast or serialization (1: apply conversion; 0: forward value as-is)
|
|
397
454
|
parameter_cast = 0
|
|
@@ -403,7 +460,7 @@ class _SQLObject:
|
|
|
403
460
|
|
|
404
461
|
# create a bit-field of required types (1: required; 0: optional)
|
|
405
462
|
required = 0
|
|
406
|
-
for index, data_type in enumerate(
|
|
463
|
+
for index, data_type in enumerate(resultset):
|
|
407
464
|
required |= (not is_optional_type(data_type)) << index
|
|
408
465
|
self._required = required
|
|
409
466
|
|
|
@@ -528,6 +585,16 @@ class _SQLObject:
|
|
|
528
585
|
case _:
|
|
529
586
|
self._raise_required_is_none(row)
|
|
530
587
|
|
|
588
|
+
def check_column(self, column: list[Any]) -> None:
|
|
589
|
+
"""
|
|
590
|
+
Verifies if the declared type matches the actual value type of a single-column resultset.
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
if self._required:
|
|
594
|
+
for i, value in enumerate(column):
|
|
595
|
+
if value is None:
|
|
596
|
+
raise NoneTypeError(f"expected: {self._resultset_data_types[0]} in row #{i}; got: NULL")
|
|
597
|
+
|
|
531
598
|
def check_value(self, value: Any) -> None:
|
|
532
599
|
"""
|
|
533
600
|
Verifies if the declared type matches the actual value type.
|
|
@@ -568,12 +635,95 @@ class _SQLObject:
|
|
|
568
635
|
:returns: List of tuples with each tuple element having the configured Python target type.
|
|
569
636
|
"""
|
|
570
637
|
|
|
638
|
+
if not rows:
|
|
639
|
+
return []
|
|
640
|
+
|
|
641
|
+
init_wrapper = self._resultset_wrapper.init
|
|
642
|
+
iterable_wrapper = self._resultset_wrapper.iterable
|
|
571
643
|
cast = self._resultset_cast
|
|
572
|
-
if cast:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
644
|
+
if not cast:
|
|
645
|
+
return [iterable_wrapper(row.values()) for row in rows]
|
|
646
|
+
|
|
647
|
+
columns = len(rows[0])
|
|
648
|
+
match columns:
|
|
649
|
+
case 1:
|
|
650
|
+
converter = self._resultset_converters[0]
|
|
651
|
+
if init_wrapper is not None:
|
|
652
|
+
return [init_wrapper(converter(value) if (value := row[0]) is not None else value) for row in rows]
|
|
653
|
+
else:
|
|
654
|
+
return [(converter(value) if (value := row[0]) is not None else value,) for row in rows]
|
|
655
|
+
case 2:
|
|
656
|
+
conv_a, conv_b = self._resultset_converters
|
|
657
|
+
cast_a = cast >> 0 & 1
|
|
658
|
+
cast_b = cast >> 1 & 1
|
|
659
|
+
if init_wrapper is not None:
|
|
660
|
+
return [
|
|
661
|
+
init_wrapper(
|
|
662
|
+
conv_a(value) if (value := row[0]) is not None and cast_a else value,
|
|
663
|
+
conv_b(value) if (value := row[1]) is not None and cast_b else value,
|
|
664
|
+
)
|
|
665
|
+
for row in rows
|
|
666
|
+
]
|
|
667
|
+
else:
|
|
668
|
+
return [
|
|
669
|
+
(
|
|
670
|
+
conv_a(value) if (value := row[0]) is not None and cast_a else value,
|
|
671
|
+
conv_b(value) if (value := row[1]) is not None and cast_b else value,
|
|
672
|
+
)
|
|
673
|
+
for row in rows
|
|
674
|
+
]
|
|
675
|
+
case 3:
|
|
676
|
+
conv_a, conv_b, conv_c = self._resultset_converters
|
|
677
|
+
cast_a = cast >> 0 & 1
|
|
678
|
+
cast_b = cast >> 1 & 1
|
|
679
|
+
cast_c = cast >> 2 & 1
|
|
680
|
+
if init_wrapper is not None:
|
|
681
|
+
return [
|
|
682
|
+
init_wrapper(
|
|
683
|
+
conv_a(value) if (value := row[0]) is not None and cast_a else value,
|
|
684
|
+
conv_b(value) if (value := row[1]) is not None and cast_b else value,
|
|
685
|
+
conv_c(value) if (value := row[2]) is not None and cast_c else value,
|
|
686
|
+
)
|
|
687
|
+
for row in rows
|
|
688
|
+
]
|
|
689
|
+
else:
|
|
690
|
+
return [
|
|
691
|
+
(
|
|
692
|
+
conv_a(value) if (value := row[0]) is not None and cast_a else value,
|
|
693
|
+
conv_b(value) if (value := row[1]) is not None and cast_b else value,
|
|
694
|
+
conv_c(value) if (value := row[2]) is not None and cast_c else value,
|
|
695
|
+
)
|
|
696
|
+
for row in rows
|
|
697
|
+
]
|
|
698
|
+
case 4:
|
|
699
|
+
conv_a, conv_b, conv_c, conv_d = self._resultset_converters
|
|
700
|
+
cast_a = cast >> 0 & 1
|
|
701
|
+
cast_b = cast >> 1 & 1
|
|
702
|
+
cast_c = cast >> 2 & 1
|
|
703
|
+
cast_d = cast >> 3 & 1
|
|
704
|
+
if init_wrapper is not None:
|
|
705
|
+
return [
|
|
706
|
+
init_wrapper(
|
|
707
|
+
conv_a(value) if (value := row[0]) is not None and cast_a else value,
|
|
708
|
+
conv_b(value) if (value := row[1]) is not None and cast_b else value,
|
|
709
|
+
conv_c(value) if (value := row[2]) is not None and cast_c else value,
|
|
710
|
+
conv_d(value) if (value := row[3]) is not None and cast_d else value,
|
|
711
|
+
)
|
|
712
|
+
for row in rows
|
|
713
|
+
]
|
|
714
|
+
else:
|
|
715
|
+
return [
|
|
716
|
+
(
|
|
717
|
+
conv_a(value) if (value := row[0]) is not None and cast_a else value,
|
|
718
|
+
conv_b(value) if (value := row[1]) is not None and cast_b else value,
|
|
719
|
+
conv_c(value) if (value := row[2]) is not None and cast_c else value,
|
|
720
|
+
conv_d(value) if (value := row[3]) is not None and cast_d else value,
|
|
721
|
+
)
|
|
722
|
+
for row in rows
|
|
723
|
+
]
|
|
724
|
+
case _:
|
|
725
|
+
converters = self._resultset_converters
|
|
726
|
+
return [iterable_wrapper((converters[i](value) if (value := row[i]) is not None and cast >> i & 1 else value) for i in range(columns)) for row in rows]
|
|
577
727
|
|
|
578
728
|
def convert_row(self, row: asyncpg.Record) -> tuple[Any, ...]:
|
|
579
729
|
"""
|
|
@@ -583,12 +733,29 @@ class _SQLObject:
|
|
|
583
733
|
:returns: A tuple with each tuple element having the configured Python target type.
|
|
584
734
|
"""
|
|
585
735
|
|
|
736
|
+
wrapper = self._resultset_wrapper.iterable
|
|
586
737
|
cast = self._resultset_cast
|
|
587
738
|
if cast:
|
|
588
739
|
converters = self._resultset_converters
|
|
589
|
-
|
|
740
|
+
columns = len(row)
|
|
741
|
+
return wrapper((converters[i](value) if (value := row[i]) is not None and cast >> i & 1 else value) for i in range(columns))
|
|
590
742
|
else:
|
|
591
|
-
return
|
|
743
|
+
return wrapper(row.values())
|
|
744
|
+
|
|
745
|
+
def convert_column(self, rows: list[asyncpg.Record]) -> list[Any]:
|
|
746
|
+
"""
|
|
747
|
+
Converts a single column in the PostgreSQL result-set to its corresponding Python target type.
|
|
748
|
+
|
|
749
|
+
:param rows: List of rows returned by PostgreSQL.
|
|
750
|
+
:returns: List of values having the configured Python target type.
|
|
751
|
+
"""
|
|
752
|
+
|
|
753
|
+
cast = self._resultset_cast
|
|
754
|
+
if cast:
|
|
755
|
+
converter = self._resultset_converters[0]
|
|
756
|
+
return [(converter(value) if (value := row[0]) is not None else value) for row in rows]
|
|
757
|
+
else:
|
|
758
|
+
return [row[0] for row in rows]
|
|
592
759
|
|
|
593
760
|
def convert_value(self, value: Any) -> Any:
|
|
594
761
|
"""
|
|
@@ -625,7 +792,7 @@ if sys.version_info >= (3, 14):
|
|
|
625
792
|
"""
|
|
626
793
|
|
|
627
794
|
_strings: tuple[str, ...]
|
|
628
|
-
_placeholders: tuple[
|
|
795
|
+
_placeholders: tuple[_Placeholder, ...]
|
|
629
796
|
|
|
630
797
|
def __init__(
|
|
631
798
|
self,
|
|
@@ -633,8 +800,10 @@ if sys.version_info >= (3, 14):
|
|
|
633
800
|
*,
|
|
634
801
|
args: tuple[TargetType, ...],
|
|
635
802
|
resultset: tuple[TargetType, ...],
|
|
803
|
+
names: tuple[str, ...] | None,
|
|
804
|
+
wrapper: _ResultsetWrapper,
|
|
636
805
|
) -> None:
|
|
637
|
-
super().__init__(args, resultset)
|
|
806
|
+
super().__init__(args=args, resultset=resultset, names=names, wrapper=wrapper)
|
|
638
807
|
|
|
639
808
|
for ip in template.interpolations:
|
|
640
809
|
if ip.conversion is not None:
|
|
@@ -648,7 +817,7 @@ if sys.version_info >= (3, 14):
|
|
|
648
817
|
|
|
649
818
|
if len(self.parameter_data_types) > 0:
|
|
650
819
|
|
|
651
|
-
def _to_placeholder(ip: Interpolation) ->
|
|
820
|
+
def _to_placeholder(ip: Interpolation) -> _Placeholder:
|
|
652
821
|
ordinal = int(ip.value)
|
|
653
822
|
if not (0 < ordinal <= len(self.parameter_data_types)):
|
|
654
823
|
raise IndexError(f"interpolation `{ip.expression}` is an ordinal out of range; expected: 0 < value <= {len(self.parameter_data_types)}")
|
|
@@ -683,8 +852,10 @@ class _SQLString(_SQLObject):
|
|
|
683
852
|
*,
|
|
684
853
|
args: tuple[TargetType, ...],
|
|
685
854
|
resultset: tuple[TargetType, ...],
|
|
855
|
+
names: tuple[str, ...] | None,
|
|
856
|
+
wrapper: _ResultsetWrapper,
|
|
686
857
|
) -> None:
|
|
687
|
-
super().__init__(args, resultset)
|
|
858
|
+
super().__init__(args=args, resultset=resultset, names=names, wrapper=wrapper)
|
|
688
859
|
self._sql = sql
|
|
689
860
|
|
|
690
861
|
def query(self) -> str:
|
|
@@ -717,8 +888,22 @@ class _SQLImpl(_SQL):
|
|
|
717
888
|
stmt = await connection.prepare(self._sql.query())
|
|
718
889
|
|
|
719
890
|
verifier = _TypeVerifier(connection)
|
|
891
|
+
|
|
892
|
+
input_count = len(self._sql.parameter_data_types)
|
|
893
|
+
parameter_count = len(stmt.get_parameters())
|
|
894
|
+
if parameter_count != input_count:
|
|
895
|
+
raise CountMismatchError(f"expected: PostgreSQL query to take {input_count} parameter(s); got: {parameter_count}")
|
|
720
896
|
for param, placeholder in zip(stmt.get_parameters(), self._sql.parameter_data_types, strict=True):
|
|
721
897
|
await verifier.check_data_type(f"parameter ${placeholder.ordinal}", param, placeholder.data_type)
|
|
898
|
+
|
|
899
|
+
output_count = len(self._sql.resultset_data_types)
|
|
900
|
+
column_count = len(stmt.get_attributes())
|
|
901
|
+
if column_count != output_count:
|
|
902
|
+
raise CountMismatchError(f"expected: PostgreSQL query to return {output_count} column(s) in result-set; got: {column_count}")
|
|
903
|
+
if self._sql.resultset_column_names is not None:
|
|
904
|
+
for index, attr, name in zip(range(output_count), stmt.get_attributes(), self._sql.resultset_column_names, strict=True):
|
|
905
|
+
if attr.name != name:
|
|
906
|
+
raise NameMismatchError(f"expected: Python field name `{name}` to match PostgreSQL result-set column name `{attr.name}` for index #{index}")
|
|
722
907
|
for attr, data_type in zip(stmt.get_attributes(), self._sql.resultset_data_types, strict=True):
|
|
723
908
|
await verifier.check_data_type(f"column `{attr.name}`", attr.type, data_type)
|
|
724
909
|
|
|
@@ -755,6 +940,13 @@ class _SQLImpl(_SQL):
|
|
|
755
940
|
self._sql.check_row(resultset)
|
|
756
941
|
return resultset
|
|
757
942
|
|
|
943
|
+
async def fetchcol(self, connection: asyncpg.Connection, *args: Any) -> list[Any]:
|
|
944
|
+
stmt = await self._prepare(connection)
|
|
945
|
+
rows = await stmt.fetch(*self._sql.convert_arg_list(args))
|
|
946
|
+
column = self._sql.convert_column(rows)
|
|
947
|
+
self._sql.check_column(column)
|
|
948
|
+
return column
|
|
949
|
+
|
|
758
950
|
async def fetchval(self, connection: asyncpg.Connection, *args: Any) -> Any:
|
|
759
951
|
stmt = await self._prepare(connection)
|
|
760
952
|
value = await stmt.fetchval(*self._sql.convert_arg_list(args))
|
|
@@ -784,6 +976,8 @@ class SQL_R1_P0(SQL_P0, Protocol[R1]):
|
|
|
784
976
|
@abstractmethod
|
|
785
977
|
async def fetchrow(self, connection: Connection) -> tuple[R1] | None: ...
|
|
786
978
|
@abstractmethod
|
|
979
|
+
async def fetchcol(self, connection: Connection) -> list[R1]: ...
|
|
980
|
+
@abstractmethod
|
|
787
981
|
async def fetchval(self, connection: Connection) -> R1: ...
|
|
788
982
|
|
|
789
983
|
|
|
@@ -809,6 +1003,8 @@ class SQL_R1_PX(SQL_PX[Unpack[PX]], Protocol[R1, Unpack[PX]]):
|
|
|
809
1003
|
@abstractmethod
|
|
810
1004
|
async def fetchrow(self, connection: Connection, *args: Unpack[PX]) -> tuple[R1] | None: ...
|
|
811
1005
|
@abstractmethod
|
|
1006
|
+
async def fetchcol(self, connection: Connection, *args: Unpack[PX]) -> list[R1]: ...
|
|
1007
|
+
@abstractmethod
|
|
812
1008
|
async def fetchval(self, connection: Connection, *args: Unpack[PX]) -> R1: ...
|
|
813
1009
|
|
|
814
1010
|
|
|
@@ -823,6 +1019,8 @@ class SQL_RX_PX(SQL_PX[Unpack[PX]], Protocol[RT, Unpack[PX]]):
|
|
|
823
1019
|
|
|
824
1020
|
### END OF AUTO-GENERATED BLOCK FOR Protocol ###
|
|
825
1021
|
|
|
1022
|
+
RS = TypeVar("RS", bound=tuple[Any, ...])
|
|
1023
|
+
|
|
826
1024
|
|
|
827
1025
|
class SQLFactory:
|
|
828
1026
|
"""
|
|
@@ -835,25 +1033,19 @@ class SQLFactory:
|
|
|
835
1033
|
@overload
|
|
836
1034
|
def sql(self, stmt: SQLExpression, *, result: type[R1]) -> SQL_R1_P0[R1]: ...
|
|
837
1035
|
@overload
|
|
838
|
-
def sql(self, stmt: SQLExpression, *, resultset: type[
|
|
839
|
-
@overload
|
|
840
|
-
def sql(self, stmt: SQLExpression, *, resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_RX_P0[tuple[R1, R2, Unpack[RX]]]: ...
|
|
1036
|
+
def sql(self, stmt: SQLExpression, *, resultset: type[RS]) -> SQL_RX_P0[RS]: ...
|
|
841
1037
|
@overload
|
|
842
1038
|
def sql(self, stmt: SQLExpression, *, arg: type[P1]) -> SQL_PX[P1]: ...
|
|
843
1039
|
@overload
|
|
844
1040
|
def sql(self, stmt: SQLExpression, *, arg: type[P1], result: type[R1]) -> SQL_R1_PX[R1, P1]: ...
|
|
845
1041
|
@overload
|
|
846
|
-
def sql(self, stmt: SQLExpression, *, arg: type[P1], resultset: type[
|
|
847
|
-
@overload
|
|
848
|
-
def sql(self, stmt: SQLExpression, *, arg: type[P1], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_RX_PX[tuple[R1, R2, Unpack[RX]], P1]: ...
|
|
1042
|
+
def sql(self, stmt: SQLExpression, *, arg: type[P1], resultset: type[RS]) -> SQL_RX_PX[RS, P1]: ...
|
|
849
1043
|
@overload
|
|
850
1044
|
def sql(self, stmt: SQLExpression, *, args: type[tuple[P1, Unpack[PX]]]) -> SQL_PX[P1, Unpack[PX]]: ...
|
|
851
1045
|
@overload
|
|
852
1046
|
def sql(self, stmt: SQLExpression, *, args: type[tuple[P1, Unpack[PX]]], result: type[R1]) -> SQL_R1_PX[R1, P1, Unpack[PX]]: ...
|
|
853
1047
|
@overload
|
|
854
|
-
def sql(self, stmt: SQLExpression, *, args: type[tuple[P1, Unpack[PX]]], resultset: type[
|
|
855
|
-
@overload
|
|
856
|
-
def sql(self, stmt: SQLExpression, *, args: type[tuple[P1, Unpack[PX]]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_RX_PX[tuple[R1, R2, Unpack[RX]], P1, Unpack[PX]]: ...
|
|
1048
|
+
def sql(self, stmt: SQLExpression, *, args: type[tuple[P1, Unpack[PX]]], resultset: type[RS]) -> SQL_RX_PX[RS, P1, Unpack[PX]]: ...
|
|
857
1049
|
|
|
858
1050
|
### END OF AUTO-GENERATED BLOCK FOR sql ###
|
|
859
1051
|
|
|
@@ -868,17 +1060,17 @@ class SQLFactory:
|
|
|
868
1060
|
:param result: Type signature for a single result column (e.g. `UUID`).
|
|
869
1061
|
"""
|
|
870
1062
|
|
|
871
|
-
input_data_types, output_data_types = _sql_args_resultset(args=args, resultset=resultset, arg=arg, result=result)
|
|
1063
|
+
input_data_types, output_data_types, names, wrapper = _sql_args_resultset(args=args, resultset=resultset, arg=arg, result=result)
|
|
872
1064
|
|
|
873
1065
|
obj: _SQLObject
|
|
874
1066
|
if sys.version_info >= (3, 14):
|
|
875
1067
|
match stmt:
|
|
876
1068
|
case Template():
|
|
877
|
-
obj = _SQLTemplate(stmt, args=input_data_types, resultset=output_data_types)
|
|
1069
|
+
obj = _SQLTemplate(stmt, args=input_data_types, resultset=output_data_types, names=names, wrapper=wrapper)
|
|
878
1070
|
case str():
|
|
879
|
-
obj = _SQLString(stmt, args=input_data_types, resultset=output_data_types)
|
|
1071
|
+
obj = _SQLString(stmt, args=input_data_types, resultset=output_data_types, names=names, wrapper=wrapper)
|
|
880
1072
|
else:
|
|
881
|
-
obj = _SQLString(stmt, args=input_data_types, resultset=output_data_types)
|
|
1073
|
+
obj = _SQLString(stmt, args=input_data_types, resultset=output_data_types, names=names, wrapper=wrapper)
|
|
882
1074
|
|
|
883
1075
|
return _SQLImpl(obj)
|
|
884
1076
|
|
|
@@ -888,25 +1080,19 @@ class SQLFactory:
|
|
|
888
1080
|
@overload
|
|
889
1081
|
def unsafe_sql(self, stmt: str, *, result: type[R1]) -> SQL_R1_P0[R1]: ...
|
|
890
1082
|
@overload
|
|
891
|
-
def unsafe_sql(self, stmt: str, *, resultset: type[
|
|
892
|
-
@overload
|
|
893
|
-
def unsafe_sql(self, stmt: str, *, resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_RX_P0[tuple[R1, R2, Unpack[RX]]]: ...
|
|
1083
|
+
def unsafe_sql(self, stmt: str, *, resultset: type[RS]) -> SQL_RX_P0[RS]: ...
|
|
894
1084
|
@overload
|
|
895
1085
|
def unsafe_sql(self, stmt: str, *, arg: type[P1]) -> SQL_PX[P1]: ...
|
|
896
1086
|
@overload
|
|
897
1087
|
def unsafe_sql(self, stmt: str, *, arg: type[P1], result: type[R1]) -> SQL_R1_PX[R1, P1]: ...
|
|
898
1088
|
@overload
|
|
899
|
-
def unsafe_sql(self, stmt: str, *, arg: type[P1], resultset: type[
|
|
900
|
-
@overload
|
|
901
|
-
def unsafe_sql(self, stmt: str, *, arg: type[P1], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_RX_PX[tuple[R1, R2, Unpack[RX]], P1]: ...
|
|
1089
|
+
def unsafe_sql(self, stmt: str, *, arg: type[P1], resultset: type[RS]) -> SQL_RX_PX[RS, P1]: ...
|
|
902
1090
|
@overload
|
|
903
1091
|
def unsafe_sql(self, stmt: str, *, args: type[tuple[P1, Unpack[PX]]]) -> SQL_PX[P1, Unpack[PX]]: ...
|
|
904
1092
|
@overload
|
|
905
1093
|
def unsafe_sql(self, stmt: str, *, args: type[tuple[P1, Unpack[PX]]], result: type[R1]) -> SQL_R1_PX[R1, P1, Unpack[PX]]: ...
|
|
906
1094
|
@overload
|
|
907
|
-
def unsafe_sql(self, stmt: str, *, args: type[tuple[P1, Unpack[PX]]], resultset: type[
|
|
908
|
-
@overload
|
|
909
|
-
def unsafe_sql(self, stmt: str, *, args: type[tuple[P1, Unpack[PX]]], resultset: type[tuple[R1, R2, Unpack[RX]]]) -> SQL_RX_PX[tuple[R1, R2, Unpack[RX]], P1, Unpack[PX]]: ...
|
|
1095
|
+
def unsafe_sql(self, stmt: str, *, args: type[tuple[P1, Unpack[PX]]], resultset: type[RS]) -> SQL_RX_PX[RS, P1, Unpack[PX]]: ...
|
|
910
1096
|
|
|
911
1097
|
### END OF AUTO-GENERATED BLOCK FOR unsafe_sql ###
|
|
912
1098
|
|
|
@@ -924,12 +1110,14 @@ class SQLFactory:
|
|
|
924
1110
|
:param result: Type signature for a single result column (e.g. `UUID`).
|
|
925
1111
|
"""
|
|
926
1112
|
|
|
927
|
-
input_data_types, output_data_types = _sql_args_resultset(args=args, resultset=resultset, arg=arg, result=result)
|
|
928
|
-
obj = _SQLString(stmt, args=input_data_types, resultset=output_data_types)
|
|
1113
|
+
input_data_types, output_data_types, names, wrapper = _sql_args_resultset(args=args, resultset=resultset, arg=arg, result=result)
|
|
1114
|
+
obj = _SQLString(stmt, args=input_data_types, resultset=output_data_types, names=names, wrapper=wrapper)
|
|
929
1115
|
return _SQLImpl(obj)
|
|
930
1116
|
|
|
931
1117
|
|
|
932
|
-
def _sql_args_resultset(
|
|
1118
|
+
def _sql_args_resultset(
|
|
1119
|
+
*, args: type[Any] | None = None, resultset: type[Any] | None = None, arg: type[Any] | None = None, result: type[Any] | None = None
|
|
1120
|
+
) -> tuple[tuple[Any, ...], tuple[Any, ...], tuple[str, ...] | None, _ResultsetWrapper]:
|
|
933
1121
|
"Parses an argument/resultset signature into input/output types."
|
|
934
1122
|
|
|
935
1123
|
if args is not None and arg is not None:
|
|
@@ -938,24 +1126,41 @@ def _sql_args_resultset(*, args: type[Any] | None = None, resultset: type[Any] |
|
|
|
938
1126
|
raise TypeError("expected: either `resultset` or `result`; got: both")
|
|
939
1127
|
|
|
940
1128
|
if args is not None:
|
|
941
|
-
if
|
|
942
|
-
|
|
943
|
-
|
|
1129
|
+
if hasattr(args, "_asdict") and hasattr(args, "_fields"):
|
|
1130
|
+
# named tuple
|
|
1131
|
+
input_data_types = tuple(tp for tp in args.__annotations__.values())
|
|
1132
|
+
else:
|
|
1133
|
+
# regular tuple
|
|
1134
|
+
if get_origin(args) is not tuple:
|
|
1135
|
+
raise TypeError(f"expected: `type[tuple[T, ...]]` for `args`; got: {args}")
|
|
1136
|
+
input_data_types = get_args(args)
|
|
944
1137
|
elif arg is not None:
|
|
945
1138
|
input_data_types = (arg,)
|
|
946
1139
|
else:
|
|
947
1140
|
input_data_types = ()
|
|
948
1141
|
|
|
949
1142
|
if resultset is not None:
|
|
950
|
-
if
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1143
|
+
if hasattr(resultset, "_asdict") and hasattr(resultset, "_fields") and hasattr(resultset, "_make"):
|
|
1144
|
+
# named tuple
|
|
1145
|
+
output_data_types = tuple(tp for tp in resultset.__annotations__.values())
|
|
1146
|
+
names = tuple(f for f in resultset._fields)
|
|
1147
|
+
wrapper = _ResultsetWrapper(resultset, resultset._make)
|
|
1148
|
+
else:
|
|
1149
|
+
# regular tuple
|
|
1150
|
+
if get_origin(resultset) is not tuple:
|
|
1151
|
+
raise TypeError(f"expected: `type[tuple[T, ...]]` for `resultset`; got: {resultset}")
|
|
1152
|
+
output_data_types = get_args(resultset)
|
|
1153
|
+
names = None
|
|
1154
|
+
wrapper = _ResultsetWrapper(None, tuple)
|
|
955
1155
|
else:
|
|
956
|
-
|
|
1156
|
+
if result is not None:
|
|
1157
|
+
output_data_types = (result,)
|
|
1158
|
+
else:
|
|
1159
|
+
output_data_types = ()
|
|
1160
|
+
names = None
|
|
1161
|
+
wrapper = _ResultsetWrapper(None, tuple)
|
|
957
1162
|
|
|
958
|
-
return input_data_types, output_data_types
|
|
1163
|
+
return input_data_types, output_data_types, names, wrapper
|
|
959
1164
|
|
|
960
1165
|
|
|
961
1166
|
FACTORY: SQLFactory = SQLFactory()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncpg_typed
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Type-safe queries for asyncpg
|
|
5
5
|
Author-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
6
6
|
Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
@@ -45,6 +45,8 @@ This Python library provides "compile-time" validation for SQL queries that lint
|
|
|
45
45
|
|
|
46
46
|
## Motivating example
|
|
47
47
|
|
|
48
|
+
### Using a plain tuple
|
|
49
|
+
|
|
48
50
|
```python
|
|
49
51
|
# create a typed object, setting expected and returned types
|
|
50
52
|
select_where_sql = sql(
|
|
@@ -66,7 +68,7 @@ try:
|
|
|
66
68
|
# ✅ Type of "rows" is "list[tuple[bool, int, str | None]]"
|
|
67
69
|
reveal_type(rows)
|
|
68
70
|
|
|
69
|
-
# ⚠️
|
|
71
|
+
# ⚠️ Expected 1 more positional argument
|
|
70
72
|
rows = await select_where_sql.fetch(conn, False)
|
|
71
73
|
|
|
72
74
|
# ⚠️ Argument of type "float" cannot be assigned to parameter "arg2" of type "int" in function "fetch"; "float" is not assignable to "int"
|
|
@@ -74,27 +76,52 @@ try:
|
|
|
74
76
|
|
|
75
77
|
finally:
|
|
76
78
|
await conn.close()
|
|
79
|
+
```
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
### Using a named tuple
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# capture resultset column names and data types as fields of a named tuple
|
|
85
|
+
class Resultset(NamedTuple):
|
|
81
86
|
boolean_value: bool
|
|
82
87
|
integer_value: int
|
|
83
88
|
string_value: str | None
|
|
84
89
|
|
|
85
|
-
# ✅ Valid initializer call
|
|
86
|
-
items = [DataObject(*row) for row in rows]
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
integer_value
|
|
92
|
-
|
|
91
|
+
# create a typed object, declaring return types with the named tuple
|
|
92
|
+
select_sql = sql(
|
|
93
|
+
"""--sql
|
|
94
|
+
SELECT boolean_value, integer_value, string_value
|
|
95
|
+
FROM sample_data
|
|
96
|
+
ORDER BY integer_value;
|
|
97
|
+
""",
|
|
98
|
+
resultset=Resultset,
|
|
99
|
+
)
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
conn = await asyncpg.connect(host="localhost", port=5432, user="postgres", password="postgres")
|
|
102
|
+
try:
|
|
103
|
+
rows = await select_sql.fetch(conn)
|
|
104
|
+
|
|
105
|
+
# ✅ Type of "rows" is "list[Resultset]"
|
|
106
|
+
reveal_type(rows)
|
|
107
|
+
|
|
108
|
+
for row in rows:
|
|
109
|
+
# use dot notation to access properties
|
|
110
|
+
if row.string_value is not None:
|
|
111
|
+
print(f"#{row.integer_value}: {row.string_value}")
|
|
112
|
+
|
|
113
|
+
# ✅ Type of "row.boolean_value" is "bool"
|
|
114
|
+
reveal_type(row.boolean_value)
|
|
97
115
|
|
|
116
|
+
# unpack named tuple
|
|
117
|
+
b, i, s = row
|
|
118
|
+
|
|
119
|
+
# ✅ Type of "s" is "str | None"
|
|
120
|
+
reveal_type(s)
|
|
121
|
+
|
|
122
|
+
finally:
|
|
123
|
+
await conn.close()
|
|
124
|
+
```
|
|
98
125
|
|
|
99
126
|
## Syntax
|
|
100
127
|
|
|
@@ -284,10 +311,12 @@ Only those functions are prompted on code completion that make sense in the cont
|
|
|
284
311
|
|
|
285
312
|
#### Run-time behavior
|
|
286
313
|
|
|
287
|
-
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. If the expected and actual signatures don't match,
|
|
314
|
+
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. If the expected and actual signatures don't match, a `TypeMismatchError` exception is raised.
|
|
315
|
+
|
|
316
|
+
If the resultset type has been declared with a subclass of `NamedTuple`, the field names of the tuple are compared against the column names of the PostgreSQL resultset. Should there be a mismatch, a `NameMismatchError` is raised. Field and column order is relevant.
|
|
288
317
|
|
|
289
|
-
The set of values for an enumeration type is validated when a prepared statement is created. The string values declared in a Python `StrEnum` are compared against the values listed in PostgreSQL `CREATE TYPE ... AS ENUM` by querying the system table `pg_enum`. If there are missing or extra values on either side, an
|
|
318
|
+
The set of values for an enumeration type is validated when a prepared statement is created. The string values declared in a Python `StrEnum` are compared against the values listed in PostgreSQL `CREATE TYPE ... AS ENUM` by querying the system table `pg_enum`. If there are missing or extra values on either side, an `EnumMismatchError` exception is raised.
|
|
290
319
|
|
|
291
|
-
Unfortunately, PostgreSQL doesn't propagate nullability via prepared statements: resultset types that are declared as required (e.g. `T` as opposed to `T | None`)
|
|
320
|
+
Unfortunately, PostgreSQL doesn't propagate nullability via prepared statements: resultset types that are declared as required (e.g. `T` as opposed to `T | None`) have to be validated at run time. When a `None` value is encountered for a required type, a `NoneTypeError` exception is raised.
|
|
292
321
|
|
|
293
322
|
PostgreSQL doesn't differentiate between IPv4 and IPv6 network definitions, or IPv4 and IPv6 addresses in the types `cidr` and `inet`. This means that semantically a union type is returned. If you specify a more restrictive type, the resultset data is validated dynamically at run time.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
asyncpg_typed/__init__.py,sha256=gGyWS0ChY1PcCDpSiyycqjE5xDrPcOCH8BjeMzBPlLc,48610
|
|
2
|
+
asyncpg_typed/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
asyncpg_typed-0.1.4.dist-info/licenses/LICENSE,sha256=fsxatgMrv7G5R_IJYOb7FqBNbXIvLidWG9blKAir8k8,1098
|
|
4
|
+
asyncpg_typed-0.1.4.dist-info/METADATA,sha256=cTwlTJjzBFHz9FYH8Ozk3S_z4uVWA0ivnszY-8tZD6A,16126
|
|
5
|
+
asyncpg_typed-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
asyncpg_typed-0.1.4.dist-info/top_level.txt,sha256=T0X1nWnXRTi5a5oTErGy572ORDbM9UV9wfhRXWLsaoY,14
|
|
7
|
+
asyncpg_typed-0.1.4.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
8
|
+
asyncpg_typed-0.1.4.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
asyncpg_typed/__init__.py,sha256=pDwWTWeNqXtw0Z0YrHRu_kneHu20X2SggFWK6aczbY8,38766
|
|
2
|
-
asyncpg_typed/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
asyncpg_typed-0.1.3.dist-info/licenses/LICENSE,sha256=rx4jD36wX8TyLZaR2HEOJ6TphFPjKUqoCSSYWzwWNRk,1093
|
|
4
|
-
asyncpg_typed-0.1.3.dist-info/METADATA,sha256=LTGsagnYy0YHn33DUpIEfkRh63mNyH1rdRxCnpyTNZk,15353
|
|
5
|
-
asyncpg_typed-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
asyncpg_typed-0.1.3.dist-info/top_level.txt,sha256=T0X1nWnXRTi5a5oTErGy572ORDbM9UV9wfhRXWLsaoY,14
|
|
7
|
-
asyncpg_typed-0.1.3.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
8
|
-
asyncpg_typed-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|