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.
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/PKG-INFO +3 -2
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/__init__.py +9 -1
- fastapi_rtk-1.0.22/fastapi_rtk/_version.py +1 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/api/model_rest_api.py +37 -30
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/hashers/pbkdf2.py +1 -2
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/hashers/scrypt.py +1 -2
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/file_manager.py +1 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/config.py +3 -1
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/file_manager.py +3 -1
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/s3_file_manager.py +22 -12
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/globals.py +29 -5
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/routers.py +49 -13
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/models.py +4 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/__init__.py +0 -5
- fastapi_rtk-1.0.22/fastapi_rtk/utils/flask_appbuilder_utils.py +16 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/pyproject.toml +2 -1
- fastapi_rtk-1.0.22/uv.lock +2011 -0
- fastapi_rtk-1.0.20/fastapi_rtk/_version.py +0 -1
- fastapi_rtk-1.0.20/fastapi_rtk/auth/hashers/utils.py +0 -122
- fastapi_rtk-1.0.20/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -76
- fastapi_rtk-1.0.20/fastapi_rtk/utils/werkzeug.py +0 -91
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/.gitignore +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/LICENSE +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/README.md +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/api/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/api/base_api.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/apis.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/auth.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/config.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/db.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/auth/strategies/jwt.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/column.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/db.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/exceptions.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/filters.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/interface.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/model.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/generic/session.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/column.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/db.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/filters.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/interface.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/model.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/backends/sqla/session.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/db.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/filter.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/interface.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/model.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/bases/session.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/cli.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/export.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/security.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/commands/translate.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/const.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/decorators.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/types.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/cli/utils.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/const.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/db.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/decorators.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/dependencies.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/exceptions.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/image_manager.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/filters.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel/cli.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel/config.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/babel.cfg +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/lazy_text.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/messages.pot +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/manager.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/middlewares.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/mixins.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/models.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/schemas.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/__init__.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/apis.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/security/sqla/security_manager.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/setting.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/types.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/async_task_runner.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/class_factory.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/csv_json_converter.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/deep_merge.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/extender_mixin.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/formatter.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/hooks.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/lazy.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/merge_schema.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/pydantic.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/run_utils.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/self_dependencies.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/sqla.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/timezone.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/update_signature.py +0 -0
- {fastapi_rtk-1.0.20 → fastapi_rtk-1.0.22}/fastapi_rtk/utils/use_default_when_none.py +0 -0
- {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.
|
|
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]>=
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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,
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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:
|
|
103
|
-
redirect_url_factory: typing.
|
|
104
|
-
|
|
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(
|
|
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:
|
|
275
|
-
redirect_url_factory: typing.
|
|
276
|
-
|
|
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(
|
|
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
|
|
443
|
+
return user_schema.model_validate(user)
|
|
408
444
|
|
|
409
445
|
return router
|
|
410
446
|
|
|
@@ -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]>=
|
|
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" }
|