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

Potentially problematic release.


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

Files changed (58) hide show
  1. {typedal-3.8.5 → typedal-3.9.0}/CHANGELOG.md +6 -0
  2. {typedal-3.8.5 → typedal-3.9.0}/PKG-INFO +1 -1
  3. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/__about__.py +1 -1
  4. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/core.py +31 -9
  5. {typedal-3.8.5 → typedal-3.9.0}/tests/test_query_builder.py +1 -0
  6. {typedal-3.8.5 → typedal-3.9.0}/tests/test_relationships.py +39 -1
  7. {typedal-3.8.5 → typedal-3.9.0}/.github/workflows/su6.yml +0 -0
  8. {typedal-3.8.5 → typedal-3.9.0}/.gitignore +0 -0
  9. {typedal-3.8.5 → typedal-3.9.0}/.readthedocs.yml +0 -0
  10. {typedal-3.8.5 → typedal-3.9.0}/README.md +0 -0
  11. {typedal-3.8.5 → typedal-3.9.0}/coverage.svg +0 -0
  12. {typedal-3.8.5 → typedal-3.9.0}/docs/1_getting_started.md +0 -0
  13. {typedal-3.8.5 → typedal-3.9.0}/docs/2_defining_tables.md +0 -0
  14. {typedal-3.8.5 → typedal-3.9.0}/docs/3_building_queries.md +0 -0
  15. {typedal-3.8.5 → typedal-3.9.0}/docs/4_relationships.md +0 -0
  16. {typedal-3.8.5 → typedal-3.9.0}/docs/5_py4web.md +0 -0
  17. {typedal-3.8.5 → typedal-3.9.0}/docs/6_migrations.md +0 -0
  18. {typedal-3.8.5 → typedal-3.9.0}/docs/7_mixins.md +0 -0
  19. {typedal-3.8.5 → typedal-3.9.0}/docs/css/code_blocks.css +0 -0
  20. {typedal-3.8.5 → typedal-3.9.0}/docs/index.md +0 -0
  21. {typedal-3.8.5 → typedal-3.9.0}/docs/requirements.txt +0 -0
  22. {typedal-3.8.5 → typedal-3.9.0}/example_new.py +0 -0
  23. {typedal-3.8.5 → typedal-3.9.0}/example_old.py +0 -0
  24. {typedal-3.8.5 → typedal-3.9.0}/mkdocs.yml +0 -0
  25. {typedal-3.8.5 → typedal-3.9.0}/pyproject.toml +0 -0
  26. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/__init__.py +0 -0
  27. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/caching.py +0 -0
  28. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/cli.py +0 -0
  29. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/config.py +0 -0
  30. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/fields.py +0 -0
  31. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/for_py4web.py +0 -0
  32. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/for_web2py.py +0 -0
  33. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/helpers.py +0 -0
  34. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/mixins.py +0 -0
  35. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/py.typed +0 -0
  36. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/serializers/as_json.py +0 -0
  37. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/types.py +0 -0
  38. {typedal-3.8.5 → typedal-3.9.0}/src/typedal/web2py_py4web_shared.py +0 -0
  39. {typedal-3.8.5 → typedal-3.9.0}/tests/__init__.py +0 -0
  40. {typedal-3.8.5 → typedal-3.9.0}/tests/configs/simple.toml +0 -0
  41. {typedal-3.8.5 → typedal-3.9.0}/tests/configs/valid.env +0 -0
  42. {typedal-3.8.5 → typedal-3.9.0}/tests/configs/valid.toml +0 -0
  43. {typedal-3.8.5 → typedal-3.9.0}/tests/test_cli.py +0 -0
  44. {typedal-3.8.5 → typedal-3.9.0}/tests/test_config.py +0 -0
  45. {typedal-3.8.5 → typedal-3.9.0}/tests/test_docs_examples.py +0 -0
  46. {typedal-3.8.5 → typedal-3.9.0}/tests/test_helpers.py +0 -0
  47. {typedal-3.8.5 → typedal-3.9.0}/tests/test_json.py +0 -0
  48. {typedal-3.8.5 → typedal-3.9.0}/tests/test_main.py +0 -0
  49. {typedal-3.8.5 → typedal-3.9.0}/tests/test_mixins.py +0 -0
  50. {typedal-3.8.5 → typedal-3.9.0}/tests/test_mypy.py +0 -0
  51. {typedal-3.8.5 → typedal-3.9.0}/tests/test_orm.py +0 -0
  52. {typedal-3.8.5 → typedal-3.9.0}/tests/test_py4web.py +0 -0
  53. {typedal-3.8.5 → typedal-3.9.0}/tests/test_row.py +0 -0
  54. {typedal-3.8.5 → typedal-3.9.0}/tests/test_stats.py +0 -0
  55. {typedal-3.8.5 → typedal-3.9.0}/tests/test_table.py +0 -0
  56. {typedal-3.8.5 → typedal-3.9.0}/tests/test_web2py.py +0 -0
  57. {typedal-3.8.5 → typedal-3.9.0}/tests/test_xx_others.py +0 -0
  58. {typedal-3.8.5 → typedal-3.9.0}/tests/timings.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.9.0 (2024-10-25)
6
+
7
+ ### Feature
8
+
9
+ * Add `condition_and` to .join so you can add additional requirements to inner joins ([`0ae688b`](https://github.com/trialandsuccess/TypeDAL/commit/0ae688ba01f421c70a660b5bb5d9672484494aa4))
10
+
5
11
  ## v3.8.5 (2024-10-24)
6
12
 
7
13
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.8.5
3
+ Version: 3.9.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
@@ -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.5"
8
+ __version__ = "3.9.0"
@@ -130,6 +130,7 @@ class Relationship(typing.Generic[To_Type]):
130
130
  _type: To_Type
131
131
  table: Type["TypedTable"] | type | str
132
132
  condition: Condition
133
+ condition_and: Condition
133
134
  on: OnQuery
134
135
  multiple: bool
135
136
  join: JOIN_OPTIONS
@@ -140,6 +141,7 @@ class Relationship(typing.Generic[To_Type]):
140
141
  condition: Condition = None,
141
142
  join: JOIN_OPTIONS = None,
142
143
  on: OnQuery = None,
144
+ condition_and: Condition = None,
143
145
  ):
144
146
  """
145
147
  Should not be called directly, use relationship() instead!
@@ -152,6 +154,7 @@ class Relationship(typing.Generic[To_Type]):
152
154
  self.condition = condition
153
155
  self.join = "left" if on else join # .on is always left join!
154
156
  self.on = on
157
+ self.condition_and = condition_and
155
158
 
156
159
  if args := typing.get_args(_type):
157
160
  self.table = unwrap_type(args[0])
@@ -172,6 +175,7 @@ class Relationship(typing.Generic[To_Type]):
172
175
  update.get("condition") or self.condition,
173
176
  update.get("join") or self.join,
174
177
  update.get("on") or self.on,
178
+ update.get("condition_and") or self.condition_and,
175
179
  )
176
180
 
177
181
  def __repr__(self) -> str:
@@ -180,6 +184,10 @@ class Relationship(typing.Generic[To_Type]):
180
184
  """
181
185
  if callback := self.condition or self.on:
182
186
  src_code = inspect.getsource(callback).strip()
187
+
188
+ if c_and := self.condition_and:
189
+ and_code = inspect.getsource(c_and).strip()
190
+ src_code += " AND " + and_code
183
191
  else:
184
192
  cls_name = self._type if isinstance(self._type, str) else self._type.__name__ # type: ignore
185
193
  src_code = f"to {cls_name} (missing condition)"
@@ -1050,11 +1058,12 @@ class TableMeta(type):
1050
1058
  method: JOIN_OPTIONS = None,
1051
1059
  on: OnQuery | list[Expression] | Expression = None,
1052
1060
  condition: Condition = None,
1061
+ condition_and: Condition = None,
1053
1062
  ) -> "QueryBuilder[T_MetaInstance]":
1054
1063
  """
1055
1064
  See QueryBuilder.join!
1056
1065
  """
1057
- return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method)
1066
+ return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
1058
1067
 
1059
1068
  def collect(self: Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
1060
1069
  """
@@ -2317,6 +2326,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2317
2326
  method: JOIN_OPTIONS = None,
2318
2327
  on: OnQuery | list[Expression] | Expression = None,
2319
2328
  condition: Condition = None,
2329
+ condition_and: Condition = None,
2320
2330
  ) -> "QueryBuilder[T_MetaInstance]":
2321
2331
  """
2322
2332
  Include relationship fields in the result.
@@ -2326,8 +2336,13 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2326
2336
 
2327
2337
  By default, the `method` defined in the relationship is used.
2328
2338
  This can be overwritten with the `method` keyword argument (left or inner)
2339
+
2340
+ `condition_and` can be used to add extra conditions to an inner join.
2329
2341
  """
2330
2342
  # todo: allow limiting amount of related rows returned for join?
2343
+ # todo: it would be nice if 'fields' could be an actual relationship
2344
+ # (Article.tags = list[Tag]) and you could change the .condition and .on
2345
+ # this could deprecate condition_and
2331
2346
 
2332
2347
  relationships = self.model.get_relationships()
2333
2348
 
@@ -2340,7 +2355,9 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2340
2355
  if isinstance(condition, pydal.objects.Query):
2341
2356
  condition = as_lambda(condition)
2342
2357
 
2343
- relationships = {str(fields[0]): Relationship(fields[0], condition=condition, join=method)}
2358
+ relationships = {
2359
+ str(fields[0]): Relationship(fields[0], condition=condition, join=method, condition_and=condition_and)
2360
+ }
2344
2361
  elif on:
2345
2362
  if len(fields) != 1:
2346
2363
  raise ValueError("join(field, on=...) can only be used with exactly one field!")
@@ -2350,15 +2367,17 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2350
2367
 
2351
2368
  if isinstance(on, list):
2352
2369
  on = as_lambda(on)
2353
- relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method)}
2370
+ relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method, condition_and=condition_and)}
2354
2371
 
2355
2372
  else:
2356
2373
  if fields:
2357
2374
  # join on every relationship
2358
- relationships = {str(k): relationships[str(k)] for k in fields}
2375
+ relationships = {str(k): relationships[str(k)].clone(condition_and=condition_and) for k in fields}
2359
2376
 
2360
2377
  if method:
2361
- relationships = {str(k): r.clone(join=method) for k, r in relationships.items()}
2378
+ relationships = {
2379
+ str(k): r.clone(join=method, condition_and=condition_and) for k, r in relationships.items()
2380
+ }
2362
2381
 
2363
2382
  return self._extend(relationships=relationships)
2364
2383
 
@@ -2579,7 +2598,6 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2579
2598
 
2580
2599
  metadata["relationships"] = set(self.relationships.keys())
2581
2600
 
2582
- # query = self._update_query_for_inner(db, model, query)
2583
2601
  join = []
2584
2602
  for key, relation in self.relationships.items():
2585
2603
  if not relation.condition or relation.join != "inner":
@@ -2587,7 +2605,11 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2587
2605
 
2588
2606
  other = relation.get_table(db)
2589
2607
  other = other.with_alias(f"{key}_{hash(relation)}")
2590
- join.append(other.on(relation.condition(model, other)))
2608
+ condition = relation.condition(model, other)
2609
+ if callable(relation.condition_and):
2610
+ condition &= relation.condition_and(model, other)
2611
+
2612
+ join.append(other.on(condition))
2591
2613
 
2592
2614
  if limitby := select_kwargs.pop("limitby", ()):
2593
2615
 
@@ -2637,12 +2659,12 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2637
2659
  # .on not given, generate it:
2638
2660
  other = other.with_alias(f"{key}_{hash(relation)}")
2639
2661
  condition = typing.cast(Query, relation.condition(model, other))
2662
+ if callable(relation.condition_and):
2663
+ condition &= relation.condition_and(model, other)
2640
2664
  left.append(other.on(condition))
2641
2665
  else:
2642
2666
  # else: inner join (handled earlier)
2643
2667
  other = other.with_alias(f"{key}_{hash(relation)}") # only for replace
2644
- # other = other.with_alias(f"{key}_{hash(relation)}")
2645
- # query &= relation.condition(model, other)
2646
2668
 
2647
2669
  # if no fields of 'other' are included, add other.ALL
2648
2670
  # else: only add other.id if missing
@@ -400,6 +400,7 @@ def test_complex_join():
400
400
 
401
401
 
402
402
  def test_reprs_and_bool():
403
+ _setup_data()
403
404
  assert TestQueryTable.where(id=1)
404
405
  assert not TestQueryTable.where(id=101)
405
406
 
@@ -295,6 +295,7 @@ def test_typedal_way():
295
295
 
296
296
 
297
297
  def test_reprs():
298
+ _setup_data()
298
299
  assert "Relationship:left on=" in repr(Article.tags)
299
300
 
300
301
  article = Article.first()
@@ -323,6 +324,16 @@ def test_reprs():
323
324
 
324
325
  assert empty.get_table_name() == "new"
325
326
 
327
+ relation = Article.join("author").relationships["author"]
328
+
329
+ assert "AND" not in repr(relation)
330
+
331
+ relation = Article.join("author",
332
+ condition_and=lambda article, author: author.name != "Hank"
333
+ ).relationships["author"]
334
+
335
+ assert "AND" in repr(relation) and "Hank" in repr(relation)
336
+
326
337
 
327
338
  @db.define()
328
339
  class CacheFirst(TypedTable):
@@ -355,6 +366,33 @@ def test_relationship_detection():
355
366
  assert user_table_relationships["extra_roles"].join == "left"
356
367
 
357
368
 
369
+ def test_join_with_different_condition():
370
+ _setup_data()
371
+
372
+ role_with_users = Role.join(
373
+ "users",
374
+ method="inner",
375
+ ).first()
376
+
377
+ assert role_with_users.users
378
+ assert role_with_users.users[0].name == "Reader 1"
379
+
380
+ role_with_users = Role.join(
381
+ "users", method="inner", condition_and=lambda role, user: ~user.name.like("Reader%")
382
+ ).first()
383
+
384
+ assert role_with_users.users
385
+ assert role_with_users.users[0].name != "Reader 1"
386
+
387
+ # left:
388
+ role_with_users = Role.join(
389
+ "users", method="left", condition_and=lambda role, user: ~user.name.like("Reader%")
390
+ ).first()
391
+
392
+ assert role_with_users.users
393
+ assert role_with_users.users[0].name != "Reader 1"
394
+
395
+
358
396
  def test_caching():
359
397
  uncached = User.join().collect_or_fail()
360
398
  cached = User.cache().join().collect_or_fail() # not actually cached yet!
@@ -538,11 +576,11 @@ def test_caching_dependencies():
538
576
 
539
577
  def test_illegal():
540
578
  with pytest.raises(ValueError), pytest.warns(UserWarning):
579
+
541
580
  class HasRelationship:
542
581
  something = relationship("...", condition=lambda: 1, on=lambda: 2)
543
582
 
544
583
 
545
-
546
584
  def test_join_with_select():
547
585
  _setup_data()
548
586
 
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