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
@@ -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 ..schemas import PRIMARY_KEY, FilterSchema
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: list[FilterSchema] | None
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 fastapi.HTTPException(
402
- status_code=400,
403
- detail=f"Invalid ID: {id}, expected {len(self.datamodel.get_pk_attrs())} values",
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 fastapi.HTTPException(status_code=400, detail="Invalid ID")
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 ..utils import lazy, uuid_namegen
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
@@ -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.")
@@ -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 .filter import AbstractBaseFilter
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__"] = {fill_columns.__name__: decorated_func}
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.
@@ -1,3 +1,6 @@
1
+ __all__ = ["BasicModel"]
2
+
3
+
1
4
  class BasicModel:
2
5
  """
3
6
  A basic model class that provides a method to update the model instance with the given data.
@@ -1,5 +1,7 @@
1
1
  import abc
2
2
 
3
+ __all__ = ["AbstractSession"]
4
+
3
5
 
4
6
  class AbstractSession(abc.ABC):
5
7
  """
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}")