TypeDAL 5.0.5__tar.gz → 5.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {typedal-5.0.5 → typedal-5.1.0}/PKG-INFO +1 -1
- {typedal-5.0.5 → typedal-5.1.0}/pyproject.toml +1 -1
- {typedal-5.0.5 → typedal-5.1.0}/pyproject.toml.orig +1 -1
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/asynchronous.py +1 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/caching.py +1 -2
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/cli.py +2 -1
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/config.py +1 -1
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/core.py +11 -6
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/define.py +5 -3
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/fields.py +4 -4
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/mixins.py +1 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/query_builder.py +64 -17
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/relationships.py +9 -8
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/rows.py +30 -7
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/serializers/typescript.py +2 -1
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/tables.py +43 -8
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/types.py +40 -54
- typedal-5.1.0/src/typedal/warnings.py +5 -0
- {typedal-5.0.5 → typedal-5.1.0}/README.md +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/__about__.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/__init__.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/constants.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/enum_helpers.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/for_py4web.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/for_web2py.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/helpers.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/py.typed +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-5.0.5 → typedal-5.1.0}/src/typedal/web2py_py4web_shared.py +0 -0
|
@@ -9,13 +9,12 @@ import json
|
|
|
9
9
|
import typing as t
|
|
10
10
|
|
|
11
11
|
import dill # nosec
|
|
12
|
-
from pydal.objects import Field, Rows, Set
|
|
13
12
|
|
|
14
13
|
from .fields import TypedField
|
|
15
14
|
from .helpers import throw
|
|
16
15
|
from .rows import TypedRows
|
|
17
16
|
from .tables import TypedTable
|
|
18
|
-
from .types import CacheStatus, Query
|
|
17
|
+
from .types import CacheStatus, Field, Query, Rows, Set
|
|
19
18
|
|
|
20
19
|
if t.TYPE_CHECKING:
|
|
21
20
|
from .core import TypeDAL
|
|
@@ -30,6 +30,7 @@ except ImportError as e:
|
|
|
30
30
|
"to fix this.",
|
|
31
31
|
source=e,
|
|
32
32
|
category=RuntimeWarning,
|
|
33
|
+
stacklevel=2,
|
|
33
34
|
)
|
|
34
35
|
exit(127) # command not found
|
|
35
36
|
|
|
@@ -226,7 +227,7 @@ def setup(
|
|
|
226
227
|
data.pop("connection", None)
|
|
227
228
|
|
|
228
229
|
# ignore any None:
|
|
229
|
-
old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None}
|
|
230
|
+
old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None} # type: ignore
|
|
230
231
|
|
|
231
232
|
with toml_path.open("w") as f:
|
|
232
233
|
tomlkit.dump(old_contents, f)
|
|
@@ -159,7 +159,7 @@ def _load_toml(path: str | bool | Path | None = True) -> tuple[str, AnyDict]:
|
|
|
159
159
|
|
|
160
160
|
return str(toml_path) or "", t.cast(AnyDict, data["tool"]["typedal"])
|
|
161
161
|
except Exception as e:
|
|
162
|
-
warnings.warn(f"Could not load typedal config toml: {e}", source=e)
|
|
162
|
+
warnings.warn(f"Could not load typedal config toml: {e}", source=e, stacklevel=2)
|
|
163
163
|
return str(toml_path) or "", {}
|
|
164
164
|
|
|
165
165
|
|
|
@@ -51,10 +51,10 @@ if t.TYPE_CHECKING:
|
|
|
51
51
|
|
|
52
52
|
def _expression_subclasses() -> t.Iterator[type]:
|
|
53
53
|
"""
|
|
54
|
-
Yield
|
|
54
|
+
Yield Expression and every (nested) subclass currently loaded, e.g. Field and TypedField.
|
|
55
55
|
"""
|
|
56
|
-
seen = {
|
|
57
|
-
stack = [
|
|
56
|
+
seen: set[type[Expression]] = {Expression}
|
|
57
|
+
stack: list[type[Expression]] = [Expression]
|
|
58
58
|
while stack:
|
|
59
59
|
for subclass in stack.pop().__subclasses__():
|
|
60
60
|
if subclass not in seen:
|
|
@@ -77,7 +77,7 @@ def _purge_dialect_expressions(adapter: t.Any) -> None:
|
|
|
77
77
|
`gc.collect()` will not free it, and with it the entire db (tables, fields, models)
|
|
78
78
|
it belongs to, for the lifetime of the process, unless explicitly undone here.
|
|
79
79
|
"""
|
|
80
|
-
expressions =
|
|
80
|
+
expressions = Expression._dialect_expressions_
|
|
81
81
|
stale = [
|
|
82
82
|
name
|
|
83
83
|
for name, wrapper in expressions.items()
|
|
@@ -476,7 +476,12 @@ class TypeDAL(_TypeDALBase):
|
|
|
476
476
|
delattr(self, tablename)
|
|
477
477
|
|
|
478
478
|
if verbose:
|
|
479
|
-
warnings.warn(
|
|
479
|
+
warnings.warn(
|
|
480
|
+
f"{model} could not be migrated, try faking",
|
|
481
|
+
source=e,
|
|
482
|
+
category=RuntimeWarning,
|
|
483
|
+
stacklevel=2,
|
|
484
|
+
)
|
|
480
485
|
|
|
481
486
|
# try again:
|
|
482
487
|
return self.define(model, migrate=self._migrate, fake_migrate=self._migrate, redefine=True)
|
|
@@ -544,7 +549,7 @@ class TypeDAL(_TypeDALBase):
|
|
|
544
549
|
|
|
545
550
|
return wrapper
|
|
546
551
|
|
|
547
|
-
def __call__(self, *_args: T_Query, **kwargs: t.Any) -> "TypedSet":
|
|
552
|
+
def __call__(self, *_args: T_Query, **kwargs: t.Any) -> "TypedSet":
|
|
548
553
|
"""
|
|
549
554
|
A db instance can be called directly to perform a query.
|
|
550
555
|
|
|
@@ -12,7 +12,6 @@ import types
|
|
|
12
12
|
import typing as t
|
|
13
13
|
import warnings
|
|
14
14
|
|
|
15
|
-
import pydal
|
|
16
15
|
from pydal.validators import IS_IN_SET, ValidationError, Validator
|
|
17
16
|
|
|
18
17
|
from .constants import BASIC_MAPPINGS
|
|
@@ -124,7 +123,10 @@ class TableDefinitionBuilder:
|
|
|
124
123
|
self.class_map[table._rname] = cls # table_model - sql name
|
|
125
124
|
cls.__on_define__(self.db)
|
|
126
125
|
else:
|
|
127
|
-
warnings.warn(
|
|
126
|
+
warnings.warn(
|
|
127
|
+
"db.define used without inheriting TypedTable. This could lead to strange problems!",
|
|
128
|
+
stacklevel=2,
|
|
129
|
+
)
|
|
128
130
|
|
|
129
131
|
if not tablename.startswith("typedal_") and cache_dependency:
|
|
130
132
|
from .caching import _remove_cache, remove_cache_for_table
|
|
@@ -163,7 +165,7 @@ class TableDefinitionBuilder:
|
|
|
163
165
|
if mapping := BASIC_MAPPINGS.get(ftype):
|
|
164
166
|
# basi types
|
|
165
167
|
return mapping
|
|
166
|
-
elif isinstance(ftype,
|
|
168
|
+
elif isinstance(ftype, Table):
|
|
167
169
|
# db.table
|
|
168
170
|
return f"reference {ftype._tablename}"
|
|
169
171
|
elif issubclass(type(ftype), type) and issubclass(ftype, TypedTable):
|
|
@@ -14,7 +14,6 @@ import uuid
|
|
|
14
14
|
|
|
15
15
|
import pydal
|
|
16
16
|
from pydal.helpers.classes import SQLCustomType
|
|
17
|
-
from pydal.objects import Table
|
|
18
17
|
|
|
19
18
|
from .core import TypeDAL
|
|
20
19
|
from .types import (
|
|
@@ -26,6 +25,7 @@ from .types import (
|
|
|
26
25
|
Query,
|
|
27
26
|
T_annotation,
|
|
28
27
|
T_MetaInstance,
|
|
28
|
+
Table,
|
|
29
29
|
Validator,
|
|
30
30
|
)
|
|
31
31
|
|
|
@@ -190,12 +190,12 @@ class TypedField[T_Value](Expression): # pragma: no cover
|
|
|
190
190
|
extra_kwargs,
|
|
191
191
|
)
|
|
192
192
|
|
|
193
|
-
def bind(self, field:
|
|
193
|
+
def bind(self, field: Field, table: Table) -> None:
|
|
194
194
|
"""
|
|
195
195
|
Bind the right db/table/field info to this class, so queries can be made using `Class.field == ...`.
|
|
196
196
|
"""
|
|
197
197
|
self._table = table
|
|
198
|
-
self._field =
|
|
198
|
+
self._field = field
|
|
199
199
|
|
|
200
200
|
def unbind(self) -> None:
|
|
201
201
|
"""Remove references to the pydal objects created during `bind`."""
|
|
@@ -280,7 +280,7 @@ class TypedField[T_Value](Expression): # pragma: no cover
|
|
|
280
280
|
return t.cast(Expression, self._field.lower())
|
|
281
281
|
|
|
282
282
|
|
|
283
|
-
def rname(field: TypedField[t.Any] | Field
|
|
283
|
+
def rname(field: TypedField[t.Any] | Field) -> str:
|
|
284
284
|
"""
|
|
285
285
|
Return the full rname (table and field).
|
|
286
286
|
"""
|
|
@@ -8,9 +8,10 @@ import datetime as dt
|
|
|
8
8
|
import math
|
|
9
9
|
import time
|
|
10
10
|
import typing as t
|
|
11
|
+
import warnings
|
|
11
12
|
from collections import defaultdict
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
from pydal.helpers.classes import SQLALL
|
|
14
15
|
|
|
15
16
|
from .asynchronous import run_async
|
|
16
17
|
from .constants import DEFAULT_JOIN_OPTION, JOIN_OPTIONS
|
|
@@ -38,15 +39,17 @@ from .types import (
|
|
|
38
39
|
Query,
|
|
39
40
|
Row,
|
|
40
41
|
Rows,
|
|
42
|
+
Select,
|
|
41
43
|
SelectKwargs,
|
|
42
44
|
T_MetaInstance,
|
|
43
45
|
Table,
|
|
44
46
|
merge_permissions,
|
|
45
47
|
require_permission,
|
|
46
48
|
)
|
|
49
|
+
from .warnings import UnusedWindowWarning
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
class QueryBuilder[T_MetaInstance: _TypedTable](
|
|
52
|
+
class QueryBuilder[T_MetaInstance: _TypedTable](Select):
|
|
50
53
|
"""
|
|
51
54
|
Abstration on top of pydal's query system.
|
|
52
55
|
"""
|
|
@@ -290,12 +293,12 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
290
293
|
|
|
291
294
|
subquery = t.cast(Query, DummyQuery())
|
|
292
295
|
for query_part in queries_or_lambdas:
|
|
293
|
-
if isinstance(query_part,
|
|
294
|
-
subquery |=
|
|
295
|
-
elif isinstance(query_part, (
|
|
296
|
+
if isinstance(query_part, Field) or is_typed_field(query_part):
|
|
297
|
+
subquery |= query_part != None
|
|
298
|
+
elif isinstance(query_part, (Query, Expression)):
|
|
296
299
|
subquery |= t.cast(Query, query_part)
|
|
297
300
|
elif callable(query_part):
|
|
298
|
-
if result := query_part(self.model):
|
|
301
|
+
if result := query_part(self.model):
|
|
299
302
|
subquery |= result
|
|
300
303
|
elif isinstance(query_part, dict):
|
|
301
304
|
subsubquery = DummyQuery()
|
|
@@ -401,7 +404,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
401
404
|
if len(fields) != 1:
|
|
402
405
|
raise ValueError("join(field, condition=...) can only be used with exactly one field!")
|
|
403
406
|
|
|
404
|
-
if isinstance(condition,
|
|
407
|
+
if isinstance(condition, Query):
|
|
405
408
|
condition = as_lambda(condition)
|
|
406
409
|
|
|
407
410
|
field = fields[0]
|
|
@@ -418,8 +421,8 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
418
421
|
if len(fields) != 1:
|
|
419
422
|
raise ValueError("join(field, on=...) can only be used with exactly one field!")
|
|
420
423
|
|
|
421
|
-
if isinstance(on,
|
|
422
|
-
on =
|
|
424
|
+
if isinstance(on, Expression):
|
|
425
|
+
on = [on]
|
|
423
426
|
|
|
424
427
|
if isinstance(on, list):
|
|
425
428
|
on = t.cast(OnQuery, as_lambda(on))
|
|
@@ -661,6 +664,17 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
661
664
|
|
|
662
665
|
query, select_args, select_kwargs = self._before_query(metadata, add_id=add_id)
|
|
663
666
|
|
|
667
|
+
window_size = metadata.get("window_size", None)
|
|
668
|
+
is_iterating = metadata.get("iterating", False)
|
|
669
|
+
|
|
670
|
+
if window_size is not None and is_iterating is not True:
|
|
671
|
+
warnings.warn(
|
|
672
|
+
"A window size was configured, but collect() was called directly. "
|
|
673
|
+
"Use iteration over the query builder to fetch rows in windows.",
|
|
674
|
+
stacklevel=2,
|
|
675
|
+
category=UnusedWindowWarning,
|
|
676
|
+
)
|
|
677
|
+
|
|
664
678
|
metadata["sql"] = db(query)._select(*select_args, **select_kwargs)
|
|
665
679
|
|
|
666
680
|
if verbose: # pragma: no cover
|
|
@@ -856,7 +870,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
856
870
|
expression_without_direction, _, direction = expression.rpartition(" ")
|
|
857
871
|
return [expression_without_direction if direction.upper() in {"ASC", "DESC"} else orderby]
|
|
858
872
|
|
|
859
|
-
if isinstance(orderby,
|
|
873
|
+
if isinstance(orderby, Field):
|
|
860
874
|
return t.cast(list[OrderBy], [orderby])
|
|
861
875
|
|
|
862
876
|
fields = []
|
|
@@ -968,8 +982,8 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
968
982
|
if not isinstance(on, list):
|
|
969
983
|
on = [on]
|
|
970
984
|
|
|
971
|
-
on = [_ for _ in on if isinstance(_,
|
|
972
|
-
left_joins.extend(on)
|
|
985
|
+
on = [_ for _ in on if isinstance(_, Expression)]
|
|
986
|
+
left_joins.extend(on)
|
|
973
987
|
elif method == "left":
|
|
974
988
|
# Generate left join condition
|
|
975
989
|
if not is_self_reference:
|
|
@@ -1032,7 +1046,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
1032
1046
|
if pre_alias != post_alias:
|
|
1033
1047
|
updated_args = []
|
|
1034
1048
|
for arg in select_args:
|
|
1035
|
-
if isinstance(arg,
|
|
1049
|
+
if isinstance(arg, SQLALL):
|
|
1036
1050
|
updated_args.extend(str(field).replace(f"{pre_alias}.", f"{post_alias}.") for field in arg._table)
|
|
1037
1051
|
else:
|
|
1038
1052
|
updated_args.append(str(arg).replace(f"{pre_alias}.", f"{post_alias}."))
|
|
@@ -1221,13 +1235,40 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
1221
1235
|
"""
|
|
1222
1236
|
You can start iterating a Query Builder object before calling collect, for ease of use.
|
|
1223
1237
|
"""
|
|
1224
|
-
|
|
1238
|
+
builder = self._extend(metadata={"iterating": True})
|
|
1239
|
+
window_size = self.metadata.get("window_size", None)
|
|
1240
|
+
|
|
1241
|
+
if not window_size:
|
|
1242
|
+
yield from builder.collect()
|
|
1243
|
+
return
|
|
1244
|
+
|
|
1245
|
+
for chunk in builder.chunk(window_size):
|
|
1246
|
+
yield from chunk
|
|
1247
|
+
|
|
1248
|
+
def __await__(self):
|
|
1249
|
+
return self.collect_async().__await__()
|
|
1250
|
+
|
|
1251
|
+
async def __aiter__(self):
|
|
1252
|
+
builder = self._extend(metadata={"iterating": True})
|
|
1253
|
+
|
|
1254
|
+
window_size = self.metadata.get("window_size", None)
|
|
1255
|
+
|
|
1256
|
+
if window_size:
|
|
1257
|
+
async for chunk in builder.chunk_async(window_size):
|
|
1258
|
+
for row in chunk:
|
|
1259
|
+
yield row
|
|
1260
|
+
else:
|
|
1261
|
+
rows = await builder.collect_async()
|
|
1262
|
+
for row in rows:
|
|
1263
|
+
yield row
|
|
1264
|
+
|
|
1265
|
+
def window(self, window_size: int) -> QueryBuilder[T_MetaInstance]:
|
|
1266
|
+
return self._extend(metadata={"window_size": window_size})
|
|
1225
1267
|
|
|
1226
1268
|
def __count(
|
|
1227
1269
|
self,
|
|
1228
1270
|
db: TypeDAL,
|
|
1229
1271
|
distinct: t.Optional[bool] = None,
|
|
1230
|
-
*,
|
|
1231
1272
|
include_left_for_distinct: bool = True,
|
|
1232
1273
|
) -> Query:
|
|
1233
1274
|
# internal, shared logic between .count and ._count
|
|
@@ -1350,7 +1391,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
1350
1391
|
pass
|
|
1351
1392
|
```
|
|
1352
1393
|
"""
|
|
1353
|
-
require_permission(
|
|
1394
|
+
# require_permission checked in .collect()
|
|
1395
|
+
if "limitby" in self.select_kwargs:
|
|
1396
|
+
raise ValueError("chunk() cannot be combined with an existing limitby")
|
|
1397
|
+
|
|
1354
1398
|
page = 1
|
|
1355
1399
|
|
|
1356
1400
|
while rows := self.__paginate(chunk_size, page).collect():
|
|
@@ -1505,7 +1549,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
|
|
|
1505
1549
|
writer can be visible halfway through the iteration; wrap it in `db.session()` if you
|
|
1506
1550
|
need every page to come from the same snapshot.
|
|
1507
1551
|
"""
|
|
1508
|
-
require_permission(
|
|
1552
|
+
# require_permission checked in .collect()
|
|
1553
|
+
if "limitby" in self.select_kwargs:
|
|
1554
|
+
raise ValueError("chunk_async() cannot be combined with an existing limitby")
|
|
1555
|
+
|
|
1509
1556
|
db = self._get_db()
|
|
1510
1557
|
page = 1
|
|
1511
1558
|
|
|
@@ -6,14 +6,12 @@ import inspect
|
|
|
6
6
|
import typing as t
|
|
7
7
|
import warnings
|
|
8
8
|
|
|
9
|
-
import pydal.objects
|
|
10
|
-
|
|
11
9
|
from .config import LazyPolicy
|
|
12
10
|
from .constants import JOIN_OPTIONS
|
|
13
11
|
from .core import ForwardRef, TypeDAL, evaluate_forward_reference
|
|
14
12
|
from .fields import TypedField
|
|
15
13
|
from .helpers import extract_type_optional, looks_like, unwrap_type
|
|
16
|
-
from .types import Condition, OnQuery, T_Field
|
|
14
|
+
from .types import Condition, OnQuery, T_Field, Table
|
|
17
15
|
|
|
18
16
|
# default lazy policy is defined at the TypeDAL() instance settings level
|
|
19
17
|
|
|
@@ -112,7 +110,7 @@ class Relationship[To_Type]:
|
|
|
112
110
|
|
|
113
111
|
@staticmethod
|
|
114
112
|
def _error_duplicate_condition(condition: Condition, on: OnQuery) -> t.Never:
|
|
115
|
-
warnings.warn(f"Relation | Both specified! {condition=} {on=}")
|
|
113
|
+
warnings.warn(f"Relation | Both specified! {condition=} {on=}", stacklevel=2)
|
|
116
114
|
raise ValueError("Please specify either a condition or an 'on' statement for this relationship!")
|
|
117
115
|
|
|
118
116
|
def __repr__(self) -> str:
|
|
@@ -199,7 +197,7 @@ class Relationship[To_Type]:
|
|
|
199
197
|
if isinstance(self.table, str):
|
|
200
198
|
return self.table
|
|
201
199
|
|
|
202
|
-
if isinstance(self.table,
|
|
200
|
+
if isinstance(self.table, Table):
|
|
203
201
|
return str(self.table)
|
|
204
202
|
|
|
205
203
|
# else: typed table
|
|
@@ -280,6 +278,7 @@ class Relationship[To_Type]:
|
|
|
280
278
|
f"Trying to access relationship '{self.name}' without joining. "
|
|
281
279
|
f"Did you forget to use .join('{self.name}')? Returning empty value.",
|
|
282
280
|
category=RuntimeWarning,
|
|
281
|
+
stacklevel=2,
|
|
283
282
|
)
|
|
284
283
|
return fallback_value
|
|
285
284
|
|
|
@@ -288,7 +287,7 @@ class Relationship[To_Type]:
|
|
|
288
287
|
resolved_table = self.get_table(instance._db)
|
|
289
288
|
|
|
290
289
|
builder = owner.where(id=instance.id).join(self.name)
|
|
291
|
-
if issubclass(resolved_table, TypedTable) or isinstance(resolved_table,
|
|
290
|
+
if issubclass(resolved_table, TypedTable) or isinstance(resolved_table, Table):
|
|
292
291
|
# is a table so we can select ALL and ignore non-required fields of parent row:
|
|
293
292
|
builder = builder.select(owner.id, resolved_table.ALL)
|
|
294
293
|
|
|
@@ -298,6 +297,7 @@ class Relationship[To_Type]:
|
|
|
298
297
|
"This performs an extra database query. "
|
|
299
298
|
f"Consider using .join('{self.name}') for better performance.",
|
|
300
299
|
category=RuntimeWarning,
|
|
300
|
+
stacklevel=2,
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
return builder.first()[self.name] # type: ignore
|
|
@@ -306,6 +306,7 @@ class Relationship[To_Type]:
|
|
|
306
306
|
f"Failed to lazy load relationship '{self.name}': {e}",
|
|
307
307
|
category=RuntimeWarning,
|
|
308
308
|
source=e,
|
|
309
|
+
stacklevel=2,
|
|
309
310
|
)
|
|
310
311
|
|
|
311
312
|
return fallback_value
|
|
@@ -503,12 +504,12 @@ def to_relationship(
|
|
|
503
504
|
try:
|
|
504
505
|
condition = _generate_relationship_condition(cls, key, field)
|
|
505
506
|
except Exception as e: # pragma: no cover
|
|
506
|
-
warnings.warn("Could not generate Relationship condition", source=e)
|
|
507
|
+
warnings.warn("Could not generate Relationship condition", source=e, stacklevel=2)
|
|
507
508
|
condition = None
|
|
508
509
|
|
|
509
510
|
if not condition: # pragma: no cover
|
|
510
511
|
# something went wrong, not a valid relationship
|
|
511
|
-
warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}")
|
|
512
|
+
warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}", stacklevel=2)
|
|
512
513
|
return None
|
|
513
514
|
|
|
514
515
|
join = "left" if optional or t.get_origin(field) is list else "inner"
|
|
@@ -8,8 +8,6 @@ import csv
|
|
|
8
8
|
import json
|
|
9
9
|
import typing as t
|
|
10
10
|
|
|
11
|
-
import pydal.objects
|
|
12
|
-
|
|
13
11
|
from .asynchronous import run_async
|
|
14
12
|
from .core import TypeDAL
|
|
15
13
|
from .helpers import mktable
|
|
@@ -25,6 +23,7 @@ from .types import (
|
|
|
25
23
|
Query,
|
|
26
24
|
Row,
|
|
27
25
|
Rows,
|
|
26
|
+
Set,
|
|
28
27
|
T_MetaInstance,
|
|
29
28
|
)
|
|
30
29
|
|
|
@@ -199,18 +198,42 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
|
|
|
199
198
|
|
|
200
199
|
return mktable(data, headers)
|
|
201
200
|
|
|
202
|
-
|
|
201
|
+
@t.overload
|
|
202
|
+
def group_by_value[T: t.Any](
|
|
203
|
+
self,
|
|
204
|
+
*fields: "TypedField[T] | str | Field",
|
|
205
|
+
one_result: t.Literal[True],
|
|
206
|
+
**kwargs: t.Any,
|
|
207
|
+
) -> dict[T, T_MetaInstance]: ...
|
|
208
|
+
|
|
209
|
+
@t.overload
|
|
210
|
+
def group_by_value[T: t.Any](
|
|
211
|
+
self,
|
|
212
|
+
*fields: "TypedField[T] | str | Field",
|
|
213
|
+
one_result: t.Literal[False] = False,
|
|
214
|
+
**kwargs: t.Any,
|
|
215
|
+
) -> dict[T, list[T_MetaInstance]]: ...
|
|
216
|
+
|
|
217
|
+
@t.overload
|
|
218
|
+
def group_by_value[T: t.Any](
|
|
219
|
+
self,
|
|
220
|
+
*fields: "TypedField[T] | str | Field",
|
|
221
|
+
one_result: bool,
|
|
222
|
+
**kwargs: t.Any,
|
|
223
|
+
) -> dict[T, T_MetaInstance] | dict[T, list[T_MetaInstance]]: ...
|
|
224
|
+
|
|
225
|
+
def group_by_value[T: t.Any](
|
|
203
226
|
self,
|
|
204
|
-
*fields: "
|
|
227
|
+
*fields: "TypedField[T] | str | Field",
|
|
205
228
|
one_result: bool = False,
|
|
206
229
|
**kwargs: t.Any,
|
|
207
|
-
) -> dict[T, list[T_MetaInstance]]:
|
|
230
|
+
) -> dict[T, T_MetaInstance] | dict[T, list[T_MetaInstance]]:
|
|
208
231
|
"""
|
|
209
232
|
Group the rows by a specific field (which will be the dict key).
|
|
210
233
|
"""
|
|
211
234
|
kwargs["one_result"] = one_result
|
|
212
235
|
result = super().group_by_value(*fields, **kwargs)
|
|
213
|
-
return t.cast(dict[T, list[T_MetaInstance]], result)
|
|
236
|
+
return t.cast(dict[T, T_MetaInstance] | dict[T, list[T_MetaInstance]], result)
|
|
214
237
|
|
|
215
238
|
def as_csv(self) -> str:
|
|
216
239
|
"""
|
|
@@ -533,7 +556,7 @@ class PaginatedRows(TypedRows[T_MetaInstance]):
|
|
|
533
556
|
return {"data": super().as_dict(), "pagination": self.pagination}
|
|
534
557
|
|
|
535
558
|
|
|
536
|
-
class TypedSet(
|
|
559
|
+
class TypedSet(Set): # pragma: no cover
|
|
537
560
|
"""
|
|
538
561
|
Used to make pydal Set more typed.
|
|
539
562
|
|
|
@@ -31,7 +31,7 @@ class TypedDictRegistry(Singleton):
|
|
|
31
31
|
def __init__(self) -> None:
|
|
32
32
|
"""Initialize the singleton registry and optional shared typtyp world."""
|
|
33
33
|
self._types: dict[type, type[dict[str, t.Any]]] = {}
|
|
34
|
-
self._world = typtyp.World() if typtyp else None
|
|
34
|
+
self._world = typtyp.World() if typtyp else None # type: ignore
|
|
35
35
|
self._names: set[str] = set()
|
|
36
36
|
|
|
37
37
|
@property
|
|
@@ -78,6 +78,7 @@ class TypedDictRegistry(Singleton):
|
|
|
78
78
|
if world is None: # pragma: ignore
|
|
79
79
|
warnings.warn(
|
|
80
80
|
f"`{caller_name}` can not be used without the typescript extra. Please install `typedal[typescript]`",
|
|
81
|
+
stacklevel=2,
|
|
81
82
|
)
|
|
82
83
|
return ""
|
|
83
84
|
|
|
@@ -13,9 +13,9 @@ import types
|
|
|
13
13
|
import typing as t
|
|
14
14
|
import uuid
|
|
15
15
|
|
|
16
|
-
import pydal
|
|
16
|
+
import pydal
|
|
17
17
|
from pydal._globals import DEFAULT
|
|
18
|
-
from pydal.helpers.classes import SQLCallableList
|
|
18
|
+
from pydal.helpers.classes import SQLALL, SQLCallableList
|
|
19
19
|
|
|
20
20
|
from .asynchronous import run_async
|
|
21
21
|
from .constants import JOIN_OPTIONS
|
|
@@ -50,7 +50,7 @@ if t.TYPE_CHECKING:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def reorder_fields(
|
|
53
|
-
table:
|
|
53
|
+
table: Table,
|
|
54
54
|
fields: t.Iterable[str | TypedField[t.Any] | Field],
|
|
55
55
|
keep_others: bool = True,
|
|
56
56
|
) -> None:
|
|
@@ -65,7 +65,7 @@ def reorder_fields(
|
|
|
65
65
|
- False: remove other fields (only keep what's specified).
|
|
66
66
|
"""
|
|
67
67
|
# Normalize input to field names
|
|
68
|
-
desired = [f.name if isinstance(f, (TypedField, Field
|
|
68
|
+
desired = [f.name if isinstance(f, (TypedField, Field)) else str(f) for f in fields]
|
|
69
69
|
|
|
70
70
|
new_order = [f for f in desired if f in table._fields]
|
|
71
71
|
|
|
@@ -105,6 +105,8 @@ class TableMeta(type):
|
|
|
105
105
|
_table: Table | None = None
|
|
106
106
|
_relationships: dict[str, Relationship[t.Any]] | None = None
|
|
107
107
|
_permissions: Permissions | None = None
|
|
108
|
+
_singular: str
|
|
109
|
+
_plural: str
|
|
108
110
|
|
|
109
111
|
#########################
|
|
110
112
|
# TypeDAL custom logic: #
|
|
@@ -136,6 +138,23 @@ class TableMeta(type):
|
|
|
136
138
|
self._relationships = None
|
|
137
139
|
self._permissions = None
|
|
138
140
|
|
|
141
|
+
def __getattribute__(self, col: str) -> t.Any:
|
|
142
|
+
"""
|
|
143
|
+
Resolve bound table fields before metaclass convenience methods.
|
|
144
|
+
|
|
145
|
+
Annotation-only fields are not stored in the model class dictionary, so
|
|
146
|
+
normal class lookup would otherwise return a same-named method defined
|
|
147
|
+
on this metaclass. For example, a model may have a 'window' column
|
|
148
|
+
while this metaclass also provides 'window()'. The column takes
|
|
149
|
+
precedence on the model class; use 'QueryBuilder(Model).window()' to
|
|
150
|
+
call the query helper in that case.
|
|
151
|
+
"""
|
|
152
|
+
table = type.__getattribute__(self, "_table")
|
|
153
|
+
if table is not None and col in table._fields:
|
|
154
|
+
return table[col]
|
|
155
|
+
|
|
156
|
+
return type.__getattribute__(self, col)
|
|
157
|
+
|
|
139
158
|
def __getattr__(self, col: str) -> t.Optional[Field]:
|
|
140
159
|
"""
|
|
141
160
|
Magic method used by TypedTableMeta to get a database field with dot notation on a class.
|
|
@@ -149,6 +168,14 @@ class TableMeta(type):
|
|
|
149
168
|
|
|
150
169
|
return None
|
|
151
170
|
|
|
171
|
+
def __setattr__(self, name: str, value: t.Any) -> None:
|
|
172
|
+
"""Keep PyDAL's table label metadata in sync with its model class."""
|
|
173
|
+
if name.startswith("_") and self._table is not None:
|
|
174
|
+
# include base table for internal stuff such as _singular/_plural:
|
|
175
|
+
setattr(self._table, name, value)
|
|
176
|
+
|
|
177
|
+
super().__setattr__(name, value)
|
|
178
|
+
|
|
152
179
|
def _ensure_table_defined(self) -> Table:
|
|
153
180
|
if not self._table:
|
|
154
181
|
raise EnvironmentError("@define or db.define is not called on this class yet!")
|
|
@@ -182,7 +209,7 @@ class TableMeta(type):
|
|
|
182
209
|
else:
|
|
183
210
|
return f"<unbound table {self.__name__}>"
|
|
184
211
|
|
|
185
|
-
def from_row(self: t.Type[T_MetaInstance], row:
|
|
212
|
+
def from_row(self: t.Type[T_MetaInstance], row: Row) -> T_MetaInstance:
|
|
186
213
|
"""
|
|
187
214
|
Create a model instance from a pydal row.
|
|
188
215
|
"""
|
|
@@ -455,6 +482,12 @@ class TableMeta(type):
|
|
|
455
482
|
"""
|
|
456
483
|
return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
|
|
457
484
|
|
|
485
|
+
def window(self: t.Type[T_MetaInstance], window_size: int) -> "QueryBuilder[T_MetaInstance]":
|
|
486
|
+
"""
|
|
487
|
+
See QueryBuilder.window!
|
|
488
|
+
"""
|
|
489
|
+
return QueryBuilder(self).window(window_size)
|
|
490
|
+
|
|
458
491
|
def collect(self: t.Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
|
|
459
492
|
"""
|
|
460
493
|
See QueryBuilder.collect!
|
|
@@ -642,7 +675,7 @@ class TableMeta(type):
|
|
|
642
675
|
return await QueryBuilder(self).collect_into_async(into=into, verbose=verbose, init=init)
|
|
643
676
|
|
|
644
677
|
@property
|
|
645
|
-
def ALL(cls) ->
|
|
678
|
+
def ALL(cls) -> SQLALL:
|
|
646
679
|
"""
|
|
647
680
|
Select all fields for this table.
|
|
648
681
|
"""
|
|
@@ -931,6 +964,8 @@ class _TypedTable(metaclass=TableMeta):
|
|
|
931
964
|
_after_delete: list[t.Callable[[Set], t.Optional[bool]]]
|
|
932
965
|
_rows: tuple[Row, ...]
|
|
933
966
|
_with: list[str]
|
|
967
|
+
_singular: t.ClassVar[str]
|
|
968
|
+
_plural: t.ClassVar[str]
|
|
934
969
|
|
|
935
970
|
@classmethod
|
|
936
971
|
def __on_define__(cls, db: TypeDAL) -> None:
|
|
@@ -1131,7 +1166,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
1131
1166
|
|
|
1132
1167
|
def __new__(
|
|
1133
1168
|
cls,
|
|
1134
|
-
row_or_id: t.Union[Row, Query,
|
|
1169
|
+
row_or_id: t.Union[Row, Query, Set, int, str, "TypedTable", None] = None,
|
|
1135
1170
|
**filters: t.Any,
|
|
1136
1171
|
) -> t.Self:
|
|
1137
1172
|
"""
|
|
@@ -1149,7 +1184,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
1149
1184
|
# existing typed table instance!
|
|
1150
1185
|
return t.cast(t.Self, row_or_id)
|
|
1151
1186
|
|
|
1152
|
-
elif isinstance(row_or_id,
|
|
1187
|
+
elif isinstance(row_or_id, Row):
|
|
1153
1188
|
row = row_or_id
|
|
1154
1189
|
elif row_or_id is not None:
|
|
1155
1190
|
row = table(row_or_id, **filters)
|
|
@@ -20,6 +20,7 @@ from pydal.objects import Field as _Field
|
|
|
20
20
|
from pydal.objects import Query as _Query
|
|
21
21
|
from pydal.objects import Row as _Row
|
|
22
22
|
from pydal.objects import Rows as _Rows
|
|
23
|
+
from pydal.objects import Select as _Select
|
|
23
24
|
from pydal.objects import Set as _Set
|
|
24
25
|
from pydal.objects import Table as _Table
|
|
25
26
|
from pydal.validators import Validator as _Validator
|
|
@@ -147,20 +148,26 @@ class FileSystemLike(t.Protocol): # pragma: no cover
|
|
|
147
148
|
# pydal Wrappers (to help mypy understand these classes)
|
|
148
149
|
# ---------------------------------------------------------------------------
|
|
149
150
|
|
|
151
|
+
# same for typing and runtime:
|
|
152
|
+
Row: t.TypeAlias = _Row
|
|
153
|
+
Validator: t.TypeAlias = _Validator
|
|
154
|
+
Table: t.TypeAlias = _Table
|
|
155
|
+
Reference: t.TypeAlias = _Reference
|
|
150
156
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class Expression(_Expression):
|
|
156
|
-
"""Pydal Expression object. Make mypy happy."""
|
|
157
|
+
# has to be (fake) subclass during type-checking:
|
|
158
|
+
if t.TYPE_CHECKING:
|
|
157
159
|
|
|
160
|
+
class Query(_Query):
|
|
161
|
+
"""Pydal Query object. Makes mypy happy."""
|
|
158
162
|
|
|
159
|
-
class
|
|
160
|
-
|
|
163
|
+
class Expression(_Expression):
|
|
164
|
+
"""Pydal Expression object. Makes mypy happy."""
|
|
161
165
|
|
|
166
|
+
class Set(_Set):
|
|
167
|
+
"""Pydal Set object. Makes mypy happy."""
|
|
162
168
|
|
|
163
|
-
|
|
169
|
+
class Select(_Select):
|
|
170
|
+
"""Pydal Select object. Makes mypy happy."""
|
|
164
171
|
|
|
165
172
|
class OpRow:
|
|
166
173
|
"""
|
|
@@ -188,57 +195,34 @@ if t.TYPE_CHECKING:
|
|
|
188
195
|
def values(self) -> t.Iterable[t.Any]:
|
|
189
196
|
"""Dictionary-like value iteration."""
|
|
190
197
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
class OpRow(_OpRow):
|
|
194
|
-
"""Runtime OpRow, using pydal's version."""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class Reference(_Reference):
|
|
198
|
-
"""Pydal Reference object. Make mypy happy."""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
class Field(_Field):
|
|
202
|
-
"""Pydal Field object. Make mypy happy."""
|
|
203
|
-
|
|
204
|
-
_rname: str
|
|
198
|
+
class Field(_Field):
|
|
199
|
+
"""Pydal Field object. Make mypy happy."""
|
|
205
200
|
|
|
206
|
-
|
|
207
|
-
"""Comparing fields produces a PyDAL query."""
|
|
208
|
-
return t.cast(Query, super().__eq__(other))
|
|
201
|
+
_rname: str
|
|
209
202
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
def __eq__(self, other: t.Any) -> Query: # ty: ignore[invalid-method-override]
|
|
204
|
+
"""Comparing fields produces a PyDAL query."""
|
|
205
|
+
return t.cast(Query, super().__eq__(other))
|
|
213
206
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
def __ne__(self, other: t.Any) -> Query: # ty: ignore[invalid-method-override]
|
|
208
|
+
"""Comparing fields produces a PyDAL query."""
|
|
209
|
+
return t.cast(Query, super().__ne__(other))
|
|
217
210
|
|
|
211
|
+
def __hash__(self) -> int:
|
|
212
|
+
"""Keep fields hashable after overriding equality."""
|
|
213
|
+
return super().__hash__()
|
|
218
214
|
|
|
219
|
-
class Rows(_Rows):
|
|
220
|
-
|
|
215
|
+
class Rows(_Rows):
|
|
216
|
+
def column(self, column: t.Any = None) -> list[t.Any]: ...
|
|
221
217
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class Row(_Row):
|
|
233
|
-
"""Pydal Row object. Make mypy happy."""
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
class Validator(_Validator):
|
|
237
|
-
"""Pydal Validator object. Make mypy happy."""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
class Table(_Table):
|
|
241
|
-
"""Pydal Table object. Make mypy happy."""
|
|
218
|
+
else:
|
|
219
|
+
Query: t.TypeAlias = _Query
|
|
220
|
+
Expression: t.TypeAlias = _Expression
|
|
221
|
+
Set: t.TypeAlias = _Set
|
|
222
|
+
Select: t.TypeAlias = _Select
|
|
223
|
+
Rows: t.TypeAlias = _Rows
|
|
224
|
+
OpRow: t.TypeAlias = _OpRow
|
|
225
|
+
Field: t.TypeAlias = _Field
|
|
242
226
|
|
|
243
227
|
|
|
244
228
|
# ---------------------------------------------------------------------------
|
|
@@ -329,6 +313,8 @@ class Metadata(t.TypedDict):
|
|
|
329
313
|
select_duration: t.NotRequired[float]
|
|
330
314
|
relationships: t.NotRequired[set[str]]
|
|
331
315
|
sql: t.NotRequired[str]
|
|
316
|
+
window_size: t.NotRequired[int]
|
|
317
|
+
iterating: t.NotRequired[bool]
|
|
332
318
|
|
|
333
319
|
|
|
334
320
|
class FieldSettings(t.TypedDict, total=False):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|