fastapi-rtk 1.0.20__tar.gz → 1.0.22__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 (136) hide show
  1. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/PKG-INFO +3 -2
  2. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/__init__.py +9 -1
  3. fastapi_rtk-1.0.22/fastapi_rtk/_version.py +1 -0
  4. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/api/model_rest_api.py +37 -30
  5. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/hashers/pbkdf2.py +1 -2
  6. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/hashers/scrypt.py +1 -2
  7. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/file_manager.py +1 -0
  8. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/config.py +3 -1
  9. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/file_manager.py +3 -1
  10. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/s3_file_manager.py +22 -12
  11. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/globals.py +29 -5
  12. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/routers.py +49 -13
  13. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/models.py +4 -0
  14. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/__init__.py +0 -5
  15. fastapi_rtk-1.0.22/fastapi_rtk/utils/flask_appbuilder_utils.py +16 -0
  16. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/pyproject.toml +2 -1
  17. fastapi_rtk-1.0.22/uv.lock +2011 -0
  18. fastapi_rtk-1.0.20/fastapi_rtk/_version.py +0 -1
  19. fastapi_rtk-1.0.20/fastapi_rtk/auth/hashers/utils.py +0 -122
  20. fastapi_rtk-1.0.20/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -76
  21. fastapi_rtk-1.0.20/fastapi_rtk/utils/werkzeug.py +0 -91
  22. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/.gitignore +0 -0
  23. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/LICENSE +0 -0
  24. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/README.md +0 -0
  25. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/api/__init__.py +0 -0
  26. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/api/base_api.py +0 -0
  27. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/apis.py +0 -0
  28. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/__init__.py +0 -0
  29. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/auth.py +0 -0
  30. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/hashers/__init__.py +0 -0
  31. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
  32. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
  33. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/__init__.py +0 -0
  34. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/config.py +0 -0
  35. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/db.py +0 -0
  36. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/jwt.py +0 -0
  37. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/__init__.py +0 -0
  38. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/__init__.py +0 -0
  39. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/column.py +0 -0
  40. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/db.py +0 -0
  41. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/exceptions.py +0 -0
  42. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/filters.py +0 -0
  43. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/interface.py +0 -0
  44. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/model.py +0 -0
  45. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/session.py +0 -0
  46. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/__init__.py +0 -0
  47. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/column.py +0 -0
  48. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/db.py +0 -0
  49. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
  50. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
  51. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
  52. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
  53. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
  54. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
  55. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
  56. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
  57. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/filters.py +0 -0
  58. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/interface.py +0 -0
  59. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/model.py +0 -0
  60. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/session.py +0 -0
  61. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/__init__.py +0 -0
  62. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/db.py +0 -0
  63. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/filter.py +0 -0
  64. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/interface.py +0 -0
  65. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/model.py +0 -0
  66. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/session.py +0 -0
  67. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/__init__.py +0 -0
  68. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/cli.py +0 -0
  69. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/__init__.py +0 -0
  70. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
  71. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
  72. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
  73. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
  74. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
  75. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
  76. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
  77. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
  78. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
  79. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/export.py +0 -0
  80. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/security.py +0 -0
  81. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/translate.py +0 -0
  82. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/const.py +0 -0
  83. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/decorators.py +0 -0
  84. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/types.py +0 -0
  85. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/utils.py +0 -0
  86. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/const.py +0 -0
  87. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/db.py +0 -0
  88. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/decorators.py +0 -0
  89. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/dependencies.py +0 -0
  90. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/exceptions.py +0 -0
  91. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
  92. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/__init__.py +0 -0
  93. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/image_manager.py +0 -0
  94. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
  95. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/filters.py +0 -0
  96. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/__init__.py +0 -0
  97. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel/__init__.py +0 -0
  98. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel/cli.py +0 -0
  99. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel/config.py +0 -0
  100. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel.cfg +0 -0
  101. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/lazy_text.py +0 -0
  102. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/messages.pot +0 -0
  103. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  104. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
  105. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  106. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
  107. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/manager.py +0 -0
  108. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/middlewares.py +0 -0
  109. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/mixins.py +0 -0
  110. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/models.py +0 -0
  111. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/schemas.py +0 -0
  112. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/__init__.py +0 -0
  113. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/__init__.py +0 -0
  114. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/apis.py +0 -0
  115. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/security_manager.py +0 -0
  116. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/setting.py +0 -0
  117. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/types.py +0 -0
  118. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/async_task_runner.py +0 -0
  119. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/class_factory.py +0 -0
  120. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/csv_json_converter.py +0 -0
  121. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/deep_merge.py +0 -0
  122. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/extender_mixin.py +0 -0
  123. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/formatter.py +0 -0
  124. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/hooks.py +0 -0
  125. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/lazy.py +0 -0
  126. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/merge_schema.py +0 -0
  127. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
  128. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/pydantic.py +0 -0
  129. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/run_utils.py +0 -0
  130. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/self_dependencies.py +0 -0
  131. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
  132. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/sqla.py +0 -0
  133. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/timezone.py +0 -0
  134. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/update_signature.py +0 -0
  135. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/use_default_when_none.py +0 -0
  136. {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rtk
3
- Version: 1.0.20
3
+ Version: 1.0.22
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
@@ -17,7 +17,7 @@ Requires-Python: >=3.10
17
17
  Requires-Dist: alembic>=1.17.0
18
18
  Requires-Dist: beautifulsoup4>=4.14.2
19
19
  Requires-Dist: fastapi-babel>=1.0.0
20
- Requires-Dist: fastapi-users[oauth,sqlalchemy]>=14.0.1
20
+ Requires-Dist: fastapi-users[oauth,sqlalchemy]>=15.0.3
21
21
  Requires-Dist: fastapi[standard]>=0.119.1
22
22
  Requires-Dist: jsonschema2md>=1.7.0
23
23
  Requires-Dist: marshmallow-sqlalchemy>=1.4.2
@@ -26,3 +26,4 @@ Requires-Dist: prometheus-fastapi-instrumentator>=7.1.0
26
26
  Requires-Dist: secweb>=1.25.2
27
27
  Requires-Dist: sqlalchemy-utils>=0.42.0
28
28
  Requires-Dist: uvicorn==0.38.0
29
+ Requires-Dist: werkzeug>=3.1.5
@@ -1,3 +1,6 @@
1
+ # Import from werkzeug to keep compatibility
2
+ from werkzeug.utils import ImportStringError, import_string, secure_filename
3
+
1
4
  # Import all submodules
2
5
  from .api import *
3
6
  from .auth import *
@@ -174,6 +177,10 @@ __all__ = [
174
177
  "S3ImageManager",
175
178
  # .globals
176
179
  "g",
180
+ "current_app",
181
+ "current_user",
182
+ "request",
183
+ "background_tasks",
177
184
  # .lang
178
185
  "lazy_text",
179
186
  "translate",
@@ -232,7 +239,6 @@ __all__ = [
232
239
  "deep_merge",
233
240
  "ExtenderMixin",
234
241
  "uuid_namegen",
235
- "secure_filename",
236
242
  "prettify_dict",
237
243
  "format_file_size",
238
244
  "hooks",
@@ -258,6 +264,8 @@ __all__ = [
258
264
  "validate_utc",
259
265
  "update_signature",
260
266
  "use_default_when_none",
267
+ # Re-exported from werkzeug.utils
261
268
  "ImportStringError",
262
269
  "import_string",
270
+ "secure_filename",
263
271
  ]
@@ -0,0 +1 @@
1
+ __version__ = "1.0.22"
@@ -1883,20 +1883,21 @@ class ModelRestApi(BaseApi):
1883
1883
  if not isinstance(filenames, list):
1884
1884
  filenames = [filenames]
1885
1885
  for filename in filenames:
1886
- old_content = await smart_run(fm.get_file, filename)
1887
1886
  before_commit_runner.add_task(
1888
1887
  lambda fm=fm, filename=filename: smart_run(
1889
1888
  fm.delete_file, filename
1890
1889
  )
1891
1890
  )
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
- )
1891
+ if fm.file_exists(filename):
1892
+ old_content = await smart_run(fm.get_file, filename)
1893
+ after_commit_runner.add_task(
1894
+ lambda fm=fm,
1895
+ content=old_content,
1896
+ filename=filename: smart_run(
1897
+ fm.save_content_to_file, content, filename
1898
+ ),
1899
+ tags=["file"],
1900
+ )
1900
1901
  await smart_run(self.datamodel.delete, session, item)
1901
1902
  after_commit_runner.remove_tasks_by_tag(
1902
1903
  "file"
@@ -2410,21 +2411,26 @@ class ModelRestApi(BaseApi):
2410
2411
  # Delete only the files or images that are not in the new old_filenames
2411
2412
  for filename in actual_old_filenames:
2412
2413
  if filename not in old_filenames:
2413
- old_content = await smart_run(fm.get_file, filename)
2414
2414
  before_commit_runner.add_task(
2415
2415
  lambda fm=fm, old_filename=filename: smart_run(
2416
2416
  fm.delete_file, old_filename
2417
2417
  ),
2418
2418
  tags=["file"],
2419
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
- )
2420
+ if fm.file_exists(filename):
2421
+ old_content = await smart_run(
2422
+ fm.get_file, filename
2423
+ )
2424
+ after_commit_runner.add_task(
2425
+ lambda fm=fm,
2426
+ content=old_content,
2427
+ filename=filename: smart_run(
2428
+ fm.save_content_to_file,
2429
+ content,
2430
+ filename,
2431
+ ),
2432
+ tags=["file"],
2433
+ )
2428
2434
 
2429
2435
  new_filenames = []
2430
2436
  # Loop through value instead of only file values so the order is maintained
@@ -2440,27 +2446,28 @@ class ModelRestApi(BaseApi):
2440
2446
  # Delete existing file or image if it is being updated
2441
2447
  if item and hasattr(item, key) and getattr(item, key):
2442
2448
  filename = getattr(item, key)
2443
- old_content = await smart_run(fm.get_file, filename)
2444
2449
  before_commit_runner = AsyncTaskRunner.get_runner(
2445
2450
  "before_commit"
2446
2451
  )
2447
- after_commit_runner = AsyncTaskRunner.get_runner(
2448
- "after_commit"
2449
- )
2450
2452
  before_commit_runner.add_task(
2451
2453
  lambda fm=fm, old_filename=filename: smart_run(
2452
2454
  fm.delete_file, old_filename
2453
2455
  ),
2454
2456
  tags=["file"],
2455
2457
  )
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
- )
2458
+ if fm.file_exists(filename):
2459
+ old_content = await smart_run(fm.get_file, filename)
2460
+ after_commit_runner = AsyncTaskRunner.get_runner(
2461
+ "after_commit"
2462
+ )
2463
+ after_commit_runner.add_task(
2464
+ lambda fm=fm,
2465
+ content=old_content,
2466
+ filename=filename: smart_run(
2467
+ fm.save_content_to_file, content, filename
2468
+ ),
2469
+ tags=["file"],
2470
+ )
2464
2471
 
2465
2472
  # Only process if the value exists and is not None
2466
2473
  if value:
@@ -2,8 +2,7 @@ import typing
2
2
 
3
3
  from pwdlib.hashers import HasherProtocol
4
4
  from pwdlib.hashers.base import ensure_str
5
-
6
- from .utils import check_password_hash, generate_password_hash
5
+ from werkzeug.security import check_password_hash, generate_password_hash
7
6
 
8
7
  __all__ = ["PBKDF2Hasher"]
9
8
 
@@ -2,8 +2,7 @@ import typing
2
2
 
3
3
  from pwdlib.hashers import HasherProtocol
4
4
  from pwdlib.hashers.base import ensure_str
5
-
6
- from .utils import check_password_hash, generate_password_hash
5
+ from werkzeug.security import check_password_hash, generate_password_hash
7
6
 
8
7
  __all__ = ["ScryptHasher"]
9
8
 
@@ -268,6 +268,7 @@ class AbstractFileManager(abc.ABC):
268
268
  return self.__class__(
269
269
  base_path=f"{self.base_path}/{subfolder}",
270
270
  allowed_extensions=self.allowed_extensions,
271
+ max_file_size=self.max_file_size,
271
272
  namegen=self.namegen,
272
273
  permission=self.permission,
273
274
  *args,
@@ -6,7 +6,9 @@ import os
6
6
  import types
7
7
  import typing as t
8
8
 
9
- from .utils import deep_merge, import_string
9
+ from werkzeug.utils import import_string
10
+
11
+ from .utils import deep_merge
10
12
 
11
13
  __all__ = ["Config"]
12
14
 
@@ -2,9 +2,11 @@ import os
2
2
  import os.path as op
3
3
  import shutil
4
4
 
5
+ from werkzeug.utils import secure_filename
6
+
5
7
  from ..bases.file_manager import AbstractFileManager
6
8
  from ..setting import Setting
7
- from ..utils import lazy, secure_filename, smart_run
9
+ from ..utils import lazy, smart_run
8
10
 
9
11
  __all__ = ["FileManager"]
10
12
 
@@ -2,7 +2,7 @@ import typing
2
2
 
3
3
  from ..bases.file_manager import AbstractFileManager
4
4
  from ..setting import Setting
5
- from ..utils import lazy, smart_run, use_default_when_none
5
+ from ..utils import T, lazy, smart_run, use_default_when_none
6
6
 
7
7
  __all__ = ["S3FileManager"]
8
8
 
@@ -14,6 +14,7 @@ class S3FileManager(AbstractFileManager):
14
14
 
15
15
  allowed_extensions = lazy(lambda: Setting.FILE_ALLOWED_EXTENSIONS)
16
16
  max_file_size = lazy(lambda: Setting.FILE_MAX_SIZE)
17
+ ERROR_CODE_FILE_NOT_FOUND = "NoSuchKey"
17
18
 
18
19
  def __init__(
19
20
  self,
@@ -57,10 +58,14 @@ class S3FileManager(AbstractFileManager):
57
58
 
58
59
  try:
59
60
  import boto3
61
+ import botocore
62
+ import botocore.client
60
63
  import smart_open
61
64
 
62
65
  self.smart_open = smart_open
63
66
  self.boto3 = boto3
67
+ self.botocore = botocore
68
+ self.botocore.client = botocore.client
64
69
  except ImportError:
65
70
  raise ImportError(
66
71
  "smart_open is required for S3FileManager. "
@@ -125,27 +130,22 @@ class S3FileManager(AbstractFileManager):
125
130
  return path
126
131
 
127
132
  def delete_file(self, filename):
128
- path = self.get_path(filename)
129
- try:
130
- self.smart_open.open(
131
- path, "rb", **self.open_params
132
- ).close() # Check if file exists
133
+ if self.file_exists(filename):
133
134
  self.boto3_client.delete_object(
134
135
  Bucket=self.bucket_name,
135
136
  Key=f"{self.bucket_subfolder}/{filename}"
136
137
  if self.bucket_subfolder
137
138
  else filename,
138
139
  )
139
- except FileNotFoundError:
140
- pass
141
140
 
142
141
  def file_exists(self, filename):
143
142
  path = self.get_path(filename)
144
143
  try:
145
- with self.smart_open.open(path, "rb", **self.open_params):
146
- return True
147
- except FileNotFoundError:
148
- return False
144
+ with self.smart_open.open(path, "rb", **self.open_params) as f:
145
+ f.read(1) # Try to read a byte to confirm existence
146
+ return True
147
+ except IOError as e:
148
+ return self._handle_io_error(e, value_to_return=False)
149
149
 
150
150
  def get_instance_with_subfolder(self, subfolder, *args, **kwargs):
151
151
  return super().get_instance_with_subfolder(
@@ -161,3 +161,13 @@ class S3FileManager(AbstractFileManager):
161
161
  *args,
162
162
  **kwargs,
163
163
  )
164
+
165
+ def _handle_io_error(self, e: IOError, *, value_to_return: T = None):
166
+ if hasattr(e, "backend_error") and isinstance(
167
+ e.backend_error, self.botocore.client.ClientError
168
+ ):
169
+ error = e.backend_error.response.get("Error", {})
170
+ error_code = error.get("Code")
171
+ if error_code == self.ERROR_CODE_FILE_NOT_FOUND:
172
+ return value_to_return
173
+ raise e
@@ -44,6 +44,7 @@ import fastapi
44
44
  from fastapi import Request, Response
45
45
  from starlette.middleware.base import BaseHTTPMiddleware
46
46
  from starlette.types import ASGIApp
47
+ from werkzeug.local import LocalProxy
47
48
 
48
49
  from .config import Config
49
50
  from .const import (
@@ -61,7 +62,7 @@ if TYPE_CHECKING:
61
62
  from .fastapi_react_toolkit import FastAPIReactToolkit
62
63
  from .security.sqla.models import User
63
64
 
64
- __all__ = ["g"]
65
+ __all__ = ["g", "current_app", "current_user", "request", "background_tasks"]
65
66
 
66
67
 
67
68
  class Globals:
@@ -71,9 +72,9 @@ class Globals:
71
72
  _defaults: dict[str, Any]
72
73
 
73
74
  # Type annotations for the attributes
74
- user: "User"
75
+ user: "User | None"
75
76
  """
76
- The current user object. It will be `None` when not used in a request context.
77
+ The current user object. It will be `None` when not used in a request context or if no user is authenticated.
77
78
  """
78
79
  auth: "AuthConfigurator"
79
80
  """
@@ -91,11 +92,11 @@ class Globals:
91
92
  """
92
93
  A dictionary used to store list of sensitive columns for each model that should not be returned in the list and get endpoints. Default is `{"User": ["password", "hashed_password"]}`.
93
94
  """
94
- background_tasks: fastapi.BackgroundTasks
95
+ background_tasks: fastapi.BackgroundTasks | None
95
96
  """
96
97
  The background tasks object to add tasks to be executed after the response is sent. It will be `None` when not used in a request context.
97
98
  """
98
- request: Request
99
+ request: Request | None
99
100
  """
100
101
  The current request object. It will be `None` when not used in a request context.
101
102
  """
@@ -108,6 +109,9 @@ class Globals:
108
109
  The image manager object to manage images in the application. Defaults to `ImageManager` from `fastapi_rtk.file_managers.image_manager`.
109
110
  """
110
111
  current_app: "FastAPIReactToolkit"
112
+ """
113
+ The current FastAPI React Toolkit application instance.
114
+ """
111
115
 
112
116
  def __init__(self) -> None:
113
117
  object.__setattr__(self, "_vars", {})
@@ -269,3 +273,23 @@ g.set_default(
269
273
  ),
270
274
  )
271
275
  g.config.add_callback(basic_callback)
276
+
277
+ # Local Proxies
278
+ current_app: "FastAPIReactToolkit" = LocalProxy(lambda: g.current_app)
279
+ """
280
+ Proxy to the current FastAPI React Toolkit application instance.
281
+ """
282
+ current_user: "User | None" = LocalProxy(lambda: g.user)
283
+ """
284
+ Proxy to the current user object. It will be `None` when not used in a request context or if no user is authenticated.
285
+ """
286
+ request: Request | None = LocalProxy(lambda: g.request)
287
+ """
288
+ Proxy to the current request object. It will be `None` when not used in a request context.
289
+ """
290
+ background_tasks: fastapi.BackgroundTasks | None = LocalProxy(
291
+ lambda: g.background_tasks
292
+ )
293
+ """
294
+ Proxy to the background tasks object to add tasks to be executed after the response is sent. It will be `None` when not used in a request context.
295
+ """
@@ -99,11 +99,9 @@ def get_oauth_router(
99
99
  backend: AuthenticationBackend[models.UP, models.ID],
100
100
  get_user_manager: UserManagerDependency[models.UP, models.ID],
101
101
  state_secret: SecretType,
102
- redirect_url: typing.Optional[str] = None,
103
- redirect_url_factory: typing.Optional[
104
- typing.Callable[[Request, list[str]], str]
105
- ] = None,
106
- redirect_url_after_callback: typing.Optional[str] = None,
102
+ redirect_url: str | None = None,
103
+ redirect_url_factory: typing.Callable[[Request, list[str]], str] | None = None,
104
+ redirect_url_after_callback: str | None = None,
107
105
  associate_by_email: bool = False,
108
106
  is_verified_by_default: bool = False,
109
107
  **kwargs: dict[str, typing.Any],
@@ -185,6 +183,18 @@ def get_oauth_router(
185
183
  "summary": "User is inactive.",
186
184
  "value": {"detail": ErrorCode.LOGIN_BAD_CREDENTIALS},
187
185
  },
186
+ ErrorCode.ACCESS_TOKEN_DECODE_ERROR: {
187
+ "summary": "Access token is error.",
188
+ "value": {
189
+ "detail": ErrorCode.ACCESS_TOKEN_DECODE_ERROR
190
+ },
191
+ },
192
+ ErrorCode.ACCESS_TOKEN_ALREADY_EXPIRED: {
193
+ "summary": "Access token is already expired.",
194
+ "value": {
195
+ "detail": ErrorCode.ACCESS_TOKEN_ALREADY_EXPIRED
196
+ },
197
+ },
188
198
  }
189
199
  }
190
200
  },
@@ -216,7 +226,15 @@ def get_oauth_router(
216
226
  try:
217
227
  state_data = decode_jwt(state, state_secret, [STATE_TOKEN_AUDIENCE])
218
228
  except jwt.DecodeError:
219
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
229
+ raise HTTPException(
230
+ status_code=status.HTTP_400_BAD_REQUEST,
231
+ detail=ErrorCode.ACCESS_TOKEN_DECODE_ERROR,
232
+ )
233
+ except jwt.ExpiredSignatureError:
234
+ raise HTTPException(
235
+ status_code=status.HTTP_400_BAD_REQUEST,
236
+ detail=ErrorCode.ACCESS_TOKEN_ALREADY_EXPIRED,
237
+ )
220
238
 
221
239
  try:
222
240
  user = await user_manager.oauth_callback(
@@ -271,11 +289,9 @@ def get_oauth_associate_router(
271
289
  get_user_manager: UserManagerDependency[models.UP, models.ID],
272
290
  user_schema: type[schemas.U],
273
291
  state_secret: SecretType,
274
- redirect_url: typing.Optional[str] = None,
275
- redirect_url_factory: typing.Optional[
276
- typing.Callable[[Request, list[str]], str]
277
- ] = None,
278
- redirect_url_after_callback: typing.Optional[str] = None,
292
+ redirect_url: str | None = None,
293
+ redirect_url_factory: typing.Callable[[Request, list[str]], str] | None = None,
294
+ redirect_url_after_callback: str | None = None,
279
295
  requires_verification: bool = False,
280
296
  ) -> APIRouter:
281
297
  """
@@ -354,6 +370,18 @@ def get_oauth_associate_router(
354
370
  "summary": "Invalid state token.",
355
371
  "value": None,
356
372
  },
373
+ ErrorCode.ACCESS_TOKEN_DECODE_ERROR: {
374
+ "summary": "Access token is error.",
375
+ "value": {
376
+ "detail": ErrorCode.ACCESS_TOKEN_DECODE_ERROR
377
+ },
378
+ },
379
+ ErrorCode.ACCESS_TOKEN_ALREADY_EXPIRED: {
380
+ "summary": "Access token is already expired.",
381
+ "value": {
382
+ "detail": ErrorCode.ACCESS_TOKEN_ALREADY_EXPIRED
383
+ },
384
+ },
357
385
  }
358
386
  }
359
387
  },
@@ -385,7 +413,15 @@ def get_oauth_associate_router(
385
413
  try:
386
414
  state_data = decode_jwt(state, state_secret, [STATE_TOKEN_AUDIENCE])
387
415
  except jwt.DecodeError:
388
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
416
+ raise HTTPException(
417
+ status_code=status.HTTP_400_BAD_REQUEST,
418
+ detail=ErrorCode.ACCESS_TOKEN_DECODE_ERROR,
419
+ )
420
+ except jwt.ExpiredSignatureError:
421
+ raise HTTPException(
422
+ status_code=status.HTTP_400_BAD_REQUEST,
423
+ detail=ErrorCode.ACCESS_TOKEN_ALREADY_EXPIRED,
424
+ )
389
425
 
390
426
  if state_data["sub"] != str(user.id):
391
427
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@@ -404,7 +440,7 @@ def get_oauth_associate_router(
404
440
  if redirect_url_after_callback:
405
441
  return RedirectResponse(redirect_url_after_callback)
406
442
 
407
- return schemas.model_validate(user_schema, user)
443
+ return user_schema.model_validate(user)
408
444
 
409
445
  return router
410
446
 
@@ -232,6 +232,10 @@ class User(Model):
232
232
  if hasattr(self, "verified"):
233
233
  self.verified = value
234
234
 
235
+ @property
236
+ def is_authenticated(self):
237
+ return self.is_active
238
+
235
239
  @property
236
240
  def is_superuser(self):
237
241
  from ...globals import g
@@ -19,7 +19,6 @@ from .sqla import *
19
19
  from .timezone import *
20
20
  from .update_signature import *
21
21
  from .use_default_when_none import *
22
- from .werkzeug import *
23
22
 
24
23
  __all__ = [
25
24
  # .async_task_runner
@@ -35,7 +34,6 @@ __all__ = [
35
34
  "ExtenderMixin",
36
35
  # .flask_appbuilder_utils
37
36
  "uuid_namegen",
38
- "secure_filename",
39
37
  # .formatter
40
38
  "prettify_dict",
41
39
  "format_file_size",
@@ -74,9 +72,6 @@ __all__ = [
74
72
  "update_signature",
75
73
  # .use_default_when_none
76
74
  "use_default_when_none",
77
- # .werkzeug
78
- "ImportStringError",
79
- "import_string",
80
75
  ]
81
76
 
82
77
  P = typing.ParamSpec("P")
@@ -0,0 +1,16 @@
1
+ import uuid
2
+
3
+ __all__ = ["uuid_namegen"]
4
+
5
+
6
+ def uuid_namegen(filename: str) -> str:
7
+ """
8
+ Generates a unique filename by combining a UUID and the original filename.
9
+
10
+ Args:
11
+ filename (str): The original filename to be used in the unique name.
12
+
13
+ Returns:
14
+ str: The generated unique filename.
15
+ """
16
+ return str(uuid.uuid1()) + "_sep_" + filename
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "alembic>=1.17.0",
31
31
  "beautifulsoup4>=4.14.2",
32
32
  "fastapi-babel>=1.0.0",
33
- "fastapi-users[oauth,sqlalchemy]>=14.0.1",
33
+ "fastapi-users[oauth,sqlalchemy]>=15.0.3",
34
34
  "fastapi[standard]>=0.119.1",
35
35
  "jsonschema2md>=1.7.0",
36
36
  "marshmallow-sqlalchemy>=1.4.2",
@@ -39,6 +39,7 @@ dependencies = [
39
39
  "secweb>=1.25.2",
40
40
  "sqlalchemy-utils>=0.42.0",
41
41
  "uvicorn==0.38.0",
42
+ "werkzeug>=3.1.5",
42
43
  ]
43
44
  urls = { Homepage = "https://codeberg.org/datatactics/fastapi-rtk", Issues = "https://codeberg.org/datatactics/fastapi-rtk/issues" }
44
45
  scripts = { "fastapi-rtk" = "fastapi_rtk.cli:main" }