fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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.
- fastapi_rtk/__init__.py +39 -35
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +476 -221
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/generic/__init__.py +6 -0
- fastapi_rtk/backends/generic/column.py +21 -12
- fastapi_rtk/backends/generic/db.py +42 -7
- fastapi_rtk/backends/generic/filters.py +21 -16
- fastapi_rtk/backends/generic/interface.py +14 -8
- fastapi_rtk/backends/generic/model.py +19 -11
- fastapi_rtk/backends/sqla/__init__.py +1 -0
- fastapi_rtk/backends/sqla/db.py +77 -17
- fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
- fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
- fastapi_rtk/backends/sqla/filters.py +50 -21
- fastapi_rtk/backends/sqla/interface.py +96 -34
- fastapi_rtk/backends/sqla/model.py +56 -39
- fastapi_rtk/bases/__init__.py +20 -0
- fastapi_rtk/bases/db.py +94 -7
- fastapi_rtk/bases/file_manager.py +47 -3
- fastapi_rtk/bases/filter.py +22 -0
- fastapi_rtk/bases/interface.py +49 -5
- fastapi_rtk/bases/model.py +3 -0
- fastapi_rtk/bases/session.py +2 -0
- fastapi_rtk/cli/cli.py +62 -9
- fastapi_rtk/cli/commands/__init__.py +23 -0
- fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
- fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
- fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
- fastapi_rtk/cli/commands/translate.py +299 -0
- fastapi_rtk/cli/decorators.py +9 -4
- fastapi_rtk/cli/utils.py +46 -0
- fastapi_rtk/config.py +41 -1
- fastapi_rtk/const.py +29 -1
- fastapi_rtk/db.py +76 -40
- fastapi_rtk/decorators.py +1 -1
- fastapi_rtk/dependencies.py +134 -62
- fastapi_rtk/exceptions.py +51 -1
- fastapi_rtk/fastapi_react_toolkit.py +186 -171
- fastapi_rtk/file_managers/file_manager.py +8 -6
- fastapi_rtk/file_managers/s3_file_manager.py +69 -33
- fastapi_rtk/globals.py +22 -12
- fastapi_rtk/lang/__init__.py +3 -0
- fastapi_rtk/lang/babel/__init__.py +4 -0
- fastapi_rtk/lang/babel/cli.py +40 -0
- fastapi_rtk/lang/babel/config.py +17 -0
- fastapi_rtk/lang/babel.cfg +1 -0
- fastapi_rtk/lang/lazy_text.py +120 -0
- fastapi_rtk/lang/messages.pot +238 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
- fastapi_rtk/manager.py +355 -37
- fastapi_rtk/mixins.py +12 -0
- fastapi_rtk/routers.py +208 -72
- fastapi_rtk/schemas.py +142 -39
- fastapi_rtk/security/sqla/apis.py +39 -13
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +369 -11
- fastapi_rtk/setting.py +446 -88
- fastapi_rtk/types.py +94 -27
- fastapi_rtk/utils/__init__.py +8 -0
- fastapi_rtk/utils/async_task_runner.py +286 -61
- fastapi_rtk/utils/csv_json_converter.py +243 -40
- fastapi_rtk/utils/hooks.py +34 -0
- fastapi_rtk/utils/merge_schema.py +3 -3
- fastapi_rtk/utils/multiple_async_contexts.py +21 -0
- fastapi_rtk/utils/pydantic.py +46 -1
- fastapi_rtk/utils/run_utils.py +31 -1
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/utils/use_default_when_none.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
- fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
- fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
- fastapi_rtk/backends/gremlinpython/column.py +0 -208
- fastapi_rtk/backends/gremlinpython/db.py +0 -228
- fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
- fastapi_rtk/backends/gremlinpython/filters.py +0 -461
- fastapi_rtk/backends/gremlinpython/interface.py +0 -734
- fastapi_rtk/backends/gremlinpython/model.py +0 -364
- fastapi_rtk/backends/gremlinpython/session.py +0 -23
- fastapi_rtk/cli/commands.py +0 -295
- fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
- fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
- fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import fastapi
|
|
4
4
|
from sqlalchemy import Select, func
|
|
5
5
|
|
|
6
|
+
from .....exceptions import HTTPWithValidationException
|
|
7
|
+
from .....lang import lazy_text
|
|
6
8
|
from ...filters import (
|
|
7
9
|
BaseFilter,
|
|
8
10
|
FilterContains,
|
|
@@ -32,13 +34,14 @@ class GeoBaseFilter(BaseFilter):
|
|
|
32
34
|
value, self._get_column(col).type.geometry_type if check else ""
|
|
33
35
|
)
|
|
34
36
|
except ValueError as e:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
raise HTTPWithValidationException(
|
|
38
|
+
fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
39
|
+
"value_error",
|
|
40
|
+
"query",
|
|
41
|
+
"filters",
|
|
42
|
+
f"Value error, {e}",
|
|
43
|
+
value,
|
|
44
|
+
)
|
|
42
45
|
|
|
43
46
|
if isinstance(value, dict):
|
|
44
47
|
return func.ST_GeomFromGeoJSON(value)
|
|
@@ -77,7 +80,7 @@ class GeoFilterNotContains(GeoBaseFilter, FilterNotContains):
|
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
class GeoFilterIntersects(GeoBaseFilter):
|
|
80
|
-
name = "Intersects"
|
|
83
|
+
name = lazy_text("Intersects")
|
|
81
84
|
arg_name = "int"
|
|
82
85
|
|
|
83
86
|
def apply(self, stmt: Select, col: str, value: Any) -> Select:
|
|
@@ -87,7 +90,7 @@ class GeoFilterIntersects(GeoBaseFilter):
|
|
|
87
90
|
|
|
88
91
|
|
|
89
92
|
class GeoFilterNotIntersects(GeoBaseFilter):
|
|
90
|
-
name = "Not Intersects"
|
|
93
|
+
name = lazy_text("Not Intersects")
|
|
91
94
|
arg_name = "nint"
|
|
92
95
|
|
|
93
96
|
def apply(self, stmt: Select, col: str, value: Any) -> Select:
|
|
@@ -97,7 +100,7 @@ class GeoFilterNotIntersects(GeoBaseFilter):
|
|
|
97
100
|
|
|
98
101
|
|
|
99
102
|
class GeoFilterOverlaps(GeoBaseFilter):
|
|
100
|
-
name = "Overlaps"
|
|
103
|
+
name = lazy_text("Overlaps")
|
|
101
104
|
arg_name = "ovl"
|
|
102
105
|
|
|
103
106
|
def apply(self, stmt: Select, col: str, value: Any) -> Select:
|
|
@@ -107,7 +110,7 @@ class GeoFilterOverlaps(GeoBaseFilter):
|
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
class GeoFilterNotOverlaps(GeoBaseFilter):
|
|
110
|
-
name = "Not Overlaps"
|
|
113
|
+
name = lazy_text("Not Overlaps")
|
|
111
114
|
arg_name = "novl"
|
|
112
115
|
|
|
113
116
|
def apply(self, stmt: Select, col: str, value: Any) -> Select:
|
|
@@ -2,13 +2,15 @@ import enum
|
|
|
2
2
|
import typing
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
|
+
import fastapi
|
|
5
6
|
import sqlalchemy
|
|
6
7
|
import sqlalchemy.types as sa_types
|
|
7
|
-
from fastapi import HTTPException
|
|
8
8
|
from sqlalchemy import Column, Select, and_, cast, func, or_, select
|
|
9
9
|
|
|
10
|
-
from ...bases.filter import AbstractBaseFilter
|
|
10
|
+
from ...bases.filter import AbstractBaseFilter, AbstractBaseOprFilter
|
|
11
11
|
from ...const import logger
|
|
12
|
+
from ...exceptions import HTTPWithValidationException
|
|
13
|
+
from ...lang import lazy_text, translate
|
|
12
14
|
from .model import Model
|
|
13
15
|
|
|
14
16
|
__all__ = [
|
|
@@ -36,6 +38,7 @@ __all__ = [
|
|
|
36
38
|
"FilterRelationOneToManyOrManyToManyIn",
|
|
37
39
|
"FilterRelationOneToManyOrManyToManyNotIn",
|
|
38
40
|
"FilterGlobal",
|
|
41
|
+
"BaseOprFilter",
|
|
39
42
|
"SQLAFilterConverter",
|
|
40
43
|
]
|
|
41
44
|
|
|
@@ -237,7 +240,7 @@ class BaseFilterRelationOneToManyOrManyToMany(BaseFilterRelationOneToOneOrManyTo
|
|
|
237
240
|
|
|
238
241
|
|
|
239
242
|
class FilterTextContains(BaseFilterTextContains):
|
|
240
|
-
name = "Text contains"
|
|
243
|
+
name = lazy_text(message="Text contains")
|
|
241
244
|
arg_name = "tc"
|
|
242
245
|
|
|
243
246
|
def apply(self, statement, col: str, value):
|
|
@@ -253,7 +256,7 @@ class FilterTextContains(BaseFilterTextContains):
|
|
|
253
256
|
|
|
254
257
|
|
|
255
258
|
class FilterEqual(BaseFilter):
|
|
256
|
-
name = "Equal to"
|
|
259
|
+
name = lazy_text("Equal to")
|
|
257
260
|
arg_name = "eq"
|
|
258
261
|
|
|
259
262
|
def apply(self, statement, col: str, value):
|
|
@@ -262,7 +265,7 @@ class FilterEqual(BaseFilter):
|
|
|
262
265
|
|
|
263
266
|
|
|
264
267
|
class FilterNotEqual(BaseFilter):
|
|
265
|
-
name = "Not
|
|
268
|
+
name = lazy_text("Not equal to")
|
|
266
269
|
arg_name = "neq"
|
|
267
270
|
|
|
268
271
|
def apply(self, statement, col: str, value):
|
|
@@ -271,7 +274,7 @@ class FilterNotEqual(BaseFilter):
|
|
|
271
274
|
|
|
272
275
|
|
|
273
276
|
class FilterStartsWith(BaseFilter):
|
|
274
|
-
name = "Starts with"
|
|
277
|
+
name = lazy_text("Starts with")
|
|
275
278
|
arg_name = "sw"
|
|
276
279
|
|
|
277
280
|
def apply(self, statement, col: str, value):
|
|
@@ -280,7 +283,7 @@ class FilterStartsWith(BaseFilter):
|
|
|
280
283
|
|
|
281
284
|
|
|
282
285
|
class FilterNotStartsWith(BaseFilter):
|
|
283
|
-
name = "Not Starts with"
|
|
286
|
+
name = lazy_text("Not Starts with")
|
|
284
287
|
arg_name = "nsw"
|
|
285
288
|
|
|
286
289
|
def apply(self, statement, col: str, value):
|
|
@@ -289,7 +292,7 @@ class FilterNotStartsWith(BaseFilter):
|
|
|
289
292
|
|
|
290
293
|
|
|
291
294
|
class FilterEndsWith(BaseFilter):
|
|
292
|
-
name = "Ends with"
|
|
295
|
+
name = lazy_text("Ends with")
|
|
293
296
|
arg_name = "ew"
|
|
294
297
|
|
|
295
298
|
def apply(self, statement, col: str, value):
|
|
@@ -298,7 +301,7 @@ class FilterEndsWith(BaseFilter):
|
|
|
298
301
|
|
|
299
302
|
|
|
300
303
|
class FilterNotEndsWith(BaseFilter):
|
|
301
|
-
name = "Not Ends with"
|
|
304
|
+
name = lazy_text("Not Ends with")
|
|
302
305
|
arg_name = "new"
|
|
303
306
|
|
|
304
307
|
def apply(self, statement, col: str, value):
|
|
@@ -307,7 +310,7 @@ class FilterNotEndsWith(BaseFilter):
|
|
|
307
310
|
|
|
308
311
|
|
|
309
312
|
class FilterContains(BaseFilter):
|
|
310
|
-
name = "Contains"
|
|
313
|
+
name = lazy_text("Contains")
|
|
311
314
|
arg_name = "ct"
|
|
312
315
|
|
|
313
316
|
def apply(self, statement, col: str, value):
|
|
@@ -316,7 +319,7 @@ class FilterContains(BaseFilter):
|
|
|
316
319
|
|
|
317
320
|
|
|
318
321
|
class FilterNotContains(BaseFilter):
|
|
319
|
-
name = "Not Contains"
|
|
322
|
+
name = lazy_text("Not Contains")
|
|
320
323
|
arg_name = "nct"
|
|
321
324
|
|
|
322
325
|
def apply(self, statement, col: str, value):
|
|
@@ -325,7 +328,7 @@ class FilterNotContains(BaseFilter):
|
|
|
325
328
|
|
|
326
329
|
|
|
327
330
|
class FilterGreater(BaseFilter):
|
|
328
|
-
name = "Greater than"
|
|
331
|
+
name = lazy_text("Greater than")
|
|
329
332
|
arg_name = "gt"
|
|
330
333
|
|
|
331
334
|
def apply(self, statement, col: str, value):
|
|
@@ -334,7 +337,7 @@ class FilterGreater(BaseFilter):
|
|
|
334
337
|
|
|
335
338
|
|
|
336
339
|
class FilterSmaller(BaseFilter):
|
|
337
|
-
name = "Smaller than"
|
|
340
|
+
name = lazy_text("Smaller than")
|
|
338
341
|
arg_name = "lt"
|
|
339
342
|
|
|
340
343
|
def apply(self, statement, col: str, value):
|
|
@@ -343,7 +346,7 @@ class FilterSmaller(BaseFilter):
|
|
|
343
346
|
|
|
344
347
|
|
|
345
348
|
class FilterGreaterEqual(BaseFilter):
|
|
346
|
-
name = "Greater equal"
|
|
349
|
+
name = lazy_text("Greater equal")
|
|
347
350
|
arg_name = "ge"
|
|
348
351
|
|
|
349
352
|
def apply(self, statement, col: str, value):
|
|
@@ -352,7 +355,7 @@ class FilterGreaterEqual(BaseFilter):
|
|
|
352
355
|
|
|
353
356
|
|
|
354
357
|
class FilterSmallerEqual(BaseFilter):
|
|
355
|
-
name = "Smaller equal"
|
|
358
|
+
name = lazy_text("Smaller equal")
|
|
356
359
|
arg_name = "le"
|
|
357
360
|
|
|
358
361
|
def apply(self, statement, col: str, value):
|
|
@@ -361,7 +364,7 @@ class FilterSmallerEqual(BaseFilter):
|
|
|
361
364
|
|
|
362
365
|
|
|
363
366
|
class FilterIn(BaseFilter):
|
|
364
|
-
name = "One of"
|
|
367
|
+
name = lazy_text("One of")
|
|
365
368
|
arg_name = "in"
|
|
366
369
|
|
|
367
370
|
def apply(self, statement, col: str, value):
|
|
@@ -370,7 +373,7 @@ class FilterIn(BaseFilter):
|
|
|
370
373
|
|
|
371
374
|
|
|
372
375
|
class FilterBetween(BaseFilter):
|
|
373
|
-
name = "Between"
|
|
376
|
+
name = lazy_text("Between")
|
|
374
377
|
arg_name = "bw"
|
|
375
378
|
|
|
376
379
|
def apply(self, statement, col: str, value):
|
|
@@ -378,7 +381,14 @@ class FilterBetween(BaseFilter):
|
|
|
378
381
|
return statement
|
|
379
382
|
|
|
380
383
|
if len(value) != 2:
|
|
381
|
-
raise
|
|
384
|
+
raise HTTPWithValidationException(
|
|
385
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
386
|
+
"greater_than" if len(value) < 2 else "less_than",
|
|
387
|
+
"query",
|
|
388
|
+
"filters",
|
|
389
|
+
translate("Between filter requires 2 values"),
|
|
390
|
+
value,
|
|
391
|
+
)
|
|
382
392
|
|
|
383
393
|
value = [self._cast_value(col, v) for v in value]
|
|
384
394
|
if value[0] is None or value[1] is None:
|
|
@@ -421,7 +431,7 @@ class FilterRelationOneToOneOrManyToOneNotEqual(
|
|
|
421
431
|
|
|
422
432
|
|
|
423
433
|
class FilterRelationOneToManyOrManyToManyIn(BaseFilterRelationOneToManyOrManyToMany):
|
|
424
|
-
name = "In"
|
|
434
|
+
name = lazy_text("In")
|
|
425
435
|
arg_name = "rel_m_m"
|
|
426
436
|
|
|
427
437
|
def apply(self, statement, col: str, value):
|
|
@@ -439,7 +449,7 @@ class FilterRelationOneToManyOrManyToManyIn(BaseFilterRelationOneToManyOrManyToM
|
|
|
439
449
|
|
|
440
450
|
|
|
441
451
|
class FilterRelationOneToManyOrManyToManyNotIn(BaseFilterRelationOneToManyOrManyToMany):
|
|
442
|
-
name = "Not In"
|
|
452
|
+
name = lazy_text("Not In")
|
|
443
453
|
arg_name = "nrel_m_m"
|
|
444
454
|
|
|
445
455
|
def apply(self, statement, col: str, value):
|
|
@@ -457,7 +467,7 @@ class FilterRelationOneToManyOrManyToManyNotIn(BaseFilterRelationOneToManyOrMany
|
|
|
457
467
|
|
|
458
468
|
|
|
459
469
|
class FilterGlobal(BaseFilterTextContains):
|
|
460
|
-
name = "Global Filter"
|
|
470
|
+
name = lazy_text("Global Filter")
|
|
461
471
|
arg_name = "global"
|
|
462
472
|
|
|
463
473
|
def apply(self, statement, cols: list[str], value):
|
|
@@ -489,6 +499,9 @@ class FilterGlobal(BaseFilterTextContains):
|
|
|
489
499
|
return statement.filter(and_(*filters))
|
|
490
500
|
|
|
491
501
|
|
|
502
|
+
class BaseOprFilter(AbstractBaseOprFilter, BaseFilter): ...
|
|
503
|
+
|
|
504
|
+
|
|
492
505
|
class SQLAFilterConverter:
|
|
493
506
|
"""
|
|
494
507
|
Helper class to get available filters for a column type.
|
|
@@ -604,6 +617,22 @@ class SQLAFilterConverter:
|
|
|
604
617
|
FilterIn,
|
|
605
618
|
],
|
|
606
619
|
),
|
|
620
|
+
(
|
|
621
|
+
"is_files",
|
|
622
|
+
[
|
|
623
|
+
FilterTextContains,
|
|
624
|
+
# TODO: Make compatible filters
|
|
625
|
+
# FilterEqual,
|
|
626
|
+
# FilterNotEqual,
|
|
627
|
+
# FilterStartsWith,
|
|
628
|
+
# FilterNotStartsWith,
|
|
629
|
+
# FilterEndsWith,
|
|
630
|
+
# FilterNotEndsWith,
|
|
631
|
+
# FilterContains,
|
|
632
|
+
# FilterNotContains,
|
|
633
|
+
# FilterIn,
|
|
634
|
+
],
|
|
635
|
+
),
|
|
607
636
|
(
|
|
608
637
|
"is_integer",
|
|
609
638
|
[
|
|
@@ -3,7 +3,7 @@ import enum
|
|
|
3
3
|
import json
|
|
4
4
|
import typing
|
|
5
5
|
from datetime import date, datetime
|
|
6
|
-
from typing import Annotated,
|
|
6
|
+
from typing import Annotated, Literal, Type
|
|
7
7
|
|
|
8
8
|
import fastapi
|
|
9
9
|
import marshmallow_sqlalchemy
|
|
@@ -22,26 +22,48 @@ from sqlalchemy.sql import sqltypes as sa_types
|
|
|
22
22
|
|
|
23
23
|
from ...bases.db import DBQueryParams
|
|
24
24
|
from ...bases.interface import AbstractInterface
|
|
25
|
+
from ...const import logger
|
|
25
26
|
from ...db import get_session_factory as _get_session_factory
|
|
27
|
+
from ...globals import g
|
|
26
28
|
from ...schemas import (
|
|
27
29
|
DatetimeUTC,
|
|
28
30
|
)
|
|
29
|
-
from ...types import
|
|
30
|
-
|
|
31
|
+
from ...types import (
|
|
32
|
+
FileColumn,
|
|
33
|
+
FileColumns,
|
|
34
|
+
ImageColumn,
|
|
35
|
+
ImageColumns,
|
|
36
|
+
JSONBFileColumns,
|
|
37
|
+
JSONBImageColumns,
|
|
38
|
+
)
|
|
39
|
+
from ...utils import (
|
|
40
|
+
AsyncTaskRunner,
|
|
41
|
+
T,
|
|
42
|
+
is_sqla_type,
|
|
43
|
+
lazy_import,
|
|
44
|
+
lazy_self,
|
|
45
|
+
safe_call,
|
|
46
|
+
smart_run,
|
|
47
|
+
)
|
|
31
48
|
from .db import SQLAQueryBuilder
|
|
32
49
|
from .filters import BaseFilter, FilterGlobal, SQLAFilterConverter
|
|
33
50
|
from .model import Model
|
|
34
51
|
|
|
35
|
-
__all__ = ["SQLAInterface"]
|
|
36
|
-
|
|
37
52
|
if typing.TYPE_CHECKING:
|
|
38
53
|
from .extensions.geoalchemy2 import GeometryConverter
|
|
39
54
|
|
|
55
|
+
__all__ = ["SQLAInterface"]
|
|
56
|
+
|
|
57
|
+
logger = logger.getChild("SQLAInterface")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ModelType = typing.TypeVar("ModelType", bound=Model)
|
|
61
|
+
|
|
40
62
|
|
|
41
63
|
class lazy(lazy_self["SQLAInterface", T]): ...
|
|
42
64
|
|
|
43
65
|
|
|
44
|
-
class SQLAInterface(AbstractInterface[
|
|
66
|
+
class SQLAInterface(AbstractInterface[ModelType, Session | AsyncSession, Column]):
|
|
45
67
|
"""
|
|
46
68
|
Datamodel interface for SQLAlchemy models.
|
|
47
69
|
"""
|
|
@@ -60,6 +82,18 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
60
82
|
"fastapi_rtk.backends.sqla.extensions.geoalchemy2",
|
|
61
83
|
lambda mod: mod.GeometryConverter(),
|
|
62
84
|
)
|
|
85
|
+
file_manager = lazy(
|
|
86
|
+
lambda self: g.file_manager.get_instance_with_subfolder(self.obj.__tablename__)
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
The file manager instance for the datamodel. Defaults to `g.file_manager` with the subfolder set to the table name.
|
|
90
|
+
"""
|
|
91
|
+
image_manager = lazy(
|
|
92
|
+
lambda self: g.image_manager.get_instance_with_subfolder(self.obj.__tablename__)
|
|
93
|
+
)
|
|
94
|
+
"""
|
|
95
|
+
The image manager instance for the datamodel. Defaults to `g.image_manager` with the subfolder set to the table name.
|
|
96
|
+
"""
|
|
63
97
|
|
|
64
98
|
_cache_field: dict[str, str] = {}
|
|
65
99
|
"""
|
|
@@ -88,26 +122,26 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
88
122
|
relevant_params.pop("page_size", None)
|
|
89
123
|
relevant_params.pop("order_column", None)
|
|
90
124
|
relevant_params.pop("order_direction", None)
|
|
91
|
-
|
|
92
125
|
statement = await self.query_count.build_query(relevant_params)
|
|
93
126
|
result = await smart_run(session.scalars, statement)
|
|
94
127
|
return result.one()
|
|
95
128
|
|
|
96
|
-
async def get_many(self, session, params=None):
|
|
129
|
+
async def get_many(self, session, params=None) -> list[ModelType]:
|
|
97
130
|
statement = await self.query.build_query(params)
|
|
98
131
|
result = await smart_run(session.scalars, statement)
|
|
99
132
|
return list(result.all())
|
|
100
133
|
|
|
101
|
-
async def get_one(self, session, params=None):
|
|
134
|
+
async def get_one(self, session, params=None) -> ModelType | None:
|
|
102
135
|
statement = await self.query.build_query(params)
|
|
103
136
|
result = await smart_run(session.scalars, statement)
|
|
104
137
|
return result.first()
|
|
105
138
|
|
|
106
|
-
async def yield_per(
|
|
139
|
+
async def yield_per(
|
|
140
|
+
self, session, params=None
|
|
141
|
+
) -> typing.AsyncGenerator[list[ModelType], None]:
|
|
107
142
|
relevant_params = params.copy() if params else DBQueryParams()
|
|
108
143
|
relevant_params.pop("page", None)
|
|
109
144
|
page_size = relevant_params.pop("page_size", 100)
|
|
110
|
-
|
|
111
145
|
statement = await self.query.build_query(relevant_params)
|
|
112
146
|
statement = statement.execution_options(stream_results=True)
|
|
113
147
|
if isinstance(session, AsyncSession):
|
|
@@ -124,30 +158,36 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
124
158
|
|
|
125
159
|
async def add(self, session, item, *, flush=True, commit=True, refresh=True):
|
|
126
160
|
await smart_run(session.add, item)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
async with AsyncTaskRunner():
|
|
162
|
+
if flush:
|
|
163
|
+
await smart_run(session.flush)
|
|
164
|
+
async with AsyncTaskRunner():
|
|
165
|
+
if commit:
|
|
166
|
+
await smart_run(session.commit)
|
|
131
167
|
if (flush or commit) and refresh:
|
|
132
168
|
await smart_run(session.refresh, item)
|
|
133
169
|
return item
|
|
134
170
|
|
|
135
171
|
async def edit(self, session, item, *, flush=True, commit=True, refresh=False):
|
|
136
|
-
result:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
result: ModelType = await smart_run(session.merge, item)
|
|
173
|
+
async with AsyncTaskRunner():
|
|
174
|
+
if flush:
|
|
175
|
+
await smart_run(session.flush)
|
|
176
|
+
async with AsyncTaskRunner():
|
|
177
|
+
if commit:
|
|
178
|
+
await smart_run(session.commit)
|
|
141
179
|
if (flush or commit) and refresh:
|
|
142
180
|
await smart_run(session.refresh, result)
|
|
143
181
|
return result
|
|
144
182
|
|
|
145
183
|
async def delete(self, session, item, *, flush=True, commit=True):
|
|
146
184
|
await smart_run(session.delete, item)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
185
|
+
async with AsyncTaskRunner():
|
|
186
|
+
if flush:
|
|
187
|
+
await smart_run(session.flush)
|
|
188
|
+
async with AsyncTaskRunner():
|
|
189
|
+
if commit:
|
|
190
|
+
await smart_run(session.commit)
|
|
151
191
|
|
|
152
192
|
"""
|
|
153
193
|
--------------------------------------------------------------------------------------------------------
|
|
@@ -173,17 +213,17 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
173
213
|
def get_edit_column_list(self):
|
|
174
214
|
return self.get_user_column_list()
|
|
175
215
|
|
|
176
|
-
def get_order_column_list(self, list_columns:
|
|
216
|
+
def get_order_column_list(self, list_columns: list[str], *, depth=0, max_depth=-1):
|
|
177
217
|
"""
|
|
178
218
|
Get all columns that can be used for ordering
|
|
179
219
|
|
|
180
220
|
Args:
|
|
181
|
-
list_columns (
|
|
221
|
+
list_columns (list[str]): Columns to be used for ordering.
|
|
182
222
|
depth (int, optional): Depth of the relation. Defaults to 0. Used for recursive calls.
|
|
183
223
|
max_depth (int, optional): Maximum depth of the relation. When set to -1, it will be ignored. Defaults to -1.
|
|
184
224
|
|
|
185
225
|
Returns:
|
|
186
|
-
|
|
226
|
+
list[str]: List of columns that can be used for ordering
|
|
187
227
|
"""
|
|
188
228
|
unique_order_columns: set[str] = set()
|
|
189
229
|
for col_name in list_columns:
|
|
@@ -215,7 +255,12 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
215
255
|
unique_order_columns.update(
|
|
216
256
|
[f"{col_name}.{sub_col}" for sub_col in sub_order_columns]
|
|
217
257
|
)
|
|
218
|
-
elif
|
|
258
|
+
elif (
|
|
259
|
+
self.is_property(col_name)
|
|
260
|
+
and not self.is_hybrid_property(col_name)
|
|
261
|
+
or self.is_files(col_name)
|
|
262
|
+
or self.is_images(col_name)
|
|
263
|
+
):
|
|
219
264
|
continue
|
|
220
265
|
|
|
221
266
|
# Allow the column to be used for ordering by default
|
|
@@ -312,12 +357,13 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
312
357
|
),
|
|
313
358
|
]
|
|
314
359
|
elif self.is_file(col) or self.is_image(col):
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
360
|
+
value_type = fastapi.UploadFile
|
|
361
|
+
annotated_str_type = typing.Annotated[
|
|
362
|
+
str | None, BeforeValidator(lambda x: None if x == "null" else x)
|
|
363
|
+
]
|
|
364
|
+
if self.is_files(col) or self.is_images(col):
|
|
365
|
+
value_type = list[value_type | annotated_str_type]
|
|
366
|
+
return value_type | annotated_str_type
|
|
321
367
|
elif self.is_date(col):
|
|
322
368
|
return date
|
|
323
369
|
elif self.is_datetime(col):
|
|
@@ -450,6 +496,22 @@ class SQLAInterface(AbstractInterface[Model, Session | AsyncSession, Column]):
|
|
|
450
496
|
except KeyError:
|
|
451
497
|
return False
|
|
452
498
|
|
|
499
|
+
def is_images(self, col_name):
|
|
500
|
+
try:
|
|
501
|
+
return is_sqla_type(
|
|
502
|
+
self.list_columns[col_name].type, ImageColumns
|
|
503
|
+
) or is_sqla_type(self.list_columns[col_name].type, JSONBImageColumns)
|
|
504
|
+
except KeyError:
|
|
505
|
+
return False
|
|
506
|
+
|
|
507
|
+
def is_files(self, col_name):
|
|
508
|
+
try:
|
|
509
|
+
return is_sqla_type(
|
|
510
|
+
self.list_columns[col_name].type, FileColumns
|
|
511
|
+
) or is_sqla_type(self.list_columns[col_name].type, JSONBFileColumns)
|
|
512
|
+
except KeyError:
|
|
513
|
+
return False
|
|
514
|
+
|
|
453
515
|
def is_image(self, col_name):
|
|
454
516
|
try:
|
|
455
517
|
return is_sqla_type(self.list_columns[col_name].type, ImageColumn)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import collections
|
|
3
2
|
import re
|
|
4
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
5
3
|
from typing import Any, Callable, Collection, Dict, Sequence, Set, Tuple
|
|
6
4
|
|
|
5
|
+
import sqlalchemy.ext.asyncio
|
|
7
6
|
from sqlalchemy import Connection, Engine, MetaData
|
|
8
7
|
from sqlalchemy import Table as SA_Table
|
|
9
8
|
from sqlalchemy.ext.declarative import declared_attr
|
|
@@ -12,6 +11,9 @@ from sqlalchemy.sql.schema import SchemaConst, SchemaItem
|
|
|
12
11
|
from sqlalchemy.util.typing import Literal
|
|
13
12
|
|
|
14
13
|
from ...bases.model import BasicModel
|
|
14
|
+
from ...const import DEFAULT_METADATA_KEY
|
|
15
|
+
from ...exceptions import FastAPIReactToolkitException
|
|
16
|
+
from ...utils import AsyncTaskRunner, smart_run_sync
|
|
15
17
|
|
|
16
18
|
__all__ = ["Model", "metadata", "metadatas", "Base"]
|
|
17
19
|
|
|
@@ -31,13 +33,13 @@ def camel_to_snake_case(name):
|
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
metadatas: dict[str, MetaData] = {
|
|
34
|
-
|
|
36
|
+
DEFAULT_METADATA_KEY: MetaData(),
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
cache_id_property: dict[str, list[str]] = {}
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
class Model(BasicModel, DeclarativeBase):
|
|
42
|
+
class Model(sqlalchemy.ext.asyncio.AsyncAttrs, BasicModel, DeclarativeBase):
|
|
41
43
|
"""
|
|
42
44
|
Use this class has the base for your models,
|
|
43
45
|
it will define your table names automatically
|
|
@@ -61,17 +63,21 @@ class Model(BasicModel, DeclarativeBase):
|
|
|
61
63
|
The bind key to use for this model. This allow you to use multiple databases. None means the default database. Default is None.
|
|
62
64
|
"""
|
|
63
65
|
|
|
64
|
-
metadata = metadatas[
|
|
66
|
+
metadata = metadatas[DEFAULT_METADATA_KEY]
|
|
65
67
|
|
|
66
68
|
def __init_subclass__(cls, **kw: Any) -> None:
|
|
67
69
|
# Set the bind key from the metadata __table__ if it exists
|
|
68
70
|
if hasattr(cls, "__table__"):
|
|
69
71
|
for key, metadata in metadatas.items():
|
|
70
|
-
if metadata is cls.__table__.metadata and key !=
|
|
72
|
+
if metadata is cls.__table__.metadata and key != DEFAULT_METADATA_KEY:
|
|
71
73
|
cls.__bind_key__ = key
|
|
72
74
|
|
|
73
75
|
# Overwrite the metadata if the bind key is set
|
|
74
76
|
if cls.__bind_key__:
|
|
77
|
+
if cls.__bind_key__ == DEFAULT_METADATA_KEY:
|
|
78
|
+
raise FastAPIReactToolkitException(
|
|
79
|
+
f"__bind_key__ cannot be '{DEFAULT_METADATA_KEY}', use None instead"
|
|
80
|
+
)
|
|
75
81
|
if cls.__bind_key__ not in metadatas:
|
|
76
82
|
metadatas[cls.__bind_key__] = MetaData()
|
|
77
83
|
cls.metadata = metadatas[cls.__bind_key__]
|
|
@@ -119,6 +125,21 @@ class Model(BasicModel, DeclarativeBase):
|
|
|
119
125
|
def get_pk_attrs(cls):
|
|
120
126
|
return [col.name for col in cls.__mapper__.primary_key]
|
|
121
127
|
|
|
128
|
+
async def load(self, *cols: list[str] | str):
|
|
129
|
+
"""
|
|
130
|
+
Asynchronously loads the specified columns of the model instance.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
*cols (list[str] | str): The columns to load. Can be a list of strings or individual string arguments.
|
|
134
|
+
"""
|
|
135
|
+
cols = [
|
|
136
|
+
item for col in cols for item in (col if isinstance(col, list) else [col])
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
async with AsyncTaskRunner() as runner:
|
|
140
|
+
for col in cols:
|
|
141
|
+
runner.add_task(getattr(self.awaitable_attrs, col))
|
|
142
|
+
|
|
122
143
|
|
|
123
144
|
class Table(SA_Table):
|
|
124
145
|
"""
|
|
@@ -160,39 +181,35 @@ class Table(SA_Table):
|
|
|
160
181
|
else:
|
|
161
182
|
db.init_db(autoload_with.engine.url)
|
|
162
183
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
future.result()
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
metadata = metadatas["default"]
|
|
184
|
+
smart_run_sync(
|
|
185
|
+
db.autoload_table,
|
|
186
|
+
lambda conn: SA_Table.__init__(
|
|
187
|
+
self,
|
|
188
|
+
name,
|
|
189
|
+
metadata,
|
|
190
|
+
*args,
|
|
191
|
+
schema=schema,
|
|
192
|
+
quote=quote,
|
|
193
|
+
quote_schema=quote_schema,
|
|
194
|
+
autoload_with=conn,
|
|
195
|
+
autoload_replace=autoload_replace,
|
|
196
|
+
keep_existing=keep_existing,
|
|
197
|
+
extend_existing=extend_existing,
|
|
198
|
+
resolve_fks=resolve_fks,
|
|
199
|
+
include_columns=include_columns,
|
|
200
|
+
implicit_returning=implicit_returning,
|
|
201
|
+
comment=comment,
|
|
202
|
+
info=info,
|
|
203
|
+
listeners=listeners,
|
|
204
|
+
prefixes=prefixes,
|
|
205
|
+
_extend_on=_extend_on,
|
|
206
|
+
_no_init=_no_init,
|
|
207
|
+
**kw,
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
metadata = metadatas[DEFAULT_METADATA_KEY]
|
|
196
213
|
|
|
197
214
|
|
|
198
215
|
"""
|