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.
Files changed (98) hide show
  1. fastapi_rtk/__init__.py +39 -35
  2. fastapi_rtk/_version.py +1 -0
  3. fastapi_rtk/api/model_rest_api.py +476 -221
  4. fastapi_rtk/auth/auth.py +0 -9
  5. fastapi_rtk/backends/generic/__init__.py +6 -0
  6. fastapi_rtk/backends/generic/column.py +21 -12
  7. fastapi_rtk/backends/generic/db.py +42 -7
  8. fastapi_rtk/backends/generic/filters.py +21 -16
  9. fastapi_rtk/backends/generic/interface.py +14 -8
  10. fastapi_rtk/backends/generic/model.py +19 -11
  11. fastapi_rtk/backends/sqla/__init__.py +1 -0
  12. fastapi_rtk/backends/sqla/db.py +77 -17
  13. fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
  14. fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
  15. fastapi_rtk/backends/sqla/filters.py +50 -21
  16. fastapi_rtk/backends/sqla/interface.py +96 -34
  17. fastapi_rtk/backends/sqla/model.py +56 -39
  18. fastapi_rtk/bases/__init__.py +20 -0
  19. fastapi_rtk/bases/db.py +94 -7
  20. fastapi_rtk/bases/file_manager.py +47 -3
  21. fastapi_rtk/bases/filter.py +22 -0
  22. fastapi_rtk/bases/interface.py +49 -5
  23. fastapi_rtk/bases/model.py +3 -0
  24. fastapi_rtk/bases/session.py +2 -0
  25. fastapi_rtk/cli/cli.py +62 -9
  26. fastapi_rtk/cli/commands/__init__.py +23 -0
  27. fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
  28. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
  29. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
  30. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
  31. fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
  32. fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
  33. fastapi_rtk/cli/commands/translate.py +299 -0
  34. fastapi_rtk/cli/decorators.py +9 -4
  35. fastapi_rtk/cli/utils.py +46 -0
  36. fastapi_rtk/config.py +41 -1
  37. fastapi_rtk/const.py +29 -1
  38. fastapi_rtk/db.py +76 -40
  39. fastapi_rtk/decorators.py +1 -1
  40. fastapi_rtk/dependencies.py +134 -62
  41. fastapi_rtk/exceptions.py +51 -1
  42. fastapi_rtk/fastapi_react_toolkit.py +186 -171
  43. fastapi_rtk/file_managers/file_manager.py +8 -6
  44. fastapi_rtk/file_managers/s3_file_manager.py +69 -33
  45. fastapi_rtk/globals.py +22 -12
  46. fastapi_rtk/lang/__init__.py +3 -0
  47. fastapi_rtk/lang/babel/__init__.py +4 -0
  48. fastapi_rtk/lang/babel/cli.py +40 -0
  49. fastapi_rtk/lang/babel/config.py +17 -0
  50. fastapi_rtk/lang/babel.cfg +1 -0
  51. fastapi_rtk/lang/lazy_text.py +120 -0
  52. fastapi_rtk/lang/messages.pot +238 -0
  53. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  54. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
  55. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  56. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
  57. fastapi_rtk/manager.py +355 -37
  58. fastapi_rtk/mixins.py +12 -0
  59. fastapi_rtk/routers.py +208 -72
  60. fastapi_rtk/schemas.py +142 -39
  61. fastapi_rtk/security/sqla/apis.py +39 -13
  62. fastapi_rtk/security/sqla/models.py +8 -23
  63. fastapi_rtk/security/sqla/security_manager.py +369 -11
  64. fastapi_rtk/setting.py +446 -88
  65. fastapi_rtk/types.py +94 -27
  66. fastapi_rtk/utils/__init__.py +8 -0
  67. fastapi_rtk/utils/async_task_runner.py +286 -61
  68. fastapi_rtk/utils/csv_json_converter.py +243 -40
  69. fastapi_rtk/utils/hooks.py +34 -0
  70. fastapi_rtk/utils/merge_schema.py +3 -3
  71. fastapi_rtk/utils/multiple_async_contexts.py +21 -0
  72. fastapi_rtk/utils/pydantic.py +46 -1
  73. fastapi_rtk/utils/run_utils.py +31 -1
  74. fastapi_rtk/utils/self_dependencies.py +1 -1
  75. fastapi_rtk/utils/use_default_when_none.py +1 -1
  76. fastapi_rtk/version.py +6 -1
  77. fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
  78. fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
  79. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
  80. fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
  81. fastapi_rtk/backends/gremlinpython/column.py +0 -208
  82. fastapi_rtk/backends/gremlinpython/db.py +0 -228
  83. fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
  84. fastapi_rtk/backends/gremlinpython/filters.py +0 -461
  85. fastapi_rtk/backends/gremlinpython/interface.py +0 -734
  86. fastapi_rtk/backends/gremlinpython/model.py +0 -364
  87. fastapi_rtk/backends/gremlinpython/session.py +0 -23
  88. fastapi_rtk/cli/commands.py +0 -295
  89. fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
  90. fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
  91. fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
  92. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
  93. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
  94. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
  95. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
  96. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
  97. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
  98. {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
- from fastapi import HTTPException
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
- detail = {
36
- "type": type(value).__name__,
37
- "loc": ["query", "filters", {"col": col, "opr": self.arg_name}],
38
- "msg": f"Value error, {e}",
39
- "input": value,
40
- }
41
- raise HTTPException(status_code=422, detail=[detail])
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 Equal to"
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 HTTPException(400, "Between filter requires two values")
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, List, Literal, Type
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 FileColumn, ImageColumn
30
- from ...utils import T, is_sqla_type, lazy_import, lazy_self, safe_call, smart_run
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[Model, Session | AsyncSession, Column]):
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(self, session, params=None):
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
- if flush:
128
- await smart_run(session.flush)
129
- if commit:
130
- await smart_run(session.commit)
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: Model = await smart_run(session.merge, item)
137
- if flush:
138
- await smart_run(session.flush)
139
- if commit:
140
- await smart_run(session.commit)
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
- if flush:
148
- await smart_run(session.flush)
149
- if commit:
150
- await smart_run(session.commit)
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: List[str], *, depth=0, max_depth=-1):
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 (List[str]): Columns to be used for ordering.
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
- List[str]: List of columns that can be used for ordering
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 self.is_property(col_name) and not self.is_hybrid_property(col_name):
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
- return (
316
- fastapi.UploadFile
317
- | typing.Annotated[
318
- str | None, BeforeValidator(lambda x: None if x == "null" else x)
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
- "default": MetaData(),
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["default"]
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 != "default":
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
- with ThreadPoolExecutor() as executor:
164
- future = executor.submit(
165
- asyncio.run,
166
- db.autoload_table(
167
- lambda conn: SA_Table.__init__(
168
- self,
169
- name,
170
- metadata,
171
- *args,
172
- schema=schema,
173
- quote=quote,
174
- quote_schema=quote_schema,
175
- autoload_with=conn,
176
- autoload_replace=autoload_replace,
177
- keep_existing=keep_existing,
178
- extend_existing=extend_existing,
179
- resolve_fks=resolve_fks,
180
- include_columns=include_columns,
181
- implicit_returning=implicit_returning,
182
- comment=comment,
183
- info=info,
184
- listeners=listeners,
185
- prefixes=prefixes,
186
- _extend_on=_extend_on,
187
- _no_init=_no_init,
188
- **kw,
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
  """