TypeDAL 4.8.2__tar.gz → 4.8.4__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.8.2 → typedal-4.8.4}/CHANGELOG.md +12 -0
- {typedal-4.8.2 → typedal-4.8.4}/PKG-INFO +1 -1
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/__about__.py +1 -1
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/caching.py +4 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/core.py +1 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/define.py +5 -6
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/enum_helpers.py +8 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/fields.py +39 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/mixins.py +1 -1
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/relationships.py +15 -5
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/rows.py +3 -3
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/types.py +15 -2
- {typedal-4.8.2 → typedal-4.8.4}/typing_again.py +5 -0
- {typedal-4.8.2 → typedal-4.8.4}/.github/workflows/su6.yml +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/.gitignore +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/.readthedocs.yml +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/README.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/coverage.svg +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/10_advanced_apis.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/1_getting_started.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/2_defining_tables.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/3_building_queries.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/4_relationships.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/5_py4web.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/6_migrations.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/7_configuration.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/8_mixins.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/9_memoization.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/css/code_blocks.css +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/index.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/docs/requirements.txt +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/example_new.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/example_old.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/mkdocs.yml +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/pyproject.toml +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/__init__.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/cli.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/config.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/constants.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/for_py4web.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/for_web2py.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/helpers.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/py.typed +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/query_builder.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/serializers/typescript.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/tables.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tasks.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/__init__.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/configs/simple.toml +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/configs/valid.env +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/configs/valid.toml +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/py314_tests.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_cli.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_config.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_docs_examples.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_helpers.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_json.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_main.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_mixins.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_mypy.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_orm.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_py4web.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_query_builder.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_relationships.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_row.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_stats.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_table.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_typescript.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_typing_mypy.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_typing_pyright.md +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_web2py.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/test_xx_others.py +0 -0
- {typedal-4.8.2 → typedal-4.8.4}/tests/timings.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.8.4 (2026-05-28)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Improved typing on OpRow; docstrings ([`064b454`](https://github.com/trialandsuccess/TypeDAL/commit/064b4546ceb709ee6124d4a749919c4193fb3752))
|
|
10
|
+
|
|
11
|
+
## v4.8.3 (2026-05-25)
|
|
12
|
+
|
|
13
|
+
### Fix
|
|
14
|
+
|
|
15
|
+
* Improved typing hints on TypedField properties ([`55b5ce4`](https://github.com/trialandsuccess/TypeDAL/commit/55b5ce4ae2657ae43b9cfa19dbe90e4386609495))
|
|
16
|
+
|
|
5
17
|
## v4.8.2 (2026-05-20)
|
|
6
18
|
|
|
7
19
|
### Fix
|
|
@@ -416,6 +416,8 @@ def _expired_and_valid_query() -> tuple[str, str]:
|
|
|
416
416
|
|
|
417
417
|
|
|
418
418
|
class Stats[T](t.TypedDict):
|
|
419
|
+
"""Typed container for total/valid/expired metric groups."""
|
|
420
|
+
|
|
419
421
|
total: T
|
|
420
422
|
valid: T
|
|
421
423
|
expired: T
|
|
@@ -578,6 +580,7 @@ def memoize[T: t.Any](
|
|
|
578
580
|
# Cache miss - compute result
|
|
579
581
|
|
|
580
582
|
def track_execute(_qb: "QueryBuilder[t.Any]", raw: Rows) -> None:
|
|
583
|
+
"""Collect table/id dependencies from executed raw query results."""
|
|
581
584
|
# find dependant table+id combinations, includes relationships:
|
|
582
585
|
deps.update(_determine_dependencies_auto(raw))
|
|
583
586
|
|
|
@@ -591,6 +594,7 @@ def memoize[T: t.Any](
|
|
|
591
594
|
deps.update({(table, 0) for table in related_tables})
|
|
592
595
|
|
|
593
596
|
def track_collect(qb: "QueryBuilder[t.Any]", _: TypedRows[t.Any], raw: Rows) -> None:
|
|
597
|
+
"""Collect dependencies from `.collect()` hooks by reusing execute tracking."""
|
|
594
598
|
return track_execute(qb, raw)
|
|
595
599
|
|
|
596
600
|
# hooks every .collect() to track extra dependencies
|
|
@@ -30,12 +30,7 @@ from .helpers import (
|
|
|
30
30
|
)
|
|
31
31
|
from .relationships import Relationship, to_relationship
|
|
32
32
|
from .tables import TypedTable
|
|
33
|
-
from .types import
|
|
34
|
-
Field,
|
|
35
|
-
T_annotation,
|
|
36
|
-
Table,
|
|
37
|
-
_Types,
|
|
38
|
-
)
|
|
33
|
+
from .types import Field, T_annotation, Table, _Types
|
|
39
34
|
|
|
40
35
|
try:
|
|
41
36
|
# python 3.14+
|
|
@@ -46,12 +41,16 @@ except ImportError: # pragma: no cover
|
|
|
46
41
|
|
|
47
42
|
|
|
48
43
|
class IS_IN_ENUM(Validator):
|
|
44
|
+
"""Validator that accepts only values present in a specific enum."""
|
|
45
|
+
|
|
49
46
|
def __init__(self, etype: type[enum.Enum], error_message: str = "value not allowed"):
|
|
47
|
+
"""Initialize the enum validator."""
|
|
50
48
|
super().__init__()
|
|
51
49
|
self.etype = etype
|
|
52
50
|
self.error_message = error_message
|
|
53
51
|
|
|
54
52
|
def validate(self, value: t.Any, _record_id: int | None = None) -> t.Any:
|
|
53
|
+
"""Validate and normalize an enum-compatible value."""
|
|
55
54
|
if value not in self.etype:
|
|
56
55
|
raise ValidationError(self.translator(self.error_message))
|
|
57
56
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Helpers for parsing enum values and preserving invalid enum input context."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import enum
|
|
@@ -7,6 +9,8 @@ from dataclasses import dataclass
|
|
|
7
9
|
|
|
8
10
|
@dataclass(frozen=True)
|
|
9
11
|
class InvalidEnumValue:
|
|
12
|
+
"""Sentinel payload used when a raw value cannot be parsed into an enum member."""
|
|
13
|
+
|
|
10
14
|
enum_type: type[enum.Enum]
|
|
11
15
|
raw: t.Any
|
|
12
16
|
value: None = None
|
|
@@ -19,6 +23,7 @@ class InvalidEnumValue:
|
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
def enum_value_type(enum_type: type[enum.Enum]) -> type[t.Any]:
|
|
26
|
+
"""Return the shared Python type of all enum member values."""
|
|
22
27
|
values = [member.value for member in enum_type]
|
|
23
28
|
if not values: # pragma: no cover
|
|
24
29
|
raise TypeError(f"Enum {enum_type.__name__} has no members.")
|
|
@@ -32,6 +37,7 @@ def enum_value_type(enum_type: type[enum.Enum]) -> type[t.Any]:
|
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
def parse_enum_value(enum_type: type[enum.Enum], raw: t.Any) -> enum.Enum | InvalidEnumValue | None:
|
|
40
|
+
"""Parse a raw value into an enum member or return an invalid sentinel."""
|
|
35
41
|
if raw is None: # pragma: no cover
|
|
36
42
|
return None
|
|
37
43
|
|
|
@@ -43,6 +49,8 @@ def parse_enum_value(enum_type: type[enum.Enum], raw: t.Any) -> enum.Enum | Inva
|
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
def make_enum_filter_out(enum_type: type[enum.Enum]) -> t.Callable[[t.Any], enum.Enum | InvalidEnumValue | None]:
|
|
52
|
+
"""Build a parser callback for enum values."""
|
|
53
|
+
|
|
46
54
|
def _filter_out(raw: t.Any) -> enum.Enum | InvalidEnumValue | None:
|
|
47
55
|
return parse_enum_value(enum_type, raw)
|
|
48
56
|
|
|
@@ -18,9 +18,11 @@ from pydal.objects import Table
|
|
|
18
18
|
|
|
19
19
|
from .core import TypeDAL
|
|
20
20
|
from .types import (
|
|
21
|
+
AnyCallable,
|
|
21
22
|
Expression,
|
|
22
23
|
Field,
|
|
23
24
|
FieldSettings,
|
|
25
|
+
FileSystemLike,
|
|
24
26
|
Query,
|
|
25
27
|
T_annotation,
|
|
26
28
|
T_MetaInstance,
|
|
@@ -50,7 +52,44 @@ class TypedField[T_Value](Expression): # pragma: no cover
|
|
|
50
52
|
_type: T_annotation
|
|
51
53
|
kwargs: t.Any
|
|
52
54
|
|
|
55
|
+
# Typed hints for common pydal.Field attrs forwarded via __getattr__ after bind().
|
|
56
|
+
type: str | type | Table | Field | SQLCustomType
|
|
57
|
+
type_name: str
|
|
58
|
+
length: int
|
|
59
|
+
default: T_Value
|
|
60
|
+
required: bool
|
|
61
|
+
ondelete: str
|
|
62
|
+
onupdate: str
|
|
63
|
+
notnull: bool
|
|
64
|
+
unique: bool
|
|
65
|
+
regex: str | None
|
|
66
|
+
options: list[t.Any] | AnyCallable | None
|
|
67
|
+
uploadfield: bool | str
|
|
68
|
+
uploadfolder: str | None
|
|
69
|
+
uploadseparate: bool
|
|
70
|
+
uploadfs: FileSystemLike | None
|
|
71
|
+
widget: AnyCallable | None
|
|
72
|
+
label: str
|
|
73
|
+
comment: str | None
|
|
74
|
+
writable: bool
|
|
75
|
+
readable: bool
|
|
76
|
+
searchable: bool
|
|
77
|
+
listable: bool
|
|
78
|
+
update: T_Value | AnyCallable | None
|
|
79
|
+
authorize: AnyCallable | None
|
|
80
|
+
autodelete: bool
|
|
53
81
|
requires: Validator | t.Iterable[Validator]
|
|
82
|
+
represent: t.Callable[[T_Value, TypedTable | None], t.Any]
|
|
83
|
+
compute: AnyCallable | None
|
|
84
|
+
custom_store: AnyCallable | None
|
|
85
|
+
custom_retrieve: AnyCallable | None
|
|
86
|
+
custom_retrieve_file_properties: AnyCallable | None
|
|
87
|
+
custom_delete: AnyCallable | None
|
|
88
|
+
filter_in: AnyCallable | None
|
|
89
|
+
filter_out: AnyCallable | None
|
|
90
|
+
custom_qualifier: AnyCallable | None
|
|
91
|
+
map_none: T_Value | None
|
|
92
|
+
_raw_rname: str | None
|
|
54
93
|
|
|
55
94
|
# NOTE: for the logic of converting a TypedField into a pydal Field, see TypeDAL._to_field
|
|
56
95
|
|
|
@@ -208,22 +208,32 @@ class Relationship[To_Type]:
|
|
|
208
208
|
@t.overload
|
|
209
209
|
def __get__(
|
|
210
210
|
self: "Relationship[list[_RelTable]]", instance: None, owner: t.Type["TypedTable"]
|
|
211
|
-
) -> "Relationship[list[_RelTable]]":
|
|
211
|
+
) -> "Relationship[list[_RelTable]]":
|
|
212
|
+
"""Return the descriptor itself when accessed on the class."""
|
|
213
|
+
...
|
|
212
214
|
|
|
213
215
|
@t.overload
|
|
214
216
|
def __get__(
|
|
215
217
|
self: "Relationship[list[_RelTable]]", instance: "TypedTable", owner: t.Type["TypedTable"]
|
|
216
|
-
) -> list[_RelTable]:
|
|
218
|
+
) -> list[_RelTable]:
|
|
219
|
+
"""Return related rows for list-valued relationships."""
|
|
220
|
+
...
|
|
217
221
|
|
|
218
222
|
@t.overload
|
|
219
223
|
def __get__(
|
|
220
224
|
self: "Relationship[_RelValue]", instance: None, owner: t.Type["TypedTable"]
|
|
221
|
-
) -> "Relationship[_RelValue]":
|
|
225
|
+
) -> "Relationship[_RelValue]":
|
|
226
|
+
"""Return the descriptor itself when accessed on the class."""
|
|
227
|
+
...
|
|
222
228
|
|
|
223
229
|
@t.overload
|
|
224
230
|
def __get__(
|
|
225
|
-
self: "Relationship[_RelValue]",
|
|
226
|
-
|
|
231
|
+
self: "Relationship[_RelValue]",
|
|
232
|
+
instance: "TypedTable",
|
|
233
|
+
owner: t.Type["TypedTable"],
|
|
234
|
+
) -> _RelValue:
|
|
235
|
+
"""Return the related object for single-valued relationships."""
|
|
236
|
+
...
|
|
227
237
|
|
|
228
238
|
def __get__(
|
|
229
239
|
self,
|
|
@@ -61,7 +61,6 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
|
|
|
61
61
|
`metadata` can be t.Any (un)structured data
|
|
62
62
|
`model` is a Typed Table class
|
|
63
63
|
"""
|
|
64
|
-
|
|
65
64
|
records = records or {self._get_id(row, model): model(row) for row in rows}
|
|
66
65
|
raw = raw or {}
|
|
67
66
|
|
|
@@ -433,8 +432,9 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
|
|
|
433
432
|
fields: list[Field] | None = None,
|
|
434
433
|
) -> t.Generator[T_MetaInstance, None, None] | T_MetaInstance:
|
|
435
434
|
"""
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
Take an index and return a copy of the indexed row with values.
|
|
436
|
+
|
|
437
|
+
Values are transformed via the "represent" attributes of the associated fields.
|
|
438
438
|
|
|
439
439
|
Args:
|
|
440
440
|
i: index. If not specified, a generator is returned for iteration
|
|
@@ -62,7 +62,7 @@ class TableProtocol(t.Protocol): # pragma: no cover
|
|
|
62
62
|
|
|
63
63
|
def on(self, query: "QueryLike") -> "Expression | _Expression":
|
|
64
64
|
"""
|
|
65
|
-
|
|
65
|
+
PyDAL `Table.on(query)` helper used in join callbacks.
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
68
|
|
|
@@ -112,13 +112,26 @@ if t.TYPE_CHECKING:
|
|
|
112
112
|
Pydal OpRow object for typing (otherwise mypy thinks it's Any).
|
|
113
113
|
"""
|
|
114
114
|
|
|
115
|
+
def __getattr__(self, name: str) -> t.Any:
|
|
116
|
+
"""Dynamic attribute access (e.g. row.email)."""
|
|
117
|
+
|
|
115
118
|
def __getitem__(self, item: str) -> t.Any:
|
|
116
119
|
"""row.item syntax."""
|
|
117
120
|
|
|
118
121
|
def __setitem__(self, key: str, value: t.Any) -> None:
|
|
119
122
|
"""row.item = key syntax."""
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
def get(self, key: str, default: t.Any = None) -> t.Any:
|
|
125
|
+
"""Dictionary-like get used by update hooks."""
|
|
126
|
+
|
|
127
|
+
def keys(self) -> t.Iterable[str]:
|
|
128
|
+
"""Dictionary-like key access."""
|
|
129
|
+
|
|
130
|
+
def items(self) -> t.Iterable[tuple[str, t.Any]]:
|
|
131
|
+
"""Dictionary-like item iteration."""
|
|
132
|
+
|
|
133
|
+
def values(self) -> t.Iterable[t.Any]:
|
|
134
|
+
"""Dictionary-like value iteration."""
|
|
122
135
|
|
|
123
136
|
else:
|
|
124
137
|
|
|
@@ -33,3 +33,8 @@ reveal_type(row.rel) # expected: OtherTable | None; pycharm: Relationship[Other
|
|
|
33
33
|
|
|
34
34
|
reveal_type(MyTable.multiple) # expected: Relationship[list[OtherTable]]; pycharm: Relationship[list[OtherTable]]
|
|
35
35
|
reveal_type(row.multiple) # expected: list[OtherTable]; pycharm: Relationship[list[OtherTable]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
MyTable.field.label = "Test Label"
|
|
40
|
+
MyTable.field.represent = lambda value, row: "Test Repr"
|
|
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
|