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.
Files changed (66) hide show
  1. {typedal-4.1.0 → typedal-4.2.1}/CHANGELOG.md +12 -0
  2. {typedal-4.1.0 → typedal-4.2.1}/PKG-INFO +1 -1
  3. {typedal-4.1.0 → typedal-4.2.1}/example_old.py +0 -2
  4. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/__about__.py +1 -1
  5. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/query_builder.py +20 -5
  6. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/relationships.py +41 -1
  7. {typedal-4.1.0 → typedal-4.2.1}/tests/test_config.py +0 -3
  8. {typedal-4.1.0 → typedal-4.2.1}/tests/test_mypy.py +0 -1
  9. {typedal-4.1.0 → typedal-4.2.1}/tests/test_query_builder.py +32 -20
  10. {typedal-4.1.0 → typedal-4.2.1}/tests/test_relationships.py +27 -26
  11. {typedal-4.1.0 → typedal-4.2.1}/.github/workflows/su6.yml +0 -0
  12. {typedal-4.1.0 → typedal-4.2.1}/.gitignore +0 -0
  13. {typedal-4.1.0 → typedal-4.2.1}/.readthedocs.yml +0 -0
  14. {typedal-4.1.0 → typedal-4.2.1}/README.md +0 -0
  15. {typedal-4.1.0 → typedal-4.2.1}/coverage.svg +0 -0
  16. {typedal-4.1.0 → typedal-4.2.1}/docs/1_getting_started.md +0 -0
  17. {typedal-4.1.0 → typedal-4.2.1}/docs/2_defining_tables.md +0 -0
  18. {typedal-4.1.0 → typedal-4.2.1}/docs/3_building_queries.md +0 -0
  19. {typedal-4.1.0 → typedal-4.2.1}/docs/4_relationships.md +0 -0
  20. {typedal-4.1.0 → typedal-4.2.1}/docs/5_py4web.md +0 -0
  21. {typedal-4.1.0 → typedal-4.2.1}/docs/6_migrations.md +0 -0
  22. {typedal-4.1.0 → typedal-4.2.1}/docs/7_configuration.md +0 -0
  23. {typedal-4.1.0 → typedal-4.2.1}/docs/8_mixins.md +0 -0
  24. {typedal-4.1.0 → typedal-4.2.1}/docs/css/code_blocks.css +0 -0
  25. {typedal-4.1.0 → typedal-4.2.1}/docs/index.md +0 -0
  26. {typedal-4.1.0 → typedal-4.2.1}/docs/requirements.txt +0 -0
  27. {typedal-4.1.0 → typedal-4.2.1}/example_new.py +0 -0
  28. {typedal-4.1.0 → typedal-4.2.1}/mkdocs.yml +0 -0
  29. {typedal-4.1.0 → typedal-4.2.1}/pyproject.toml +0 -0
  30. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/__init__.py +0 -0
  31. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/caching.py +0 -0
  32. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/cli.py +0 -0
  33. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/config.py +0 -0
  34. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/constants.py +0 -0
  35. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/core.py +0 -0
  36. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/define.py +0 -0
  37. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/fields.py +0 -0
  38. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/for_py4web.py +0 -0
  39. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/for_web2py.py +0 -0
  40. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/helpers.py +0 -0
  41. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/mixins.py +0 -0
  42. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/py.typed +0 -0
  43. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/rows.py +0 -0
  44. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/serializers/as_json.py +0 -0
  45. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/tables.py +0 -0
  46. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/types.py +0 -0
  47. {typedal-4.1.0 → typedal-4.2.1}/src/typedal/web2py_py4web_shared.py +0 -0
  48. {typedal-4.1.0 → typedal-4.2.1}/tests/__init__.py +0 -0
  49. {typedal-4.1.0 → typedal-4.2.1}/tests/configs/simple.toml +0 -0
  50. {typedal-4.1.0 → typedal-4.2.1}/tests/configs/valid.env +0 -0
  51. {typedal-4.1.0 → typedal-4.2.1}/tests/configs/valid.toml +0 -0
  52. {typedal-4.1.0 → typedal-4.2.1}/tests/py314_tests.py +0 -0
  53. {typedal-4.1.0 → typedal-4.2.1}/tests/test_cli.py +0 -0
  54. {typedal-4.1.0 → typedal-4.2.1}/tests/test_docs_examples.py +0 -0
  55. {typedal-4.1.0 → typedal-4.2.1}/tests/test_helpers.py +0 -0
  56. {typedal-4.1.0 → typedal-4.2.1}/tests/test_json.py +0 -0
  57. {typedal-4.1.0 → typedal-4.2.1}/tests/test_main.py +0 -0
  58. {typedal-4.1.0 → typedal-4.2.1}/tests/test_mixins.py +0 -0
  59. {typedal-4.1.0 → typedal-4.2.1}/tests/test_orm.py +0 -0
  60. {typedal-4.1.0 → typedal-4.2.1}/tests/test_py4web.py +0 -0
  61. {typedal-4.1.0 → typedal-4.2.1}/tests/test_row.py +0 -0
  62. {typedal-4.1.0 → typedal-4.2.1}/tests/test_stats.py +0 -0
  63. {typedal-4.1.0 → typedal-4.2.1}/tests/test_table.py +0 -0
  64. {typedal-4.1.0 → typedal-4.2.1}/tests/test_web2py.py +0 -0
  65. {typedal-4.1.0 → typedal-4.2.1}/tests/test_xx_others.py +0 -0
  66. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.1.0
3
+ Version: 4.2.1
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
@@ -1,5 +1,3 @@
1
- import datetime
2
-
3
1
  from pydal import DAL, Field
4
2
 
5
3
  from typedal.helpers import utcnow
@@ -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__ = "4.1.0"
8
+ __version__ = "4.2.1"
@@ -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 = model._ensure_table_defined()
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.model._ensure_table_defined()
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.model._ensure_table_defined()
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.model._ensure_table_defined()
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,4 +1,3 @@
1
- import sys
2
1
  import typing
3
2
 
4
3
  import pydal.objects
@@ -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.types import CacheFn, CacheModel, CacheTuple, Rows
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"], condition=lambda self, other: self.id == other.querytable, join="left"
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
- "test_query_table"."number",
52
- "relations_8106139955393"."id",
53
- "relations_8106139955393"."name",
54
- "relations_8106139955393"."value",
55
- "relations_8106139955393"."querytable"
56
- FROM "test_query_table"
57
- LEFT JOIN "test_relationship" AS "relations_8106139955393"
58
- ON ("relations_8106139955393"."querytable" = "test_query_table"."id")
59
- WHERE ("test_query_table"."id" IN (SELECT "test_query_table"."id"
60
- FROM "test_query_table"
61
- WHERE ("test_query_table"."id" > 0)
62
- ORDER BY "test_query_table"."id"
63
- LIMIT 3 OFFSET 0))
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
- len(author1.as_dict()["articles"]) == len(author1.__dict__["articles"]) == len(
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
- len(uncached2)
487
- == len(uncached)
488
- == len(cached2)
489
- == len(cached)
490
- == len(cached_user_only2)
491
- == len(cached_user_only)
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
- [_.name for _ in uncached2.first().roles]
500
- == [_.name for _ in cached.first().roles]
501
- == [_.name for _ in cached2.first().roles]
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(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(Tag.articles,
654
- on=lambda tag, article: [
655
- tagged := Tagged.unique_alias(),
656
- (tagged.tag == tag.id) & (article.gid == tagged.entity) & (article.author == 3)
657
- ],
658
- method="inner",
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