TypeDAL 4.6.1__tar.gz → 4.6.2__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.6.1 → typedal-4.6.2}/CHANGELOG.md +6 -0
- {typedal-4.6.1 → typedal-4.6.2}/PKG-INFO +1 -1
- {typedal-4.6.1 → typedal-4.6.2}/pyproject.toml +8 -1
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/__about__.py +1 -1
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/core.py +4 -1
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/mixins.py +25 -10
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/rows.py +2 -2
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/types.py +3 -1
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_mixins.py +46 -0
- {typedal-4.6.1 → typedal-4.6.2}/.crush/.gitignore +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.crush/crush.db-shm +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.crush/crush.db-wal +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.crush/init +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.crush/logs/crush.log +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.github/workflows/su6.yml +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.gitignore +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/.readthedocs.yml +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/README.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/coverage.svg +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/10_advanced_apis.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/1_getting_started.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/2_defining_tables.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/3_building_queries.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/4_relationships.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/5_py4web.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/6_migrations.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/7_configuration.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/8_mixins.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/9_memoization.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/css/code_blocks.css +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/index.md +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/docs/requirements.txt +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/example_new.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/example_old.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/mkdocs.yml +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/__init__.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/caching.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/cli.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/config.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/constants.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/define.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/fields.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/for_py4web.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/for_web2py.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/helpers.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/py.typed +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/query_builder.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/relationships.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/tables.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tasks.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/__init__.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/configs/simple.toml +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/configs/valid.env +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/configs/valid.toml +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/py314_tests.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_cli.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_config.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_docs_examples.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_helpers.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_json.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_main.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_mypy.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_orm.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_py4web.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_query_builder.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_relationships.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_row.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_stats.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_table.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_web2py.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/test_xx_others.py +0 -0
- {typedal-4.6.1 → typedal-4.6.2}/tests/timings.py +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.6.2 (2026-03-13)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Move pydantic visibility/lazy-load filtering into schema-converter path used by FastAPI ([`63c2e2c`](https://github.com/trialandsuccess/TypeDAL/commit/63c2e2c2e3c78f8aa817911dd269f650f5ba3d7d))
|
|
10
|
+
|
|
5
11
|
## v4.6.1 (2026-03-13)
|
|
6
12
|
|
|
7
13
|
### Fix
|
|
@@ -183,6 +183,7 @@ select = [
|
|
|
183
183
|
# "COM", # comma's - NO: annoying
|
|
184
184
|
# "PTH", # use pathlib - NO: annoying
|
|
185
185
|
"RUF", # ruff rules
|
|
186
|
+
# "D", # docs
|
|
186
187
|
]
|
|
187
188
|
unfixable = [
|
|
188
189
|
# Don't touch unused imports
|
|
@@ -191,13 +192,19 @@ unfixable = [
|
|
|
191
192
|
extend-ignore = [
|
|
192
193
|
# db.field == None should NOT be fixed to db.field is None
|
|
193
194
|
"E711",
|
|
195
|
+
"D200",
|
|
196
|
+
"D212",
|
|
197
|
+
"D418",
|
|
194
198
|
]
|
|
195
199
|
|
|
196
|
-
|
|
197
200
|
ignore = [
|
|
198
201
|
"RUF013" # implicit optional
|
|
199
202
|
]
|
|
200
203
|
|
|
204
|
+
|
|
205
|
+
[tool.ruff.lint.pydocstyle]
|
|
206
|
+
convention = "google"
|
|
207
|
+
|
|
201
208
|
[tool.bandit]
|
|
202
209
|
# bandit -c pyproject.toml -r .
|
|
203
210
|
exclude_dirs = [".bak", "venv"]
|
|
@@ -4,6 +4,7 @@ Core functionality of TypeDAL.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
# noinspection PyUnusedImports
|
|
7
8
|
import datetime as dt
|
|
8
9
|
import sys
|
|
9
10
|
import typing as t
|
|
@@ -21,7 +22,9 @@ from .helpers import (
|
|
|
21
22
|
sql_expression,
|
|
22
23
|
to_snake,
|
|
23
24
|
)
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
# noinspection PyUnusedImports
|
|
27
|
+
from .types import CacheStatus, Field, Template
|
|
25
28
|
|
|
26
29
|
try:
|
|
27
30
|
# python 3.14+
|
|
@@ -376,6 +376,12 @@ class PydanticMixin(Mixin):
|
|
|
376
376
|
field_name: cls._unwrap_pydantic_field_type(field_type) for field_name, field_type in annotations.items()
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
# Respect pyDAL visibility: unreadable DB fields should not be part of pydantic output/schema.
|
|
380
|
+
for field_name in list(fields):
|
|
381
|
+
model_attr = full_dict.get(field_name, getattr(cls, field_name, None))
|
|
382
|
+
if hasattr(model_attr, "readable") and not getattr(model_attr, "readable"):
|
|
383
|
+
fields.pop(field_name, None)
|
|
384
|
+
|
|
379
385
|
for field_name, field_type in fields.items():
|
|
380
386
|
cls._ensure_pydantic_compatible_type(field_name, field_type)
|
|
381
387
|
|
|
@@ -420,8 +426,9 @@ class PydanticMixin(Mixin):
|
|
|
420
426
|
)
|
|
421
427
|
|
|
422
428
|
@staticmethod
|
|
423
|
-
def _make_instance_converter(
|
|
429
|
+
def _make_instance_converter(model_cls: type, fields: dict[str, t.Any]) -> t.Callable[[t.Any], t.Any]:
|
|
424
430
|
_PRIMITIVES = (str, float, bool, bytes)
|
|
431
|
+
relationship_names = set(model_cls.get_relationships()) if hasattr(model_cls, "get_relationships") else set()
|
|
425
432
|
|
|
426
433
|
def convert(value: t.Any) -> t.Any:
|
|
427
434
|
if isinstance(value, dict):
|
|
@@ -432,7 +439,16 @@ class PydanticMixin(Mixin):
|
|
|
432
439
|
if isinstance(value, _PRIMITIVES) or value is None:
|
|
433
440
|
return value
|
|
434
441
|
# Handles both TypedTable instances and raw pydal Row objects
|
|
435
|
-
|
|
442
|
+
values_dict = getattr(value, "__dict__", {})
|
|
443
|
+
result: dict[str, t.Any] = {}
|
|
444
|
+
for field_name in fields:
|
|
445
|
+
# Never trigger lazy-loads during pydantic conversion.
|
|
446
|
+
if field_name in relationship_names and field_name not in values_dict:
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
result[field_name] = getattr(value, field_name, None)
|
|
450
|
+
|
|
451
|
+
return result
|
|
436
452
|
|
|
437
453
|
return convert
|
|
438
454
|
|
|
@@ -529,11 +545,9 @@ class PydanticMixin(Mixin):
|
|
|
529
545
|
if field_name in _required_fields:
|
|
530
546
|
# Always computed — keep required and non-nullable for clean TS types
|
|
531
547
|
return core_schema.typed_dict_field(inner, required=True)
|
|
532
|
-
# DB fields / relationships: TypeDAL can return partial rows, so allow None
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
required=False,
|
|
536
|
-
)
|
|
548
|
+
# DB fields / relationships: TypeDAL can return partial rows, so allow None when present,
|
|
549
|
+
# but keep missing fields absent (don't auto-fill null/default values).
|
|
550
|
+
return core_schema.typed_dict_field(core_schema.nullable_schema(inner), required=False)
|
|
537
551
|
|
|
538
552
|
schema_fields = {field_name: make_field(field_name, field_type) for field_name, field_type in fields.items()}
|
|
539
553
|
|
|
@@ -586,14 +600,15 @@ class PydanticMixin(Mixin):
|
|
|
586
600
|
def model_dump(self, mode: str = "python", *, _shallow: bool = False) -> dict[str, t.Any]:
|
|
587
601
|
"""Serialize this model to a dict, with optional shallow nested output."""
|
|
588
602
|
cls = type(self)
|
|
603
|
+
relationship_names = set(cls.get_relationships())
|
|
589
604
|
data: dict[str, t.Any] = {}
|
|
590
605
|
for field_name in self._pydantic_fields(
|
|
591
606
|
include_relationships=not _shallow,
|
|
592
607
|
include_properties=not _shallow,
|
|
593
608
|
):
|
|
594
|
-
#
|
|
595
|
-
|
|
596
|
-
if
|
|
609
|
+
# Only include relationship data that was already selected/joined.
|
|
610
|
+
# This prevents model_dump from triggering lazy-loading queries.
|
|
611
|
+
if field_name in relationship_names and field_name not in self.__dict__:
|
|
597
612
|
continue
|
|
598
613
|
|
|
599
614
|
data[field_name] = getattr(self, field_name, None)
|
|
@@ -421,8 +421,8 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
|
|
|
421
421
|
fields: list[Field] | None = None,
|
|
422
422
|
) -> t.Generator[T_MetaInstance, None, None] | T_MetaInstance:
|
|
423
423
|
"""
|
|
424
|
-
Takes an index and returns a copy of the indexed row with values
|
|
425
|
-
|
|
424
|
+
Takes an index and returns a copy of the indexed row with values
|
|
425
|
+
transformed via the "represent" attributes of the associated fields.
|
|
426
426
|
|
|
427
427
|
Args:
|
|
428
428
|
i: index. If not specified, a generator is returned for iteration
|
|
@@ -35,7 +35,9 @@ except ImportError:
|
|
|
35
35
|
# Internal references
|
|
36
36
|
if t.TYPE_CHECKING:
|
|
37
37
|
from .fields import TypedField
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
# noinspection PyUnusedImports
|
|
40
|
+
from .tables import TypedTable, _TypedTable
|
|
39
41
|
|
|
40
42
|
# ---------------------------------------------------------------------------
|
|
41
43
|
# Aliases
|
|
@@ -237,6 +237,11 @@ class PydanticHiddenTypedField(TypedTable, PydanticMixin):
|
|
|
237
237
|
hidden = TypedField(str, readable=False)
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
class PydanticHiddenRelationshipHost(TypedTable, PydanticMixin):
|
|
241
|
+
name: str
|
|
242
|
+
with_hidden = relationship(list[PydanticHiddenTypedField], lambda self, other: self.id == other.id, lazy="allow")
|
|
243
|
+
|
|
244
|
+
|
|
240
245
|
@pytest.fixture
|
|
241
246
|
def pydantic_db():
|
|
242
247
|
db = TypeDAL("sqlite:memory")
|
|
@@ -247,6 +252,7 @@ def pydantic_db():
|
|
|
247
252
|
db.define(PydanticGenericResolvedRelationship)
|
|
248
253
|
db.define(PydanticGenericUnresolvedRelationship)
|
|
249
254
|
db.define(PydanticHiddenTypedField)
|
|
255
|
+
db.define(PydanticHiddenRelationshipHost)
|
|
250
256
|
db.define(NonPydanticAuthor)
|
|
251
257
|
yield db
|
|
252
258
|
|
|
@@ -460,6 +466,46 @@ def test_pydantic_skips_unreadable_typedfield_in_model_dump(pydantic_db):
|
|
|
460
466
|
assert data == {"id": row.id}
|
|
461
467
|
|
|
462
468
|
|
|
469
|
+
def test_pydantic_skips_unreadable_typedfield_in_nested_list_relationship_dump(pydantic_db):
|
|
470
|
+
hidden = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
|
|
471
|
+
host = PydanticHiddenRelationshipHost.insert(name="Host")
|
|
472
|
+
assert hidden.id == host.id
|
|
473
|
+
|
|
474
|
+
joined = PydanticHiddenRelationshipHost.where(id=host.id).join("with_hidden").first()
|
|
475
|
+
assert joined is not None
|
|
476
|
+
|
|
477
|
+
data = joined.model_dump(mode="json")
|
|
478
|
+
assert data["with_hidden"] == [{"id": hidden.id, "visible": "show"}]
|
|
479
|
+
assert joined.with_hidden[0].hidden == "hide"
|
|
480
|
+
assert [item.model_dump() for item in joined.with_hidden] == [{"id": hidden.id, "visible": "show"}]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def test_pydantic_model_dump_never_lazy_loads_unjoined_relationships(pydantic_db):
|
|
484
|
+
hidden = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
|
|
485
|
+
host = PydanticHiddenRelationshipHost.insert(name="Host")
|
|
486
|
+
assert hidden.id == host.id
|
|
487
|
+
|
|
488
|
+
data = host.model_dump(mode="json")
|
|
489
|
+
assert "with_hidden" not in data
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_pydantic_type_adapter_skips_unreadable_fields(pydantic_db):
|
|
493
|
+
row = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
|
|
494
|
+
ta = pydantic.TypeAdapter(PydanticHiddenTypedField)
|
|
495
|
+
data = ta.validate_python(row)
|
|
496
|
+
assert data == {"id": row.id, "visible": "show"}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def test_pydantic_type_adapter_never_lazy_loads_unjoined_relationships(pydantic_db):
|
|
500
|
+
hidden = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
|
|
501
|
+
host = PydanticHiddenRelationshipHost.insert(name="Host")
|
|
502
|
+
assert hidden.id == host.id
|
|
503
|
+
|
|
504
|
+
ta = pydantic.TypeAdapter(PydanticHiddenRelationshipHost)
|
|
505
|
+
data = ta.validate_python(host)
|
|
506
|
+
assert "with_hidden" not in data
|
|
507
|
+
|
|
508
|
+
|
|
463
509
|
def test_pydantic_compatibility_non_type_and_missing_relationship_type():
|
|
464
510
|
# ForwardRef is not a runtime type; this should be a no-op, not a crash.
|
|
465
511
|
PydanticMixin._ensure_pydantic_compatible_type("x", typing.ForwardRef("Anything"))
|
|
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
|
|
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
|