TypeDAL 3.17.3__py3-none-any.whl → 4.0.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.
Potentially problematic release.
This version of TypeDAL might be problematic. Click here for more details.
- typedal/__about__.py +1 -1
- typedal/__init__.py +9 -9
- typedal/caching.py +36 -33
- typedal/config.py +15 -16
- typedal/constants.py +25 -0
- typedal/core.py +176 -3168
- typedal/define.py +188 -0
- typedal/fields.py +254 -29
- typedal/for_web2py.py +1 -1
- typedal/helpers.py +268 -61
- typedal/mixins.py +21 -25
- typedal/query_builder.py +1059 -0
- typedal/relationships.py +264 -0
- typedal/rows.py +524 -0
- typedal/serializers/as_json.py +9 -10
- typedal/tables.py +1122 -0
- typedal/types.py +183 -177
- typedal/web2py_py4web_shared.py +1 -1
- {typedal-3.17.3.dist-info → typedal-4.0.0.dist-info}/METADATA +8 -7
- typedal-4.0.0.dist-info/RECORD +25 -0
- typedal-3.17.3.dist-info/RECORD +0 -19
- {typedal-3.17.3.dist-info → typedal-4.0.0.dist-info}/WHEEL +0 -0
- {typedal-3.17.3.dist-info → typedal-4.0.0.dist-info}/entry_points.txt +0 -0
typedal/relationships.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains base functionality related to Relationships.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import typing as t
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import pydal.objects
|
|
10
|
+
|
|
11
|
+
from .constants import JOIN_OPTIONS
|
|
12
|
+
from .core import TypeDAL
|
|
13
|
+
from .fields import TypedField
|
|
14
|
+
from .helpers import extract_type_optional, looks_like, unwrap_type
|
|
15
|
+
from .types import Condition, OnQuery, T_Field
|
|
16
|
+
|
|
17
|
+
To_Type = t.TypeVar("To_Type")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Relationship(t.Generic[To_Type]):
|
|
21
|
+
"""
|
|
22
|
+
Define a relationship to another table.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_type: t.Type[To_Type]
|
|
26
|
+
table: t.Type["TypedTable"] | type | str
|
|
27
|
+
condition: Condition
|
|
28
|
+
condition_and: Condition
|
|
29
|
+
on: OnQuery
|
|
30
|
+
multiple: bool
|
|
31
|
+
join: JOIN_OPTIONS
|
|
32
|
+
nested: dict[str, t.Self]
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
_type: t.Type[To_Type],
|
|
37
|
+
condition: Condition = None,
|
|
38
|
+
join: JOIN_OPTIONS = None,
|
|
39
|
+
on: OnQuery = None,
|
|
40
|
+
condition_and: Condition = None,
|
|
41
|
+
nested: dict[str, t.Self] = None,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Should not be called directly, use relationship() instead!
|
|
45
|
+
"""
|
|
46
|
+
if condition and on:
|
|
47
|
+
warnings.warn(f"Relation | Both specified! {condition=} {on=} {_type=}")
|
|
48
|
+
raise ValueError("Please specify either a condition or an 'on' statement for this relationship!")
|
|
49
|
+
|
|
50
|
+
self._type = _type
|
|
51
|
+
self.condition = condition
|
|
52
|
+
self.join = "left" if on else join # .on is always left join!
|
|
53
|
+
self.on = on
|
|
54
|
+
self.condition_and = condition_and
|
|
55
|
+
|
|
56
|
+
if args := t.get_args(_type):
|
|
57
|
+
self.table = unwrap_type(args[0])
|
|
58
|
+
self.multiple = True
|
|
59
|
+
else:
|
|
60
|
+
self.table = t.cast(type[TypedTable], _type)
|
|
61
|
+
self.multiple = False
|
|
62
|
+
|
|
63
|
+
if isinstance(self.table, str):
|
|
64
|
+
self.table = TypeDAL.to_snake(self.table)
|
|
65
|
+
|
|
66
|
+
self.nested = nested or {}
|
|
67
|
+
|
|
68
|
+
def clone(self, **update: t.Any) -> "Relationship[To_Type]":
|
|
69
|
+
"""
|
|
70
|
+
Create a copy of the relationship, possibly updated.
|
|
71
|
+
"""
|
|
72
|
+
return self.__class__(
|
|
73
|
+
update.get("_type") or self._type,
|
|
74
|
+
update.get("condition") or self.condition,
|
|
75
|
+
update.get("join") or self.join,
|
|
76
|
+
update.get("on") or self.on,
|
|
77
|
+
update.get("condition_and") or self.condition_and,
|
|
78
|
+
(self.nested | extra) if (extra := update.get("nested")) else self.nested, # type: ignore
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Representation of the relationship.
|
|
84
|
+
"""
|
|
85
|
+
if callback := self.condition or self.on:
|
|
86
|
+
src_code = inspect.getsource(callback).strip()
|
|
87
|
+
|
|
88
|
+
if c_and := self.condition_and:
|
|
89
|
+
and_code = inspect.getsource(c_and).strip()
|
|
90
|
+
src_code += " AND " + and_code
|
|
91
|
+
else:
|
|
92
|
+
cls_name = self._type if isinstance(self._type, str) else self._type.__name__
|
|
93
|
+
src_code = f"to {cls_name} (missing condition)"
|
|
94
|
+
|
|
95
|
+
join = f":{self.join}" if self.join else ""
|
|
96
|
+
return f"<Relationship{join} {src_code}>"
|
|
97
|
+
|
|
98
|
+
def get_table(self, db: "TypeDAL") -> t.Type["TypedTable"]:
|
|
99
|
+
"""
|
|
100
|
+
Get the table this relationship is bound to.
|
|
101
|
+
"""
|
|
102
|
+
table = self.table # can be a string because db wasn't available yet
|
|
103
|
+
|
|
104
|
+
if isinstance(table, str):
|
|
105
|
+
if mapped := db._class_map.get(table):
|
|
106
|
+
# yay
|
|
107
|
+
return mapped
|
|
108
|
+
|
|
109
|
+
# boo, fall back to untyped table but pretend it is typed:
|
|
110
|
+
return t.cast(t.Type["TypedTable"], db[table]) # eh close enough!
|
|
111
|
+
|
|
112
|
+
return table
|
|
113
|
+
|
|
114
|
+
def get_table_name(self) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Get the name of the table this relationship is bound to.
|
|
117
|
+
"""
|
|
118
|
+
if isinstance(self.table, str):
|
|
119
|
+
return self.table
|
|
120
|
+
|
|
121
|
+
if isinstance(self.table, pydal.objects.Table):
|
|
122
|
+
return str(self.table)
|
|
123
|
+
|
|
124
|
+
# else: typed table
|
|
125
|
+
try:
|
|
126
|
+
table = self.table._ensure_table_defined() if issubclass(self.table, TypedTable) else self.table
|
|
127
|
+
except Exception: # pragma: no cover
|
|
128
|
+
table = self.table
|
|
129
|
+
|
|
130
|
+
return str(table)
|
|
131
|
+
|
|
132
|
+
def __get__(self, instance: t.Any, owner: t.Any) -> "t.Optional[list[t.Any]] | Relationship[To_Type]":
|
|
133
|
+
"""
|
|
134
|
+
Relationship is a descriptor class, which can be returned from a class but not an instance.
|
|
135
|
+
|
|
136
|
+
For an instance, using .join() will replace the Relationship with the actual data.
|
|
137
|
+
If you forgot to join, a warning will be shown and empty data will be returned.
|
|
138
|
+
"""
|
|
139
|
+
if not instance:
|
|
140
|
+
# relationship queried on class, that's allowed
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
warnings.warn(
|
|
144
|
+
"Trying to get data from a relationship object! Did you forget to join it?",
|
|
145
|
+
category=RuntimeWarning,
|
|
146
|
+
)
|
|
147
|
+
if self.multiple:
|
|
148
|
+
return []
|
|
149
|
+
else:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def relationship(
|
|
154
|
+
_type: t.Type[To_Type],
|
|
155
|
+
condition: Condition = None,
|
|
156
|
+
join: JOIN_OPTIONS = None,
|
|
157
|
+
on: OnQuery = None,
|
|
158
|
+
) -> To_Type:
|
|
159
|
+
"""
|
|
160
|
+
Define a relationship to another table, when its id is not stored in the current table.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
class User(TypedTable):
|
|
164
|
+
name: str
|
|
165
|
+
|
|
166
|
+
posts = relationship(list["Post"], condition=lambda self, post: self.id == post.author, join='left')
|
|
167
|
+
|
|
168
|
+
class Post(TypedTable):
|
|
169
|
+
title: str
|
|
170
|
+
author: User
|
|
171
|
+
|
|
172
|
+
User.join("posts").first() # User instance with list[Post] in .posts
|
|
173
|
+
|
|
174
|
+
Here, Post stores the User ID, but `relationship(list["Post"])` still allows you to get the user's posts.
|
|
175
|
+
In this case, the join strategy is set to LEFT so users without posts are also still selected.
|
|
176
|
+
|
|
177
|
+
For complex queries with a pivot table, a `on` can be set insteaad of `condition`:
|
|
178
|
+
class User(TypedTable):
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
tags = relationship(list["Tag"], on=lambda self, tag: [
|
|
182
|
+
Tagged.on(Tagged.entity == entity.gid),
|
|
183
|
+
Tag.on((Tagged.tag == tag.id)),
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
If you'd try to capture this in a single 'condition', pydal would create a cross join which is much less efficient.
|
|
187
|
+
"""
|
|
188
|
+
return t.cast(
|
|
189
|
+
# note: The descriptor `Relationship[To_Type]` is more correct, but pycharm doesn't really get that.
|
|
190
|
+
# so for ease of use, just cast to the refered type for now!
|
|
191
|
+
# e.g. x = relationship(Author) -> x: Author
|
|
192
|
+
To_Type,
|
|
193
|
+
Relationship(_type, condition, join, on),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _generate_relationship_condition(_: t.Type["TypedTable"], key: str, field: T_Field) -> Condition:
|
|
198
|
+
origin = t.get_origin(field)
|
|
199
|
+
# else: generic
|
|
200
|
+
|
|
201
|
+
if origin is list:
|
|
202
|
+
# field = typing.get_args(field)[0] # actual field
|
|
203
|
+
# return lambda _self, _other: cls[key].contains(field)
|
|
204
|
+
|
|
205
|
+
return lambda _self, _other: _self[key].contains(_other.id)
|
|
206
|
+
else:
|
|
207
|
+
# normal reference
|
|
208
|
+
# return lambda _self, _other: cls[key] == field.id
|
|
209
|
+
return lambda _self, _other: _self[key] == _other.id
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def to_relationship(
|
|
213
|
+
cls: t.Type["TypedTable"] | type[t.Any],
|
|
214
|
+
key: str,
|
|
215
|
+
field: T_Field,
|
|
216
|
+
) -> t.Optional[Relationship[t.Any]]:
|
|
217
|
+
"""
|
|
218
|
+
Used to automatically create relationship instance for reference fields.
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
class MyTable(TypedTable):
|
|
222
|
+
reference: OtherTable
|
|
223
|
+
|
|
224
|
+
`reference` contains the id of an Other Table row.
|
|
225
|
+
MyTable.relationships should have 'reference' as a relationship, so `MyTable.join('reference')` should work.
|
|
226
|
+
|
|
227
|
+
This function will automatically perform this logic (called in db.define):
|
|
228
|
+
to_relationship(MyTable, 'reference', OtherTable) -> Relationship[OtherTable]
|
|
229
|
+
|
|
230
|
+
Also works for list:reference (list[OtherTable]) and TypedField[OtherTable].
|
|
231
|
+
"""
|
|
232
|
+
if looks_like(field, TypedField):
|
|
233
|
+
# typing.get_args works for list[str] but not for TypedField[role] :(
|
|
234
|
+
if args := t.get_args(field):
|
|
235
|
+
# TypedField[SomeType] -> SomeType
|
|
236
|
+
field = args[0]
|
|
237
|
+
elif hasattr(field, "_type"):
|
|
238
|
+
# TypedField(SomeType) -> SomeType
|
|
239
|
+
field = t.cast(T_Field, field._type)
|
|
240
|
+
else: # pragma: no cover
|
|
241
|
+
# weird
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
field, optional = extract_type_optional(field)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
condition = _generate_relationship_condition(cls, key, field)
|
|
248
|
+
except Exception as e: # pragma: no cover
|
|
249
|
+
warnings.warn("Could not generate Relationship condition", source=e)
|
|
250
|
+
condition = None
|
|
251
|
+
|
|
252
|
+
if not condition: # pragma: no cover
|
|
253
|
+
# something went wrong, not a valid relationship
|
|
254
|
+
warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}")
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
join = "left" if optional or t.get_origin(field) is list else "inner"
|
|
258
|
+
|
|
259
|
+
return Relationship(t.cast(type[TypedTable], field), condition, t.cast(JOIN_OPTIONS, join))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# note: these imports exist at the bottom of this file to prevent circular import issues:
|
|
263
|
+
|
|
264
|
+
from .tables import TypedTable # noqa: E402
|