TypeDAL 3.8.5__tar.gz → 3.9.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.
Potentially problematic release.
This version of TypeDAL might be problematic. Click here for more details.
- {typedal-3.8.5 → typedal-3.9.1}/CHANGELOG.md +12 -0
- {typedal-3.8.5 → typedal-3.9.1}/PKG-INFO +1 -1
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/__about__.py +1 -1
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/core.py +43 -11
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_query_builder.py +10 -2
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_relationships.py +39 -1
- {typedal-3.8.5 → typedal-3.9.1}/.github/workflows/su6.yml +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/.gitignore +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/.readthedocs.yml +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/README.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/coverage.svg +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/1_getting_started.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/2_defining_tables.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/3_building_queries.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/4_relationships.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/5_py4web.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/6_migrations.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/7_mixins.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/css/code_blocks.css +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/index.md +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/docs/requirements.txt +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/example_new.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/example_old.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/mkdocs.yml +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/pyproject.toml +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/__init__.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/caching.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/cli.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/config.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/fields.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/for_py4web.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/for_web2py.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/helpers.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/mixins.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/py.typed +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/serializers/as_json.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/types.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/__init__.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/configs/simple.toml +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/configs/valid.env +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/configs/valid.toml +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_cli.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_config.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_docs_examples.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_helpers.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_json.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_main.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_mixins.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_mypy.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_orm.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_py4web.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_row.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_stats.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_table.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_web2py.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/test_xx_others.py +0 -0
- {typedal-3.8.5 → typedal-3.9.1}/tests/timings.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v3.9.1 (2024-10-25)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Bool(QueryBuilder) should NOT look at the data but if any filters were applied ([`962ddf3`](https://github.com/trialandsuccess/TypeDAL/commit/962ddf37f5d1ac014b73fd6349099100cc7efb5f))
|
|
10
|
+
|
|
11
|
+
## v3.9.0 (2024-10-25)
|
|
12
|
+
|
|
13
|
+
### Feature
|
|
14
|
+
|
|
15
|
+
* Add `condition_and` to .join so you can add additional requirements to inner joins ([`0ae688b`](https://github.com/trialandsuccess/TypeDAL/commit/0ae688ba01f421c70a660b5bb5d9672484494aa4))
|
|
16
|
+
|
|
5
17
|
## v3.8.5 (2024-10-24)
|
|
6
18
|
|
|
7
19
|
### Fix
|
|
@@ -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
|
"""
|
|
@@ -2221,9 +2230,19 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2221
2230
|
|
|
2222
2231
|
def __bool__(self) -> bool:
|
|
2223
2232
|
"""
|
|
2224
|
-
Querybuilder is truthy if it has
|
|
2233
|
+
Querybuilder is truthy if it has any conditions.
|
|
2225
2234
|
"""
|
|
2226
|
-
|
|
2235
|
+
table = self.model._ensure_table_defined()
|
|
2236
|
+
default_query = typing.cast(Query, table.id > 0)
|
|
2237
|
+
return any(
|
|
2238
|
+
[
|
|
2239
|
+
self.query != default_query,
|
|
2240
|
+
self.select_args,
|
|
2241
|
+
self.select_kwargs,
|
|
2242
|
+
self.relationships,
|
|
2243
|
+
self.metadata,
|
|
2244
|
+
]
|
|
2245
|
+
)
|
|
2227
2246
|
|
|
2228
2247
|
def _extend(
|
|
2229
2248
|
self,
|
|
@@ -2317,6 +2336,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2317
2336
|
method: JOIN_OPTIONS = None,
|
|
2318
2337
|
on: OnQuery | list[Expression] | Expression = None,
|
|
2319
2338
|
condition: Condition = None,
|
|
2339
|
+
condition_and: Condition = None,
|
|
2320
2340
|
) -> "QueryBuilder[T_MetaInstance]":
|
|
2321
2341
|
"""
|
|
2322
2342
|
Include relationship fields in the result.
|
|
@@ -2326,8 +2346,13 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2326
2346
|
|
|
2327
2347
|
By default, the `method` defined in the relationship is used.
|
|
2328
2348
|
This can be overwritten with the `method` keyword argument (left or inner)
|
|
2349
|
+
|
|
2350
|
+
`condition_and` can be used to add extra conditions to an inner join.
|
|
2329
2351
|
"""
|
|
2330
2352
|
# todo: allow limiting amount of related rows returned for join?
|
|
2353
|
+
# todo: it would be nice if 'fields' could be an actual relationship
|
|
2354
|
+
# (Article.tags = list[Tag]) and you could change the .condition and .on
|
|
2355
|
+
# this could deprecate condition_and
|
|
2331
2356
|
|
|
2332
2357
|
relationships = self.model.get_relationships()
|
|
2333
2358
|
|
|
@@ -2340,7 +2365,9 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2340
2365
|
if isinstance(condition, pydal.objects.Query):
|
|
2341
2366
|
condition = as_lambda(condition)
|
|
2342
2367
|
|
|
2343
|
-
relationships = {
|
|
2368
|
+
relationships = {
|
|
2369
|
+
str(fields[0]): Relationship(fields[0], condition=condition, join=method, condition_and=condition_and)
|
|
2370
|
+
}
|
|
2344
2371
|
elif on:
|
|
2345
2372
|
if len(fields) != 1:
|
|
2346
2373
|
raise ValueError("join(field, on=...) can only be used with exactly one field!")
|
|
@@ -2350,15 +2377,17 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2350
2377
|
|
|
2351
2378
|
if isinstance(on, list):
|
|
2352
2379
|
on = as_lambda(on)
|
|
2353
|
-
relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method)}
|
|
2380
|
+
relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method, condition_and=condition_and)}
|
|
2354
2381
|
|
|
2355
2382
|
else:
|
|
2356
2383
|
if fields:
|
|
2357
2384
|
# join on every relationship
|
|
2358
|
-
relationships = {str(k): relationships[str(k)] for k in fields}
|
|
2385
|
+
relationships = {str(k): relationships[str(k)].clone(condition_and=condition_and) for k in fields}
|
|
2359
2386
|
|
|
2360
2387
|
if method:
|
|
2361
|
-
relationships = {
|
|
2388
|
+
relationships = {
|
|
2389
|
+
str(k): r.clone(join=method, condition_and=condition_and) for k, r in relationships.items()
|
|
2390
|
+
}
|
|
2362
2391
|
|
|
2363
2392
|
return self._extend(relationships=relationships)
|
|
2364
2393
|
|
|
@@ -2579,7 +2608,6 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2579
2608
|
|
|
2580
2609
|
metadata["relationships"] = set(self.relationships.keys())
|
|
2581
2610
|
|
|
2582
|
-
# query = self._update_query_for_inner(db, model, query)
|
|
2583
2611
|
join = []
|
|
2584
2612
|
for key, relation in self.relationships.items():
|
|
2585
2613
|
if not relation.condition or relation.join != "inner":
|
|
@@ -2587,7 +2615,11 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2587
2615
|
|
|
2588
2616
|
other = relation.get_table(db)
|
|
2589
2617
|
other = other.with_alias(f"{key}_{hash(relation)}")
|
|
2590
|
-
|
|
2618
|
+
condition = relation.condition(model, other)
|
|
2619
|
+
if callable(relation.condition_and):
|
|
2620
|
+
condition &= relation.condition_and(model, other)
|
|
2621
|
+
|
|
2622
|
+
join.append(other.on(condition))
|
|
2591
2623
|
|
|
2592
2624
|
if limitby := select_kwargs.pop("limitby", ()):
|
|
2593
2625
|
|
|
@@ -2637,12 +2669,12 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2637
2669
|
# .on not given, generate it:
|
|
2638
2670
|
other = other.with_alias(f"{key}_{hash(relation)}")
|
|
2639
2671
|
condition = typing.cast(Query, relation.condition(model, other))
|
|
2672
|
+
if callable(relation.condition_and):
|
|
2673
|
+
condition &= relation.condition_and(model, other)
|
|
2640
2674
|
left.append(other.on(condition))
|
|
2641
2675
|
else:
|
|
2642
2676
|
# else: inner join (handled earlier)
|
|
2643
2677
|
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
2678
|
|
|
2647
2679
|
# if no fields of 'other' are included, add other.ALL
|
|
2648
2680
|
# else: only add other.id if missing
|
|
@@ -400,8 +400,16 @@ def test_complex_join():
|
|
|
400
400
|
|
|
401
401
|
|
|
402
402
|
def test_reprs_and_bool():
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
_setup_data()
|
|
404
|
+
|
|
405
|
+
# NOTE: This logic changed: any 'empty' query table is False, and adding any conditions make it True.
|
|
406
|
+
empty = TestQueryTable.where()
|
|
407
|
+
notempty = TestQueryTable.where(id=1)
|
|
408
|
+
assert (empty or notempty) is notempty
|
|
409
|
+
assert (notempty or empty) is notempty
|
|
410
|
+
assert not empty
|
|
411
|
+
assert notempty
|
|
412
|
+
assert TestQueryTable.where(id=10000000000)
|
|
405
413
|
|
|
406
414
|
assert repr(TestQueryTable.where(id=1))
|
|
407
415
|
assert str(TestQueryTable.where(id=1))
|
|
@@ -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
|
|
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
|