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/tables.py
ADDED
|
@@ -0,0 +1,1131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains base functionality related to Tables.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import csv
|
|
9
|
+
import functools
|
|
10
|
+
import json
|
|
11
|
+
import typing as t
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
import pydal.objects
|
|
15
|
+
from pydal._globals import DEFAULT
|
|
16
|
+
|
|
17
|
+
from .constants import JOIN_OPTIONS
|
|
18
|
+
from .core import TypeDAL
|
|
19
|
+
from .helpers import classproperty, throw
|
|
20
|
+
from .serializers import as_json
|
|
21
|
+
from .types import (
|
|
22
|
+
AnyDict,
|
|
23
|
+
Condition,
|
|
24
|
+
Expression,
|
|
25
|
+
Field,
|
|
26
|
+
OnQuery,
|
|
27
|
+
OpRow,
|
|
28
|
+
OrderBy,
|
|
29
|
+
P,
|
|
30
|
+
Query,
|
|
31
|
+
R,
|
|
32
|
+
Reference,
|
|
33
|
+
Row,
|
|
34
|
+
SelectKwargs,
|
|
35
|
+
Set,
|
|
36
|
+
T,
|
|
37
|
+
T_MetaInstance,
|
|
38
|
+
T_Query,
|
|
39
|
+
Table,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if t.TYPE_CHECKING:
|
|
43
|
+
from .relationships import Relationship
|
|
44
|
+
from .rows import PaginatedRows, TypedRows
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def reorder_fields(
|
|
48
|
+
table: pydal.objects.Table,
|
|
49
|
+
fields: t.Iterable[str | TypedField[t.Any] | Field],
|
|
50
|
+
keep_others: bool = True,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Reorder fields of a pydal table.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
table: The pydal table object (e.g., db.mytable).
|
|
57
|
+
fields: List of field names (str) or Field objects in desired order.
|
|
58
|
+
keep_others (bool):
|
|
59
|
+
- True (default): keep other fields at the end, in their original order.
|
|
60
|
+
- False: remove other fields (only keep what's specified).
|
|
61
|
+
"""
|
|
62
|
+
# Normalize input to field names
|
|
63
|
+
desired = [f.name if isinstance(f, (TypedField, Field, pydal.objects.Field)) else str(f) for f in fields]
|
|
64
|
+
|
|
65
|
+
new_order = [f for f in desired if f in table._fields]
|
|
66
|
+
|
|
67
|
+
if keep_others:
|
|
68
|
+
# Start with desired fields, then append the rest
|
|
69
|
+
new_order.extend(f for f in table._fields if f not in desired)
|
|
70
|
+
|
|
71
|
+
table._fields = new_order
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TableMeta(type):
|
|
75
|
+
"""
|
|
76
|
+
This metaclass contains functionality on table classes, that doesn't exist on its instances.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
class MyTable(TypedTable):
|
|
80
|
+
some_field: TypedField[int]
|
|
81
|
+
|
|
82
|
+
MyTable.update_or_insert(...) # should work
|
|
83
|
+
|
|
84
|
+
MyTable.some_field # -> Field, can be used to query etc.
|
|
85
|
+
|
|
86
|
+
row = MyTable.first() # returns instance of MyTable
|
|
87
|
+
|
|
88
|
+
# row.update_or_insert(...) # shouldn't work!
|
|
89
|
+
|
|
90
|
+
row.some_field # -> int, with actual data
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# set up by db.define:
|
|
95
|
+
# _db: TypeDAL | None = None
|
|
96
|
+
# _table: Table | None = None
|
|
97
|
+
_db: TypeDAL | None = None
|
|
98
|
+
_table: Table | None = None
|
|
99
|
+
_relationships: dict[str, Relationship[t.Any]] | None = None
|
|
100
|
+
|
|
101
|
+
#########################
|
|
102
|
+
# TypeDAL custom logic: #
|
|
103
|
+
#########################
|
|
104
|
+
|
|
105
|
+
def __set_internals__(self, db: pydal.DAL, table: Table, relationships: dict[str, Relationship[t.Any]]) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Store the related database and pydal table for later usage.
|
|
108
|
+
"""
|
|
109
|
+
self._db = db
|
|
110
|
+
self._table = table
|
|
111
|
+
self._relationships = relationships
|
|
112
|
+
|
|
113
|
+
def __getattr__(self, col: str) -> t.Optional[Field]:
|
|
114
|
+
"""
|
|
115
|
+
Magic method used by TypedTableMeta to get a database field with dot notation on a class.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
SomeTypedTable.col -> db.table.col (via TypedTableMeta.__getattr__)
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
if self._table:
|
|
122
|
+
return getattr(self._table, col, None)
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def _ensure_table_defined(self) -> Table:
|
|
127
|
+
if not self._table:
|
|
128
|
+
raise EnvironmentError("@define or db.define is not called on this class yet!")
|
|
129
|
+
return self._table
|
|
130
|
+
|
|
131
|
+
def __iter__(self) -> t.Generator[Field, None, None]:
|
|
132
|
+
"""
|
|
133
|
+
Loop through the columns of this model.
|
|
134
|
+
"""
|
|
135
|
+
table = self._ensure_table_defined()
|
|
136
|
+
yield from iter(table)
|
|
137
|
+
|
|
138
|
+
def __getitem__(self, item: str) -> Field:
|
|
139
|
+
"""
|
|
140
|
+
Allow dict notation to get a column of this table (-> Field instance).
|
|
141
|
+
"""
|
|
142
|
+
table = self._ensure_table_defined()
|
|
143
|
+
return table[item]
|
|
144
|
+
|
|
145
|
+
def __str__(self) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Normally, just returns the underlying table name, but with a fallback if the model is unbound.
|
|
148
|
+
"""
|
|
149
|
+
if self._table:
|
|
150
|
+
return str(self._table)
|
|
151
|
+
else:
|
|
152
|
+
return f"<unbound table {self.__name__}>"
|
|
153
|
+
|
|
154
|
+
def from_row(self: t.Type[T_MetaInstance], row: pydal.objects.Row) -> T_MetaInstance:
|
|
155
|
+
"""
|
|
156
|
+
Create a model instance from a pydal row.
|
|
157
|
+
"""
|
|
158
|
+
return self(row)
|
|
159
|
+
|
|
160
|
+
def all(self: t.Type[T_MetaInstance]) -> "TypedRows[T_MetaInstance]":
|
|
161
|
+
"""
|
|
162
|
+
Return all rows for this model.
|
|
163
|
+
"""
|
|
164
|
+
return self.collect()
|
|
165
|
+
|
|
166
|
+
def get_relationships(self) -> dict[str, Relationship[t.Any]]:
|
|
167
|
+
"""
|
|
168
|
+
Return the registered relationships of the current model.
|
|
169
|
+
"""
|
|
170
|
+
return self._relationships or {}
|
|
171
|
+
|
|
172
|
+
##########################
|
|
173
|
+
# TypeDAL Modified Logic #
|
|
174
|
+
##########################
|
|
175
|
+
|
|
176
|
+
def insert(self: t.Type[T_MetaInstance], **fields: t.Any) -> T_MetaInstance:
|
|
177
|
+
"""
|
|
178
|
+
This is only called when db.define is not used as a decorator.
|
|
179
|
+
|
|
180
|
+
cls.__table functions as 'self'
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
**fields: t.Anything you want to insert in the database
|
|
184
|
+
|
|
185
|
+
Returns: the ID of the new row.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
table = self._ensure_table_defined()
|
|
189
|
+
|
|
190
|
+
result = table.insert(**fields)
|
|
191
|
+
# it already is an int but mypy doesn't understand that
|
|
192
|
+
return self(result)
|
|
193
|
+
|
|
194
|
+
def _insert(self, **fields: t.Any) -> str:
|
|
195
|
+
table = self._ensure_table_defined()
|
|
196
|
+
|
|
197
|
+
return str(table._insert(**fields))
|
|
198
|
+
|
|
199
|
+
def bulk_insert(self: t.Type[T_MetaInstance], items: list[AnyDict]) -> "TypedRows[T_MetaInstance]":
|
|
200
|
+
"""
|
|
201
|
+
Insert multiple rows, returns a TypedRows set of new instances.
|
|
202
|
+
"""
|
|
203
|
+
table = self._ensure_table_defined()
|
|
204
|
+
result = table.bulk_insert(items)
|
|
205
|
+
return self.where(lambda row: row.id.belongs(result)).collect()
|
|
206
|
+
|
|
207
|
+
def update_or_insert(
|
|
208
|
+
self: t.Type[T_MetaInstance],
|
|
209
|
+
query: T_Query | AnyDict = DEFAULT,
|
|
210
|
+
**values: t.Any,
|
|
211
|
+
) -> T_MetaInstance:
|
|
212
|
+
"""
|
|
213
|
+
Update a row if query matches, else insert a new one.
|
|
214
|
+
|
|
215
|
+
Returns the created or updated instance.
|
|
216
|
+
"""
|
|
217
|
+
table = self._ensure_table_defined()
|
|
218
|
+
|
|
219
|
+
if query is DEFAULT:
|
|
220
|
+
record = table(**values)
|
|
221
|
+
elif isinstance(query, dict):
|
|
222
|
+
record = table(**query)
|
|
223
|
+
else:
|
|
224
|
+
record = table(query)
|
|
225
|
+
|
|
226
|
+
if not record:
|
|
227
|
+
return self.insert(**values)
|
|
228
|
+
|
|
229
|
+
record.update_record(**values)
|
|
230
|
+
return self(record)
|
|
231
|
+
|
|
232
|
+
def validate_and_insert(
|
|
233
|
+
self: t.Type[T_MetaInstance],
|
|
234
|
+
**fields: t.Any,
|
|
235
|
+
) -> tuple[t.Optional[T_MetaInstance], t.Optional[dict[str, str]]]:
|
|
236
|
+
"""
|
|
237
|
+
Validate input data and then insert a row.
|
|
238
|
+
|
|
239
|
+
Returns a tuple of (the created instance, a dict of errors).
|
|
240
|
+
"""
|
|
241
|
+
table = self._ensure_table_defined()
|
|
242
|
+
result = table.validate_and_insert(**fields)
|
|
243
|
+
if row_id := result.get("id"):
|
|
244
|
+
return self(row_id), None
|
|
245
|
+
else:
|
|
246
|
+
return None, result.get("errors")
|
|
247
|
+
|
|
248
|
+
def validate_and_update(
|
|
249
|
+
self: t.Type[T_MetaInstance],
|
|
250
|
+
query: Query,
|
|
251
|
+
**fields: t.Any,
|
|
252
|
+
) -> tuple[t.Optional[T_MetaInstance], t.Optional[dict[str, str]]]:
|
|
253
|
+
"""
|
|
254
|
+
Validate input data and then update max 1 row.
|
|
255
|
+
|
|
256
|
+
Returns a tuple of (the updated instance, a dict of errors).
|
|
257
|
+
"""
|
|
258
|
+
table = self._ensure_table_defined()
|
|
259
|
+
|
|
260
|
+
result = table.validate_and_update(query, **fields)
|
|
261
|
+
|
|
262
|
+
if errors := result.get("errors"):
|
|
263
|
+
return None, errors
|
|
264
|
+
elif row_id := result.get("id"):
|
|
265
|
+
return self(row_id), None
|
|
266
|
+
else: # pragma: no cover
|
|
267
|
+
# update on query without result (shouldnt happen)
|
|
268
|
+
return None, None
|
|
269
|
+
|
|
270
|
+
def validate_and_update_or_insert(
|
|
271
|
+
self: t.Type[T_MetaInstance],
|
|
272
|
+
query: Query,
|
|
273
|
+
**fields: t.Any,
|
|
274
|
+
) -> tuple[t.Optional[T_MetaInstance], t.Optional[dict[str, str]]]:
|
|
275
|
+
"""
|
|
276
|
+
Validate input data and then update_and_insert (on max 1 row).
|
|
277
|
+
|
|
278
|
+
Returns a tuple of (the updated/created instance, a dict of errors).
|
|
279
|
+
"""
|
|
280
|
+
table = self._ensure_table_defined()
|
|
281
|
+
result = table.validate_and_update_or_insert(query, **fields)
|
|
282
|
+
|
|
283
|
+
if errors := result.get("errors"):
|
|
284
|
+
return None, errors
|
|
285
|
+
elif row_id := result.get("id"):
|
|
286
|
+
return self(row_id), None
|
|
287
|
+
else: # pragma: no cover
|
|
288
|
+
# update on query without result (shouldnt happen)
|
|
289
|
+
return None, None
|
|
290
|
+
|
|
291
|
+
def select(self: t.Type[T_MetaInstance], *a: t.Any, **kw: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
292
|
+
"""
|
|
293
|
+
See QueryBuilder.select!
|
|
294
|
+
"""
|
|
295
|
+
return QueryBuilder(self).select(*a, **kw)
|
|
296
|
+
|
|
297
|
+
def column(self: t.Type[T_MetaInstance], field: T | TypedField[T], **options: t.Unpack[SelectKwargs]) -> list[T]:
|
|
298
|
+
"""
|
|
299
|
+
Get all values in a specific column.
|
|
300
|
+
|
|
301
|
+
Shortcut for `.select(field).execute().column(field)`.
|
|
302
|
+
"""
|
|
303
|
+
return QueryBuilder(self).select(field, **options).execute().column(field)
|
|
304
|
+
|
|
305
|
+
def paginate(self: t.Type[T_MetaInstance], limit: int, page: int = 1) -> "PaginatedRows[T_MetaInstance]":
|
|
306
|
+
"""
|
|
307
|
+
See QueryBuilder.paginate!
|
|
308
|
+
"""
|
|
309
|
+
return QueryBuilder(self).paginate(limit=limit, page=page)
|
|
310
|
+
|
|
311
|
+
def chunk(self: t.Type[T_MetaInstance], chunk_size: int) -> t.Generator["TypedRows[T_MetaInstance]", t.Any, None]:
|
|
312
|
+
"""
|
|
313
|
+
See QueryBuilder.chunk!
|
|
314
|
+
"""
|
|
315
|
+
return QueryBuilder(self).chunk(chunk_size)
|
|
316
|
+
|
|
317
|
+
def where(self: t.Type[T_MetaInstance], *a: t.Any, **kw: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
318
|
+
"""
|
|
319
|
+
See QueryBuilder.where!
|
|
320
|
+
"""
|
|
321
|
+
return QueryBuilder(self).where(*a, **kw)
|
|
322
|
+
|
|
323
|
+
def orderby(self: t.Type[T_MetaInstance], *fields: OrderBy) -> "QueryBuilder[T_MetaInstance]":
|
|
324
|
+
"""
|
|
325
|
+
See QueryBuilder.orderby!
|
|
326
|
+
"""
|
|
327
|
+
return QueryBuilder(self).orderby(*fields)
|
|
328
|
+
|
|
329
|
+
def cache(self: t.Type[T_MetaInstance], *deps: t.Any, **kwargs: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
330
|
+
"""
|
|
331
|
+
See QueryBuilder.cache!
|
|
332
|
+
"""
|
|
333
|
+
return QueryBuilder(self).cache(*deps, **kwargs)
|
|
334
|
+
|
|
335
|
+
def count(self: t.Type[T_MetaInstance]) -> int:
|
|
336
|
+
"""
|
|
337
|
+
See QueryBuilder.count!
|
|
338
|
+
"""
|
|
339
|
+
return QueryBuilder(self).count()
|
|
340
|
+
|
|
341
|
+
def exists(self: t.Type[T_MetaInstance]) -> bool:
|
|
342
|
+
"""
|
|
343
|
+
See QueryBuilder.exists!
|
|
344
|
+
"""
|
|
345
|
+
return QueryBuilder(self).exists()
|
|
346
|
+
|
|
347
|
+
def first(self: t.Type[T_MetaInstance]) -> T_MetaInstance | None:
|
|
348
|
+
"""
|
|
349
|
+
See QueryBuilder.first!
|
|
350
|
+
"""
|
|
351
|
+
return QueryBuilder(self).first()
|
|
352
|
+
|
|
353
|
+
def first_or_fail(self: t.Type[T_MetaInstance]) -> T_MetaInstance:
|
|
354
|
+
"""
|
|
355
|
+
See QueryBuilder.first_or_fail!
|
|
356
|
+
"""
|
|
357
|
+
return QueryBuilder(self).first_or_fail()
|
|
358
|
+
|
|
359
|
+
def join(
|
|
360
|
+
self: t.Type[T_MetaInstance],
|
|
361
|
+
*fields: str | t.Type[TypedTable] | Relationship[t.Any],
|
|
362
|
+
method: JOIN_OPTIONS = None,
|
|
363
|
+
on: OnQuery | list[Expression] | Expression = None,
|
|
364
|
+
condition: Condition = None,
|
|
365
|
+
condition_and: Condition = None,
|
|
366
|
+
) -> "QueryBuilder[T_MetaInstance]":
|
|
367
|
+
"""
|
|
368
|
+
See QueryBuilder.join!
|
|
369
|
+
"""
|
|
370
|
+
return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
|
|
371
|
+
|
|
372
|
+
def collect(self: t.Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
|
|
373
|
+
"""
|
|
374
|
+
See QueryBuilder.collect!
|
|
375
|
+
"""
|
|
376
|
+
return QueryBuilder(self).collect(verbose=verbose)
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def ALL(cls) -> pydal.objects.SQLALL:
|
|
380
|
+
"""
|
|
381
|
+
Select all fields for this table.
|
|
382
|
+
"""
|
|
383
|
+
table = cls._ensure_table_defined()
|
|
384
|
+
|
|
385
|
+
return table.ALL
|
|
386
|
+
|
|
387
|
+
##########################
|
|
388
|
+
# TypeDAL Shadowed Logic #
|
|
389
|
+
##########################
|
|
390
|
+
fields: list[str]
|
|
391
|
+
|
|
392
|
+
# other table methods:
|
|
393
|
+
|
|
394
|
+
def truncate(self, mode: str = "") -> None:
|
|
395
|
+
"""
|
|
396
|
+
Remove all data and reset index.
|
|
397
|
+
"""
|
|
398
|
+
table = self._ensure_table_defined()
|
|
399
|
+
table.truncate(mode)
|
|
400
|
+
|
|
401
|
+
def drop(self, mode: str = "") -> None:
|
|
402
|
+
"""
|
|
403
|
+
Remove the underlying table.
|
|
404
|
+
"""
|
|
405
|
+
table = self._ensure_table_defined()
|
|
406
|
+
table.drop(mode)
|
|
407
|
+
|
|
408
|
+
def create_index(self, name: str, *fields: str | Field, **kwargs: t.Any) -> bool:
|
|
409
|
+
"""
|
|
410
|
+
Add an index on some columns of this table.
|
|
411
|
+
"""
|
|
412
|
+
table = self._ensure_table_defined()
|
|
413
|
+
result = table.create_index(name, *fields, **kwargs)
|
|
414
|
+
return t.cast(bool, result)
|
|
415
|
+
|
|
416
|
+
def drop_index(self, name: str, if_exists: bool = False) -> bool:
|
|
417
|
+
"""
|
|
418
|
+
Remove an index from this table.
|
|
419
|
+
"""
|
|
420
|
+
table = self._ensure_table_defined()
|
|
421
|
+
result = table.drop_index(name, if_exists)
|
|
422
|
+
return t.cast(bool, result)
|
|
423
|
+
|
|
424
|
+
def import_from_csv_file(
|
|
425
|
+
self,
|
|
426
|
+
csvfile: t.TextIO,
|
|
427
|
+
id_map: dict[str, str] = None,
|
|
428
|
+
null: t.Any = "<NULL>",
|
|
429
|
+
unique: str = "uuid",
|
|
430
|
+
id_offset: dict[str, int] = None, # id_offset used only when id_map is None
|
|
431
|
+
transform: t.Callable[[dict[t.Any, t.Any]], dict[t.Any, t.Any]] = None,
|
|
432
|
+
validate: bool = False,
|
|
433
|
+
encoding: str = "utf-8",
|
|
434
|
+
delimiter: str = ",",
|
|
435
|
+
quotechar: str = '"',
|
|
436
|
+
quoting: int = csv.QUOTE_MINIMAL,
|
|
437
|
+
restore: bool = False,
|
|
438
|
+
**kwargs: t.Any,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""
|
|
441
|
+
Load a csv file into the database.
|
|
442
|
+
"""
|
|
443
|
+
table = self._ensure_table_defined()
|
|
444
|
+
table.import_from_csv_file(
|
|
445
|
+
csvfile,
|
|
446
|
+
id_map=id_map,
|
|
447
|
+
null=null,
|
|
448
|
+
unique=unique,
|
|
449
|
+
id_offset=id_offset,
|
|
450
|
+
transform=transform,
|
|
451
|
+
validate=validate,
|
|
452
|
+
encoding=encoding,
|
|
453
|
+
delimiter=delimiter,
|
|
454
|
+
quotechar=quotechar,
|
|
455
|
+
quoting=quoting,
|
|
456
|
+
restore=restore,
|
|
457
|
+
**kwargs,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def on(self, query: bool | Query) -> Expression:
|
|
461
|
+
"""
|
|
462
|
+
Shadow Table.on.
|
|
463
|
+
|
|
464
|
+
Used for joins.
|
|
465
|
+
|
|
466
|
+
See Also:
|
|
467
|
+
http://web2py.com/books/default/chapter/29/06/the-database-abstraction-layer?search=export_to_csv_file#One-to-mt.Any-relation
|
|
468
|
+
"""
|
|
469
|
+
table = self._ensure_table_defined()
|
|
470
|
+
return t.cast(Expression, table.on(query))
|
|
471
|
+
|
|
472
|
+
def with_alias(self: t.Type[T_MetaInstance], alias: str) -> t.Type[T_MetaInstance]:
|
|
473
|
+
"""
|
|
474
|
+
Shadow Table.with_alias.
|
|
475
|
+
|
|
476
|
+
Useful for joins when joining the same table multiple times.
|
|
477
|
+
|
|
478
|
+
See Also:
|
|
479
|
+
http://web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#One-to-mt.Any-relation
|
|
480
|
+
"""
|
|
481
|
+
table = self._ensure_table_defined()
|
|
482
|
+
return t.cast(t.Type[T_MetaInstance], table.with_alias(alias))
|
|
483
|
+
|
|
484
|
+
def unique_alias(self: t.Type[T_MetaInstance]) -> t.Type[T_MetaInstance]:
|
|
485
|
+
"""
|
|
486
|
+
Generates a unique alias for this table.
|
|
487
|
+
|
|
488
|
+
Useful for joins when joining the same table multiple times
|
|
489
|
+
and you don't want to keep track of aliases yourself.
|
|
490
|
+
"""
|
|
491
|
+
key = f"{self.__name__.lower()}_{hash(uuid.uuid4())}"
|
|
492
|
+
return self.with_alias(key)
|
|
493
|
+
|
|
494
|
+
# hooks:
|
|
495
|
+
def _hook_once(
|
|
496
|
+
cls: t.Type[T_MetaInstance],
|
|
497
|
+
hooks: list[t.Callable[P, R]],
|
|
498
|
+
fn: t.Callable[P, R],
|
|
499
|
+
) -> t.Type[T_MetaInstance]:
|
|
500
|
+
@functools.wraps(fn)
|
|
501
|
+
def wraps(*a: P.args, **kw: P.kwargs) -> R:
|
|
502
|
+
try:
|
|
503
|
+
return fn(*a, **kw)
|
|
504
|
+
finally:
|
|
505
|
+
hooks.remove(wraps)
|
|
506
|
+
|
|
507
|
+
hooks.append(wraps)
|
|
508
|
+
return cls
|
|
509
|
+
|
|
510
|
+
def before_insert(
|
|
511
|
+
cls: t.Type[T_MetaInstance],
|
|
512
|
+
fn: t.Callable[[T_MetaInstance], t.Optional[bool]] | t.Callable[[OpRow], t.Optional[bool]],
|
|
513
|
+
) -> t.Type[T_MetaInstance]:
|
|
514
|
+
"""
|
|
515
|
+
Add a before insert hook.
|
|
516
|
+
"""
|
|
517
|
+
if fn not in cls._before_insert:
|
|
518
|
+
cls._before_insert.append(fn)
|
|
519
|
+
return cls
|
|
520
|
+
|
|
521
|
+
def before_insert_once(
|
|
522
|
+
cls: t.Type[T_MetaInstance],
|
|
523
|
+
fn: t.Callable[[T_MetaInstance], t.Optional[bool]] | t.Callable[[OpRow], t.Optional[bool]],
|
|
524
|
+
) -> t.Type[T_MetaInstance]:
|
|
525
|
+
"""
|
|
526
|
+
Add a before insert hook that only fires once and then removes itself.
|
|
527
|
+
"""
|
|
528
|
+
return cls._hook_once(cls._before_insert, fn) # type: ignore
|
|
529
|
+
|
|
530
|
+
def after_insert(
|
|
531
|
+
cls: t.Type[T_MetaInstance],
|
|
532
|
+
fn: (
|
|
533
|
+
t.Callable[[T_MetaInstance, Reference], t.Optional[bool]] | t.Callable[[OpRow, Reference], t.Optional[bool]]
|
|
534
|
+
),
|
|
535
|
+
) -> t.Type[T_MetaInstance]:
|
|
536
|
+
"""
|
|
537
|
+
Add an after insert hook.
|
|
538
|
+
"""
|
|
539
|
+
if fn not in cls._after_insert:
|
|
540
|
+
cls._after_insert.append(fn)
|
|
541
|
+
return cls
|
|
542
|
+
|
|
543
|
+
def after_insert_once(
|
|
544
|
+
cls: t.Type[T_MetaInstance],
|
|
545
|
+
fn: (
|
|
546
|
+
t.Callable[[T_MetaInstance, Reference], t.Optional[bool]] | t.Callable[[OpRow, Reference], t.Optional[bool]]
|
|
547
|
+
),
|
|
548
|
+
) -> t.Type[T_MetaInstance]:
|
|
549
|
+
"""
|
|
550
|
+
Add an after insert hook that only fires once and then removes itself.
|
|
551
|
+
"""
|
|
552
|
+
return cls._hook_once(cls._after_insert, fn) # type: ignore
|
|
553
|
+
|
|
554
|
+
def before_update(
|
|
555
|
+
cls: t.Type[T_MetaInstance],
|
|
556
|
+
fn: t.Callable[[Set, T_MetaInstance], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]],
|
|
557
|
+
) -> t.Type[T_MetaInstance]:
|
|
558
|
+
"""
|
|
559
|
+
Add a before update hook.
|
|
560
|
+
"""
|
|
561
|
+
if fn not in cls._before_update:
|
|
562
|
+
cls._before_update.append(fn)
|
|
563
|
+
return cls
|
|
564
|
+
|
|
565
|
+
def before_update_once(
|
|
566
|
+
cls,
|
|
567
|
+
fn: t.Callable[[Set, T_MetaInstance], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]],
|
|
568
|
+
) -> t.Type[T_MetaInstance]:
|
|
569
|
+
"""
|
|
570
|
+
Add a before update hook that only fires once and then removes itself.
|
|
571
|
+
"""
|
|
572
|
+
return cls._hook_once(cls._before_update, fn) # type: ignore
|
|
573
|
+
|
|
574
|
+
def after_update(
|
|
575
|
+
cls: t.Type[T_MetaInstance],
|
|
576
|
+
fn: t.Callable[[Set, T_MetaInstance], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]],
|
|
577
|
+
) -> t.Type[T_MetaInstance]:
|
|
578
|
+
"""
|
|
579
|
+
Add an after update hook.
|
|
580
|
+
"""
|
|
581
|
+
if fn not in cls._after_update:
|
|
582
|
+
cls._after_update.append(fn)
|
|
583
|
+
return cls
|
|
584
|
+
|
|
585
|
+
def after_update_once(
|
|
586
|
+
cls: t.Type[T_MetaInstance],
|
|
587
|
+
fn: t.Callable[[Set, T_MetaInstance], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]],
|
|
588
|
+
) -> t.Type[T_MetaInstance]:
|
|
589
|
+
"""
|
|
590
|
+
Add an after update hook that only fires once and then removes itself.
|
|
591
|
+
"""
|
|
592
|
+
return cls._hook_once(cls._after_update, fn) # type: ignore
|
|
593
|
+
|
|
594
|
+
def before_delete(cls: t.Type[T_MetaInstance], fn: t.Callable[[Set], t.Optional[bool]]) -> t.Type[T_MetaInstance]:
|
|
595
|
+
"""
|
|
596
|
+
Add a before delete hook.
|
|
597
|
+
"""
|
|
598
|
+
if fn not in cls._before_delete:
|
|
599
|
+
cls._before_delete.append(fn)
|
|
600
|
+
return cls
|
|
601
|
+
|
|
602
|
+
def before_delete_once(
|
|
603
|
+
cls: t.Type[T_MetaInstance],
|
|
604
|
+
fn: t.Callable[[Set], t.Optional[bool]],
|
|
605
|
+
) -> t.Type[T_MetaInstance]:
|
|
606
|
+
"""
|
|
607
|
+
Add a before delete hook that only fires once and then removes itself.
|
|
608
|
+
"""
|
|
609
|
+
return cls._hook_once(cls._before_delete, fn)
|
|
610
|
+
|
|
611
|
+
def after_delete(cls: t.Type[T_MetaInstance], fn: t.Callable[[Set], t.Optional[bool]]) -> t.Type[T_MetaInstance]:
|
|
612
|
+
"""
|
|
613
|
+
Add an after delete hook.
|
|
614
|
+
"""
|
|
615
|
+
if fn not in cls._after_delete:
|
|
616
|
+
cls._after_delete.append(fn)
|
|
617
|
+
return cls
|
|
618
|
+
|
|
619
|
+
def after_delete_once(
|
|
620
|
+
cls: t.Type[T_MetaInstance],
|
|
621
|
+
fn: t.Callable[[Set], t.Optional[bool]],
|
|
622
|
+
) -> t.Type[T_MetaInstance]:
|
|
623
|
+
"""
|
|
624
|
+
Add an after delete hook that only fires once and then removes itself.
|
|
625
|
+
"""
|
|
626
|
+
return cls._hook_once(cls._after_delete, fn)
|
|
627
|
+
|
|
628
|
+
def reorder_fields(cls, *fields: str | Field | TypedField[t.Any], keep_others: bool = True) -> None:
|
|
629
|
+
"""
|
|
630
|
+
Reorder fields of a typedal table.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
fields: List of field names (str) or Field objects in desired order.
|
|
634
|
+
keep_others (bool):
|
|
635
|
+
- True (default): keep other fields at the end, in their original order.
|
|
636
|
+
- False: remove other fields (only keep what's specified).
|
|
637
|
+
"""
|
|
638
|
+
return reorder_fields(cls._table, fields, keep_others=keep_others)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class _TypedTable:
|
|
642
|
+
"""
|
|
643
|
+
This class is a final shared parent between TypedTable and Mixins.
|
|
644
|
+
|
|
645
|
+
This needs to exist because otherwise the __on_define__ of Mixins are not executed.
|
|
646
|
+
Notably, this class exists at a level ABOVE the `metaclass=TableMeta`,
|
|
647
|
+
because otherwise typing gets confused when Mixins are used and multiple types could satisfy
|
|
648
|
+
generic 'T subclass of TypedTable'
|
|
649
|
+
-> Setting 'TypedTable' as the parent for Mixin does not work at runtime (and works semi at type check time)
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
id: "TypedField[int]"
|
|
653
|
+
|
|
654
|
+
_before_insert: list[t.Callable[[t.Self], t.Optional[bool]] | t.Callable[[OpRow], t.Optional[bool]]]
|
|
655
|
+
_after_insert: list[
|
|
656
|
+
t.Callable[[t.Self, Reference], t.Optional[bool]] | t.Callable[[OpRow, Reference], t.Optional[bool]]
|
|
657
|
+
]
|
|
658
|
+
_before_update: list[t.Callable[[Set, t.Self], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]]]
|
|
659
|
+
_after_update: list[t.Callable[[Set, t.Self], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]]]
|
|
660
|
+
_before_delete: list[t.Callable[[Set], t.Optional[bool]]]
|
|
661
|
+
_after_delete: list[t.Callable[[Set], t.Optional[bool]]]
|
|
662
|
+
|
|
663
|
+
@classmethod
|
|
664
|
+
def __on_define__(cls, db: TypeDAL) -> None:
|
|
665
|
+
"""
|
|
666
|
+
Method that can be implemented by tables to do an action after db.define is completed.
|
|
667
|
+
|
|
668
|
+
This can be useful if you need to add something like requires=IS_NOT_IN_DB(db, "table.field"),
|
|
669
|
+
where you need a reference to the current database, which may not exist yet when defining the model.
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
@classproperty
|
|
673
|
+
def _hooks(cls) -> dict[str, list[t.Callable[..., t.Optional[bool]]]]:
|
|
674
|
+
return {
|
|
675
|
+
"before_insert": cls._before_insert,
|
|
676
|
+
"after_insert": cls._after_insert,
|
|
677
|
+
"before_update": cls._before_update,
|
|
678
|
+
"after_update": cls._after_update,
|
|
679
|
+
"before_delete": cls._before_delete,
|
|
680
|
+
"after_delete": cls._after_delete,
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
685
|
+
"""
|
|
686
|
+
Enhanded modeling system on top of pydal's Table that adds typing and additional functionality.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
# set up by 'new':
|
|
690
|
+
_row: Row | None = None
|
|
691
|
+
_rows: tuple[Row, ...] = ()
|
|
692
|
+
|
|
693
|
+
_with: list[str]
|
|
694
|
+
|
|
695
|
+
def _setup_instance_methods(self) -> None:
|
|
696
|
+
self.as_dict = self._as_dict # type: ignore
|
|
697
|
+
self.__json__ = self.as_json = self._as_json # type: ignore
|
|
698
|
+
# self.as_yaml = self._as_yaml # type: ignore
|
|
699
|
+
self.as_xml = self._as_xml # type: ignore
|
|
700
|
+
|
|
701
|
+
self.update = self._update # type: ignore
|
|
702
|
+
|
|
703
|
+
self.delete_record = self._delete_record # type: ignore
|
|
704
|
+
self.update_record = self._update_record # type: ignore
|
|
705
|
+
|
|
706
|
+
def __new__(
|
|
707
|
+
cls,
|
|
708
|
+
row_or_id: t.Union[Row, Query, pydal.objects.Set, int, str, None, "TypedTable"] = None,
|
|
709
|
+
**filters: t.Any,
|
|
710
|
+
) -> t.Self:
|
|
711
|
+
"""
|
|
712
|
+
Create a Typed Rows model instance from an existing row, ID or query.
|
|
713
|
+
|
|
714
|
+
Examples:
|
|
715
|
+
MyTable(1)
|
|
716
|
+
MyTable(id=1)
|
|
717
|
+
MyTable(MyTable.id == 1)
|
|
718
|
+
"""
|
|
719
|
+
table = cls._ensure_table_defined()
|
|
720
|
+
inst = super().__new__(cls)
|
|
721
|
+
|
|
722
|
+
if isinstance(row_or_id, TypedTable):
|
|
723
|
+
# existing typed table instance!
|
|
724
|
+
return t.cast(t.Self, row_or_id)
|
|
725
|
+
|
|
726
|
+
elif isinstance(row_or_id, pydal.objects.Row):
|
|
727
|
+
row = row_or_id
|
|
728
|
+
elif row_or_id is not None:
|
|
729
|
+
row = table(row_or_id, **filters)
|
|
730
|
+
elif filters:
|
|
731
|
+
row = table(**filters)
|
|
732
|
+
else:
|
|
733
|
+
# dummy object
|
|
734
|
+
return inst
|
|
735
|
+
|
|
736
|
+
if not row:
|
|
737
|
+
return None # type: ignore
|
|
738
|
+
|
|
739
|
+
inst._row = row
|
|
740
|
+
|
|
741
|
+
if hasattr(row, "id"):
|
|
742
|
+
inst.__dict__.update(row)
|
|
743
|
+
else:
|
|
744
|
+
# deal with _extra (and possibly others?)
|
|
745
|
+
# Row <{actual: {}, _extra: ...}>
|
|
746
|
+
inst.__dict__.update(row[str(cls)])
|
|
747
|
+
|
|
748
|
+
inst._setup_instance_methods()
|
|
749
|
+
return inst
|
|
750
|
+
|
|
751
|
+
def __iter__(self) -> t.Generator[t.Any, None, None]:
|
|
752
|
+
"""
|
|
753
|
+
Allows looping through the columns.
|
|
754
|
+
"""
|
|
755
|
+
row = self._ensure_matching_row()
|
|
756
|
+
yield from iter(row)
|
|
757
|
+
|
|
758
|
+
def __getitem__(self, item: str) -> t.Any:
|
|
759
|
+
"""
|
|
760
|
+
Allows dictionary notation to get columns.
|
|
761
|
+
"""
|
|
762
|
+
if item in self.__dict__:
|
|
763
|
+
return self.__dict__.get(item)
|
|
764
|
+
|
|
765
|
+
# fallback to lookup in row
|
|
766
|
+
if self._row:
|
|
767
|
+
return self._row[item]
|
|
768
|
+
|
|
769
|
+
# nothing found!
|
|
770
|
+
raise KeyError(item)
|
|
771
|
+
|
|
772
|
+
def __getattr__(self, item: str) -> t.Any:
|
|
773
|
+
"""
|
|
774
|
+
Allows dot notation to get columns.
|
|
775
|
+
"""
|
|
776
|
+
if value := self.get(item):
|
|
777
|
+
return value
|
|
778
|
+
|
|
779
|
+
raise AttributeError(item)
|
|
780
|
+
|
|
781
|
+
def __eq__(self, other: t.Any) -> bool:
|
|
782
|
+
"""
|
|
783
|
+
Compare equal classes via their _row, since the other data is irrelevant for equality.
|
|
784
|
+
"""
|
|
785
|
+
if type(self) is not type(other):
|
|
786
|
+
return False
|
|
787
|
+
|
|
788
|
+
return self._row == other._row # type: ignore
|
|
789
|
+
|
|
790
|
+
def keys(self) -> list[str]:
|
|
791
|
+
"""
|
|
792
|
+
Return the combination of row + relationship keys.
|
|
793
|
+
|
|
794
|
+
Used by dict(row).
|
|
795
|
+
"""
|
|
796
|
+
return list(self._row.keys() if self._row else ()) + getattr(self, "_with", [])
|
|
797
|
+
|
|
798
|
+
def get(self, item: str, default: t.Any = None) -> t.Any:
|
|
799
|
+
"""
|
|
800
|
+
Try to get a column from this instance, else return default.
|
|
801
|
+
"""
|
|
802
|
+
try:
|
|
803
|
+
return self.__getitem__(item)
|
|
804
|
+
except KeyError:
|
|
805
|
+
return default
|
|
806
|
+
|
|
807
|
+
def __setitem__(self, key: str, value: t.Any) -> None:
|
|
808
|
+
"""
|
|
809
|
+
Data can both be updated via dot and dict notation.
|
|
810
|
+
"""
|
|
811
|
+
return setattr(self, key, value)
|
|
812
|
+
|
|
813
|
+
def __int__(self) -> int:
|
|
814
|
+
"""
|
|
815
|
+
Calling int on a model instance will return its id.
|
|
816
|
+
"""
|
|
817
|
+
return getattr(self, "id", 0)
|
|
818
|
+
|
|
819
|
+
def __bool__(self) -> bool:
|
|
820
|
+
"""
|
|
821
|
+
If the instance has an underlying row with data, it is truthy.
|
|
822
|
+
"""
|
|
823
|
+
return bool(getattr(self, "_row", False))
|
|
824
|
+
|
|
825
|
+
def _ensure_matching_row(self) -> Row:
|
|
826
|
+
row = getattr(self, "_row", None)
|
|
827
|
+
return t.cast(Row, row) or throw(
|
|
828
|
+
EnvironmentError("Trying to access non-existant row. Maybe it was deleted or not yet initialized?")
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
def __repr__(self) -> str:
|
|
832
|
+
"""
|
|
833
|
+
String representation of the model instance.
|
|
834
|
+
"""
|
|
835
|
+
model_name = self.__class__.__name__
|
|
836
|
+
model_data = {}
|
|
837
|
+
|
|
838
|
+
if self._row:
|
|
839
|
+
model_data = self._row.as_json()
|
|
840
|
+
|
|
841
|
+
details = model_name
|
|
842
|
+
details += f"({model_data})"
|
|
843
|
+
|
|
844
|
+
if relationships := getattr(self, "_with", []):
|
|
845
|
+
details += f" + {relationships}"
|
|
846
|
+
|
|
847
|
+
return f"<{details}>"
|
|
848
|
+
|
|
849
|
+
# serialization
|
|
850
|
+
# underscore variants work for class instances (set up by _setup_instance_methods)
|
|
851
|
+
|
|
852
|
+
@classmethod
|
|
853
|
+
def as_dict(cls, flat: bool = False, sanitize: bool = True) -> AnyDict:
|
|
854
|
+
"""
|
|
855
|
+
Dump the object to a plain dict.
|
|
856
|
+
|
|
857
|
+
Can be used as both a class or instance method:
|
|
858
|
+
- dumps the table info if it's a class
|
|
859
|
+
- dumps the row info if it's an instance (see _as_dict)
|
|
860
|
+
"""
|
|
861
|
+
table = cls._ensure_table_defined()
|
|
862
|
+
result = table.as_dict(flat, sanitize)
|
|
863
|
+
return t.cast(AnyDict, result)
|
|
864
|
+
|
|
865
|
+
@classmethod
|
|
866
|
+
def as_json(cls, sanitize: bool = True, indent: t.Optional[int] = None, **kwargs: t.Any) -> str:
|
|
867
|
+
"""
|
|
868
|
+
Dump the object to json.
|
|
869
|
+
|
|
870
|
+
Can be used as both a class or instance method:
|
|
871
|
+
- dumps the table info if it's a class
|
|
872
|
+
- dumps the row info if it's an instance (see _as_json)
|
|
873
|
+
"""
|
|
874
|
+
data = cls.as_dict(sanitize=sanitize)
|
|
875
|
+
return as_json.encode(data, indent=indent, **kwargs)
|
|
876
|
+
|
|
877
|
+
@classmethod
|
|
878
|
+
def as_xml(cls, sanitize: bool = True) -> str: # pragma: no cover
|
|
879
|
+
"""
|
|
880
|
+
Dump the object to xml.
|
|
881
|
+
|
|
882
|
+
Can be used as both a class or instance method:
|
|
883
|
+
- dumps the table info if it's a class
|
|
884
|
+
- dumps the row info if it's an instance (see _as_xml)
|
|
885
|
+
"""
|
|
886
|
+
table = cls._ensure_table_defined()
|
|
887
|
+
return t.cast(str, table.as_xml(sanitize))
|
|
888
|
+
|
|
889
|
+
@classmethod
|
|
890
|
+
def as_yaml(cls, sanitize: bool = True) -> str:
|
|
891
|
+
"""
|
|
892
|
+
Dump the object to yaml.
|
|
893
|
+
|
|
894
|
+
Can be used as both a class or instance method:
|
|
895
|
+
- dumps the table info if it's a class
|
|
896
|
+
- dumps the row info if it's an instance (see _as_yaml)
|
|
897
|
+
"""
|
|
898
|
+
table = cls._ensure_table_defined()
|
|
899
|
+
return t.cast(str, table.as_yaml(sanitize))
|
|
900
|
+
|
|
901
|
+
def _as_dict(
|
|
902
|
+
self,
|
|
903
|
+
datetime_to_str: bool = False,
|
|
904
|
+
custom_types: t.Iterable[type] | type | None = None,
|
|
905
|
+
) -> AnyDict:
|
|
906
|
+
row = self._ensure_matching_row()
|
|
907
|
+
|
|
908
|
+
result = row.as_dict(datetime_to_str=datetime_to_str, custom_types=custom_types)
|
|
909
|
+
|
|
910
|
+
def asdict_method(obj: t.Any) -> t.Any: # pragma: no cover
|
|
911
|
+
if hasattr(obj, "_as_dict"): # typedal
|
|
912
|
+
return obj._as_dict()
|
|
913
|
+
elif hasattr(obj, "as_dict"): # pydal
|
|
914
|
+
return obj.as_dict()
|
|
915
|
+
else: # something else??
|
|
916
|
+
return obj.__dict__
|
|
917
|
+
|
|
918
|
+
if _with := getattr(self, "_with", None):
|
|
919
|
+
for relationship in _with:
|
|
920
|
+
data = self.get(relationship)
|
|
921
|
+
|
|
922
|
+
if isinstance(data, list):
|
|
923
|
+
data = [asdict_method(_) for _ in data]
|
|
924
|
+
elif data:
|
|
925
|
+
data = asdict_method(data)
|
|
926
|
+
|
|
927
|
+
result[relationship] = data
|
|
928
|
+
|
|
929
|
+
return t.cast(AnyDict, result)
|
|
930
|
+
|
|
931
|
+
def _as_json(
|
|
932
|
+
self,
|
|
933
|
+
default: t.Callable[[t.Any], t.Any] = None,
|
|
934
|
+
indent: t.Optional[int] = None,
|
|
935
|
+
**kwargs: t.Any,
|
|
936
|
+
) -> str:
|
|
937
|
+
data = self._as_dict()
|
|
938
|
+
return as_json.encode(data, default=default, indent=indent, **kwargs)
|
|
939
|
+
|
|
940
|
+
def _as_xml(self, sanitize: bool = True) -> str: # pragma: no cover
|
|
941
|
+
row = self._ensure_matching_row()
|
|
942
|
+
return t.cast(str, row.as_xml(sanitize))
|
|
943
|
+
|
|
944
|
+
# def _as_yaml(self, sanitize: bool = True) -> str:
|
|
945
|
+
# row = self._ensure_matching_row()
|
|
946
|
+
# return t.cast(str, row.as_yaml(sanitize))
|
|
947
|
+
|
|
948
|
+
def __setattr__(self, key: str, value: t.Any) -> None:
|
|
949
|
+
"""
|
|
950
|
+
When setting a property on a Typed Table model instance, also update the underlying row.
|
|
951
|
+
"""
|
|
952
|
+
if self._row and key in self._row.__dict__ and not callable(value):
|
|
953
|
+
# enables `row.key = value; row.update_record()`
|
|
954
|
+
self._row[key] = value
|
|
955
|
+
|
|
956
|
+
super().__setattr__(key, value)
|
|
957
|
+
|
|
958
|
+
@classmethod
|
|
959
|
+
def update(cls: t.Type[T_MetaInstance], query: Query, **fields: t.Any) -> T_MetaInstance | None:
|
|
960
|
+
"""
|
|
961
|
+
Update one record.
|
|
962
|
+
|
|
963
|
+
Example:
|
|
964
|
+
MyTable.update(MyTable.id == 1, name="NewName") -> MyTable
|
|
965
|
+
"""
|
|
966
|
+
# todo: update multiple?
|
|
967
|
+
if record := cls(query):
|
|
968
|
+
return record.update_record(**fields)
|
|
969
|
+
else:
|
|
970
|
+
return None
|
|
971
|
+
|
|
972
|
+
def _update(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance:
|
|
973
|
+
row = self._ensure_matching_row()
|
|
974
|
+
row.update(**fields)
|
|
975
|
+
self.__dict__.update(**fields)
|
|
976
|
+
return self
|
|
977
|
+
|
|
978
|
+
def _update_record(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance:
|
|
979
|
+
row = self._ensure_matching_row()
|
|
980
|
+
new_row = row.update_record(**fields)
|
|
981
|
+
self.update(**new_row)
|
|
982
|
+
return self
|
|
983
|
+
|
|
984
|
+
def update_record(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance: # pragma: no cover
|
|
985
|
+
"""
|
|
986
|
+
Here as a placeholder for _update_record.
|
|
987
|
+
|
|
988
|
+
Will be replaced on instance creation!
|
|
989
|
+
"""
|
|
990
|
+
return self._update_record(**fields)
|
|
991
|
+
|
|
992
|
+
def _delete_record(self) -> int:
|
|
993
|
+
"""
|
|
994
|
+
Actual logic in `pydal.helpers.classes.RecordDeleter`.
|
|
995
|
+
"""
|
|
996
|
+
row = self._ensure_matching_row()
|
|
997
|
+
result = row.delete_record()
|
|
998
|
+
self.__dict__ = {} # empty self, since row is no more.
|
|
999
|
+
self._row = None # just to be sure
|
|
1000
|
+
self._setup_instance_methods()
|
|
1001
|
+
# ^ instance methods might've been deleted by emptying dict,
|
|
1002
|
+
# but we still want .as_dict to show an error, not the table's as_dict.
|
|
1003
|
+
return t.cast(int, result)
|
|
1004
|
+
|
|
1005
|
+
def delete_record(self) -> int: # pragma: no cover
|
|
1006
|
+
"""
|
|
1007
|
+
Here as a placeholder for _delete_record.
|
|
1008
|
+
|
|
1009
|
+
Will be replaced on instance creation!
|
|
1010
|
+
"""
|
|
1011
|
+
return self._delete_record()
|
|
1012
|
+
|
|
1013
|
+
# __del__ is also called on the end of a scope so don't remove records on every del!!
|
|
1014
|
+
|
|
1015
|
+
# pickling:
|
|
1016
|
+
|
|
1017
|
+
def __getstate__(self) -> AnyDict:
|
|
1018
|
+
"""
|
|
1019
|
+
State to save when pickling.
|
|
1020
|
+
|
|
1021
|
+
Prevents db connection from being pickled.
|
|
1022
|
+
Similar to as_dict but without changing the data of the relationships (dill does that recursively)
|
|
1023
|
+
"""
|
|
1024
|
+
row = self._ensure_matching_row()
|
|
1025
|
+
result: AnyDict = row.as_dict()
|
|
1026
|
+
|
|
1027
|
+
if _with := getattr(self, "_with", None):
|
|
1028
|
+
result["_with"] = _with
|
|
1029
|
+
for relationship in _with:
|
|
1030
|
+
data = self.get(relationship)
|
|
1031
|
+
|
|
1032
|
+
result[relationship] = data
|
|
1033
|
+
|
|
1034
|
+
result["_row"] = self._row.as_json() if self._row else ""
|
|
1035
|
+
return result
|
|
1036
|
+
|
|
1037
|
+
def __setstate__(self, state: AnyDict) -> None:
|
|
1038
|
+
"""
|
|
1039
|
+
Used by dill when loading from a bytestring.
|
|
1040
|
+
"""
|
|
1041
|
+
# as_dict also includes table info, so dump as json to only get the actual row data
|
|
1042
|
+
# then create a new (more empty) row object:
|
|
1043
|
+
state["_row"] = Row(json.loads(state["_row"]))
|
|
1044
|
+
self.__dict__ |= state
|
|
1045
|
+
|
|
1046
|
+
@classmethod
|
|
1047
|
+
def _sql(cls) -> str:
|
|
1048
|
+
"""
|
|
1049
|
+
Generate SQL Schema for this table via pydal2sql (if 'migrations' extra is installed).
|
|
1050
|
+
"""
|
|
1051
|
+
try:
|
|
1052
|
+
import pydal2sql
|
|
1053
|
+
except ImportError as e: # pragma: no cover
|
|
1054
|
+
raise RuntimeError("Can not generate SQL without the 'migration' extra or `pydal2sql` installed!") from e
|
|
1055
|
+
|
|
1056
|
+
return pydal2sql.generate_sql(cls)
|
|
1057
|
+
|
|
1058
|
+
def render(self, fields: list[Field] = None, compact: bool = False) -> t.Self:
|
|
1059
|
+
"""
|
|
1060
|
+
Renders a copy of the object with potentially modified values.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
fields: A list of fields to render. Defaults to all representable fields in the table.
|
|
1064
|
+
compact: Whether to return only the value of the first field if there is only one field.
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
A copy of the object with potentially modified values.
|
|
1068
|
+
"""
|
|
1069
|
+
row = copy.deepcopy(self)
|
|
1070
|
+
keys = list(row)
|
|
1071
|
+
if not fields:
|
|
1072
|
+
fields = [self._table[f] for f in self._table._fields]
|
|
1073
|
+
fields = [f for f in fields if isinstance(f, Field) and f.represent]
|
|
1074
|
+
|
|
1075
|
+
for field in fields:
|
|
1076
|
+
if field._table == self._table:
|
|
1077
|
+
row[field.name] = self._db.represent(
|
|
1078
|
+
"rows_render",
|
|
1079
|
+
field,
|
|
1080
|
+
row[field.name],
|
|
1081
|
+
row,
|
|
1082
|
+
)
|
|
1083
|
+
# else: relationship, different logic:
|
|
1084
|
+
|
|
1085
|
+
for relation_name in getattr(row, "_with", []):
|
|
1086
|
+
if relation := self._relationships.get(relation_name):
|
|
1087
|
+
relation_table = relation.table
|
|
1088
|
+
if isinstance(relation_table, str):
|
|
1089
|
+
relation_table = self._db[relation_table]
|
|
1090
|
+
|
|
1091
|
+
relation_row = row[relation_name]
|
|
1092
|
+
|
|
1093
|
+
if isinstance(relation_row, list):
|
|
1094
|
+
# list of rows
|
|
1095
|
+
combined = []
|
|
1096
|
+
|
|
1097
|
+
for related_og in relation_row:
|
|
1098
|
+
related = copy.deepcopy(related_og)
|
|
1099
|
+
for fieldname in related:
|
|
1100
|
+
field = relation_table[fieldname]
|
|
1101
|
+
related[field.name] = self._db.represent(
|
|
1102
|
+
"rows_render",
|
|
1103
|
+
field,
|
|
1104
|
+
related[field.name],
|
|
1105
|
+
related,
|
|
1106
|
+
)
|
|
1107
|
+
combined.append(related)
|
|
1108
|
+
|
|
1109
|
+
row[relation_name] = combined
|
|
1110
|
+
else:
|
|
1111
|
+
# 1 row
|
|
1112
|
+
for fieldname in relation_row:
|
|
1113
|
+
field = relation_table[fieldname]
|
|
1114
|
+
row[relation_name][fieldname] = self._db.represent(
|
|
1115
|
+
"rows_render",
|
|
1116
|
+
field,
|
|
1117
|
+
relation_row[field.name],
|
|
1118
|
+
relation_row,
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
if compact and len(keys) == 1 and keys[0] != "_extra": # pragma: no cover
|
|
1122
|
+
return t.cast(t.Self, row[keys[0]])
|
|
1123
|
+
return row
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
# backwards compat:
|
|
1127
|
+
TypedRow = TypedTable
|
|
1128
|
+
|
|
1129
|
+
# note: at the bottom to prevent circular import issues:
|
|
1130
|
+
from .fields import TypedField # noqa: E402
|
|
1131
|
+
from .query_builder import QueryBuilder # noqa: E402
|