fastapi-rtk 1.0.18__tar.gz → 1.0.20__tar.gz

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 (134) hide show
  1. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/PKG-INFO +1 -1
  2. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/__init__.py +4 -1
  3. fastapi_rtk-1.0.20/fastapi_rtk/_version.py +1 -0
  4. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/api/model_rest_api.py +195 -117
  5. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/__init__.py +2 -0
  6. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/file_manager.py +82 -10
  7. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/db.py +8 -8
  8. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/file_managers/file_manager.py +1 -0
  9. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/file_managers/image_manager.py +1 -0
  10. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/file_managers/s3_file_manager.py +30 -13
  11. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/file_managers/s3_image_manager.py +5 -0
  12. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/messages.pot +33 -28
  13. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  14. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +33 -26
  15. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  16. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +31 -26
  17. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/setting.py +8 -0
  18. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/__init__.py +4 -3
  19. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/async_task_runner.py +27 -2
  20. fastapi_rtk-1.0.18/fastapi_rtk/utils/prettify_dict.py → fastapi_rtk-1.0.20/fastapi_rtk/utils/formatter.py +23 -1
  21. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/run_utils.py +3 -2
  22. fastapi_rtk-1.0.18/fastapi_rtk/_version.py +0 -1
  23. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/.gitignore +0 -0
  24. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/LICENSE +0 -0
  25. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/README.md +0 -0
  26. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/api/__init__.py +0 -0
  27. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/api/base_api.py +0 -0
  28. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/apis.py +0 -0
  29. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/__init__.py +0 -0
  30. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/auth.py +0 -0
  31. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/hashers/__init__.py +0 -0
  32. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
  33. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
  34. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/hashers/utils.py +0 -0
  35. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
  36. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
  37. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/strategies/__init__.py +0 -0
  38. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/strategies/config.py +0 -0
  39. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/strategies/db.py +0 -0
  40. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/auth/strategies/jwt.py +0 -0
  41. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/__init__.py +0 -0
  42. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/__init__.py +0 -0
  43. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/column.py +0 -0
  44. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/db.py +0 -0
  45. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/exceptions.py +0 -0
  46. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/filters.py +0 -0
  47. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/interface.py +0 -0
  48. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/model.py +0 -0
  49. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/generic/session.py +0 -0
  50. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/__init__.py +0 -0
  51. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/column.py +0 -0
  52. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/db.py +0 -0
  53. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
  54. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
  55. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
  56. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
  57. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
  58. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
  59. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
  60. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
  61. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/filters.py +0 -0
  62. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/interface.py +0 -0
  63. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/model.py +0 -0
  64. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/backends/sqla/session.py +0 -0
  65. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/db.py +0 -0
  66. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/filter.py +0 -0
  67. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/interface.py +0 -0
  68. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/model.py +0 -0
  69. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/bases/session.py +0 -0
  70. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/__init__.py +0 -0
  71. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/cli.py +0 -0
  72. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/__init__.py +0 -0
  73. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
  74. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
  75. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
  76. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
  77. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
  78. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
  79. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
  80. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
  81. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
  82. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/export.py +0 -0
  83. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/security.py +0 -0
  84. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/commands/translate.py +0 -0
  85. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/const.py +0 -0
  86. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/decorators.py +0 -0
  87. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/types.py +0 -0
  88. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/cli/utils.py +0 -0
  89. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/config.py +0 -0
  90. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/const.py +0 -0
  91. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/decorators.py +0 -0
  92. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/dependencies.py +0 -0
  93. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/exceptions.py +0 -0
  94. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
  95. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/file_managers/__init__.py +0 -0
  96. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/filters.py +0 -0
  97. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/globals.py +0 -0
  98. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/__init__.py +0 -0
  99. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/babel/__init__.py +0 -0
  100. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/babel/cli.py +0 -0
  101. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/babel/config.py +0 -0
  102. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/babel.cfg +0 -0
  103. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/lang/lazy_text.py +0 -0
  104. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/manager.py +0 -0
  105. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/middlewares.py +0 -0
  106. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/mixins.py +0 -0
  107. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/models.py +0 -0
  108. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/routers.py +0 -0
  109. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/schemas.py +0 -0
  110. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/security/__init__.py +0 -0
  111. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/security/sqla/__init__.py +0 -0
  112. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/security/sqla/apis.py +0 -0
  113. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/security/sqla/models.py +0 -0
  114. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/security/sqla/security_manager.py +0 -0
  115. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/types.py +0 -0
  116. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/class_factory.py +0 -0
  117. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/csv_json_converter.py +0 -0
  118. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/deep_merge.py +0 -0
  119. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/extender_mixin.py +0 -0
  120. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
  121. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/hooks.py +0 -0
  122. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/lazy.py +0 -0
  123. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/merge_schema.py +0 -0
  124. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
  125. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/pydantic.py +0 -0
  126. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/self_dependencies.py +0 -0
  127. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
  128. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/sqla.py +0 -0
  129. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/timezone.py +0 -0
  130. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/update_signature.py +0 -0
  131. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/use_default_when_none.py +0 -0
  132. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/utils/werkzeug.py +0 -0
  133. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/fastapi_rtk/version.py +0 -0
  134. {fastapi_rtk-1.0.18 → fastapi_rtk-1.0.20}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rtk
3
- Version: 1.0.18
3
+ Version: 1.0.20
4
4
  Summary: A package that provides a set of tools to build a FastAPI application with a Class-Based CRUD API.
5
5
  Project-URL: Homepage, https://codeberg.org/datatactics/fastapi-rtk
6
6
  Project-URL: Issues, https://codeberg.org/datatactics/fastapi-rtk/issues
@@ -130,7 +130,9 @@ __all__ = [
130
130
  # .bases
131
131
  "DBQueryParams",
132
132
  "AbstractQueryBuilder",
133
+ "BaseFileException",
133
134
  "FileNotAllowedException",
135
+ "FileTooLargeException",
134
136
  "AbstractFileManager",
135
137
  "AbstractImageManager",
136
138
  "AbstractBaseFilter",
@@ -231,13 +233,14 @@ __all__ = [
231
233
  "ExtenderMixin",
232
234
  "uuid_namegen",
233
235
  "secure_filename",
236
+ "prettify_dict",
237
+ "format_file_size",
234
238
  "hooks",
235
239
  "lazy",
236
240
  "lazy_import",
237
241
  "lazy_self",
238
242
  "merge_schema",
239
243
  "multiple_async_contexts",
240
- "prettify_dict",
241
244
  "generate_schema_from_typed_dict",
242
245
  "get_pydantic_model_field",
243
246
  "smart_run",
@@ -0,0 +1 @@
1
+ __version__ = "1.0.20"
@@ -47,6 +47,7 @@ from ..utils import (
47
47
  SelfType,
48
48
  T,
49
49
  deep_merge,
50
+ format_file_size,
50
51
  lazy_self,
51
52
  merge_schema,
52
53
  smart_run,
@@ -1681,29 +1682,36 @@ class ModelRestApi(BaseApi):
1681
1682
  If you are overriding this method, make sure to copy all the decorators too.
1682
1683
  """
1683
1684
  async with AsyncTaskRunner():
1684
- async with AsyncTaskRunner():
1685
- body_json = await smart_run(
1686
- self._process_body,
1687
- session,
1688
- body,
1689
- self.add_query_rel_fields,
1690
- self.add_schema_extra_fields.keys()
1691
- if self.add_schema_extra_fields
1692
- else None,
1693
- )
1694
- item = self.datamodel.obj(**body_json)
1695
- pre_add = await smart_run(
1696
- self.pre_add,
1697
- item,
1698
- PARAM_BODY_SESSION(body=body, session=session),
1699
- )
1700
- if pre_add is not None:
1701
- if isinstance(pre_add, Model):
1702
- item = pre_add
1703
- else:
1704
- AsyncTaskRunner.remove_tasks_by_tag("file")
1705
- return pre_add
1685
+ async with AsyncTaskRunner(
1686
+ "after_commit", run_tasks_even_if_exception=True
1687
+ ) as after_commit_runner:
1688
+ async with AsyncTaskRunner("before_commit") as before_commit_runner:
1689
+ body_json = await smart_run(
1690
+ self._process_body,
1691
+ session,
1692
+ body,
1693
+ self.add_query_rel_fields,
1694
+ self.add_schema_extra_fields.keys()
1695
+ if self.add_schema_extra_fields
1696
+ else None,
1697
+ )
1698
+ item = self.datamodel.obj(**body_json)
1699
+ pre_add = await smart_run(
1700
+ self.pre_add,
1701
+ item,
1702
+ PARAM_BODY_SESSION(body=body, session=session),
1703
+ )
1704
+ if pre_add is not None:
1705
+ if isinstance(pre_add, Model):
1706
+ item = pre_add
1707
+ else:
1708
+ before_commit_runner.remove_tasks_by_tag("file")
1709
+ after_commit_runner.remove_tasks_by_tag("file")
1710
+ return pre_add
1706
1711
  item = await smart_run(self.datamodel.add, session, item)
1712
+ after_commit_runner.remove_tasks_by_tag(
1713
+ "file"
1714
+ ) # Delete any file tasks scheduled to revert files on error
1707
1715
  post_add = await smart_run(
1708
1716
  self.post_add,
1709
1717
  item,
@@ -1739,50 +1747,57 @@ class ModelRestApi(BaseApi):
1739
1747
  If you are overriding this method, make sure to copy all the decorators too.
1740
1748
  """
1741
1749
  async with AsyncTaskRunner():
1742
- async with AsyncTaskRunner():
1743
- item = await smart_run(
1744
- self.datamodel.get_one,
1745
- session,
1746
- params={
1747
- "list_columns": self.show_select_columns,
1748
- "where_id": id,
1749
- "filter_classes": self.base_filters,
1750
- "opr_filter_classes": self.base_opr_filters,
1751
- },
1752
- )
1753
- if not item:
1754
- raise HTTPException(
1755
- fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
1750
+ async with AsyncTaskRunner(
1751
+ "after_commit", run_tasks_even_if_exception=True
1752
+ ) as after_commit_runner:
1753
+ async with AsyncTaskRunner("before_commit") as before_commit_runner:
1754
+ item = await smart_run(
1755
+ self.datamodel.get_one,
1756
+ session,
1757
+ params={
1758
+ "list_columns": self.show_select_columns,
1759
+ "where_id": id,
1760
+ "filter_classes": self.base_filters,
1761
+ "opr_filter_classes": self.base_opr_filters,
1762
+ },
1756
1763
  )
1757
- body_json = await smart_run(
1758
- self._process_body,
1759
- session,
1760
- body,
1761
- self.edit_query_rel_fields,
1762
- self.edit_schema_extra_fields.keys()
1763
- if self.edit_schema_extra_fields
1764
- else None,
1765
- item=item,
1766
- )
1767
- await smart_run(
1768
- self.pre_update_merge,
1769
- item,
1770
- body_json,
1771
- PARAM_BODY_SESSION(body=body, session=session),
1772
- )
1773
- item.update(body_json)
1774
- pre_update = await smart_run(
1775
- self.pre_update,
1776
- item,
1777
- PARAM_BODY_SESSION(body=body, session=session),
1778
- )
1779
- if pre_update is not None:
1780
- if isinstance(pre_update, Model):
1781
- item = pre_update
1782
- else:
1783
- AsyncTaskRunner.remove_tasks_by_tag("file")
1784
- return pre_update
1764
+ if not item:
1765
+ raise HTTPException(
1766
+ fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
1767
+ )
1768
+ body_json = await smart_run(
1769
+ self._process_body,
1770
+ session,
1771
+ body,
1772
+ self.edit_query_rel_fields,
1773
+ self.edit_schema_extra_fields.keys()
1774
+ if self.edit_schema_extra_fields
1775
+ else None,
1776
+ item=item,
1777
+ )
1778
+ await smart_run(
1779
+ self.pre_update_merge,
1780
+ item,
1781
+ body_json,
1782
+ PARAM_BODY_SESSION(body=body, session=session),
1783
+ )
1784
+ item.update(body_json)
1785
+ pre_update = await smart_run(
1786
+ self.pre_update,
1787
+ item,
1788
+ PARAM_BODY_SESSION(body=body, session=session),
1789
+ )
1790
+ if pre_update is not None:
1791
+ if isinstance(pre_update, Model):
1792
+ item = pre_update
1793
+ else:
1794
+ before_commit_runner.remove_tasks_by_tag("file")
1795
+ after_commit_runner.remove_tasks_by_tag("file")
1796
+ return pre_update
1785
1797
  item = await smart_run(self.datamodel.edit, session, item)
1798
+ after_commit_runner.remove_tasks_by_tag(
1799
+ "file"
1800
+ ) # Delete any file tasks scheduled to revert files on error
1786
1801
  post_update = await smart_run(
1787
1802
  self.post_update,
1788
1803
  item,
@@ -1817,60 +1832,75 @@ class ModelRestApi(BaseApi):
1817
1832
  If you are overriding this method, make sure to copy all the decorators too.
1818
1833
  """
1819
1834
  async with AsyncTaskRunner():
1820
- async with AsyncTaskRunner():
1821
- item = await smart_run(
1822
- self.datamodel.get_one,
1823
- session,
1824
- params={
1825
- "list_columns": self.show_select_columns,
1826
- "where_id": id,
1827
- "filter_classes": self.base_filters,
1828
- "opr_filter_classes": self.base_opr_filters,
1829
- },
1830
- )
1831
- if not item:
1832
- raise HTTPException(
1833
- fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
1835
+ async with AsyncTaskRunner(
1836
+ "after_commit", run_tasks_even_if_exception=True
1837
+ ) as after_commit_runner:
1838
+ async with AsyncTaskRunner("before_commit") as before_commit_runner:
1839
+ item = await smart_run(
1840
+ self.datamodel.get_one,
1841
+ session,
1842
+ params={
1843
+ "list_columns": self.show_select_columns,
1844
+ "where_id": id,
1845
+ "filter_classes": self.base_filters,
1846
+ "opr_filter_classes": self.base_opr_filters,
1847
+ },
1834
1848
  )
1835
- pre_delete = await smart_run(
1836
- self.pre_delete,
1837
- item,
1838
- PARAM_ID_SESSION(id=id, session=session),
1839
- )
1840
- if pre_delete is not None:
1841
- if isinstance(pre_delete, Model):
1842
- item = pre_delete
1843
- else:
1844
- return pre_delete
1845
- # Delete all related files and images
1846
- file_and_image_columns = [
1847
- x
1848
- for x in self.datamodel.get_file_column_list()
1849
- + self.datamodel.get_image_column_list()
1850
- ]
1851
- schema = self.datamodel.generate_schema(
1852
- file_and_image_columns,
1853
- with_id=False,
1854
- with_name=False,
1855
- with_property=False,
1856
- )
1857
- schema_data = schema.model_validate(item, from_attributes=True)
1858
- for column in file_and_image_columns:
1859
- fm = (
1860
- self.datamodel.file_manager
1861
- if self.datamodel.is_file(column)
1862
- else self.datamodel.image_manager
1849
+ if not item:
1850
+ raise HTTPException(
1851
+ fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
1852
+ )
1853
+ pre_delete = await smart_run(
1854
+ self.pre_delete,
1855
+ item,
1856
+ PARAM_ID_SESSION(id=id, session=session),
1863
1857
  )
1864
- filenames = getattr(schema_data, column, None) or []
1865
- if not isinstance(filenames, list):
1866
- filenames = [filenames]
1867
- for filename in filenames:
1868
- AsyncTaskRunner.add_task(
1869
- lambda fm=fm, filename=filename: smart_run(
1870
- fm.delete_file, filename
1871
- )
1858
+ if pre_delete is not None:
1859
+ if isinstance(pre_delete, Model):
1860
+ item = pre_delete
1861
+ else:
1862
+ return pre_delete
1863
+ # Delete all related files and images
1864
+ file_and_image_columns = [
1865
+ x
1866
+ for x in self.datamodel.get_file_column_list()
1867
+ + self.datamodel.get_image_column_list()
1868
+ ]
1869
+ schema = self.datamodel.generate_schema(
1870
+ file_and_image_columns,
1871
+ with_id=False,
1872
+ with_name=False,
1873
+ with_property=False,
1874
+ )
1875
+ schema_data = schema.model_validate(item, from_attributes=True)
1876
+ for column in file_and_image_columns:
1877
+ fm = (
1878
+ self.datamodel.file_manager
1879
+ if self.datamodel.is_file(column)
1880
+ else self.datamodel.image_manager
1872
1881
  )
1882
+ filenames = getattr(schema_data, column, None) or []
1883
+ if not isinstance(filenames, list):
1884
+ filenames = [filenames]
1885
+ for filename in filenames:
1886
+ old_content = await smart_run(fm.get_file, filename)
1887
+ before_commit_runner.add_task(
1888
+ lambda fm=fm, filename=filename: smart_run(
1889
+ fm.delete_file, filename
1890
+ )
1891
+ )
1892
+ after_commit_runner.add_task(
1893
+ lambda fm=fm,
1894
+ content=old_content,
1895
+ filename=filename: smart_run(
1896
+ fm.save_content_to_file, content, filename
1897
+ ),
1898
+ tags=["file"],
1899
+ )
1873
1900
  await smart_run(self.datamodel.delete, session, item)
1901
+ after_commit_runner.remove_tasks_by_tag(
1902
+ "file"
1903
+ ) # Delete any file tasks scheduled to revert files on error
1874
1904
  post_delete = await smart_run(
1875
1905
  self.post_delete,
1876
1906
  item,
@@ -2371,15 +2401,30 @@ class ModelRestApi(BaseApi):
2371
2401
  )
2372
2402
  if item and hasattr(item, key) and getattr(item, key):
2373
2403
  actual_old_filenames = getattr(item, key)
2404
+ before_commit_runner = AsyncTaskRunner.get_runner(
2405
+ "before_commit"
2406
+ )
2407
+ after_commit_runner = AsyncTaskRunner.get_runner(
2408
+ "after_commit"
2409
+ )
2374
2410
  # Delete only the files or images that are not in the new old_filenames
2375
2411
  for filename in actual_old_filenames:
2376
2412
  if filename not in old_filenames:
2377
- AsyncTaskRunner.add_task(
2413
+ old_content = await smart_run(fm.get_file, filename)
2414
+ before_commit_runner.add_task(
2378
2415
  lambda fm=fm, old_filename=filename: smart_run(
2379
2416
  fm.delete_file, old_filename
2380
2417
  ),
2381
2418
  tags=["file"],
2382
2419
  )
2420
+ after_commit_runner.add_task(
2421
+ lambda fm=fm,
2422
+ content=old_content,
2423
+ filename=filename: smart_run(
2424
+ fm.save_content_to_file, content, filename
2425
+ ),
2426
+ tags=["file"],
2427
+ )
2383
2428
 
2384
2429
  new_filenames = []
2385
2430
  # Loop through value instead of only file values so the order is maintained
@@ -2395,12 +2440,27 @@ class ModelRestApi(BaseApi):
2395
2440
  # Delete existing file or image if it is being updated
2396
2441
  if item and hasattr(item, key) and getattr(item, key):
2397
2442
  filename = getattr(item, key)
2398
- AsyncTaskRunner.add_task(
2443
+ old_content = await smart_run(fm.get_file, filename)
2444
+ before_commit_runner = AsyncTaskRunner.get_runner(
2445
+ "before_commit"
2446
+ )
2447
+ after_commit_runner = AsyncTaskRunner.get_runner(
2448
+ "after_commit"
2449
+ )
2450
+ before_commit_runner.add_task(
2399
2451
  lambda fm=fm, old_filename=filename: smart_run(
2400
2452
  fm.delete_file, old_filename
2401
2453
  ),
2402
2454
  tags=["file"],
2403
2455
  )
2456
+ after_commit_runner.add_task(
2457
+ lambda fm=fm,
2458
+ content=old_content,
2459
+ filename=filename: smart_run(
2460
+ fm.save_content_to_file, content, filename
2461
+ ),
2462
+ tags=["file"],
2463
+ )
2404
2464
 
2405
2465
  # Only process if the value exists and is not None
2406
2466
  if value:
@@ -2501,12 +2561,30 @@ class ModelRestApi(BaseApi):
2501
2561
  ),
2502
2562
  )
2503
2563
  content = await file.read()
2504
- AsyncTaskRunner.add_task(
2564
+ if not fm.is_file_size_allowed(content):
2565
+ raise HTTPWithValidationException(
2566
+ fastapi.status.HTTP_400_BAD_REQUEST,
2567
+ "bytes_too_long",
2568
+ "body",
2569
+ key,
2570
+ translate(
2571
+ "File size from '{filename}' exceeds the allowed limit {file_size_limit}.",
2572
+ filename=file.filename,
2573
+ file_size_limit=format_file_size(fm.max_file_size),
2574
+ ),
2575
+ )
2576
+ before_commit_runner = AsyncTaskRunner.get_runner("before_commit")
2577
+ after_commit_runner = AsyncTaskRunner.get_runner("after_commit")
2578
+ before_commit_runner.add_task(
2505
2579
  lambda fm=fm, content=content, new_name=new_name: smart_run(
2506
2580
  fm.save_content_to_file, content, new_name
2507
2581
  ),
2508
2582
  tags=["file"],
2509
2583
  )
2584
+ after_commit_runner.add_task(
2585
+ lambda fm=fm, new_name=new_name: smart_run(fm.delete_file, new_name),
2586
+ tags=["file"],
2587
+ )
2510
2588
  return new_name
2511
2589
 
2512
2590
  """
@@ -8,7 +8,9 @@ from .session import *
8
8
  __all__ = [
9
9
  "DBQueryParams",
10
10
  "AbstractQueryBuilder",
11
+ "BaseFileException",
11
12
  "FileNotAllowedException",
13
+ "FileTooLargeException",
12
14
  "AbstractFileManager",
13
15
  "AbstractImageManager",
14
16
  "AbstractBaseFilter",
@@ -6,10 +6,22 @@ import fastapi
6
6
  from ..exceptions import FastAPIReactToolkitException
7
7
  from ..utils import hooks, lazy, uuid_namegen
8
8
 
9
- __all__ = ["FileNotAllowedException", "AbstractFileManager", "AbstractImageManager"]
9
+ __all__ = [
10
+ "BaseFileException",
11
+ "FileNotAllowedException",
12
+ "FileTooLargeException",
13
+ "AbstractFileManager",
14
+ "AbstractImageManager",
15
+ ]
10
16
 
11
17
 
12
- class FileNotAllowedException(FastAPIReactToolkitException):
18
+ class BaseFileException(FastAPIReactToolkitException):
19
+ """
20
+ Base exception class for file-related errors.
21
+ """
22
+
23
+
24
+ class FileNotAllowedException(BaseFileException):
13
25
  """
14
26
  Exception raised when a file is not allowed based on its extension.
15
27
  """
@@ -18,13 +30,25 @@ class FileNotAllowedException(FastAPIReactToolkitException):
18
30
  super().__init__(f"File '{filename}' is not allowed.")
19
31
 
20
32
 
33
+ class FileTooLargeException(BaseFileException):
34
+ """
35
+ Exception raised when a file exceeds the maximum allowed size.
36
+ """
37
+
38
+ def __init__(self, filename: str, max_size: int):
39
+ super().__init__(
40
+ f"File '{filename}' exceeds the maximum allowed size of {max_size} bytes."
41
+ )
42
+
43
+
21
44
  class AbstractFileManager(abc.ABC):
22
45
  """
23
46
  Abstract base class for file managers.
24
47
  """
25
48
 
26
49
  base_path: str = None
27
- allowed_extensions: list[str] = None
50
+ allowed_extensions: list[str] | None = None
51
+ max_file_size: int | None = None
28
52
  namegen = lazy(lambda: uuid_namegen)
29
53
  permission = lazy(lambda: 0o755)
30
54
 
@@ -32,6 +56,7 @@ class AbstractFileManager(abc.ABC):
32
56
  self,
33
57
  base_path: str | None = None,
34
58
  allowed_extensions: list[str] | None = None,
59
+ max_file_size: int | None = None,
35
60
  namegen: typing.Callable[[str], str] | None = None,
36
61
  permission: int | None = None,
37
62
  ):
@@ -41,6 +66,7 @@ class AbstractFileManager(abc.ABC):
41
66
  Args:
42
67
  base_path (str | None, optional): Base path for file storage. Defaults to None.
43
68
  allowed_extensions (list[str] | None, optional): Allowed file extensions. Defaults to None.
69
+ max_file_size (int | None, optional): Maximum file size allowed. Defaults to None.
44
70
  namegen (typing.Callable[[str], str] | None, optional): Callable for generating file names. Defaults to None.
45
71
  permission (int | None, optional): File permission settings. Defaults to None.
46
72
 
@@ -51,6 +77,8 @@ class AbstractFileManager(abc.ABC):
51
77
  self.base_path = base_path
52
78
  if allowed_extensions is not None:
53
79
  self.allowed_extensions = allowed_extensions
80
+ if max_file_size is not None:
81
+ self.max_file_size = max_file_size
54
82
  if namegen is not None:
55
83
  self.namegen = namegen
56
84
  if permission is not None:
@@ -66,15 +94,26 @@ class AbstractFileManager(abc.ABC):
66
94
 
67
95
  def __init_subclass__(cls):
68
96
  # 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):
97
+ def check_is_file_allowed(self: typing.Self, *args, **kwargs):
98
+ filename = kwargs.get("filename", args[1] if len(args) > 1 else None)
99
+ if not filename:
100
+ raise ValueError("Filename must be provided.")
101
+ if not self.is_filename_allowed(filename):
76
102
  raise FileNotAllowedException(filename)
77
103
 
104
+ # Add pre-hook to save_file and save_content_to_file to check if the file size is allowed
105
+ def check_is_file_size_allowed(self: typing.Self, *args, **kwargs):
106
+ input = kwargs.get(
107
+ "file_data", args[0] if len(args) > 0 else None
108
+ ) or kwargs.get("content", args[0] if len(args) > 0 else None)
109
+ filename = kwargs.get("filename", args[1] if len(args) > 1 else None)
110
+ if not input or not filename:
111
+ raise ValueError(
112
+ "Both filename and file data/content must be provided."
113
+ )
114
+ if not self.is_file_size_allowed(input):
115
+ raise FileTooLargeException(filename, self.max_file_size)
116
+
78
117
  if cls.save_file is not AbstractFileManager.save_file:
79
118
  wrapped_save_file = hooks(pre=check_is_file_allowed)(cls.save_file)
80
119
  cls.save_file = wrapped_save_file
@@ -83,6 +122,16 @@ class AbstractFileManager(abc.ABC):
83
122
  cls.save_content_to_file
84
123
  )
85
124
  cls.save_content_to_file = wrapped_save_content_to_file
125
+ if cls.save_file is not AbstractFileManager.save_file:
126
+ wrapped_save_file_size = hooks(pre=check_is_file_size_allowed)(
127
+ cls.save_file
128
+ )
129
+ cls.save_file = wrapped_save_file_size
130
+ if cls.save_content_to_file is not AbstractFileManager.save_content_to_file:
131
+ wrapped_save_content_size = hooks(pre=check_is_file_size_allowed)(
132
+ cls.save_content_to_file
133
+ )
134
+ cls.save_content_to_file = wrapped_save_content_size
86
135
 
87
136
  """
88
137
  --------------------------------------------------------------------------------------------------------
@@ -243,6 +292,29 @@ class AbstractFileManager(abc.ABC):
243
292
  and filename.rsplit(".", 1)[1].lower() in self.allowed_extensions
244
293
  )
245
294
 
295
+ def is_file_size_allowed(self, file_size: int | fastapi.UploadFile | bytes | str):
296
+ """
297
+ Check if a file size is allowed based on the maximum file size.
298
+
299
+ Args:
300
+ file_size (int | fastapi.UploadFile | bytes | str): The size of the file in bytes or the file data.
301
+
302
+ Returns:
303
+ bool: True if the file size is allowed, False otherwise.
304
+ """
305
+ if self.max_file_size is None:
306
+ return True
307
+
308
+ size = 0
309
+ if isinstance(file_size, int):
310
+ size = file_size
311
+ elif isinstance(file_size, fastapi.UploadFile):
312
+ size = file_size.size or 0
313
+ elif isinstance(file_size, (bytes, str)):
314
+ size = len(file_size)
315
+
316
+ return size <= self.max_file_size
317
+
246
318
  def generate_name(self, filename: str) -> str:
247
319
  """
248
320
  Generates a name for the given file data.
@@ -285,14 +285,14 @@ class DatabaseSessionManager:
285
285
  """
286
286
  Initializes the tables required for FastAPI RTK to function.
287
287
  """
288
- await self.create_all(
289
- None,
290
- tables=[
291
- table
292
- for key, table in metadata.tables.items()
293
- if key in FASTAPI_RTK_TABLES
294
- ],
295
- )
288
+ async with self.connect() as conn:
289
+ copy_metadata = MetaData()
290
+ for table_name in FASTAPI_RTK_TABLES:
291
+ table = metadata.tables.get(table_name)
292
+ if table is None:
293
+ continue
294
+ table.to_metadata(copy_metadata)
295
+ await self._create_all(conn, copy_metadata)
296
296
 
297
297
  async def close(self):
298
298
  """
@@ -16,6 +16,7 @@ class FileManager(AbstractFileManager):
16
16
 
17
17
  base_path = lazy(lambda: Setting.UPLOAD_FOLDER)
18
18
  allowed_extensions = lazy(lambda: Setting.FILE_ALLOWED_EXTENSIONS)
19
+ max_file_size = lazy(lambda: Setting.FILE_MAX_SIZE)
19
20
 
20
21
  def post_init(self):
21
22
  if not self.base_path:
@@ -13,6 +13,7 @@ class ImageManager(FileManager, AbstractImageManager):
13
13
 
14
14
  base_path = lazy(lambda: Setting.IMG_UPLOAD_FOLDER)
15
15
  allowed_extensions = lazy(lambda: Setting.IMG_ALLOWED_EXTENSIONS)
16
+ max_file_size = lazy(lambda: Setting.IMG_MAX_SIZE)
16
17
 
17
18
  def post_init(self):
18
19
  if not self.base_path: