TypeDAL 4.6.0__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.
Files changed (75) hide show
  1. {typedal-4.6.0 → typedal-4.6.2}/.crush/crush.db-shm +0 -0
  2. typedal-4.6.2/.crush/crush.db-wal +0 -0
  3. {typedal-4.6.0 → typedal-4.6.2}/CHANGELOG.md +12 -0
  4. {typedal-4.6.0 → typedal-4.6.2}/PKG-INFO +1 -1
  5. {typedal-4.6.0 → typedal-4.6.2}/docs/8_mixins.md +8 -1
  6. {typedal-4.6.0 → typedal-4.6.2}/pyproject.toml +8 -1
  7. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/__about__.py +1 -1
  8. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/caching.py +2 -0
  9. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/core.py +4 -1
  10. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/mixins.py +34 -14
  11. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/rows.py +2 -2
  12. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/types.py +3 -1
  13. {typedal-4.6.0 → typedal-4.6.2}/tests/test_mixins.py +63 -0
  14. typedal-4.6.0/.crush/crush.db-wal +0 -0
  15. {typedal-4.6.0 → typedal-4.6.2}/.crush/.gitignore +0 -0
  16. {typedal-4.6.0 → typedal-4.6.2}/.crush/init +0 -0
  17. {typedal-4.6.0 → typedal-4.6.2}/.crush/logs/crush.log +0 -0
  18. {typedal-4.6.0 → typedal-4.6.2}/.github/workflows/su6.yml +0 -0
  19. {typedal-4.6.0 → typedal-4.6.2}/.gitignore +0 -0
  20. {typedal-4.6.0 → typedal-4.6.2}/.readthedocs.yml +0 -0
  21. {typedal-4.6.0 → typedal-4.6.2}/README.md +0 -0
  22. {typedal-4.6.0 → typedal-4.6.2}/coverage.svg +0 -0
  23. {typedal-4.6.0 → typedal-4.6.2}/docs/10_advanced_apis.md +0 -0
  24. {typedal-4.6.0 → typedal-4.6.2}/docs/1_getting_started.md +0 -0
  25. {typedal-4.6.0 → typedal-4.6.2}/docs/2_defining_tables.md +0 -0
  26. {typedal-4.6.0 → typedal-4.6.2}/docs/3_building_queries.md +0 -0
  27. {typedal-4.6.0 → typedal-4.6.2}/docs/4_relationships.md +0 -0
  28. {typedal-4.6.0 → typedal-4.6.2}/docs/5_py4web.md +0 -0
  29. {typedal-4.6.0 → typedal-4.6.2}/docs/6_migrations.md +0 -0
  30. {typedal-4.6.0 → typedal-4.6.2}/docs/7_configuration.md +0 -0
  31. {typedal-4.6.0 → typedal-4.6.2}/docs/9_memoization.md +0 -0
  32. {typedal-4.6.0 → typedal-4.6.2}/docs/css/code_blocks.css +0 -0
  33. {typedal-4.6.0 → typedal-4.6.2}/docs/index.md +0 -0
  34. {typedal-4.6.0 → typedal-4.6.2}/docs/requirements.txt +0 -0
  35. {typedal-4.6.0 → typedal-4.6.2}/example_new.py +0 -0
  36. {typedal-4.6.0 → typedal-4.6.2}/example_old.py +0 -0
  37. {typedal-4.6.0 → typedal-4.6.2}/mkdocs.yml +0 -0
  38. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/__init__.py +0 -0
  39. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/cli.py +0 -0
  40. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/config.py +0 -0
  41. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/constants.py +0 -0
  42. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/define.py +0 -0
  43. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/fields.py +0 -0
  44. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/for_py4web.py +0 -0
  45. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/for_web2py.py +0 -0
  46. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/helpers.py +0 -0
  47. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/py.typed +0 -0
  48. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/query_builder.py +0 -0
  49. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/relationships.py +0 -0
  50. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/serializers/as_json.py +0 -0
  51. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/tables.py +0 -0
  52. {typedal-4.6.0 → typedal-4.6.2}/src/typedal/web2py_py4web_shared.py +0 -0
  53. {typedal-4.6.0 → typedal-4.6.2}/tasks.py +0 -0
  54. {typedal-4.6.0 → typedal-4.6.2}/tests/__init__.py +0 -0
  55. {typedal-4.6.0 → typedal-4.6.2}/tests/configs/simple.toml +0 -0
  56. {typedal-4.6.0 → typedal-4.6.2}/tests/configs/valid.env +0 -0
  57. {typedal-4.6.0 → typedal-4.6.2}/tests/configs/valid.toml +0 -0
  58. {typedal-4.6.0 → typedal-4.6.2}/tests/py314_tests.py +0 -0
  59. {typedal-4.6.0 → typedal-4.6.2}/tests/test_cli.py +0 -0
  60. {typedal-4.6.0 → typedal-4.6.2}/tests/test_config.py +0 -0
  61. {typedal-4.6.0 → typedal-4.6.2}/tests/test_docs_examples.py +0 -0
  62. {typedal-4.6.0 → typedal-4.6.2}/tests/test_helpers.py +0 -0
  63. {typedal-4.6.0 → typedal-4.6.2}/tests/test_json.py +0 -0
  64. {typedal-4.6.0 → typedal-4.6.2}/tests/test_main.py +0 -0
  65. {typedal-4.6.0 → typedal-4.6.2}/tests/test_mypy.py +0 -0
  66. {typedal-4.6.0 → typedal-4.6.2}/tests/test_orm.py +0 -0
  67. {typedal-4.6.0 → typedal-4.6.2}/tests/test_py4web.py +0 -0
  68. {typedal-4.6.0 → typedal-4.6.2}/tests/test_query_builder.py +0 -0
  69. {typedal-4.6.0 → typedal-4.6.2}/tests/test_relationships.py +0 -0
  70. {typedal-4.6.0 → typedal-4.6.2}/tests/test_row.py +0 -0
  71. {typedal-4.6.0 → typedal-4.6.2}/tests/test_stats.py +0 -0
  72. {typedal-4.6.0 → typedal-4.6.2}/tests/test_table.py +0 -0
  73. {typedal-4.6.0 → typedal-4.6.2}/tests/test_web2py.py +0 -0
  74. {typedal-4.6.0 → typedal-4.6.2}/tests/test_xx_others.py +0 -0
  75. {typedal-4.6.0 → typedal-4.6.2}/tests/timings.py +0 -0
Binary file
@@ -2,6 +2,18 @@
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
+
11
+ ## v4.6.1 (2026-03-13)
12
+
13
+ ### Fix
14
+
15
+ * **pydantic:** Exclude fields with 'readable=False' similar to pydal/py4web/web2py behavior ([`e47cc44`](https://github.com/trialandsuccess/TypeDAL/commit/e47cc442aa7736ba1177bc32a7dc6de58c2551aa))
16
+
5
17
  ## v4.6.0 (2026-03-13)
6
18
 
7
19
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.6.0
3
+ Version: 4.6.2
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
@@ -56,7 +56,7 @@ or use them with `pydantic.TypeAdapter`.
56
56
  Add the mixin to enable `model_dump()` for serialization, including support for relationships and computed properties:
57
57
 
58
58
  ```python
59
- from typedal import TypedTable
59
+ from typedal import TypedField, TypedTable
60
60
  from typedal.mixins import PydanticMixin
61
61
 
62
62
 
@@ -67,6 +67,7 @@ class Author(TypedTable, PydanticMixin):
67
67
  class Book(TypedTable, PydanticMixin):
68
68
  title: str
69
69
  author: Author
70
+ private_notes = TypedField(str, readable=False)
70
71
 
71
72
  @property
72
73
  def display_title(self) -> str:
@@ -79,6 +80,12 @@ book = Book.where(id=1).join("author").first()
79
80
  # model_dump() serializes the full object graph
80
81
  data = book.model_dump()
81
82
  # -> {"id": 1, "title": "...", "author": {"id": 1, "name": "..."}, "display_title": "..."}
83
+ # (private_notes is omitted because readable=False)
84
+
85
+ # Runtime readability flags are also respected by model_dump()
86
+ Book.title.readable = False
87
+ data = book.model_dump()
88
+ # -> {"id": 1, "author": {"id": 1, "name": "..."}, "display_title": "..."}
82
89
 
83
90
  # Use mode="json" for JSON-serializable output (dates as ISO strings, etc.)
84
91
  data = book.model_dump(mode="json")
@@ -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"]
@@ -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.6.0"
8
+ __version__ = "4.6.2"
@@ -203,6 +203,7 @@ def _remove_cache(s: Set, tablename: str) -> None:
203
203
  indeces = s.select("id").column("id")
204
204
  remove_cache(indeces, tablename)
205
205
 
206
+
206
207
  def get_expire(
207
208
  expires_at: t.Optional[dt.datetime] = None,
208
209
  ttl: t.Optional[int | dt.timedelta] = None,
@@ -419,6 +420,7 @@ class Stats[T](t.TypedDict):
419
420
  valid: T
420
421
  expired: T
421
422
 
423
+
422
424
  RowStats = t.TypedDict(
423
425
  "RowStats",
424
426
  {
@@ -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
- from .types import CacheStatus, Field, Template # noqa: F401
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(_: type, fields: dict[str, t.Any]) -> t.Callable[[t.Any], t.Any]:
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
- return {k: getattr(value, k, None) for k in fields}
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
- return core_schema.typed_dict_field(
534
- core_schema.with_default_schema(core_schema.nullable_schema(inner), default=None),
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
 
@@ -585,13 +599,19 @@ class PydanticMixin(Mixin):
585
599
 
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
- data = {
589
- field_name: getattr(self, field_name, None)
590
- for field_name in self._pydantic_fields(
591
- include_relationships=not _shallow,
592
- include_properties=not _shallow,
593
- )
594
- }
602
+ cls = type(self)
603
+ relationship_names = set(cls.get_relationships())
604
+ data: dict[str, t.Any] = {}
605
+ for field_name in self._pydantic_fields(
606
+ include_relationships=not _shallow,
607
+ include_properties=not _shallow,
608
+ ):
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__:
612
+ continue
613
+
614
+ data[field_name] = getattr(self, field_name, None)
595
615
 
596
616
  if mode == "json":
597
617
  return dump_pydantic(data, _shallow_nested=True)
@@ -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
- transformed via the "represent" attributes of the associated fields.
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
- from .tables import TypedTable, _TypedTable # noqa: F401
38
+
39
+ # noinspection PyUnusedImports
40
+ from .tables import TypedTable, _TypedTable
39
41
 
40
42
  # ---------------------------------------------------------------------------
41
43
  # Aliases
@@ -232,6 +232,16 @@ class PydanticTupleAndLiteralSchema(TypedTable, PydanticMixin):
232
232
  status: typing.Literal["draft", "published"]
233
233
 
234
234
 
235
+ class PydanticHiddenTypedField(TypedTable, PydanticMixin):
236
+ visible = TypedField(str)
237
+ hidden = TypedField(str, readable=False)
238
+
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
+
235
245
  @pytest.fixture
236
246
  def pydantic_db():
237
247
  db = TypeDAL("sqlite:memory")
@@ -241,6 +251,8 @@ def pydantic_db():
241
251
  db.define(PydanticStringRelationship)
242
252
  db.define(PydanticGenericResolvedRelationship)
243
253
  db.define(PydanticGenericUnresolvedRelationship)
254
+ db.define(PydanticHiddenTypedField)
255
+ db.define(PydanticHiddenRelationshipHost)
244
256
  db.define(NonPydanticAuthor)
245
257
  yield db
246
258
 
@@ -443,6 +455,57 @@ def test_pydantic_fields_include_typedfield_and_skip_no_getter_property(pydantic
443
455
  assert "empty_prop" not in property_fields
444
456
 
445
457
 
458
+ def test_pydantic_skips_unreadable_typedfield_in_model_dump(pydantic_db):
459
+ row = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
460
+ data = row.model_dump()
461
+ assert data == {"id": row.id, "visible": "show"}
462
+
463
+ PydanticHiddenTypedField.visible.readable = False
464
+ row = PydanticHiddenTypedField.insert(visible="show-2", hidden="hide-2")
465
+ data = row.model_dump()
466
+ assert data == {"id": row.id}
467
+
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
+
446
509
  def test_pydantic_compatibility_non_type_and_missing_relationship_type():
447
510
  # ForwardRef is not a runtime type; this should be a no-op, not a crash.
448
511
  PydanticMixin._ensure_pydantic_compatible_type("x", typing.ForwardRef("Anything"))
Binary file
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