TypeDAL 4.1.0__tar.gz → 4.2.1__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-4.1.0 → typedal-4.2.1}/CHANGELOG.md +12 -0
- {typedal-4.1.0 → typedal-4.2.1}/PKG-INFO +1 -1
- {typedal-4.1.0 → typedal-4.2.1}/example_old.py +0 -2
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/__about__.py +1 -1
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/query_builder.py +20 -5
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/relationships.py +41 -1
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_config.py +0 -3
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_mypy.py +0 -1
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_query_builder.py +32 -20
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_relationships.py +27 -26
- {typedal-4.1.0 → typedal-4.2.1}/.github/workflows/su6.yml +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/.gitignore +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/.readthedocs.yml +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/README.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/coverage.svg +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/1_getting_started.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/2_defining_tables.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/3_building_queries.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/4_relationships.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/5_py4web.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/6_migrations.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/7_configuration.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/8_mixins.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/css/code_blocks.css +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/index.md +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/docs/requirements.txt +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/example_new.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/mkdocs.yml +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/pyproject.toml +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/__init__.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/caching.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/cli.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/config.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/constants.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/core.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/define.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/fields.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/for_py4web.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/for_web2py.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/helpers.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/mixins.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/py.typed +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/rows.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/tables.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/types.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/__init__.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/configs/simple.toml +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/configs/valid.env +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/configs/valid.toml +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/py314_tests.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_cli.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_docs_examples.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_helpers.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_json.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_main.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_mixins.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_orm.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_py4web.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_row.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_stats.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_table.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_web2py.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/test_xx_others.py +0 -0
- {typedal-4.1.0 → typedal-4.2.1}/tests/timings.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.2.1 (2025-12-10)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Improved type hints for relationships (non-list) ([`3fb53bc`](https://github.com/trialandsuccess/TypeDAL/commit/3fb53bc7a9b4c53c1bc533124ed205c5ec46fa92))
|
|
10
|
+
|
|
11
|
+
## v4.2.0 (2025-11-28)
|
|
12
|
+
|
|
13
|
+
### Feature
|
|
14
|
+
|
|
15
|
+
* Minimal support for using querybuilder on old-style pydal tables ([`ec8baeb`](https://github.com/trialandsuccess/TypeDAL/commit/ec8baebbbaae5a2cb5d48254997a6322a0670d7d))
|
|
16
|
+
|
|
5
17
|
## v4.1.0 (2025-11-26)
|
|
6
18
|
|
|
7
19
|
### Feature
|
|
@@ -22,7 +22,7 @@ from .helpers import (
|
|
|
22
22
|
normalize_table_keys,
|
|
23
23
|
throw,
|
|
24
24
|
)
|
|
25
|
-
from .tables import TypedTable
|
|
25
|
+
from .tables import TableMeta, TypedTable
|
|
26
26
|
from .types import (
|
|
27
27
|
CacheMetadata,
|
|
28
28
|
Condition,
|
|
@@ -36,6 +36,7 @@ from .types import (
|
|
|
36
36
|
SelectKwargs,
|
|
37
37
|
T,
|
|
38
38
|
T_MetaInstance,
|
|
39
|
+
Table,
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
|
|
@@ -67,7 +68,8 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
67
68
|
MyTable.where(...) -> QueryBuilder[MyTable]
|
|
68
69
|
"""
|
|
69
70
|
self.model = model
|
|
70
|
-
table =
|
|
71
|
+
table = self._ensure_table_defined()
|
|
72
|
+
|
|
71
73
|
default_query = table.id > 0
|
|
72
74
|
self.query = add_query or default_query
|
|
73
75
|
self.select_args = select_args or []
|
|
@@ -75,6 +77,14 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
75
77
|
self.relationships = relationships or {}
|
|
76
78
|
self.metadata = metadata or {}
|
|
77
79
|
|
|
80
|
+
def _ensure_table_defined(self) -> Table:
|
|
81
|
+
model = self.model
|
|
82
|
+
if hasattr(model, "_ensure_table_defined"):
|
|
83
|
+
return model._ensure_table_defined()
|
|
84
|
+
else:
|
|
85
|
+
# already a pydal table
|
|
86
|
+
return t.cast(Table, model)
|
|
87
|
+
|
|
78
88
|
def __str__(self) -> str:
|
|
79
89
|
"""
|
|
80
90
|
Simple string representation for the query builder.
|
|
@@ -99,7 +109,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
99
109
|
"""
|
|
100
110
|
Querybuilder is truthy if it has t.Any conditions.
|
|
101
111
|
"""
|
|
102
|
-
table = self.
|
|
112
|
+
table = self._ensure_table_defined()
|
|
103
113
|
default_query = table.id > 0
|
|
104
114
|
return any(
|
|
105
115
|
[
|
|
@@ -191,7 +201,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
191
201
|
.where(lambda table: table.id == 5, lambda table: table.id == 6) == (table.id == 5) | (table.id=6)
|
|
192
202
|
"""
|
|
193
203
|
new_query = self.query
|
|
194
|
-
table = self.
|
|
204
|
+
table = self._ensure_table_defined()
|
|
195
205
|
|
|
196
206
|
queries_or_lambdas = (
|
|
197
207
|
*queries_or_lambdas,
|
|
@@ -531,6 +541,11 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
531
541
|
if _to is None:
|
|
532
542
|
_to = TypedRows
|
|
533
543
|
|
|
544
|
+
if not isinstance(self.model, TableMeta):
|
|
545
|
+
# tried to use querybuilder with a non-typedal table,
|
|
546
|
+
# fallback to execute:
|
|
547
|
+
return self.execute(add_id=add_id)
|
|
548
|
+
|
|
534
549
|
db = self._get_db()
|
|
535
550
|
metadata = self.metadata.copy()
|
|
536
551
|
|
|
@@ -792,7 +807,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
792
807
|
Transform the raw rows into Typed Table model instances with nested relationships.
|
|
793
808
|
"""
|
|
794
809
|
db = self._get_db()
|
|
795
|
-
main_table = self.
|
|
810
|
+
main_table = self._ensure_table_defined()
|
|
796
811
|
|
|
797
812
|
# id: Model
|
|
798
813
|
records: dict[t.Any, T_MetaInstance] = {}
|
|
@@ -257,6 +257,27 @@ class Relationship(t.Generic[To_Type]):
|
|
|
257
257
|
return fallback_value
|
|
258
258
|
|
|
259
259
|
|
|
260
|
+
@t.overload
|
|
261
|
+
def relationship(
|
|
262
|
+
_type: type[list[To_Type]],
|
|
263
|
+
condition: Condition = None,
|
|
264
|
+
join: JOIN_OPTIONS = None,
|
|
265
|
+
on: OnQuery = None,
|
|
266
|
+
lazy: LazyPolicy | None = None,
|
|
267
|
+
explicit: bool = False,
|
|
268
|
+
) -> list[To_Type]:
|
|
269
|
+
"""
|
|
270
|
+
Define a relationship that returns a list of related instances.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
_type: A list type hint like list[Office] to indicate multiple related records.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
A list of related instances.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@t.overload
|
|
260
281
|
def relationship(
|
|
261
282
|
_type: t.Type[To_Type] | str,
|
|
262
283
|
condition: Condition = None,
|
|
@@ -264,7 +285,26 @@ def relationship(
|
|
|
264
285
|
on: OnQuery = None,
|
|
265
286
|
lazy: LazyPolicy | None = None,
|
|
266
287
|
explicit: bool = False,
|
|
267
|
-
) -> To_Type:
|
|
288
|
+
) -> To_Type | None:
|
|
289
|
+
"""
|
|
290
|
+
Define a relationship that returns a single optional related instance.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
_type: A type or string reference like City to indicate a single related record.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
A single related instance or None.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def relationship(
|
|
301
|
+
_type: type[list[To_Type]] | t.Type[To_Type] | str,
|
|
302
|
+
condition: Condition = None,
|
|
303
|
+
join: JOIN_OPTIONS = None,
|
|
304
|
+
on: OnQuery = None,
|
|
305
|
+
lazy: LazyPolicy | None = None,
|
|
306
|
+
explicit: bool = False,
|
|
307
|
+
) -> list[To_Type] | To_Type | None:
|
|
268
308
|
"""
|
|
269
309
|
Define a relationship to another table, when its id is not stored in the current table.
|
|
270
310
|
|
|
@@ -6,10 +6,7 @@ import uuid
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
|
-
|
|
10
|
-
# from contextlib import chdir
|
|
11
9
|
from contextlib_chdir import chdir
|
|
12
|
-
from pydal2sql import generate_sql
|
|
13
10
|
from testcontainers.postgres import PostgresContainer
|
|
14
11
|
|
|
15
12
|
from src.typedal import TypeDAL, TypedField, TypedTable
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
1
|
import pytest
|
|
5
2
|
from pydal.objects import Query
|
|
6
3
|
|
|
7
4
|
from src.typedal import TypeDAL, TypedField, TypedTable, relationship
|
|
8
|
-
from typedal
|
|
5
|
+
from typedal import QueryBuilder
|
|
9
6
|
|
|
10
7
|
db = TypeDAL("sqlite:memory")
|
|
11
8
|
|
|
@@ -17,7 +14,9 @@ class TestQueryTable(TypedTable):
|
|
|
17
14
|
yet_another = TypedField(list[str], default=["something", "and", "other", "things"])
|
|
18
15
|
|
|
19
16
|
relations = relationship(
|
|
20
|
-
list["TestRelationship"],
|
|
17
|
+
list["TestRelationship"],
|
|
18
|
+
condition=lambda self, other: self.id == other.querytable,
|
|
19
|
+
join="left",
|
|
21
20
|
)
|
|
22
21
|
|
|
23
22
|
|
|
@@ -47,21 +46,21 @@ def test_query_type():
|
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
"""
|
|
50
|
-
SELECT "test_query_table"."id"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
FROM "test_query_table"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
WHERE ("test_query_table"."id" IN (SELECT "test_query_table"."id"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
ORDER BY "test_query_table"."number" DESC;
|
|
49
|
+
SELECT "test_query_table"."id"
|
|
50
|
+
, "test_query_table"."number"
|
|
51
|
+
, "relations_8106139955393"."id"
|
|
52
|
+
, "relations_8106139955393"."name"
|
|
53
|
+
, "relations_8106139955393"."value"
|
|
54
|
+
, "relations_8106139955393"."querytable"
|
|
55
|
+
FROM "test_query_table"
|
|
56
|
+
LEFT JOIN "test_relationship" AS "relations_8106139955393"
|
|
57
|
+
ON ("relations_8106139955393"."querytable" = "test_query_table"."id")
|
|
58
|
+
WHERE ("test_query_table"."id" IN (SELECT "test_query_table"."id"
|
|
59
|
+
FROM "test_query_table"
|
|
60
|
+
WHERE ("test_query_table"."id" > 0)
|
|
61
|
+
ORDER BY "test_query_table"."id"
|
|
62
|
+
LIMIT 3 OFFSET 0))
|
|
63
|
+
ORDER BY "test_query_table"."number" DESC;
|
|
65
64
|
"""
|
|
66
65
|
|
|
67
66
|
|
|
@@ -532,3 +531,16 @@ def test_collect_with_extra_fields():
|
|
|
532
531
|
|
|
533
532
|
with pytest.raises(HTTP):
|
|
534
533
|
TestRelationship.where(TestRelationship.id == 3245892384).first_or_fail(HTTP(404))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_minimal_functionality_on_pydal_style_tables():
|
|
537
|
+
_setup_data()
|
|
538
|
+
|
|
539
|
+
qb1 = TestQueryTable.where(number=2).collect()
|
|
540
|
+
qb2 = QueryBuilder(db.test_query_table).where(number=2).collect()
|
|
541
|
+
|
|
542
|
+
assert len(qb1) == len(qb2)
|
|
543
|
+
assert qb1.first().id == qb2.first().id
|
|
544
|
+
|
|
545
|
+
assert qb2
|
|
546
|
+
assert len(qb2) == 1
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
-
import json
|
|
3
2
|
import time
|
|
4
3
|
import types
|
|
5
4
|
import typing
|
|
@@ -102,8 +101,7 @@ class Tagged(TypedTable): # pivot table
|
|
|
102
101
|
|
|
103
102
|
|
|
104
103
|
@db.define()
|
|
105
|
-
class Empty(TypedTable):
|
|
106
|
-
...
|
|
104
|
+
class Empty(TypedTable): ...
|
|
107
105
|
|
|
108
106
|
|
|
109
107
|
def _setup_data():
|
|
@@ -314,8 +312,7 @@ def test_typedal_way():
|
|
|
314
312
|
author1 = User.where(id=4).join("articles").first()
|
|
315
313
|
|
|
316
314
|
assert (
|
|
317
|
-
|
|
318
|
-
dict(author1)["articles"]) == 2
|
|
315
|
+
len(author1.as_dict()["articles"]) == len(author1.__dict__["articles"]) == len(dict(author1)["articles"]) == 2
|
|
319
316
|
)
|
|
320
317
|
|
|
321
318
|
|
|
@@ -483,12 +480,12 @@ def test_caching():
|
|
|
483
480
|
cached_user_only2 = User.join().cache(User.id).collect_or_fail()
|
|
484
481
|
|
|
485
482
|
assert (
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
483
|
+
len(uncached2)
|
|
484
|
+
== len(uncached)
|
|
485
|
+
== len(cached2)
|
|
486
|
+
== len(cached)
|
|
487
|
+
== len(cached_user_only2)
|
|
488
|
+
== len(cached_user_only)
|
|
492
489
|
)
|
|
493
490
|
|
|
494
491
|
assert uncached.as_json() == uncached2.as_json() == cached.as_json() == cached2.as_json()
|
|
@@ -496,9 +493,9 @@ def test_caching():
|
|
|
496
493
|
assert cached.first().gid == cached2.first().gid
|
|
497
494
|
|
|
498
495
|
assert (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
496
|
+
[_.name for _ in uncached2.first().roles]
|
|
497
|
+
== [_.name for _ in cached.first().roles]
|
|
498
|
+
== [_.name for _ in cached2.first().roles]
|
|
502
499
|
)
|
|
503
500
|
|
|
504
501
|
assert not uncached2.metadata.get("cache", {}).get("enabled")
|
|
@@ -636,27 +633,31 @@ def test_caching_dependencies():
|
|
|
636
633
|
|
|
637
634
|
def test_illegal():
|
|
638
635
|
with pytest.raises(ValueError), pytest.warns(UserWarning):
|
|
636
|
+
|
|
639
637
|
class HasRelationship:
|
|
640
638
|
something = relationship("...", condition=lambda: 1, on=lambda: 2)
|
|
641
639
|
|
|
642
640
|
with pytest.raises(ValueError), pytest.warns(UserWarning):
|
|
643
641
|
Tag.join(Tag.articles, condition=lambda: 1, on=lambda: 2)
|
|
644
642
|
|
|
643
|
+
|
|
645
644
|
def test_join_relationship_custom_on():
|
|
646
645
|
_setup_data()
|
|
647
646
|
|
|
648
|
-
rows1 = Tag.join(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
647
|
+
rows1 = Tag.join(
|
|
648
|
+
Tag.articles,
|
|
649
|
+
condition=lambda tag, article: (Tagged.tag == tag.id) & (article.gid == Tagged.entity) & (article.author == 3),
|
|
650
|
+
method="inner",
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
rows2 = Tag.join(
|
|
654
|
+
Tag.articles,
|
|
655
|
+
on=lambda tag, article: [
|
|
656
|
+
tagged := Tagged.unique_alias(),
|
|
657
|
+
(tagged.tag == tag.id) & (article.gid == tagged.entity) & (article.author == 3),
|
|
658
|
+
],
|
|
659
|
+
method="inner",
|
|
660
|
+
)
|
|
660
661
|
|
|
661
662
|
assert all([row.articles for row in rows1])
|
|
662
663
|
assert all([row.articles for row in rows2])
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|