TypeDAL 3.8.3__tar.gz → 3.8.5__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.

Potentially problematic release.


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

Files changed (58) hide show
  1. {typedal-3.8.3 → typedal-3.8.5}/CHANGELOG.md +13 -0
  2. {typedal-3.8.3 → typedal-3.8.5}/PKG-INFO +1 -1
  3. {typedal-3.8.3 → typedal-3.8.5}/example_new.py +3 -1
  4. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/__about__.py +1 -1
  5. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/core.py +33 -9
  6. {typedal-3.8.3 → typedal-3.8.5}/tests/test_query_builder.py +15 -0
  7. {typedal-3.8.3 → typedal-3.8.5}/tests/test_relationships.py +53 -7
  8. {typedal-3.8.3 → typedal-3.8.5}/.github/workflows/su6.yml +0 -0
  9. {typedal-3.8.3 → typedal-3.8.5}/.gitignore +0 -0
  10. {typedal-3.8.3 → typedal-3.8.5}/.readthedocs.yml +0 -0
  11. {typedal-3.8.3 → typedal-3.8.5}/README.md +0 -0
  12. {typedal-3.8.3 → typedal-3.8.5}/coverage.svg +0 -0
  13. {typedal-3.8.3 → typedal-3.8.5}/docs/1_getting_started.md +0 -0
  14. {typedal-3.8.3 → typedal-3.8.5}/docs/2_defining_tables.md +0 -0
  15. {typedal-3.8.3 → typedal-3.8.5}/docs/3_building_queries.md +0 -0
  16. {typedal-3.8.3 → typedal-3.8.5}/docs/4_relationships.md +0 -0
  17. {typedal-3.8.3 → typedal-3.8.5}/docs/5_py4web.md +0 -0
  18. {typedal-3.8.3 → typedal-3.8.5}/docs/6_migrations.md +0 -0
  19. {typedal-3.8.3 → typedal-3.8.5}/docs/7_mixins.md +0 -0
  20. {typedal-3.8.3 → typedal-3.8.5}/docs/css/code_blocks.css +0 -0
  21. {typedal-3.8.3 → typedal-3.8.5}/docs/index.md +0 -0
  22. {typedal-3.8.3 → typedal-3.8.5}/docs/requirements.txt +0 -0
  23. {typedal-3.8.3 → typedal-3.8.5}/example_old.py +0 -0
  24. {typedal-3.8.3 → typedal-3.8.5}/mkdocs.yml +0 -0
  25. {typedal-3.8.3 → typedal-3.8.5}/pyproject.toml +0 -0
  26. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/__init__.py +0 -0
  27. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/caching.py +0 -0
  28. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/cli.py +0 -0
  29. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/config.py +0 -0
  30. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/fields.py +0 -0
  31. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/for_py4web.py +0 -0
  32. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/for_web2py.py +0 -0
  33. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/helpers.py +0 -0
  34. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/mixins.py +0 -0
  35. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/py.typed +0 -0
  36. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/serializers/as_json.py +0 -0
  37. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/types.py +0 -0
  38. {typedal-3.8.3 → typedal-3.8.5}/src/typedal/web2py_py4web_shared.py +0 -0
  39. {typedal-3.8.3 → typedal-3.8.5}/tests/__init__.py +0 -0
  40. {typedal-3.8.3 → typedal-3.8.5}/tests/configs/simple.toml +0 -0
  41. {typedal-3.8.3 → typedal-3.8.5}/tests/configs/valid.env +0 -0
  42. {typedal-3.8.3 → typedal-3.8.5}/tests/configs/valid.toml +0 -0
  43. {typedal-3.8.3 → typedal-3.8.5}/tests/test_cli.py +0 -0
  44. {typedal-3.8.3 → typedal-3.8.5}/tests/test_config.py +0 -0
  45. {typedal-3.8.3 → typedal-3.8.5}/tests/test_docs_examples.py +0 -0
  46. {typedal-3.8.3 → typedal-3.8.5}/tests/test_helpers.py +0 -0
  47. {typedal-3.8.3 → typedal-3.8.5}/tests/test_json.py +0 -0
  48. {typedal-3.8.3 → typedal-3.8.5}/tests/test_main.py +0 -0
  49. {typedal-3.8.3 → typedal-3.8.5}/tests/test_mixins.py +0 -0
  50. {typedal-3.8.3 → typedal-3.8.5}/tests/test_mypy.py +0 -0
  51. {typedal-3.8.3 → typedal-3.8.5}/tests/test_orm.py +0 -0
  52. {typedal-3.8.3 → typedal-3.8.5}/tests/test_py4web.py +0 -0
  53. {typedal-3.8.3 → typedal-3.8.5}/tests/test_row.py +0 -0
  54. {typedal-3.8.3 → typedal-3.8.5}/tests/test_stats.py +0 -0
  55. {typedal-3.8.3 → typedal-3.8.5}/tests/test_table.py +0 -0
  56. {typedal-3.8.3 → typedal-3.8.5}/tests/test_web2py.py +0 -0
  57. {typedal-3.8.3 → typedal-3.8.5}/tests/test_xx_others.py +0 -0
  58. {typedal-3.8.3 → typedal-3.8.5}/tests/timings.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.8.5 (2024-10-24)
6
+
7
+ ### Fix
8
+
9
+ * Use right 'timestamp' field ([`2aedb02`](https://github.com/trialandsuccess/TypeDAL/commit/2aedb027b4490f8284253e9c7f427b0660b6bc13))
10
+ * Allow specifying a field to Builder.count(...); support selecting extra fields (e.g. MyField.count()) ([`ce28a79`](https://github.com/trialandsuccess/TypeDAL/commit/ce28a7995a6d817424462f8b18383c85fa349ba4))
11
+
12
+ ## v3.8.4 (2024-10-24)
13
+
14
+ ### Fix
15
+
16
+ * Paginate with limit=0 will yield all rows instead of crashing pt2 ([`78f1ae7`](https://github.com/trialandsuccess/TypeDAL/commit/78f1ae7257af0ba90040a7356ad579d3c77aa231))
17
+
5
18
  ## v3.8.3 (2024-10-24)
6
19
 
7
20
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.8.3
3
+ Version: 3.8.5
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
@@ -8,6 +8,8 @@ from src.typedal.fields import TextField
8
8
  from src.typedal.helpers import utcnow
9
9
  from pydal.validators import IS_NOT_EMPTY
10
10
 
11
+ from typedal.fields import TimestampField
12
+
11
13
  db = TypeDAL("sqlite:memory")
12
14
 
13
15
 
@@ -20,7 +22,7 @@ class Person(TypedTable):
20
22
  age = TypedField(int, default=18, requires=IS_NOT_EMPTY())
21
23
  nicknames: list[str]
22
24
 
23
- ts = TypedField(dt.datetime, type="timestamp")
25
+ ts = TimestampField()
24
26
 
25
27
 
26
28
  assert db.person._format == "%(name)s"
@@ -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.8.3"
8
+ __version__ = "3.8.5"
@@ -1500,7 +1500,14 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1500
1500
  return None # type: ignore
1501
1501
 
1502
1502
  inst._row = row
1503
- inst.__dict__.update(row)
1503
+
1504
+ if hasattr(row, "id"):
1505
+ inst.__dict__.update(row)
1506
+ else:
1507
+ # deal with _extra (and possibly others?)
1508
+ # Row <{actual: {}, _extra: ...}>
1509
+ inst.__dict__.update(row[str(cls)])
1510
+
1504
1511
  inst._setup_instance_methods()
1505
1512
  return inst
1506
1513
 
@@ -1828,7 +1835,22 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1828
1835
  `metadata` can be any (un)structured data
1829
1836
  `model` is a Typed Table class
1830
1837
  """
1831
- records = records or {row.id: model(row) for row in rows}
1838
+
1839
+ def _get_id(row: Row) -> int:
1840
+ """
1841
+ Try to find the id field in a row.
1842
+
1843
+ If _extra exists, the row changes:
1844
+ <Row {'test_relationship': {'id': 1}, '_extra': {'COUNT("test_relationship"."querytable")': 8}}>
1845
+ """
1846
+ if idx := getattr(row, "id", None):
1847
+ return typing.cast(int, idx)
1848
+ elif main := getattr(row, str(model), None):
1849
+ return typing.cast(int, main.id)
1850
+ else: # pragma: no cover
1851
+ raise NotImplementedError(f"`id` could not be found for {row}")
1852
+
1853
+ records = records or {_get_id(row): model(row) for row in rows}
1832
1854
  super().__init__(rows.db, records, rows.colnames, rows.compact, rows.response, rows.fields)
1833
1855
  self.model = model
1834
1856
  self.metadata = metadata or {}
@@ -2721,7 +2743,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2721
2743
  """
2722
2744
  yield from self.collect()
2723
2745
 
2724
- def count(self) -> int:
2746
+ def count(self, distinct: bool = None) -> int:
2725
2747
  """
2726
2748
  Return the amount of rows matching the current query.
2727
2749
  """
@@ -2730,25 +2752,27 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2730
2752
  query = self.query
2731
2753
 
2732
2754
  for key, relation in self.relationships.items():
2733
- if not relation.condition or relation.join != "inner":
2755
+ if (not relation.condition or relation.join != "inner") and not distinct:
2734
2756
  continue
2735
2757
 
2736
2758
  other = relation.get_table(db)
2737
- other = other.with_alias(f"{key}_{hash(relation)}")
2759
+ if not distinct:
2760
+ # todo: can this lead to other issues?
2761
+ other = other.with_alias(f"{key}_{hash(relation)}")
2738
2762
  query &= relation.condition(model, other)
2739
2763
 
2740
- return db(query).count()
2764
+ return db(query).count(distinct)
2741
2765
 
2742
2766
  def __paginate(
2743
2767
  self,
2744
2768
  limit: int,
2745
2769
  page: int = 1,
2746
2770
  ) -> "QueryBuilder[T_MetaInstance]":
2747
- _from = limit * (page - 1)
2748
- _to = limit * page
2749
-
2750
2771
  available = self.count()
2751
2772
 
2773
+ _from = limit * (page - 1)
2774
+ _to = (limit * page) if limit else available
2775
+
2752
2776
  metadata: Metadata = {}
2753
2777
 
2754
2778
  metadata["pagination"] = {
@@ -275,6 +275,7 @@ def test_paginate():
275
275
 
276
276
  # what if no limit?
277
277
  all_rows = TestQueryTable.join(method="left").paginate(limit=0, page=1)
278
+ assert len(all_rows) == 5
278
279
  assert all_rows.pagination["total_items"] == 5
279
280
  assert all_rows.pagination["total_pages"] == 1
280
281
  assert not all_rows.pagination["has_next_page"]
@@ -470,3 +471,17 @@ def test_column():
470
471
 
471
472
  assert len(rows) == 4
472
473
  assert set(rows) == {33}
474
+
475
+
476
+ def test_collect_with_extra_fields():
477
+ _setup_data()
478
+ builder = TestRelationship.select(TestRelationship.id, TestRelationship.name, TestRelationship.querytable.count())
479
+
480
+ assert builder.execute()
481
+
482
+ row = builder.first_or_fail()
483
+
484
+ assert row.id
485
+ assert row.name
486
+ assert row._extra
487
+ assert row[TestRelationship.querytable.count()]
@@ -43,7 +43,7 @@ class Role(TypedTable, TaggableMixin):
43
43
  @db.define()
44
44
  class User(TypedTable, TaggableMixin):
45
45
  gid = TypedField(str, default=uuid4)
46
- name: str
46
+ name: TypedField[str]
47
47
  roles: TypedField[list[Role]]
48
48
  main_role = TypedField(Role)
49
49
  extra_roles = TypedField(list[Role])
@@ -110,6 +110,11 @@ def _setup_data():
110
110
  ]
111
111
  )
112
112
 
113
+ # no relationships:
114
+ new_author = User.insert(name="Untagged Author", roles=[], main_role=writer, extra_roles=[])
115
+ untagged1 = Article.insert(title="Untagged Article 1", author=new_author)
116
+ untagged2 = Article.insert(title="Untagged Article 2", author=new_author)
117
+
113
118
  # articles
114
119
 
115
120
  article1, article2 = Article.bulk_insert(
@@ -184,11 +189,11 @@ def test_typedal_way():
184
189
 
185
190
  all_articles = Article.join().collect().as_dict()
186
191
 
187
- assert all_articles[1]["final_editor"]["name"] == "Editor 1"
188
- assert all_articles[2]["secondary_author"]["name"] == "Editor 1"
192
+ assert all_articles[3]["final_editor"]["name"] == "Editor 1"
193
+ assert all_articles[4]["secondary_author"]["name"] == "Editor 1"
189
194
 
190
- assert all_articles[1]["secondary_author"] is None
191
- assert all_articles[2]["final_editor"] is None
195
+ assert all_articles[3]["secondary_author"] is None
196
+ assert all_articles[4]["final_editor"] is None
192
197
 
193
198
  assert Article.first_or_fail()
194
199
 
@@ -244,7 +249,7 @@ def test_typedal_way():
244
249
 
245
250
  users = User.join().collect()
246
251
 
247
- assert len(users) == 3 # reader, writer, editor
252
+ assert len(users) == 4 # reader, writer, editor, untagged
248
253
 
249
254
  # get by id:
250
255
  reader = users[1]
@@ -533,6 +538,47 @@ def test_caching_dependencies():
533
538
 
534
539
  def test_illegal():
535
540
  with pytest.raises(ValueError), pytest.warns(UserWarning):
536
-
537
541
  class HasRelationship:
538
542
  something = relationship("...", condition=lambda: 1, on=lambda: 2)
543
+
544
+
545
+
546
+ def test_join_with_select():
547
+ _setup_data()
548
+
549
+ builder = User.select(User.id, User.gid, Article.id, Article.gid).where(id=2).join("articles")
550
+ user = builder.first_or_fail()
551
+
552
+ assert user.id
553
+ assert user.gid
554
+ assert not user.name
555
+ assert user.articles[0].id
556
+ assert user.articles[0].gid
557
+ assert not hasattr(user.articles[0], "title")
558
+
559
+ for user in builder.paginate(limit=1, page=1):
560
+ assert user.id
561
+ assert user.gid
562
+ assert not user.name
563
+ assert user.articles[0].id
564
+ assert user.articles[0].gid
565
+ assert not hasattr(user.articles[0], "title")
566
+
567
+ for user in builder.collect():
568
+ assert user.id
569
+ assert user.gid
570
+ assert not user.name
571
+ assert user.articles[0].id
572
+ assert user.articles[0].gid
573
+ assert not hasattr(user.articles[0], "title")
574
+
575
+
576
+ def test_count_with_join():
577
+ _setup_data()
578
+
579
+ # 0. count via select:
580
+ row = User.select(User.id, User.gid, Article.id, Article.gid).where(id=4).join("articles").first_or_fail()
581
+ assert len(row.articles) == 2
582
+
583
+ assert User.where(id=4).join("articles").count(User.id) == 1
584
+ assert User.where(id=4).join("articles").count(Article.id) == 2
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