TypeDAL 3.3.0__py3-none-any.whl → 3.4.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.

Potentially problematic release.


This version of TypeDAL might be problematic. Click here for more details.

typedal/__about__.py CHANGED
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "3.3.0"
8
+ __version__ = "3.4.0"
typedal/core.py CHANGED
@@ -15,7 +15,7 @@ from collections import defaultdict
15
15
  from copy import copy
16
16
  from decimal import Decimal
17
17
  from pathlib import Path
18
- from typing import Any, Optional
18
+ from typing import Any, Optional, Type
19
19
 
20
20
  import pydal
21
21
  from pydal._globals import DEFAULT
@@ -65,8 +65,8 @@ from .types import (
65
65
  )
66
66
 
67
67
  # use typing.cast(type, ...) to make mypy happy with unions
68
- T_annotation = typing.Type[Any] | types.UnionType
69
- T_Query = typing.Union["Table", Query, bool, None, "TypedTable", typing.Type["TypedTable"]]
68
+ T_annotation = Type[Any] | types.UnionType
69
+ T_Query = typing.Union["Table", Query, bool, None, "TypedTable", Type["TypedTable"]]
70
70
  T_Value = typing.TypeVar("T_Value") # actual type of the Field (via Generic)
71
71
  T_MetaInstance = typing.TypeVar("T_MetaInstance", bound="TypedTable") # bound="TypedTable"; bound="TableMeta"
72
72
  T = typing.TypeVar("T")
@@ -102,7 +102,7 @@ JOIN_OPTIONS = typing.Literal["left", "inner", None]
102
102
  DEFAULT_JOIN_OPTION: JOIN_OPTIONS = "left"
103
103
 
104
104
  # table-ish paramter:
105
- P_Table = typing.Union[typing.Type["TypedTable"], pydal.objects.Table]
105
+ P_Table = typing.Union[Type["TypedTable"], pydal.objects.Table]
106
106
 
107
107
  Condition: typing.TypeAlias = typing.Optional[
108
108
  typing.Callable[
@@ -120,7 +120,7 @@ OnQuery: typing.TypeAlias = typing.Optional[
120
120
  ]
121
121
  ]
122
122
 
123
- To_Type = typing.TypeVar("To_Type", type[Any], typing.Type[Any], str)
123
+ To_Type = typing.TypeVar("To_Type", type[Any], Type[Any], str)
124
124
 
125
125
 
126
126
  class Relationship(typing.Generic[To_Type]):
@@ -129,7 +129,7 @@ class Relationship(typing.Generic[To_Type]):
129
129
  """
130
130
 
131
131
  _type: To_Type
132
- table: typing.Type["TypedTable"] | type | str
132
+ table: Type["TypedTable"] | type | str
133
133
  condition: Condition
134
134
  on: OnQuery
135
135
  multiple: bool
@@ -188,7 +188,7 @@ class Relationship(typing.Generic[To_Type]):
188
188
  join = f":{self.join}" if self.join else ""
189
189
  return f"<Relationship{join} {src_code}>"
190
190
 
191
- def get_table(self, db: "TypeDAL") -> typing.Type["TypedTable"]:
191
+ def get_table(self, db: "TypeDAL") -> Type["TypedTable"]:
192
192
  """
193
193
  Get the table this relationship is bound to.
194
194
  """
@@ -199,7 +199,7 @@ class Relationship(typing.Generic[To_Type]):
199
199
  return mapped
200
200
 
201
201
  # boo, fall back to untyped table but pretend it is typed:
202
- return typing.cast(typing.Type["TypedTable"], db[table]) # eh close enough!
202
+ return typing.cast(Type["TypedTable"], db[table]) # eh close enough!
203
203
 
204
204
  return table
205
205
 
@@ -241,9 +241,7 @@ class Relationship(typing.Generic[To_Type]):
241
241
  return None
242
242
 
243
243
 
244
- def relationship(
245
- _type: To_Type, condition: Condition = None, join: JOIN_OPTIONS = None, on: OnQuery = None
246
- ) -> Relationship[To_Type]:
244
+ def relationship(_type: To_Type, condition: Condition = None, join: JOIN_OPTIONS = None, on: OnQuery = None) -> To_Type:
247
245
  """
248
246
  Define a relationship to another table, when its id is not stored in the current table.
249
247
 
@@ -273,11 +271,17 @@ def relationship(
273
271
 
274
272
  If you'd try to capture this in a single 'condition', pydal would create a cross join which is much less efficient.
275
273
  """
276
- return Relationship(_type, condition, join, on)
274
+ return typing.cast(
275
+ # note: The descriptor `Relationship[To_Type]` is more correct, but pycharm doesn't really get that.
276
+ # so for ease of use, just cast to the refered type for now!
277
+ # e.g. x = relationship(Author) -> x: Author
278
+ To_Type,
279
+ Relationship(_type, condition, join, on),
280
+ )
277
281
 
278
282
 
279
283
  def _generate_relationship_condition(
280
- _: typing.Type["TypedTable"], key: str, field: typing.Union["TypedField[Any]", "Table", typing.Type["TypedTable"]]
284
+ _: Type["TypedTable"], key: str, field: typing.Union["TypedField[Any]", "Table", Type["TypedTable"]]
281
285
  ) -> Condition:
282
286
  origin = typing.get_origin(field)
283
287
  # else: generic
@@ -294,9 +298,9 @@ def _generate_relationship_condition(
294
298
 
295
299
 
296
300
  def to_relationship(
297
- cls: typing.Type["TypedTable"] | type[Any],
301
+ cls: Type["TypedTable"] | type[Any],
298
302
  key: str,
299
- field: typing.Union["TypedField[Any]", "Table", typing.Type["TypedTable"]],
303
+ field: typing.Union["TypedField[Any]", "Table", Type["TypedTable"]],
300
304
  ) -> typing.Optional[Relationship[Any]]:
301
305
  """
302
306
  Used to automatically create relationship instance for reference fields.
@@ -427,7 +431,7 @@ class TypeDAL(pydal.DAL): # type: ignore
427
431
  self.try_define(_TypedalCache)
428
432
  self.try_define(_TypedalCacheDependency)
429
433
 
430
- def try_define(self, model: typing.Type[T], verbose: bool = False) -> typing.Type[T]:
434
+ def try_define(self, model: Type[T], verbose: bool = False) -> Type[T]:
431
435
  """
432
436
  Try to define a model with migrate or fall back to fake migrate.
433
437
  """
@@ -451,9 +455,9 @@ class TypeDAL(pydal.DAL): # type: ignore
451
455
  }
452
456
 
453
457
  # maps table name to typedal class, for resolving future references
454
- _class_map: typing.ClassVar[dict[str, typing.Type["TypedTable"]]] = {}
458
+ _class_map: typing.ClassVar[dict[str, Type["TypedTable"]]] = {}
455
459
 
456
- def _define(self, cls: typing.Type[T], **kwargs: Any) -> typing.Type[T]:
460
+ def _define(self, cls: Type[T], **kwargs: Any) -> Type[T]:
457
461
  # todo: new relationship item added should also invalidate (previously unrelated) cache result
458
462
 
459
463
  # todo: option to enable/disable cache dependency behavior:
@@ -555,7 +559,7 @@ class TypeDAL(pydal.DAL): # type: ignore
555
559
  return cls
556
560
 
557
561
  @typing.overload
558
- def define(self, maybe_cls: None = None, **kwargs: Any) -> typing.Callable[[typing.Type[T]], typing.Type[T]]:
562
+ def define(self, maybe_cls: None = None, **kwargs: Any) -> typing.Callable[[Type[T]], Type[T]]:
559
563
  """
560
564
  Typing Overload for define without a class.
561
565
 
@@ -564,7 +568,7 @@ class TypeDAL(pydal.DAL): # type: ignore
564
568
  """
565
569
 
566
570
  @typing.overload
567
- def define(self, maybe_cls: typing.Type[T], **kwargs: Any) -> typing.Type[T]:
571
+ def define(self, maybe_cls: Type[T], **kwargs: Any) -> Type[T]:
568
572
  """
569
573
  Typing Overload for define with a class.
570
574
 
@@ -572,9 +576,7 @@ class TypeDAL(pydal.DAL): # type: ignore
572
576
  class MyTable(TypedTable): ...
573
577
  """
574
578
 
575
- def define(
576
- self, maybe_cls: typing.Type[T] | None = None, **kwargs: Any
577
- ) -> typing.Type[T] | typing.Callable[[typing.Type[T]], typing.Type[T]]:
579
+ def define(self, maybe_cls: Type[T] | None = None, **kwargs: Any) -> Type[T] | typing.Callable[[Type[T]], Type[T]]:
578
580
  """
579
581
  Can be used as a decorator on a class that inherits `TypedTable`, \
580
582
  or as a regular method if you need to define your classes before you have access to a 'db' instance.
@@ -597,7 +599,7 @@ class TypeDAL(pydal.DAL): # type: ignore
597
599
  the result of pydal.define_table
598
600
  """
599
601
 
600
- def wrapper(cls: typing.Type[T]) -> typing.Type[T]:
602
+ def wrapper(cls: Type[T]) -> Type[T]:
601
603
  return self._define(cls, **kwargs)
602
604
 
603
605
  if maybe_cls:
@@ -647,7 +649,7 @@ class TypeDAL(pydal.DAL): # type: ignore
647
649
 
648
650
  if isinstance(cls, type) and issubclass(type(cls), type) and issubclass(cls, TypedTable):
649
651
  # table defined without @db.define decorator!
650
- _cls: typing.Type[TypedTable] = cls
652
+ _cls: Type[TypedTable] = cls
651
653
  args[0] = _cls.id != None
652
654
 
653
655
  _set = super().__call__(*args, **kwargs)
@@ -671,11 +673,11 @@ class TypeDAL(pydal.DAL): # type: ignore
671
673
  cls, _ftype: T_annotation, mut_kw: typing.MutableMapping[str, Any]
672
674
  ) -> Optional[str]:
673
675
  # ftype can be a union or type. typing.cast is sometimes used to tell mypy when it's not a union.
674
- ftype = typing.cast(type, _ftype) # cast from typing.Type to type to make mypy happy)
676
+ ftype = typing.cast(type, _ftype) # cast from Type to type to make mypy happy)
675
677
 
676
678
  if isinstance(ftype, str):
677
679
  # extract type from string
678
- ftype = typing.get_args(typing.Type[ftype])[0]._evaluate(
680
+ ftype = typing.get_args(Type[ftype])[0]._evaluate(
679
681
  localns=locals(), globalns=globals(), recursive_guard=frozenset()
680
682
  )
681
683
 
@@ -835,13 +837,13 @@ class TableMeta(type):
835
837
  else:
836
838
  return f"<unbound table {self.__name__}>"
837
839
 
838
- def from_row(self: typing.Type[T_MetaInstance], row: pydal.objects.Row) -> T_MetaInstance:
840
+ def from_row(self: Type[T_MetaInstance], row: pydal.objects.Row) -> T_MetaInstance:
839
841
  """
840
842
  Create a model instance from a pydal row.
841
843
  """
842
844
  return self(row)
843
845
 
844
- def all(self: typing.Type[T_MetaInstance]) -> "TypedRows[T_MetaInstance]":
846
+ def all(self: Type[T_MetaInstance]) -> "TypedRows[T_MetaInstance]":
845
847
  """
846
848
  Return all rows for this model.
847
849
  """
@@ -857,7 +859,7 @@ class TableMeta(type):
857
859
  # TypeDAL Modified Logic #
858
860
  ##########################
859
861
 
860
- def insert(self: typing.Type[T_MetaInstance], **fields: Any) -> T_MetaInstance:
862
+ def insert(self: Type[T_MetaInstance], **fields: Any) -> T_MetaInstance:
861
863
  """
862
864
  This is only called when db.define is not used as a decorator.
863
865
 
@@ -880,7 +882,7 @@ class TableMeta(type):
880
882
 
881
883
  return str(table._insert(**fields))
882
884
 
883
- def bulk_insert(self: typing.Type[T_MetaInstance], items: list[AnyDict]) -> "TypedRows[T_MetaInstance]":
885
+ def bulk_insert(self: Type[T_MetaInstance], items: list[AnyDict]) -> "TypedRows[T_MetaInstance]":
884
886
  """
885
887
  Insert multiple rows, returns a TypedRows set of new instances.
886
888
  """
@@ -889,7 +891,7 @@ class TableMeta(type):
889
891
  return self.where(lambda row: row.id.belongs(result)).collect()
890
892
 
891
893
  def update_or_insert(
892
- self: typing.Type[T_MetaInstance], query: T_Query | AnyDict = DEFAULT, **values: Any
894
+ self: Type[T_MetaInstance], query: T_Query | AnyDict = DEFAULT, **values: Any
893
895
  ) -> T_MetaInstance:
894
896
  """
895
897
  Update a row if query matches, else insert a new one.
@@ -912,7 +914,7 @@ class TableMeta(type):
912
914
  return self(record)
913
915
 
914
916
  def validate_and_insert(
915
- self: typing.Type[T_MetaInstance], **fields: Any
917
+ self: Type[T_MetaInstance], **fields: Any
916
918
  ) -> tuple[Optional[T_MetaInstance], Optional[dict[str, str]]]:
917
919
  """
918
920
  Validate input data and then insert a row.
@@ -927,7 +929,7 @@ class TableMeta(type):
927
929
  return None, result.get("errors")
928
930
 
929
931
  def validate_and_update(
930
- self: typing.Type[T_MetaInstance], query: Query, **fields: Any
932
+ self: Type[T_MetaInstance], query: Query, **fields: Any
931
933
  ) -> tuple[Optional[T_MetaInstance], Optional[dict[str, str]]]:
932
934
  """
933
935
  Validate input data and then update max 1 row.
@@ -947,7 +949,7 @@ class TableMeta(type):
947
949
  return None, None
948
950
 
949
951
  def validate_and_update_or_insert(
950
- self: typing.Type[T_MetaInstance], query: Query, **fields: Any
952
+ self: Type[T_MetaInstance], query: Query, **fields: Any
951
953
  ) -> tuple[Optional[T_MetaInstance], Optional[dict[str, str]]]:
952
954
  """
953
955
  Validate input data and then update_and_insert (on max 1 row).
@@ -965,59 +967,57 @@ class TableMeta(type):
965
967
  # update on query without result (shouldnt happen)
966
968
  return None, None
967
969
 
968
- def select(self: typing.Type[T_MetaInstance], *a: Any, **kw: Any) -> "QueryBuilder[T_MetaInstance]":
970
+ def select(self: Type[T_MetaInstance], *a: Any, **kw: Any) -> "QueryBuilder[T_MetaInstance]":
969
971
  """
970
972
  See QueryBuilder.select!
971
973
  """
972
974
  return QueryBuilder(self).select(*a, **kw)
973
975
 
974
- def paginate(self: typing.Type[T_MetaInstance], limit: int, page: int = 1) -> "PaginatedRows[T_MetaInstance]":
976
+ def paginate(self: Type[T_MetaInstance], limit: int, page: int = 1) -> "PaginatedRows[T_MetaInstance]":
975
977
  """
976
978
  See QueryBuilder.paginate!
977
979
  """
978
980
  return QueryBuilder(self).paginate(limit=limit, page=page)
979
981
 
980
- def chunk(
981
- self: typing.Type[T_MetaInstance], chunk_size: int
982
- ) -> typing.Generator["TypedRows[T_MetaInstance]", Any, None]:
982
+ def chunk(self: Type[T_MetaInstance], chunk_size: int) -> typing.Generator["TypedRows[T_MetaInstance]", Any, None]:
983
983
  """
984
984
  See QueryBuilder.chunk!
985
985
  """
986
986
  return QueryBuilder(self).chunk(chunk_size)
987
987
 
988
- def where(self: typing.Type[T_MetaInstance], *a: Any, **kw: Any) -> "QueryBuilder[T_MetaInstance]":
988
+ def where(self: Type[T_MetaInstance], *a: Any, **kw: Any) -> "QueryBuilder[T_MetaInstance]":
989
989
  """
990
990
  See QueryBuilder.where!
991
991
  """
992
992
  return QueryBuilder(self).where(*a, **kw)
993
993
 
994
- def cache(self: typing.Type[T_MetaInstance], *deps: Any, **kwargs: Any) -> "QueryBuilder[T_MetaInstance]":
994
+ def cache(self: Type[T_MetaInstance], *deps: Any, **kwargs: Any) -> "QueryBuilder[T_MetaInstance]":
995
995
  """
996
996
  See QueryBuilder.cache!
997
997
  """
998
998
  return QueryBuilder(self).cache(*deps, **kwargs)
999
999
 
1000
- def count(self: typing.Type[T_MetaInstance]) -> int:
1000
+ def count(self: Type[T_MetaInstance]) -> int:
1001
1001
  """
1002
1002
  See QueryBuilder.count!
1003
1003
  """
1004
1004
  return QueryBuilder(self).count()
1005
1005
 
1006
- def first(self: typing.Type[T_MetaInstance]) -> T_MetaInstance | None:
1006
+ def first(self: Type[T_MetaInstance]) -> T_MetaInstance | None:
1007
1007
  """
1008
1008
  See QueryBuilder.first!
1009
1009
  """
1010
1010
  return QueryBuilder(self).first()
1011
1011
 
1012
- def first_or_fail(self: typing.Type[T_MetaInstance]) -> T_MetaInstance:
1012
+ def first_or_fail(self: Type[T_MetaInstance]) -> T_MetaInstance:
1013
1013
  """
1014
1014
  See QueryBuilder.first_or_fail!
1015
1015
  """
1016
1016
  return QueryBuilder(self).first_or_fail()
1017
1017
 
1018
1018
  def join(
1019
- self: typing.Type[T_MetaInstance],
1020
- *fields: str | typing.Type["TypedTable"],
1019
+ self: Type[T_MetaInstance],
1020
+ *fields: str | Type["TypedTable"],
1021
1021
  method: JOIN_OPTIONS = None,
1022
1022
  on: OnQuery | list[Expression] | Expression = None,
1023
1023
  condition: Condition = None,
@@ -1027,7 +1027,7 @@ class TableMeta(type):
1027
1027
  """
1028
1028
  return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method)
1029
1029
 
1030
- def collect(self: typing.Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
1030
+ def collect(self: Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
1031
1031
  """
1032
1032
  See QueryBuilder.collect!
1033
1033
  """
@@ -1127,7 +1127,7 @@ class TableMeta(type):
1127
1127
  table = self._ensure_table_defined()
1128
1128
  return typing.cast(Expression, table.on(query))
1129
1129
 
1130
- def with_alias(self, alias: str) -> _Table:
1130
+ def with_alias(self: Type[T_MetaInstance], alias: str) -> Type[T_MetaInstance]:
1131
1131
  """
1132
1132
  Shadow Table.with_alias.
1133
1133
 
@@ -1137,7 +1137,7 @@ class TableMeta(type):
1137
1137
  http://web2py.com/books/default/chapter/29/06/the-database-abstraction-layer?search=export_to_csv_file#One-to-many-relation
1138
1138
  """
1139
1139
  table = self._ensure_table_defined()
1140
- return table.with_alias(alias)
1140
+ return typing.cast(Type[T_MetaInstance], table.with_alias(alias))
1141
1141
 
1142
1142
  # @typing.dataclass_transform()
1143
1143
 
@@ -1159,7 +1159,7 @@ class TypedField(Expression, typing.Generic[T_Value]): # pragma: no cover
1159
1159
 
1160
1160
  requires: Validator | typing.Iterable[Validator]
1161
1161
 
1162
- def __init__(self, _type: typing.Type[T_Value] | types.UnionType = str, /, **settings: Any) -> None: # type: ignore
1162
+ def __init__(self, _type: Type[T_Value] | types.UnionType = str, /, **settings: Any) -> None: # type: ignore
1163
1163
  """
1164
1164
  A TypedFieldType should not be inited manually, but TypedField (from `fields.py`) should be used!
1165
1165
  """
@@ -1168,19 +1168,19 @@ class TypedField(Expression, typing.Generic[T_Value]): # pragma: no cover
1168
1168
  # super().__init__()
1169
1169
 
1170
1170
  @typing.overload
1171
- def __get__(self, instance: T_MetaInstance, owner: typing.Type[T_MetaInstance]) -> T_Value: # pragma: no cover
1171
+ def __get__(self, instance: T_MetaInstance, owner: Type[T_MetaInstance]) -> T_Value: # pragma: no cover
1172
1172
  """
1173
1173
  row.field -> (actual data).
1174
1174
  """
1175
1175
 
1176
1176
  @typing.overload
1177
- def __get__(self, instance: None, owner: "typing.Type[TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
1177
+ def __get__(self, instance: None, owner: "Type[TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
1178
1178
  """
1179
1179
  Table.field -> Field.
1180
1180
  """
1181
1181
 
1182
1182
  def __get__(
1183
- self, instance: T_MetaInstance | None, owner: typing.Type[T_MetaInstance]
1183
+ self, instance: T_MetaInstance | None, owner: Type[T_MetaInstance]
1184
1184
  ) -> typing.Union[T_Value, "TypedField[T_Value]"]:
1185
1185
  """
1186
1186
  Since this class is a Descriptor field, \
@@ -1374,7 +1374,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1374
1374
 
1375
1375
  def __new__(
1376
1376
  cls, row_or_id: typing.Union[Row, Query, pydal.objects.Set, int, str, None, "TypedTable"] = None, **filters: Any
1377
- ) -> typing.Self:
1377
+ ) -> Self:
1378
1378
  """
1379
1379
  Create a Typed Rows model instance from an existing row, ID or query.
1380
1380
 
@@ -1596,7 +1596,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1596
1596
  super().__setattr__(key, value)
1597
1597
 
1598
1598
  @classmethod
1599
- def update(cls: typing.Type[T_MetaInstance], query: Query, **fields: Any) -> T_MetaInstance | None:
1599
+ def update(cls: Type[T_MetaInstance], query: Query, **fields: Any) -> T_MetaInstance | None:
1600
1600
  """
1601
1601
  Update one record.
1602
1602
 
@@ -1695,7 +1695,7 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1695
1695
 
1696
1696
  records: dict[int, T_MetaInstance]
1697
1697
  # _rows: Rows
1698
- model: typing.Type[T_MetaInstance]
1698
+ model: Type[T_MetaInstance]
1699
1699
  metadata: Metadata
1700
1700
 
1701
1701
  # pseudo-properties: actually stored in _rows
@@ -1708,7 +1708,7 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1708
1708
  def __init__(
1709
1709
  self,
1710
1710
  rows: Rows,
1711
- model: typing.Type[T_MetaInstance],
1711
+ model: Type[T_MetaInstance],
1712
1712
  records: dict[int, T_MetaInstance] = None,
1713
1713
  metadata: Metadata = None,
1714
1714
  ) -> None:
@@ -1841,15 +1841,6 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1841
1841
  result = super().group_by_value(*fields, **kwargs)
1842
1842
  return typing.cast(dict[T, list[T_MetaInstance]], result)
1843
1843
 
1844
- def column(self, column: str = None) -> list[Any]:
1845
- """
1846
- Get a list of all values in a specific column.
1847
-
1848
- Example:
1849
- rows.column('name') -> ['Name 1', 'Name 2', ...]
1850
- """
1851
- return typing.cast(list[Any], super().column(column))
1852
-
1853
1844
  def as_csv(self) -> str:
1854
1845
  """
1855
1846
  Dump the data to csv.
@@ -1938,7 +1929,7 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1938
1929
  Update the current rows in the database with new_values.
1939
1930
  """
1940
1931
  # cast to make mypy understand .id is a TypedField and not an int!
1941
- table = typing.cast(typing.Type[TypedTable], self.model._ensure_table_defined())
1932
+ table = typing.cast(Type[TypedTable], self.model._ensure_table_defined())
1942
1933
 
1943
1934
  ids = set(self.column("id"))
1944
1935
  query = table.id.belongs(ids)
@@ -1949,7 +1940,7 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1949
1940
  Delete the currently selected rows from the database.
1950
1941
  """
1951
1942
  # cast to make mypy understand .id is a TypedField and not an int!
1952
- table = typing.cast(typing.Type[TypedTable], self.model._ensure_table_defined())
1943
+ table = typing.cast(Type[TypedTable], self.model._ensure_table_defined())
1953
1944
 
1954
1945
  ids = set(self.column("id"))
1955
1946
  query = table.id.belongs(ids)
@@ -2004,7 +1995,7 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2004
1995
 
2005
1996
  @classmethod
2006
1997
  def from_rows(
2007
- cls, rows: Rows, model: typing.Type[T_MetaInstance], metadata: Metadata = None
1998
+ cls, rows: Rows, model: Type[T_MetaInstance], metadata: Metadata = None
2008
1999
  ) -> "TypedRows[T_MetaInstance]":
2009
2000
  """
2010
2001
  Internal method to convert a Rows object to a TypedRows.
@@ -2047,7 +2038,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2047
2038
  Abstration on top of pydal's query system.
2048
2039
  """
2049
2040
 
2050
- model: typing.Type[T_MetaInstance]
2041
+ model: Type[T_MetaInstance]
2051
2042
  query: Query
2052
2043
  select_args: list[Any]
2053
2044
  select_kwargs: SelectKwargs
@@ -2056,7 +2047,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2056
2047
 
2057
2048
  def __init__(
2058
2049
  self,
2059
- model: typing.Type[T_MetaInstance],
2050
+ model: Type[T_MetaInstance],
2060
2051
  add_query: Optional[Query] = None,
2061
2052
  select_args: Optional[list[Any]] = None,
2062
2053
  select_kwargs: Optional[SelectKwargs] = None,
@@ -2151,7 +2142,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2151
2142
 
2152
2143
  def where(
2153
2144
  self,
2154
- *queries_or_lambdas: Query | typing.Callable[[typing.Type[T_MetaInstance]], Query],
2145
+ *queries_or_lambdas: Query | typing.Callable[[Type[T_MetaInstance]], Query],
2155
2146
  **filters: Any,
2156
2147
  ) -> "QueryBuilder[T_MetaInstance]":
2157
2148
  """
@@ -2192,7 +2183,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2192
2183
 
2193
2184
  def join(
2194
2185
  self,
2195
- *fields: str | typing.Type[TypedTable],
2186
+ *fields: str | Type[TypedTable],
2196
2187
  method: JOIN_OPTIONS = None,
2197
2188
  on: OnQuery | list[Expression] | Expression = None,
2198
2189
  condition: Condition = None,
@@ -2219,7 +2210,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2219
2210
  if isinstance(condition, pydal.objects.Query):
2220
2211
  condition = as_lambda(condition)
2221
2212
 
2222
- relationships = {str(fields[0]): relationship(fields[0], condition=condition, join=method)}
2213
+ relationships = {str(fields[0]): Relationship(fields[0], condition=condition, join=method)}
2223
2214
  elif on:
2224
2215
  if len(fields) != 1:
2225
2216
  raise ValueError("join(field, on=...) can only be used with exactly one field!")
@@ -2229,7 +2220,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2229
2220
 
2230
2221
  if isinstance(on, list):
2231
2222
  on = as_lambda(on)
2232
- relationships = {str(fields[0]): relationship(fields[0], on=on, join=method)}
2223
+ relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method)}
2233
2224
 
2234
2225
  else:
2235
2226
  if fields:
@@ -2383,7 +2374,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2383
2374
  return db(query).select(*select_args, **select_kwargs)
2384
2375
 
2385
2376
  def collect(
2386
- self, verbose: bool = False, _to: typing.Type["TypedRows[Any]"] = None, add_id: bool = True
2377
+ self, verbose: bool = False, _to: Type["TypedRows[Any]"] = None, add_id: bool = True
2387
2378
  ) -> "TypedRows[T_MetaInstance]":
2388
2379
  """
2389
2380
  Execute the built query and turn it into model instances, while handling relationships.
@@ -2426,6 +2417,26 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2426
2417
  # only saves if requested in metadata:
2427
2418
  return save_to_cache(typed_rows, rows)
2428
2419
 
2420
+ @typing.overload
2421
+ def column(self, field: TypedField[T]) -> list[T]:
2422
+ """
2423
+ If a typedfield is passed, the output type can be safely determined.
2424
+ """
2425
+
2426
+ @typing.overload
2427
+ def column(self, field: T) -> list[T]:
2428
+ """
2429
+ Otherwise, the output type is loosely determined (assumes `field: type` or Any).
2430
+ """
2431
+
2432
+ def column(self, field: TypedField[T] | T) -> list[T]:
2433
+ """
2434
+ Get all values in a specific column.
2435
+
2436
+ Shortcut for `.select(field).execute().column(field)`.
2437
+ """
2438
+ return self.select(field).execute().column(field)
2439
+
2429
2440
  def _handle_relationships_pre_select(
2430
2441
  self,
2431
2442
  query: Query,
@@ -2521,7 +2532,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2521
2532
  return query, select_args
2522
2533
 
2523
2534
  def _collect_with_relationships(
2524
- self, rows: Rows, metadata: Metadata, _to: typing.Type["TypedRows[Any]"]
2535
+ self, rows: Rows, metadata: Metadata, _to: Type["TypedRows[Any]"]
2525
2536
  ) -> "TypedRows[T_MetaInstance]":
2526
2537
  """
2527
2538
  Transform the raw rows into Typed Table model instances.
@@ -2802,7 +2813,7 @@ class TypedSet(pydal.objects.Set): # type: ignore # pragma: no cover
2802
2813
  result: TypedRows[MyTable] = db(MyTable.id > 0).select()
2803
2814
 
2804
2815
  for row in result:
2805
- typing.reveal_type(row) # MyTable
2816
+ reveal_type(row) # MyTable
2806
2817
  """
2807
2818
  rows = super().select(*fields, **attributes)
2808
2819
  return typing.cast(TypedRows[T_MetaInstance], rows)
typedal/helpers.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Helpers that work independently of core.
3
3
  """
4
4
 
5
+ import datetime as dt
5
6
  import fnmatch
6
7
  import io
7
8
  import types
@@ -265,3 +266,11 @@ def match_strings(patterns: list[str] | str, string_list: list[str]) -> list[str
265
266
  matches.extend([s for s in string_list if fnmatch.fnmatch(s, pattern)])
266
267
 
267
268
  return matches
269
+
270
+
271
+ def utcnow() -> dt.datetime:
272
+ """
273
+ Replacement of datetime.utcnow.
274
+ """
275
+ # return dt.datetime.now(dt.UTC)
276
+ return dt.datetime.now(dt.timezone.utc)
typedal/types.py CHANGED
@@ -79,6 +79,15 @@ class Rows(_Rows): # type: ignore
79
79
  Make mypy happy.
80
80
  """
81
81
 
82
+ def column(self, column: typing.Any = None) -> list[typing.Any]:
83
+ """
84
+ Get a list of all values in a specific column.
85
+
86
+ Example:
87
+ rows.column('name') -> ['Name 1', 'Name 2', ...]
88
+ """
89
+ return [r[str(column) if column else self.colnames[0]] for r in self]
90
+
82
91
 
83
92
  class Validator(_Validator): # type: ignore
84
93
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: TypeDAL
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -29,14 +29,17 @@ Requires-Dist: tabulate; extra == 'all'
29
29
  Requires-Dist: tomlkit; extra == 'all'
30
30
  Requires-Dist: typer; extra == 'all'
31
31
  Provides-Extra: dev
32
+ Requires-Dist: contextlib-chdir; extra == 'dev'
32
33
  Requires-Dist: hatch; extra == 'dev'
33
34
  Requires-Dist: mkdocs; extra == 'dev'
34
35
  Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
35
36
  Requires-Dist: pytest-mypy-testing; extra == 'dev'
36
37
  Requires-Dist: python-semantic-release<8; extra == 'dev'
37
- Requires-Dist: su6[all]; extra == 'dev'
38
+ Requires-Dist: requests<2.32; extra == 'dev'
39
+ Requires-Dist: su6[all]>=1.9.0; extra == 'dev'
38
40
  Requires-Dist: testcontainers; extra == 'dev'
39
41
  Requires-Dist: types-pyyaml; extra == 'dev'
42
+ Requires-Dist: types-requests; extra == 'dev'
40
43
  Requires-Dist: types-tabulate; extra == 'dev'
41
44
  Provides-Extra: migrations
42
45
  Requires-Dist: edwh-migrate>=0.8.0; extra == 'migrations'
@@ -1,19 +1,19 @@
1
- typedal/__about__.py,sha256=3-Xok9ZOvvrfDNbH5xUjdw3tCd6M_cLiUWxppT-gDuk,206
1
+ typedal/__about__.py,sha256=4H4aYxAonMoSZ4TwJQKsDojr3VmwSW8DHN3jahxVVsA,206
2
2
  typedal/__init__.py,sha256=QQpLiVl9w9hm2LBxey49Y_tCF_VB2bScVaS_mCjYy54,366
3
3
  typedal/caching.py,sha256=SMcJsahLlZ79yykWCveERFx1ZJUNEKhA9SPmCTIuLp8,11798
4
4
  typedal/cli.py,sha256=BXXu1w9WRRK9GA82xh_JLvvfpNDFWICXKCAWNg4a3-Y,18180
5
5
  typedal/config.py,sha256=0qy1zrTUdtmXPM9jHzFnSR1DJsqGJqcdG6pvhzKQHe0,11625
6
- typedal/core.py,sha256=Wa6Vu7ppD278tIPAl4eW8rHZRXYETM1rI3-raJ4_8GE,96148
6
+ typedal/core.py,sha256=b8pFiaORXEC0PWaBSmPkQuPtJfk0mwqUiAqxSmX4ZtM,96318
7
7
  typedal/fields.py,sha256=z2PD9vLWqBR_zXtiY0DthqTG4AeF3yxKoeuVfGXnSdg,5197
8
8
  typedal/for_py4web.py,sha256=d07b8hL_PvNDUS26Z5fDH2OxWb-IETBuAFPSzrRwm04,1285
9
9
  typedal/for_web2py.py,sha256=4RHgzGXgKIO_BYB-7adC5e35u52rX-p1t4tPEz-NK24,1867
10
- typedal/helpers.py,sha256=mtRYPFlS0dx2wK8kYSJ4vm1wsTXRkdPumFRlOAjF_xU,7177
10
+ typedal/helpers.py,sha256=KbgP4ZRW7KCroyHwTwdErPqylOX4dsqVnKJeZ5TtTFY,7363
11
11
  typedal/mixins.py,sha256=OHhVYLBGTzPSJKgp1uovBs1Y6NJa0d-DxHTOk9LyOXA,5414
12
12
  typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- typedal/types.py,sha256=AsUHrAXk058zmdqDbZlpf6sE85ID1Q2vofsGgCXni1s,4851
13
+ typedal/types.py,sha256=fyTOAhORFufZsAKnf1dMcWJxLK-UMX6UFl-JbiqFmSk,5161
14
14
  typedal/web2py_py4web_shared.py,sha256=VK9T8P5UwVLvfNBsY4q79ANcABv-jX76YKADt1Zz_co,1539
15
15
  typedal/serializers/as_json.py,sha256=ffo152W-sARYXym4BzwX709rrO2-QwKk2KunWY8RNl4,2229
16
- typedal-3.3.0.dist-info/METADATA,sha256=IiO1uKvVp-xZHz_9xeAyizZVAA-WQlAuPhmOxRRVp7E,9316
17
- typedal-3.3.0.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
18
- typedal-3.3.0.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
19
- typedal-3.3.0.dist-info/RECORD,,
16
+ typedal-3.4.0.dist-info/METADATA,sha256=onWL5IwfJALWrdfIPUQNYrmFdIcMNZqRAlGixK88Tv0,9462
17
+ typedal-3.4.0.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
18
+ typedal-3.4.0.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
19
+ typedal-3.4.0.dist-info/RECORD,,