TypeDAL 3.16.4__py3-none-any.whl → 4.2.0__py3-none-any.whl
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/__about__.py +1 -1
- typedal/__init__.py +21 -3
- typedal/caching.py +37 -34
- typedal/config.py +18 -16
- typedal/constants.py +25 -0
- typedal/core.py +188 -3115
- typedal/define.py +188 -0
- typedal/fields.py +293 -34
- typedal/for_py4web.py +1 -1
- typedal/for_web2py.py +1 -1
- typedal/helpers.py +329 -40
- typedal/mixins.py +23 -27
- typedal/query_builder.py +1119 -0
- typedal/relationships.py +390 -0
- typedal/rows.py +524 -0
- typedal/serializers/as_json.py +9 -10
- typedal/tables.py +1131 -0
- typedal/types.py +187 -179
- typedal/web2py_py4web_shared.py +1 -1
- {typedal-3.16.4.dist-info → typedal-4.2.0.dist-info}/METADATA +8 -7
- typedal-4.2.0.dist-info/RECORD +25 -0
- {typedal-3.16.4.dist-info → typedal-4.2.0.dist-info}/WHEEL +1 -1
- typedal-3.16.4.dist-info/RECORD +0 -19
- {typedal-3.16.4.dist-info → typedal-4.2.0.dist-info}/entry_points.txt +0 -0
typedal/define.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Seperates the table definition code from core DAL code.
|
|
3
|
+
|
|
4
|
+
Since otherwise helper methods would clutter up the TypeDAl class.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import types
|
|
11
|
+
import typing as t
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import pydal
|
|
15
|
+
|
|
16
|
+
from .constants import BASIC_MAPPINGS
|
|
17
|
+
from .core import TypeDAL, evaluate_forward_reference, resolve_annotation
|
|
18
|
+
from .fields import TypedField, is_typed_field
|
|
19
|
+
from .helpers import (
|
|
20
|
+
all_annotations,
|
|
21
|
+
all_dict,
|
|
22
|
+
filter_out,
|
|
23
|
+
instanciate,
|
|
24
|
+
is_union,
|
|
25
|
+
origin_is_subclass,
|
|
26
|
+
to_snake,
|
|
27
|
+
)
|
|
28
|
+
from .relationships import Relationship, to_relationship
|
|
29
|
+
from .tables import TypedTable
|
|
30
|
+
from .types import (
|
|
31
|
+
Field,
|
|
32
|
+
T,
|
|
33
|
+
T_annotation,
|
|
34
|
+
Table,
|
|
35
|
+
_Types,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# python 3.14+
|
|
40
|
+
from annotationlib import ForwardRef
|
|
41
|
+
except ImportError: # pragma: no cover
|
|
42
|
+
# python 3.13-
|
|
43
|
+
from typing import ForwardRef
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TableDefinitionBuilder:
|
|
47
|
+
"""Handles the conversion of TypedTable classes to pydal tables."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, db: "TypeDAL"):
|
|
50
|
+
"""
|
|
51
|
+
Before, the `class_map` was a singleton on the pydal class; now it's per database.
|
|
52
|
+
"""
|
|
53
|
+
self.db = db
|
|
54
|
+
self.class_map: dict[str, t.Type["TypedTable"]] = {}
|
|
55
|
+
|
|
56
|
+
def define(self, cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
|
|
57
|
+
"""Build and register a table from a TypedTable class."""
|
|
58
|
+
full_dict = all_dict(cls)
|
|
59
|
+
tablename = to_snake(cls.__name__)
|
|
60
|
+
annotations = all_annotations(cls)
|
|
61
|
+
annotations |= {k: t.cast(type, v) for k, v in full_dict.items() if is_typed_field(v)}
|
|
62
|
+
annotations = {k: v for k, v in annotations.items() if not k.startswith("_")}
|
|
63
|
+
|
|
64
|
+
typedfields: dict[str, TypedField[t.Any]] = {
|
|
65
|
+
k: instanciate(v, True) for k, v in annotations.items() if is_typed_field(v)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
relationships: dict[str, type[Relationship[t.Any]]] = filter_out(annotations, Relationship) # type: ignore
|
|
69
|
+
fields = {fname: self.to_field(fname, ftype) for fname, ftype in annotations.items()}
|
|
70
|
+
|
|
71
|
+
other_kwargs = kwargs | {
|
|
72
|
+
k: v for k, v in cls.__dict__.items() if k not in annotations and not k.startswith("_")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for key, field in typedfields.items():
|
|
76
|
+
clone = copy.copy(field)
|
|
77
|
+
setattr(cls, key, clone)
|
|
78
|
+
typedfields[key] = clone
|
|
79
|
+
|
|
80
|
+
relationships = filter_out(full_dict, Relationship) | relationships | filter_out(other_kwargs, Relationship) # type: ignore
|
|
81
|
+
|
|
82
|
+
reference_field_keys = [
|
|
83
|
+
k for k, v in fields.items() if str(v.type).split(" ")[0] in ("list:reference", "reference")
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
relationships |= {
|
|
87
|
+
k: new_relationship
|
|
88
|
+
for k in reference_field_keys
|
|
89
|
+
if k not in relationships and (new_relationship := to_relationship(cls, k, annotations[k]))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cache_dependency = self.db._config.caching and kwargs.pop("cache_dependency", True)
|
|
93
|
+
table: Table = self.db.define_table(tablename, *fields.values(), **kwargs)
|
|
94
|
+
|
|
95
|
+
for name, typed_field in typedfields.items():
|
|
96
|
+
field = fields[name]
|
|
97
|
+
typed_field.bind(field, table)
|
|
98
|
+
|
|
99
|
+
if issubclass(cls, TypedTable):
|
|
100
|
+
cls.__set_internals__(
|
|
101
|
+
db=self.db,
|
|
102
|
+
table=table,
|
|
103
|
+
relationships=t.cast(dict[str, Relationship[t.Any]], relationships),
|
|
104
|
+
)
|
|
105
|
+
self.class_map[str(table)] = cls
|
|
106
|
+
self.class_map[table._rname] = cls
|
|
107
|
+
cls.__on_define__(self.db)
|
|
108
|
+
else:
|
|
109
|
+
warnings.warn("db.define used without inheriting TypedTable. This could lead to strange problems!")
|
|
110
|
+
|
|
111
|
+
if not tablename.startswith("typedal_") and cache_dependency:
|
|
112
|
+
from .caching import _remove_cache
|
|
113
|
+
|
|
114
|
+
table._before_update.append(lambda s, _: _remove_cache(s, tablename))
|
|
115
|
+
table._before_delete.append(lambda s: _remove_cache(s, tablename))
|
|
116
|
+
|
|
117
|
+
return cls
|
|
118
|
+
|
|
119
|
+
def to_field(self, fname: str, ftype: type, **kw: t.Any) -> Field:
|
|
120
|
+
"""Convert annotation to pydal Field."""
|
|
121
|
+
fname = to_snake(fname)
|
|
122
|
+
if converted_type := self.annotation_to_pydal_fieldtype(ftype, kw):
|
|
123
|
+
return self.build_field(fname, converted_type, **kw)
|
|
124
|
+
else:
|
|
125
|
+
raise NotImplementedError(f"Unsupported type {ftype}/{type(ftype)}")
|
|
126
|
+
|
|
127
|
+
def annotation_to_pydal_fieldtype(
|
|
128
|
+
self,
|
|
129
|
+
ftype_annotation: T_annotation,
|
|
130
|
+
mut_kw: t.MutableMapping[str, t.Any],
|
|
131
|
+
) -> t.Optional[str]:
|
|
132
|
+
"""Convert Python type annotation to pydal field type string."""
|
|
133
|
+
ftype = t.cast(type, ftype_annotation) # cast from Type to type to make mypy happy)
|
|
134
|
+
|
|
135
|
+
if isinstance(ftype, str):
|
|
136
|
+
# extract type from string
|
|
137
|
+
ftype = resolve_annotation(ftype)
|
|
138
|
+
|
|
139
|
+
if isinstance(ftype, ForwardRef):
|
|
140
|
+
known_classes = {table.__name__: table for table in self.class_map.values()}
|
|
141
|
+
|
|
142
|
+
ftype = evaluate_forward_reference(ftype, namespace=known_classes)
|
|
143
|
+
|
|
144
|
+
if mapping := BASIC_MAPPINGS.get(ftype):
|
|
145
|
+
# basi types
|
|
146
|
+
return mapping
|
|
147
|
+
elif isinstance(ftype, pydal.objects.Table):
|
|
148
|
+
# db.table
|
|
149
|
+
return f"reference {ftype._tablename}"
|
|
150
|
+
elif issubclass(type(ftype), type) and issubclass(ftype, TypedTable):
|
|
151
|
+
# SomeTable
|
|
152
|
+
snakename = to_snake(ftype.__name__)
|
|
153
|
+
return f"reference {snakename}"
|
|
154
|
+
elif isinstance(ftype, TypedField):
|
|
155
|
+
# FieldType(type, ...)
|
|
156
|
+
return ftype._to_field(mut_kw, self)
|
|
157
|
+
elif origin_is_subclass(ftype, TypedField):
|
|
158
|
+
# TypedField[int]
|
|
159
|
+
return self.annotation_to_pydal_fieldtype(t.get_args(ftype)[0], mut_kw)
|
|
160
|
+
elif isinstance(ftype, types.GenericAlias) and t.get_origin(ftype) in (list, TypedField):
|
|
161
|
+
# list[str] -> str -> string -> list:string
|
|
162
|
+
_child_type = t.get_args(ftype)[0]
|
|
163
|
+
_child_type = self.annotation_to_pydal_fieldtype(_child_type, mut_kw)
|
|
164
|
+
return f"list:{_child_type}"
|
|
165
|
+
elif is_union(ftype):
|
|
166
|
+
# str | int -> UnionType
|
|
167
|
+
# typing.Union[str | int] -> typing._UnionGenericAlias
|
|
168
|
+
|
|
169
|
+
# Optional[type] == type | None
|
|
170
|
+
|
|
171
|
+
match t.get_args(ftype):
|
|
172
|
+
case (_child_type, _Types.NONETYPE) | (_Types.NONETYPE, _child_type):
|
|
173
|
+
# good union of Nullable
|
|
174
|
+
|
|
175
|
+
# if a field is optional, it is nullable:
|
|
176
|
+
mut_kw["notnull"] = False
|
|
177
|
+
return self.annotation_to_pydal_fieldtype(_child_type, mut_kw)
|
|
178
|
+
case _:
|
|
179
|
+
# two types is not supported by the db!
|
|
180
|
+
return None
|
|
181
|
+
else:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def build_field(cls, name: str, field_type: str, **kw: t.Any) -> Field:
|
|
186
|
+
"""Create a pydal Field with default kwargs."""
|
|
187
|
+
kw_combined = TypeDAL.default_kwargs | kw
|
|
188
|
+
return Field(name, field_type, **kw_combined)
|
typedal/fields.py
CHANGED
|
@@ -2,27 +2,248 @@
|
|
|
2
2
|
This file contains available Field types.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
import ast
|
|
8
|
+
import contextlib
|
|
6
9
|
import datetime as dt
|
|
7
10
|
import decimal
|
|
8
|
-
import
|
|
11
|
+
import types
|
|
12
|
+
import typing as t
|
|
9
13
|
import uuid
|
|
10
14
|
|
|
15
|
+
import pydal
|
|
11
16
|
from pydal.helpers.classes import SQLCustomType
|
|
12
17
|
from pydal.objects import Table
|
|
13
|
-
from typing_extensions import Unpack
|
|
14
18
|
|
|
15
|
-
from .core import TypeDAL
|
|
16
|
-
from .types import
|
|
19
|
+
from .core import TypeDAL
|
|
20
|
+
from .types import (
|
|
21
|
+
Expression,
|
|
22
|
+
Field,
|
|
23
|
+
FieldSettings,
|
|
24
|
+
Query,
|
|
25
|
+
T_annotation,
|
|
26
|
+
T_MetaInstance,
|
|
27
|
+
T_subclass,
|
|
28
|
+
T_Value,
|
|
29
|
+
Validator,
|
|
30
|
+
)
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
if t.TYPE_CHECKING:
|
|
33
|
+
# will be imported for real later:
|
|
34
|
+
from .tables import TypedTable
|
|
19
35
|
|
|
20
36
|
|
|
21
37
|
## general
|
|
22
38
|
|
|
23
39
|
|
|
40
|
+
class TypedField(Expression, t.Generic[T_Value]): # pragma: no cover
|
|
41
|
+
"""
|
|
42
|
+
Typed version of pydal.Field, which will be converted to a normal Field in the background.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# will be set by .bind on db.define
|
|
46
|
+
name = ""
|
|
47
|
+
_db: t.Optional[pydal.DAL] = None
|
|
48
|
+
_rname: t.Optional[str] = None
|
|
49
|
+
_table: t.Optional[Table] = None
|
|
50
|
+
_field: t.Optional[Field] = None
|
|
51
|
+
|
|
52
|
+
_type: T_annotation
|
|
53
|
+
kwargs: t.Any
|
|
54
|
+
|
|
55
|
+
requires: Validator | t.Iterable[Validator]
|
|
56
|
+
|
|
57
|
+
# NOTE: for the logic of converting a TypedField into a pydal Field, see TypeDAL._to_field
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
_type: t.Type[T_Value] | types.UnionType = str, # type: ignore
|
|
62
|
+
/,
|
|
63
|
+
**settings: t.Unpack[FieldSettings],
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Typed version of pydal.Field, which will be converted to a normal Field in the background.
|
|
67
|
+
|
|
68
|
+
Provide the Python type for this field as the first positional argument
|
|
69
|
+
and any other settings to Field() as keyword parameters.
|
|
70
|
+
"""
|
|
71
|
+
self._type = _type
|
|
72
|
+
self.kwargs = settings
|
|
73
|
+
# super().__init__()
|
|
74
|
+
|
|
75
|
+
@t.overload
|
|
76
|
+
def __get__(self, instance: T_MetaInstance, owner: t.Type[T_MetaInstance]) -> T_Value: # pragma: no cover
|
|
77
|
+
"""
|
|
78
|
+
row.field -> (actual data).
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@t.overload
|
|
82
|
+
def __get__(self, instance: None, owner: "t.Type[TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
|
|
83
|
+
"""
|
|
84
|
+
Table.field -> Field.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __get__(
|
|
88
|
+
self,
|
|
89
|
+
instance: T_MetaInstance | None,
|
|
90
|
+
owner: t.Type[T_MetaInstance],
|
|
91
|
+
) -> t.Union[T_Value, "TypedField[T_Value]"]:
|
|
92
|
+
"""
|
|
93
|
+
Since this class is a Descriptor field, \
|
|
94
|
+
it returns something else depending on if it's called on a class or instance.
|
|
95
|
+
|
|
96
|
+
(this is mostly for mypy/typing)
|
|
97
|
+
"""
|
|
98
|
+
if instance:
|
|
99
|
+
# this is only reached in a very specific case:
|
|
100
|
+
# an instance of the object was created with a specific set of fields selected (excluding the current one)
|
|
101
|
+
# in that case, no value was stored in the owner -> return None (since the field was not selected)
|
|
102
|
+
return t.cast(T_Value, None) # cast as T_Value so mypy understands it for selected fields
|
|
103
|
+
else:
|
|
104
|
+
# getting as class -> return actual field so pydal understands it when using in query etc.
|
|
105
|
+
return t.cast(TypedField[T_Value], self._field) # pretend it's still typed for IDE support
|
|
106
|
+
|
|
107
|
+
def __str__(self) -> str:
|
|
108
|
+
"""
|
|
109
|
+
String representation of a Typed Field.
|
|
110
|
+
|
|
111
|
+
If `type` is set explicitly (e.g. TypedField(str, type="text")), that type is used: `TypedField.text`,
|
|
112
|
+
otherwise the type annotation is used (e.g. TypedField(str) -> TypedField.str)
|
|
113
|
+
"""
|
|
114
|
+
return str(self._field) if self._field else ""
|
|
115
|
+
|
|
116
|
+
def __repr__(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
More detailed string representation of a Typed Field.
|
|
119
|
+
|
|
120
|
+
Uses __str__ and adds the provided extra options (kwargs) in the representation.
|
|
121
|
+
"""
|
|
122
|
+
string_value = self.__str__()
|
|
123
|
+
|
|
124
|
+
if "type" in self.kwargs:
|
|
125
|
+
# manual type in kwargs supplied
|
|
126
|
+
typename = self.kwargs["type"]
|
|
127
|
+
elif issubclass(type, type(self._type)):
|
|
128
|
+
# normal type, str.__name__ = 'str'
|
|
129
|
+
typename = getattr(self._type, "__name__", str(self._type))
|
|
130
|
+
elif t_args := t.get_args(self._type):
|
|
131
|
+
# list[str] -> 'str'
|
|
132
|
+
typename = t_args[0].__name__
|
|
133
|
+
else: # pragma: no cover
|
|
134
|
+
# fallback - something else, may not even happen, I'm not sure
|
|
135
|
+
typename = self._type
|
|
136
|
+
|
|
137
|
+
string_value = f"TypedField[{typename}].{string_value}" if string_value else f"TypedField[{typename}]"
|
|
138
|
+
|
|
139
|
+
kw = self.kwargs.copy()
|
|
140
|
+
kw.pop("type", None)
|
|
141
|
+
return f"<{string_value} with options {kw}>"
|
|
142
|
+
|
|
143
|
+
def _to_field(self, extra_kwargs: t.MutableMapping[str, t.Any], builder: TableDefinitionBuilder) -> t.Optional[str]:
|
|
144
|
+
"""
|
|
145
|
+
Convert a Typed Field instance to a pydal.Field.
|
|
146
|
+
|
|
147
|
+
Actual logic in TypeDAL._to_field but this function creates the pydal type name and updates the kwarg settings.
|
|
148
|
+
"""
|
|
149
|
+
other_kwargs = self.kwargs.copy()
|
|
150
|
+
extra_kwargs.update(other_kwargs) # <- modifies and overwrites the default kwargs with user-specified ones
|
|
151
|
+
return extra_kwargs.pop("type", False) or builder.annotation_to_pydal_fieldtype(
|
|
152
|
+
self._type,
|
|
153
|
+
extra_kwargs,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def bind(self, field: pydal.objects.Field, table: pydal.objects.Table) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Bind the right db/table/field info to this class, so queries can be made using `Class.field == ...`.
|
|
159
|
+
"""
|
|
160
|
+
self._table = table
|
|
161
|
+
self._field = field
|
|
162
|
+
|
|
163
|
+
def __getattr__(self, key: str) -> t.Any:
|
|
164
|
+
"""
|
|
165
|
+
If the regular getattribute does not work, try to get info from the related Field.
|
|
166
|
+
"""
|
|
167
|
+
with contextlib.suppress(AttributeError):
|
|
168
|
+
return super().__getattribute__(key)
|
|
169
|
+
|
|
170
|
+
# try on actual field:
|
|
171
|
+
return getattr(self._field, key)
|
|
172
|
+
|
|
173
|
+
def __eq__(self, other: t.Any) -> Query:
|
|
174
|
+
"""
|
|
175
|
+
Performing == on a Field will result in a Query.
|
|
176
|
+
"""
|
|
177
|
+
return t.cast(Query, self._field == other)
|
|
178
|
+
|
|
179
|
+
def __ne__(self, other: t.Any) -> Query:
|
|
180
|
+
"""
|
|
181
|
+
Performing != on a Field will result in a Query.
|
|
182
|
+
"""
|
|
183
|
+
return t.cast(Query, self._field != other)
|
|
184
|
+
|
|
185
|
+
def __gt__(self, other: t.Any) -> Query:
|
|
186
|
+
"""
|
|
187
|
+
Performing > on a Field will result in a Query.
|
|
188
|
+
"""
|
|
189
|
+
return t.cast(Query, self._field > other)
|
|
190
|
+
|
|
191
|
+
def __lt__(self, other: t.Any) -> Query:
|
|
192
|
+
"""
|
|
193
|
+
Performing < on a Field will result in a Query.
|
|
194
|
+
"""
|
|
195
|
+
return t.cast(Query, self._field < other)
|
|
196
|
+
|
|
197
|
+
def __ge__(self, other: t.Any) -> Query:
|
|
198
|
+
"""
|
|
199
|
+
Performing >= on a Field will result in a Query.
|
|
200
|
+
"""
|
|
201
|
+
return t.cast(Query, self._field >= other)
|
|
202
|
+
|
|
203
|
+
def __le__(self, other: t.Any) -> Query:
|
|
204
|
+
"""
|
|
205
|
+
Performing <= on a Field will result in a Query.
|
|
206
|
+
"""
|
|
207
|
+
return t.cast(Query, self._field <= other)
|
|
208
|
+
|
|
209
|
+
def __hash__(self) -> int:
|
|
210
|
+
"""
|
|
211
|
+
Shadow Field.__hash__.
|
|
212
|
+
"""
|
|
213
|
+
return hash(self._field)
|
|
214
|
+
|
|
215
|
+
def __invert__(self) -> Expression:
|
|
216
|
+
"""
|
|
217
|
+
Performing ~ on a Field will result in an Expression.
|
|
218
|
+
"""
|
|
219
|
+
if not self._field: # pragma: no cover
|
|
220
|
+
raise ValueError("Unbound Field can not be inverted!")
|
|
221
|
+
|
|
222
|
+
return t.cast(Expression, ~self._field)
|
|
223
|
+
|
|
224
|
+
def lower(self) -> Expression:
|
|
225
|
+
"""
|
|
226
|
+
For string-fields: compare lowercased values.
|
|
227
|
+
"""
|
|
228
|
+
if not self._field: # pragma: no cover
|
|
229
|
+
raise ValueError("Unbound Field can not be lowered!")
|
|
230
|
+
|
|
231
|
+
return t.cast(Expression, self._field.lower())
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def is_typed_field(cls: t.Any) -> t.TypeGuard["TypedField[t.Any]"]:
|
|
235
|
+
"""
|
|
236
|
+
Is `cls` an instance or subclass of TypedField?
|
|
237
|
+
|
|
238
|
+
Deprecated
|
|
239
|
+
"""
|
|
240
|
+
return isinstance(cls, TypedField) or (
|
|
241
|
+
isinstance(t.get_origin(cls), type) and issubclass(t.get_origin(cls), TypedField)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
24
245
|
## specific
|
|
25
|
-
def StringField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
246
|
+
def StringField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
|
|
26
247
|
"""
|
|
27
248
|
Pydal type is string, Python type is str.
|
|
28
249
|
"""
|
|
@@ -33,7 +254,7 @@ def StringField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
|
33
254
|
String = StringField
|
|
34
255
|
|
|
35
256
|
|
|
36
|
-
def TextField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
257
|
+
def TextField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
|
|
37
258
|
"""
|
|
38
259
|
Pydal type is text, Python type is str.
|
|
39
260
|
"""
|
|
@@ -44,7 +265,7 @@ def TextField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
|
44
265
|
Text = TextField
|
|
45
266
|
|
|
46
267
|
|
|
47
|
-
def BlobField(**kw: Unpack[FieldSettings]) -> TypedField[bytes]:
|
|
268
|
+
def BlobField(**kw: t.Unpack[FieldSettings]) -> TypedField[bytes]:
|
|
48
269
|
"""
|
|
49
270
|
Pydal type is blob, Python type is bytes.
|
|
50
271
|
"""
|
|
@@ -55,7 +276,7 @@ def BlobField(**kw: Unpack[FieldSettings]) -> TypedField[bytes]:
|
|
|
55
276
|
Blob = BlobField
|
|
56
277
|
|
|
57
278
|
|
|
58
|
-
def BooleanField(**kw: Unpack[FieldSettings]) -> TypedField[bool]:
|
|
279
|
+
def BooleanField(**kw: t.Unpack[FieldSettings]) -> TypedField[bool]:
|
|
59
280
|
"""
|
|
60
281
|
Pydal type is boolean, Python type is bool.
|
|
61
282
|
"""
|
|
@@ -66,7 +287,7 @@ def BooleanField(**kw: Unpack[FieldSettings]) -> TypedField[bool]:
|
|
|
66
287
|
Boolean = BooleanField
|
|
67
288
|
|
|
68
289
|
|
|
69
|
-
def IntegerField(**kw: Unpack[FieldSettings]) -> TypedField[int]:
|
|
290
|
+
def IntegerField(**kw: t.Unpack[FieldSettings]) -> TypedField[int]:
|
|
70
291
|
"""
|
|
71
292
|
Pydal type is integer, Python type is int.
|
|
72
293
|
"""
|
|
@@ -77,7 +298,7 @@ def IntegerField(**kw: Unpack[FieldSettings]) -> TypedField[int]:
|
|
|
77
298
|
Integer = IntegerField
|
|
78
299
|
|
|
79
300
|
|
|
80
|
-
def DoubleField(**kw: Unpack[FieldSettings]) -> TypedField[float]:
|
|
301
|
+
def DoubleField(**kw: t.Unpack[FieldSettings]) -> TypedField[float]:
|
|
81
302
|
"""
|
|
82
303
|
Pydal type is double, Python type is float.
|
|
83
304
|
"""
|
|
@@ -88,7 +309,7 @@ def DoubleField(**kw: Unpack[FieldSettings]) -> TypedField[float]:
|
|
|
88
309
|
Double = DoubleField
|
|
89
310
|
|
|
90
311
|
|
|
91
|
-
def DecimalField(n: int, m: int, **kw: Unpack[FieldSettings]) -> TypedField[decimal.Decimal]:
|
|
312
|
+
def DecimalField(n: int, m: int, **kw: t.Unpack[FieldSettings]) -> TypedField[decimal.Decimal]:
|
|
92
313
|
"""
|
|
93
314
|
Pydal type is decimal, Python type is Decimal.
|
|
94
315
|
"""
|
|
@@ -99,7 +320,7 @@ def DecimalField(n: int, m: int, **kw: Unpack[FieldSettings]) -> TypedField[deci
|
|
|
99
320
|
Decimal = DecimalField
|
|
100
321
|
|
|
101
322
|
|
|
102
|
-
def DateField(**kw: Unpack[FieldSettings]) -> TypedField[dt.date]:
|
|
323
|
+
def DateField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.date]:
|
|
103
324
|
"""
|
|
104
325
|
Pydal type is date, Python type is datetime.date.
|
|
105
326
|
"""
|
|
@@ -110,7 +331,7 @@ def DateField(**kw: Unpack[FieldSettings]) -> TypedField[dt.date]:
|
|
|
110
331
|
Date = DateField
|
|
111
332
|
|
|
112
333
|
|
|
113
|
-
def TimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.time]:
|
|
334
|
+
def TimeField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.time]:
|
|
114
335
|
"""
|
|
115
336
|
Pydal type is time, Python type is datetime.time.
|
|
116
337
|
"""
|
|
@@ -121,7 +342,7 @@ def TimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.time]:
|
|
|
121
342
|
Time = TimeField
|
|
122
343
|
|
|
123
344
|
|
|
124
|
-
def DatetimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
345
|
+
def DatetimeField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
125
346
|
"""
|
|
126
347
|
Pydal type is datetime, Python type is datetime.datetime.
|
|
127
348
|
"""
|
|
@@ -132,7 +353,7 @@ def DatetimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
|
132
353
|
Datetime = DatetimeField
|
|
133
354
|
|
|
134
355
|
|
|
135
|
-
def PasswordField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
356
|
+
def PasswordField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
|
|
136
357
|
"""
|
|
137
358
|
Pydal type is password, Python type is str.
|
|
138
359
|
"""
|
|
@@ -143,7 +364,7 @@ def PasswordField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
|
143
364
|
Password = PasswordField
|
|
144
365
|
|
|
145
366
|
|
|
146
|
-
def UploadField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
367
|
+
def UploadField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
|
|
147
368
|
"""
|
|
148
369
|
Pydal type is upload, Python type is str.
|
|
149
370
|
"""
|
|
@@ -153,11 +374,10 @@ def UploadField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
|
|
|
153
374
|
|
|
154
375
|
Upload = UploadField
|
|
155
376
|
|
|
156
|
-
T_subclass = typing.TypeVar("T_subclass", TypedTable, Table)
|
|
157
|
-
|
|
158
377
|
|
|
159
378
|
def ReferenceField(
|
|
160
|
-
other_table: str |
|
|
379
|
+
other_table: str | t.Type[TypedTable] | TypedTable | Table | T_subclass,
|
|
380
|
+
**kw: t.Unpack[FieldSettings],
|
|
161
381
|
) -> TypedField[int]:
|
|
162
382
|
"""
|
|
163
383
|
Pydal type is reference, Python type is int (id).
|
|
@@ -180,7 +400,7 @@ def ReferenceField(
|
|
|
180
400
|
Reference = ReferenceField
|
|
181
401
|
|
|
182
402
|
|
|
183
|
-
def ListStringField(**kw: Unpack[FieldSettings]) -> TypedField[list[str]]:
|
|
403
|
+
def ListStringField(**kw: t.Unpack[FieldSettings]) -> TypedField[list[str]]:
|
|
184
404
|
"""
|
|
185
405
|
Pydal type is list:string, Python type is list of str.
|
|
186
406
|
"""
|
|
@@ -191,7 +411,7 @@ def ListStringField(**kw: Unpack[FieldSettings]) -> TypedField[list[str]]:
|
|
|
191
411
|
ListString = ListStringField
|
|
192
412
|
|
|
193
413
|
|
|
194
|
-
def ListIntegerField(**kw: Unpack[FieldSettings]) -> TypedField[list[int]]:
|
|
414
|
+
def ListIntegerField(**kw: t.Unpack[FieldSettings]) -> TypedField[list[int]]:
|
|
195
415
|
"""
|
|
196
416
|
Pydal type is list:integer, Python type is list of int.
|
|
197
417
|
"""
|
|
@@ -202,7 +422,7 @@ def ListIntegerField(**kw: Unpack[FieldSettings]) -> TypedField[list[int]]:
|
|
|
202
422
|
ListInteger = ListIntegerField
|
|
203
423
|
|
|
204
424
|
|
|
205
|
-
def ListReferenceField(other_table: str, **kw: Unpack[FieldSettings]) -> TypedField[list[int]]:
|
|
425
|
+
def ListReferenceField(other_table: str, **kw: t.Unpack[FieldSettings]) -> TypedField[list[int]]:
|
|
206
426
|
"""
|
|
207
427
|
Pydal type is list:reference, Python type is list of int (id).
|
|
208
428
|
"""
|
|
@@ -213,7 +433,7 @@ def ListReferenceField(other_table: str, **kw: Unpack[FieldSettings]) -> TypedFi
|
|
|
213
433
|
ListReference = ListReferenceField
|
|
214
434
|
|
|
215
435
|
|
|
216
|
-
def JSONField(**kw: Unpack[FieldSettings]) -> TypedField[object]:
|
|
436
|
+
def JSONField(**kw: t.Unpack[FieldSettings]) -> TypedField[object]:
|
|
217
437
|
"""
|
|
218
438
|
Pydal type is json, Python type is object (can be anything JSON-encodable).
|
|
219
439
|
"""
|
|
@@ -221,7 +441,7 @@ def JSONField(**kw: Unpack[FieldSettings]) -> TypedField[object]:
|
|
|
221
441
|
return TypedField(object, **kw)
|
|
222
442
|
|
|
223
443
|
|
|
224
|
-
def BigintField(**kw: Unpack[FieldSettings]) -> TypedField[int]:
|
|
444
|
+
def BigintField(**kw: t.Unpack[FieldSettings]) -> TypedField[int]:
|
|
225
445
|
"""
|
|
226
446
|
Pydal type is bigint, Python type is int.
|
|
227
447
|
"""
|
|
@@ -241,7 +461,7 @@ NativeTimestampField = SQLCustomType(
|
|
|
241
461
|
)
|
|
242
462
|
|
|
243
463
|
|
|
244
|
-
def TimestampField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
464
|
+
def TimestampField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
245
465
|
"""
|
|
246
466
|
Database type is timestamp, Python type is datetime.
|
|
247
467
|
|
|
@@ -256,18 +476,50 @@ def TimestampField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
|
256
476
|
)
|
|
257
477
|
|
|
258
478
|
|
|
259
|
-
def safe_decode_native_point(value: str | None):
|
|
479
|
+
def safe_decode_native_point(value: str | None) -> tuple[float, ...]:
|
|
480
|
+
"""
|
|
481
|
+
Safely decode a string into a tuple of floats.
|
|
482
|
+
|
|
483
|
+
The function attempts to parse the input string using `ast.literal_eval`.
|
|
484
|
+
If the parsing is successful, the function casts the parsed value to a tuple of floats and returns it.
|
|
485
|
+
Otherwise, the function returns an empty tuple.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
value: The string to decode.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
A tuple of floats.
|
|
492
|
+
"""
|
|
260
493
|
if not value:
|
|
261
494
|
return ()
|
|
262
495
|
|
|
263
496
|
try:
|
|
264
|
-
|
|
497
|
+
parsed = ast.literal_eval(value)
|
|
498
|
+
return t.cast(tuple[float, ...], parsed)
|
|
265
499
|
except ValueError: # pragma: no cover
|
|
266
500
|
# should not happen when inserted with `safe_encode_native_point` but you never know
|
|
267
501
|
return ()
|
|
268
502
|
|
|
269
503
|
|
|
270
|
-
def safe_encode_native_point(value: tuple[str, str] | str) -> str:
|
|
504
|
+
def safe_encode_native_point(value: tuple[str, str] | tuple[float, float] | str) -> str:
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
Safe encodes a point value.
|
|
508
|
+
|
|
509
|
+
The function takes a point value as input.
|
|
510
|
+
It can be a string in the format "x,y" or a tuple of two numbers.
|
|
511
|
+
The function converts the string to a tuple if necessary, validates the tuple,
|
|
512
|
+
and formats it into the expected string format.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
value: The point value to be encoded.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
The encoded point value as a string in the format "x,y".
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
ValueError: If the input value is not a valid point.
|
|
522
|
+
"""
|
|
271
523
|
if not value:
|
|
272
524
|
return ""
|
|
273
525
|
|
|
@@ -276,13 +528,15 @@ def safe_encode_native_point(value: tuple[str, str] | str) -> str:
|
|
|
276
528
|
value = value.strip("() ")
|
|
277
529
|
if not value:
|
|
278
530
|
return ""
|
|
279
|
-
|
|
531
|
+
value_tup = tuple(float(x.strip()) for x in value.split(","))
|
|
532
|
+
else:
|
|
533
|
+
value_tup = value # type: ignore
|
|
280
534
|
|
|
281
535
|
# Validate and format
|
|
282
|
-
if len(
|
|
536
|
+
if len(value_tup) != 2:
|
|
283
537
|
raise ValueError("Point must have exactly 2 coordinates")
|
|
284
538
|
|
|
285
|
-
x, y =
|
|
539
|
+
x, y = value_tup
|
|
286
540
|
return f"({x},{y})"
|
|
287
541
|
|
|
288
542
|
|
|
@@ -294,7 +548,7 @@ NativePointField = SQLCustomType(
|
|
|
294
548
|
)
|
|
295
549
|
|
|
296
550
|
|
|
297
|
-
def PointField(**kw: Unpack[FieldSettings]) -> TypedField[tuple[float, float]]:
|
|
551
|
+
def PointField(**kw: t.Unpack[FieldSettings]) -> TypedField[tuple[float, float]]:
|
|
298
552
|
"""
|
|
299
553
|
Database type is point, Python type is tuple[float, float].
|
|
300
554
|
"""
|
|
@@ -310,9 +564,14 @@ NativeUUIDField = SQLCustomType(
|
|
|
310
564
|
)
|
|
311
565
|
|
|
312
566
|
|
|
313
|
-
def UUIDField(**kw: Unpack[FieldSettings]) -> TypedField[uuid.UUID]:
|
|
567
|
+
def UUIDField(**kw: t.Unpack[FieldSettings]) -> TypedField[uuid.UUID]:
|
|
314
568
|
"""
|
|
315
569
|
Database type is uuid, Python type is UUID.
|
|
316
570
|
"""
|
|
317
571
|
kw["type"] = NativeUUIDField
|
|
318
572
|
return TypedField(uuid.UUID, **kw)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# note: import at the end to prevent circular imports:
|
|
576
|
+
from .define import TableDefinitionBuilder # noqa: E402
|
|
577
|
+
from .tables import TypedTable # noqa: E402
|