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
fastapi_rtk/bases/__init__.py
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .db import *
|
|
2
|
+
from .file_manager import *
|
|
3
|
+
from .filter import *
|
|
4
|
+
from .interface import *
|
|
5
|
+
from .model import *
|
|
6
|
+
from .session import *
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DBQueryParams",
|
|
10
|
+
"AbstractQueryBuilder",
|
|
11
|
+
"FileNotAllowedException",
|
|
12
|
+
"AbstractFileManager",
|
|
13
|
+
"AbstractImageManager",
|
|
14
|
+
"AbstractBaseFilter",
|
|
15
|
+
"AbstractBaseOprFilter",
|
|
16
|
+
"PydanticGenerationSchema",
|
|
17
|
+
"AbstractInterface",
|
|
18
|
+
"BasicModel",
|
|
19
|
+
"AbstractSession",
|
|
20
|
+
]
|
fastapi_rtk/bases/db.py
CHANGED
|
@@ -4,13 +4,17 @@ import typing
|
|
|
4
4
|
import fastapi
|
|
5
5
|
|
|
6
6
|
from ..const import logger
|
|
7
|
-
from ..
|
|
7
|
+
from ..exceptions import HTTPWithValidationException
|
|
8
|
+
from ..lang import translate
|
|
9
|
+
from ..schemas import PRIMARY_KEY, FilterSchema, OprFilterSchema
|
|
8
10
|
from ..utils import T, safe_call, use_default_when_none
|
|
9
|
-
from .filter import AbstractBaseFilter
|
|
11
|
+
from .filter import AbstractBaseFilter, AbstractBaseOprFilter
|
|
10
12
|
|
|
11
13
|
if typing.TYPE_CHECKING:
|
|
12
14
|
from .interface import AbstractInterface
|
|
13
15
|
|
|
16
|
+
__all__ = ["DBQueryParams", "AbstractQueryBuilder"]
|
|
17
|
+
|
|
14
18
|
BUILD_STEPS = typing.Literal[
|
|
15
19
|
"list_columns",
|
|
16
20
|
"page|page_size",
|
|
@@ -21,6 +25,8 @@ BUILD_STEPS = typing.Literal[
|
|
|
21
25
|
"where_id_in",
|
|
22
26
|
"filters",
|
|
23
27
|
"filter_classes",
|
|
28
|
+
"opr_filters",
|
|
29
|
+
"opr_filter_classes",
|
|
24
30
|
"global_filter",
|
|
25
31
|
]
|
|
26
32
|
DEFAULT_ORDER = [
|
|
@@ -33,6 +39,8 @@ DEFAULT_ORDER = [
|
|
|
33
39
|
"where_id_in",
|
|
34
40
|
"filters",
|
|
35
41
|
"filter_classes",
|
|
42
|
+
"opr_filters",
|
|
43
|
+
"opr_filter_classes",
|
|
36
44
|
"global_filter",
|
|
37
45
|
]
|
|
38
46
|
|
|
@@ -47,8 +55,16 @@ class DBQueryParams(typing.TypedDict):
|
|
|
47
55
|
where_in: tuple[str, list[typing.Any]] | None
|
|
48
56
|
where_id: PRIMARY_KEY | None
|
|
49
57
|
where_id_in: list[PRIMARY_KEY] | None
|
|
50
|
-
filters:
|
|
58
|
+
filters: (
|
|
59
|
+
list[FilterSchema | dict[str, typing.Any] | tuple[str, str, typing.Any] | list]
|
|
60
|
+
| None
|
|
61
|
+
)
|
|
51
62
|
filter_classes: list[tuple[str, AbstractBaseFilter, typing.Any]] | None
|
|
63
|
+
opr_filters: (
|
|
64
|
+
list[OprFilterSchema | dict[str, typing.Any] | tuple[str, typing.Any] | list]
|
|
65
|
+
| None
|
|
66
|
+
)
|
|
67
|
+
opr_filter_classes: list[tuple[AbstractBaseFilter, typing.Any]] | None
|
|
52
68
|
global_filter: tuple[list[str], str] | None
|
|
53
69
|
|
|
54
70
|
|
|
@@ -172,12 +188,34 @@ class AbstractQueryBuilder(abc.ABC, typing.Generic[T]):
|
|
|
172
188
|
)
|
|
173
189
|
elif step == "filters" and params.get("filters"):
|
|
174
190
|
for filter in params["filters"]:
|
|
191
|
+
if isinstance(filter, dict):
|
|
192
|
+
filter = FilterSchema(**filter)
|
|
193
|
+
elif isinstance(filter, (list, tuple)):
|
|
194
|
+
filter = FilterSchema(
|
|
195
|
+
col=filter[0], opr=filter[1], value=filter[2]
|
|
196
|
+
)
|
|
175
197
|
statement = await safe_call(self.apply_filter(statement, filter))
|
|
176
198
|
elif step == "filter_classes" and params.get("filter_classes"):
|
|
177
199
|
for filter_class in params["filter_classes"]:
|
|
178
200
|
statement = await safe_call(
|
|
179
201
|
self.apply_filter_class(statement, *filter_class)
|
|
180
202
|
)
|
|
203
|
+
elif step == "opr_filters" and params.get("opr_filters"):
|
|
204
|
+
for opr_filter in params["opr_filters"]:
|
|
205
|
+
if isinstance(opr_filter, dict):
|
|
206
|
+
opr_filter = OprFilterSchema(**opr_filter)
|
|
207
|
+
elif isinstance(opr_filter, (list, tuple)):
|
|
208
|
+
opr_filter = OprFilterSchema(
|
|
209
|
+
opr=opr_filter[0], value=opr_filter[1]
|
|
210
|
+
)
|
|
211
|
+
statement = await safe_call(
|
|
212
|
+
self.apply_opr_filter(statement, opr_filter)
|
|
213
|
+
)
|
|
214
|
+
elif step == "opr_filter_classes" and params.get("opr_filter_classes"):
|
|
215
|
+
for opr_filter_class in params["opr_filter_classes"]:
|
|
216
|
+
statement = await safe_call(
|
|
217
|
+
self.apply_opr_filter_class(statement, *opr_filter_class)
|
|
218
|
+
)
|
|
181
219
|
elif step == "global_filter" and params.get("global_filter"):
|
|
182
220
|
columns, global_filter = params["global_filter"]
|
|
183
221
|
statement = await safe_call(
|
|
@@ -310,6 +348,40 @@ class AbstractQueryBuilder(abc.ABC, typing.Generic[T]):
|
|
|
310
348
|
"""
|
|
311
349
|
raise NotImplementedError("This method should be implemented by subclasses.")
|
|
312
350
|
|
|
351
|
+
@abc.abstractmethod
|
|
352
|
+
async def apply_opr_filter(self, statement: T, filter: OprFilterSchema) -> T:
|
|
353
|
+
"""
|
|
354
|
+
Handles the filtering of the query based on an OprFilterSchema.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
statement (T): The SQLAlchemy query statement to modify.
|
|
358
|
+
filter (OprFilterSchema): The filter schema to apply without the column name.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
T: The modified SQLAlchemy query statement with the filter applied.
|
|
362
|
+
"""
|
|
363
|
+
raise NotImplementedError("This method should be implemented by subclasses.")
|
|
364
|
+
|
|
365
|
+
@abc.abstractmethod
|
|
366
|
+
async def apply_opr_filter_class(
|
|
367
|
+
self,
|
|
368
|
+
statement: T,
|
|
369
|
+
filter_class: AbstractBaseOprFilter[T] | AbstractBaseFilter[T],
|
|
370
|
+
value,
|
|
371
|
+
) -> T:
|
|
372
|
+
"""
|
|
373
|
+
Handles the filtering of the query using an operation filter class without a column name.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
statement (T): The SQLAlchemy query statement to modify.
|
|
377
|
+
filter_class (AbstractBaseFilter[T]): The filter class to apply.
|
|
378
|
+
value: The value to filter by.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
T: The modified SQLAlchemy query statement with the operation filter class applied.
|
|
382
|
+
"""
|
|
383
|
+
raise NotImplementedError("This method should be implemented by subclasses.")
|
|
384
|
+
|
|
313
385
|
@abc.abstractmethod
|
|
314
386
|
async def apply_global_filter(
|
|
315
387
|
self, statement: T, columns: list[str], global_filter: str
|
|
@@ -398,14 +470,29 @@ class AbstractQueryBuilder(abc.ABC, typing.Generic[T]):
|
|
|
398
470
|
# Assume the ID is a string, split the string to ','
|
|
399
471
|
id = id.split(",") if isinstance(id, str) else id
|
|
400
472
|
if len(id) != len(self.datamodel.get_pk_attrs()):
|
|
401
|
-
raise
|
|
402
|
-
|
|
403
|
-
|
|
473
|
+
raise HTTPWithValidationException(
|
|
474
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
475
|
+
"greater_than"
|
|
476
|
+
if len(id) < len(self.datamodel.get_pk_attrs())
|
|
477
|
+
else "less_than",
|
|
478
|
+
"path",
|
|
479
|
+
"id",
|
|
480
|
+
translate(
|
|
481
|
+
"Invalid ID: {id}, expected {count} values",
|
|
482
|
+
id=id,
|
|
483
|
+
count=len(self.datamodel.get_pk_attrs()),
|
|
484
|
+
),
|
|
404
485
|
)
|
|
405
486
|
for pk_key in self.datamodel.get_pk_attrs():
|
|
406
487
|
pk_dict[pk_key] = id.pop(0)
|
|
407
488
|
except Exception:
|
|
408
|
-
raise
|
|
489
|
+
raise HTTPWithValidationException(
|
|
490
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
491
|
+
"string_type",
|
|
492
|
+
"path",
|
|
493
|
+
"id",
|
|
494
|
+
translate("Invalid ID"),
|
|
495
|
+
)
|
|
409
496
|
else:
|
|
410
497
|
pk_dict[self.datamodel.get_pk_attr()] = id
|
|
411
498
|
|
|
@@ -3,7 +3,19 @@ import typing
|
|
|
3
3
|
|
|
4
4
|
import fastapi
|
|
5
5
|
|
|
6
|
-
from ..
|
|
6
|
+
from ..exceptions import FastAPIReactToolkitException
|
|
7
|
+
from ..utils import hooks, lazy, uuid_namegen
|
|
8
|
+
|
|
9
|
+
__all__ = ["FileNotAllowedException", "AbstractFileManager", "AbstractImageManager"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileNotAllowedException(FastAPIReactToolkitException):
|
|
13
|
+
"""
|
|
14
|
+
Exception raised when a file is not allowed based on its extension.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, filename: str):
|
|
18
|
+
super().__init__(f"File '{filename}' is not allowed.")
|
|
7
19
|
|
|
8
20
|
|
|
9
21
|
class AbstractFileManager(abc.ABC):
|
|
@@ -11,8 +23,8 @@ class AbstractFileManager(abc.ABC):
|
|
|
11
23
|
Abstract base class for file managers.
|
|
12
24
|
"""
|
|
13
25
|
|
|
14
|
-
base_path = None
|
|
15
|
-
allowed_extensions = None
|
|
26
|
+
base_path: str = None
|
|
27
|
+
allowed_extensions: list[str] = None
|
|
16
28
|
namegen = lazy(lambda: uuid_namegen)
|
|
17
29
|
permission = lazy(lambda: 0o755)
|
|
18
30
|
|
|
@@ -23,6 +35,18 @@ class AbstractFileManager(abc.ABC):
|
|
|
23
35
|
namegen: typing.Callable[[str], str] | None = None,
|
|
24
36
|
permission: int | None = None,
|
|
25
37
|
):
|
|
38
|
+
"""
|
|
39
|
+
Initializes the AbstractFileManager.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
base_path (str | None, optional): Base path for file storage. Defaults to None.
|
|
43
|
+
allowed_extensions (list[str] | None, optional): Allowed file extensions. Defaults to None.
|
|
44
|
+
namegen (typing.Callable[[str], str] | None, optional): Callable for generating file names. Defaults to None.
|
|
45
|
+
permission (int | None, optional): File permission settings. Defaults to None.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If `base_path` is not set.
|
|
49
|
+
"""
|
|
26
50
|
if base_path is not None:
|
|
27
51
|
self.base_path = base_path
|
|
28
52
|
if allowed_extensions is not None:
|
|
@@ -40,6 +64,26 @@ class AbstractFileManager(abc.ABC):
|
|
|
40
64
|
# Ensure base_path does not end with a slash
|
|
41
65
|
self.base_path = self.base_path.rstrip("/")
|
|
42
66
|
|
|
67
|
+
def __init_subclass__(cls):
|
|
68
|
+
# Add pre-hook to save_file and save_content_to_file to check if the file is allowed
|
|
69
|
+
def check_is_file_allowed(self, *args, **kwargs):
|
|
70
|
+
filename = None
|
|
71
|
+
if "filename" in kwargs:
|
|
72
|
+
filename = kwargs["filename"]
|
|
73
|
+
elif len(args) > 1:
|
|
74
|
+
filename = args[1]
|
|
75
|
+
if filename and not self.is_filename_allowed(filename):
|
|
76
|
+
raise FileNotAllowedException(filename)
|
|
77
|
+
|
|
78
|
+
if cls.save_file is not AbstractFileManager.save_file:
|
|
79
|
+
wrapped_save_file = hooks(pre=check_is_file_allowed)(cls.save_file)
|
|
80
|
+
cls.save_file = wrapped_save_file
|
|
81
|
+
if cls.save_content_to_file is not AbstractFileManager.save_content_to_file:
|
|
82
|
+
wrapped_save_content_to_file = hooks(pre=check_is_file_allowed)(
|
|
83
|
+
cls.save_content_to_file
|
|
84
|
+
)
|
|
85
|
+
cls.save_content_to_file = wrapped_save_content_to_file
|
|
86
|
+
|
|
43
87
|
"""
|
|
44
88
|
--------------------------------------------------------------------------------------------------------
|
|
45
89
|
CRUD METHODS - to be implemented
|
fastapi_rtk/bases/filter.py
CHANGED
|
@@ -6,6 +6,8 @@ from ..utils import T
|
|
|
6
6
|
if typing.TYPE_CHECKING:
|
|
7
7
|
from .interface import AbstractInterface
|
|
8
8
|
|
|
9
|
+
__all__ = ["AbstractBaseFilter", "AbstractBaseOprFilter"]
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class AbstractBaseFilter(abc.ABC, typing.Generic[T]):
|
|
11
13
|
"""
|
|
@@ -41,3 +43,23 @@ class AbstractBaseFilter(abc.ABC, typing.Generic[T]):
|
|
|
41
43
|
T: The modified SQLAlchemy query statement.
|
|
42
44
|
"""
|
|
43
45
|
raise NotImplementedError("Subclasses must implement this method.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AbstractBaseOprFilter(AbstractBaseFilter[T]):
|
|
49
|
+
"""
|
|
50
|
+
Abstract base class to apply filters to select statements without a column name.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
def apply(self, statement: T, value) -> T:
|
|
55
|
+
"""
|
|
56
|
+
Apply the filter to the given SQLAlchemy Select statement.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
statement (T): The SQLAlchemy query statement to modify.
|
|
60
|
+
value: The value to filter the column by.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
T: The modified SQLAlchemy query statement.
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
fastapi_rtk/bases/interface.py
CHANGED
|
@@ -12,7 +12,8 @@ from ..globals import g
|
|
|
12
12
|
from ..schemas import PRIMARY_KEY, ColumnEnumInfo, ColumnInfo, ColumnRelationInfo
|
|
13
13
|
from ..utils import AsyncTaskRunner, C, T, deep_merge, lazy_self, smart_run
|
|
14
14
|
from .db import DBQueryParams
|
|
15
|
-
from .
|
|
15
|
+
from .file_manager import AbstractFileManager, AbstractImageManager
|
|
16
|
+
from .filter import AbstractBaseFilter, AbstractBaseOprFilter
|
|
16
17
|
|
|
17
18
|
CT = typing.TypeVar("CT")
|
|
18
19
|
|
|
@@ -23,6 +24,8 @@ if typing.TYPE_CHECKING:
|
|
|
23
24
|
|
|
24
25
|
filter_converter_type = SQLAFilterConverter
|
|
25
26
|
|
|
27
|
+
__all__ = ["PydanticGenerationSchema", "AbstractInterface"]
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
class PydanticGenerationSchema(typing.TypedDict):
|
|
28
31
|
"""
|
|
@@ -80,6 +83,22 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
80
83
|
"""
|
|
81
84
|
Filter class to be used for global filtering.
|
|
82
85
|
"""
|
|
86
|
+
file_manager = lazy(
|
|
87
|
+
lambda: raise_exception(
|
|
88
|
+
"file_manager must be set in the subclass", AbstractFileManager
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
"""
|
|
92
|
+
The file manager instance for the datamodel.
|
|
93
|
+
"""
|
|
94
|
+
image_manager = lazy(
|
|
95
|
+
lambda: raise_exception(
|
|
96
|
+
"image_manager must be set in the subclass", AbstractImageManager
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
"""
|
|
100
|
+
The image manager instance for the datamodel.
|
|
101
|
+
"""
|
|
83
102
|
|
|
84
103
|
id_schema = lazy(lambda self: self.init_id_schema())
|
|
85
104
|
"""
|
|
@@ -113,6 +132,10 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
113
132
|
|
|
114
133
|
Automatically filled by `ModelRestApi`.
|
|
115
134
|
"""
|
|
135
|
+
opr_filters = lazy(lambda: list[AbstractBaseOprFilter | AbstractBaseFilter]())
|
|
136
|
+
"""
|
|
137
|
+
A list of filters to apply to the model when filtering without a specific column.
|
|
138
|
+
"""
|
|
116
139
|
|
|
117
140
|
_cache_schema: dict[str, typing.Type[pydantic.BaseModel]] = {}
|
|
118
141
|
|
|
@@ -480,6 +503,12 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
480
503
|
def is_unique(self, col_name: str) -> bool:
|
|
481
504
|
return False
|
|
482
505
|
|
|
506
|
+
def is_images(self, col_name: str) -> bool:
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
def is_files(self, col_name: str) -> bool:
|
|
510
|
+
return False
|
|
511
|
+
|
|
483
512
|
def is_image(self, col_name: str) -> bool:
|
|
484
513
|
return False
|
|
485
514
|
|
|
@@ -848,6 +877,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
848
877
|
optional=False,
|
|
849
878
|
name="",
|
|
850
879
|
hide_sensitive_columns=True,
|
|
880
|
+
is_form_data=False,
|
|
851
881
|
related_kwargs: dict[str, typing.Any] | None = None,
|
|
852
882
|
):
|
|
853
883
|
"""
|
|
@@ -860,7 +890,8 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
860
890
|
with_property (bool, optional): Whether to include @property columns. Defaults to True.
|
|
861
891
|
optional (bool, optional): Whether the columns should be optional. Defaults to False.
|
|
862
892
|
name (str, optional): The name of the schema. If not specified, the name is generated based on the object's name and the specified options. Defaults to ''.
|
|
863
|
-
hide_sensitive_columns (bool, optional): Whether to hide sensitive columns such as `password`. Defaults to True.
|
|
893
|
+
hide_sensitive_columns (bool, optional): Whether to hide sensitive columns such as `password`. The sensitive columns are retrieved from `g.sensitive_data`. Defaults to True.
|
|
894
|
+
is_form_data (bool, optional): Whether the schema is for form data. Defaults to False.
|
|
864
895
|
related_kwargs (dict[str, typing.Any] | None, optional): Additional keyword arguments for schema generation of the related models. The options are the same as this function with `with_fk`. Defaults to None.
|
|
865
896
|
|
|
866
897
|
Returns:
|
|
@@ -887,6 +918,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
887
918
|
optional=optional,
|
|
888
919
|
name=name,
|
|
889
920
|
hide_sensitive_columns=hide_sensitive_columns,
|
|
921
|
+
is_form_data=is_form_data,
|
|
890
922
|
related_kwargs=related_kwargs,
|
|
891
923
|
)
|
|
892
924
|
)
|
|
@@ -922,7 +954,8 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
922
954
|
return self
|
|
923
955
|
|
|
924
956
|
decorated_func = pydantic.model_validator(mode="after")(fill_columns)
|
|
925
|
-
schema_dict["__validators__"] =
|
|
957
|
+
schema_dict["__validators__"] = schema_dict.get("__validators__", {})
|
|
958
|
+
schema_dict["__validators__"][fill_columns.__name__] = decorated_func
|
|
926
959
|
|
|
927
960
|
if self.is_pk_composite():
|
|
928
961
|
self.id_schema = str
|
|
@@ -963,6 +996,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
963
996
|
optional: bool,
|
|
964
997
|
name: str,
|
|
965
998
|
hide_sensitive_columns: bool,
|
|
999
|
+
is_form_data: bool,
|
|
966
1000
|
related_kwargs: dict[str, typing.Any] | None = None,
|
|
967
1001
|
):
|
|
968
1002
|
"""
|
|
@@ -975,7 +1009,8 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
975
1009
|
with_property (bool): Whether to include @property columns.
|
|
976
1010
|
optional (bool): Whether the columns should be optional.
|
|
977
1011
|
name (str): The name of the schema. If not specified, the name is generated based on the object's name and the specified options.
|
|
978
|
-
hide_sensitive_columns (bool): Whether to hide sensitive columns such as `password`.
|
|
1012
|
+
hide_sensitive_columns (bool): Whether to hide sensitive columns such as `password`. The sensitive columns are retrieved from `g.sensitive_data`.
|
|
1013
|
+
is_form_data (bool): Whether the schema is for form data.
|
|
979
1014
|
related_kwargs (dict[str, typing.Any] | None, optional): Additional keyword arguments for schema generation of the related models. The options are the same as this function with `with_fk`. Defaults to None.
|
|
980
1015
|
|
|
981
1016
|
Returns:
|
|
@@ -1071,6 +1106,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
1071
1106
|
hide_sensitive_columns=sub_hide_sensitive_columns,
|
|
1072
1107
|
),
|
|
1073
1108
|
hide_sensitive_columns=sub_hide_sensitive_columns,
|
|
1109
|
+
is_form_data=is_form_data,
|
|
1074
1110
|
related_kwargs=related_kwargs,
|
|
1075
1111
|
),
|
|
1076
1112
|
)
|
|
@@ -1095,6 +1131,14 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
1095
1131
|
if self.is_nullable(col) or optional:
|
|
1096
1132
|
params["default"] = None
|
|
1097
1133
|
type = type | None
|
|
1134
|
+
if is_form_data:
|
|
1135
|
+
type = (
|
|
1136
|
+
typing.Annotated[
|
|
1137
|
+
typing.Literal["null"],
|
|
1138
|
+
pydantic.AfterValidator(lambda _: None),
|
|
1139
|
+
]
|
|
1140
|
+
| type
|
|
1141
|
+
)
|
|
1098
1142
|
# if self.get_max_length(col) != -1:
|
|
1099
1143
|
# params["max_length"] = self.get_max_length(col)
|
|
1100
1144
|
current_schema[col] = (type, pydantic.Field(**params))
|
|
@@ -1120,7 +1164,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
1120
1164
|
with_name (bool, optional): Whether to include the name_ column. Defaults to False.
|
|
1121
1165
|
with_property (bool, optional): Whether to include @property columns. Defaults to False.
|
|
1122
1166
|
optional (bool, optional): Whether the columns should be optional. Defaults to False.
|
|
1123
|
-
hide_sensitive_columns (bool, optional): Whether to hide sensitive columns such as `password`. Defaults to True.
|
|
1167
|
+
hide_sensitive_columns (bool, optional): Whether to hide sensitive columns such as `password`. The sensitive columns are retrieved from `g.sensitive_data`. Defaults to True.
|
|
1124
1168
|
|
|
1125
1169
|
Returns:
|
|
1126
1170
|
str: The generated name for the schema.
|
fastapi_rtk/bases/model.py
CHANGED
fastapi_rtk/bases/session.py
CHANGED
fastapi_rtk/cli/cli.py
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import typing
|
|
3
|
+
import zipfile
|
|
1
4
|
from pathlib import Path
|
|
2
5
|
from typing import Annotated, Union
|
|
3
6
|
|
|
7
|
+
import httpx
|
|
4
8
|
import typer
|
|
5
9
|
|
|
6
|
-
from .commands import path_callback, version_callback
|
|
7
|
-
from .db import db_app
|
|
8
|
-
from .export import export_app
|
|
9
|
-
from .security import security_app
|
|
10
|
-
|
|
11
10
|
app = typer.Typer(rich_markup_mode="rich")
|
|
12
|
-
|
|
13
|
-
app.add_typer(db_app, name="db")
|
|
14
|
-
app.add_typer(security_app, name="security")
|
|
15
|
-
app.add_typer(export_app, name="export")
|
|
11
|
+
from .commands import path_callback, version_callback # noqa: E402
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
@app.callback()
|
|
@@ -38,5 +34,62 @@ def callback(
|
|
|
38
34
|
"""
|
|
39
35
|
|
|
40
36
|
|
|
37
|
+
@app.command()
|
|
38
|
+
def create_app(
|
|
39
|
+
directory: typing.Annotated[
|
|
40
|
+
str,
|
|
41
|
+
typer.Option(
|
|
42
|
+
...,
|
|
43
|
+
help="The directory where the new FastAPI RTK application will be created.",
|
|
44
|
+
),
|
|
45
|
+
] = ".",
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Create a new FastAPI RTK application with a predefined structure from https://codeberg.org/datatactics/fastapi-rtk-skeleton.
|
|
49
|
+
|
|
50
|
+
This command sets up the necessary files and directories for a FastAPI RTK project,
|
|
51
|
+
allowing you to get started quickly with development.
|
|
52
|
+
"""
|
|
53
|
+
with httpx.Client() as client:
|
|
54
|
+
url = "https://codeberg.org/datatactics/fastapi-rtk-skeleton/archive/main.zip"
|
|
55
|
+
response = client.get(url)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
|
|
58
|
+
# Unzip the content into the specified directory
|
|
59
|
+
with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref:
|
|
60
|
+
# Get all members (files/folders) in the zip
|
|
61
|
+
members = zip_ref.namelist()
|
|
62
|
+
|
|
63
|
+
# Find the top-level directory name
|
|
64
|
+
top_level_dir = members[0].split("/")[0] + "/"
|
|
65
|
+
|
|
66
|
+
# Extract each file, stripping the top-level directory
|
|
67
|
+
for member in members:
|
|
68
|
+
# Skip the top-level directory itself
|
|
69
|
+
if member == top_level_dir:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# Get the path without the top-level directory
|
|
73
|
+
target_path = member[len(top_level_dir) :]
|
|
74
|
+
|
|
75
|
+
# Skip if empty (this was the root folder)
|
|
76
|
+
if not target_path:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Get the source file
|
|
80
|
+
source = zip_ref.open(member)
|
|
81
|
+
target_file = Path(directory) / target_path
|
|
82
|
+
|
|
83
|
+
# Create directories if needed
|
|
84
|
+
if member.endswith("/"):
|
|
85
|
+
target_file.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
else:
|
|
87
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
with open(target_file, "wb") as f:
|
|
89
|
+
f.write(source.read())
|
|
90
|
+
|
|
91
|
+
typer.echo(f"FastAPI RTK application skeleton created in {directory}")
|
|
92
|
+
|
|
93
|
+
|
|
41
94
|
def main():
|
|
42
95
|
app()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Import every modules in this directory
|
|
2
|
+
import importlib
|
|
3
|
+
import pathlib
|
|
4
|
+
import pkgutil
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ...globals import g
|
|
9
|
+
from ...version import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def version_callback(value: bool) -> None:
|
|
13
|
+
if value:
|
|
14
|
+
print(f"FastAPI-RTK CLI version: [green]{__version__}[/green]")
|
|
15
|
+
raise typer.Exit()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def path_callback(value: pathlib.Path | None) -> None:
|
|
19
|
+
g.path = value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
|
|
23
|
+
importlib.import_module(f"{__name__}.{module_name}")
|