TypeDAL 3.17.2__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.

@@ -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