fastapi-rtk 1.0.20__py3-none-any.whl → 1.0.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_rtk/__init__.py +9 -1
- fastapi_rtk/_version.py +1 -1
- fastapi_rtk/api/model_rest_api.py +37 -30
- fastapi_rtk/auth/hashers/pbkdf2.py +1 -2
- fastapi_rtk/auth/hashers/scrypt.py +1 -2
- fastapi_rtk/bases/file_manager.py +1 -0
- fastapi_rtk/config.py +3 -1
- fastapi_rtk/file_managers/file_manager.py +3 -1
- fastapi_rtk/file_managers/s3_file_manager.py +22 -12
- fastapi_rtk/globals.py +29 -5
- fastapi_rtk/routers.py +49 -13
- fastapi_rtk/security/sqla/models.py +4 -0
- fastapi_rtk/utils/__init__.py +0 -5
- fastapi_rtk/utils/flask_appbuilder_utils.py +1 -61
- {fastapi_rtk-1.0.20.dist-info → fastapi_rtk-1.0.22.dist-info}/METADATA +3 -2
- {fastapi_rtk-1.0.20.dist-info → fastapi_rtk-1.0.22.dist-info}/RECORD +19 -21
- fastapi_rtk/auth/hashers/utils.py +0 -122
- fastapi_rtk/utils/werkzeug.py +0 -91
- {fastapi_rtk-1.0.20.dist-info → fastapi_rtk-1.0.22.dist-info}/WHEEL +0 -0
- {fastapi_rtk-1.0.20.dist-info → fastapi_rtk-1.0.22.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-1.0.20.dist-info → fastapi_rtk-1.0.22.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/__init__.py
CHANGED
|
@@ -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
|
]
|
fastapi_rtk/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
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,
|
fastapi_rtk/config.py
CHANGED
|
@@ -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
|
fastapi_rtk/globals.py
CHANGED
|
@@ -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
|
+
"""
|
fastapi_rtk/routers.py
CHANGED
|
@@ -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
|
|
fastapi_rtk/utils/__init__.py
CHANGED
|
@@ -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")
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import unicodedata
|
|
4
1
|
import uuid
|
|
5
2
|
|
|
6
|
-
__all__ = ["uuid_namegen"
|
|
7
|
-
|
|
8
|
-
_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]")
|
|
9
|
-
_windows_device_files = {
|
|
10
|
-
"CON",
|
|
11
|
-
"PRN",
|
|
12
|
-
"AUX",
|
|
13
|
-
"NUL",
|
|
14
|
-
*(f"COM{i}" for i in range(10)),
|
|
15
|
-
*(f"LPT{i}" for i in range(10)),
|
|
16
|
-
}
|
|
3
|
+
__all__ = ["uuid_namegen"]
|
|
17
4
|
|
|
18
5
|
|
|
19
6
|
def uuid_namegen(filename: str) -> str:
|
|
@@ -27,50 +14,3 @@ def uuid_namegen(filename: str) -> str:
|
|
|
27
14
|
str: The generated unique filename.
|
|
28
15
|
"""
|
|
29
16
|
return str(uuid.uuid1()) + "_sep_" + filename
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def secure_filename(filename: str) -> str:
|
|
33
|
-
r"""Pass it a filename and it will return a secure version of it. This
|
|
34
|
-
filename can then safely be stored on a regular file system and passed
|
|
35
|
-
to :func:`os.path.join`. The filename returned is an ASCII only string
|
|
36
|
-
for maximum portability.
|
|
37
|
-
|
|
38
|
-
On windows systems the function also makes sure that the file is not
|
|
39
|
-
named after one of the special device files.
|
|
40
|
-
|
|
41
|
-
>>> secure_filename("My cool movie.mov")
|
|
42
|
-
'My_cool_movie.mov'
|
|
43
|
-
>>> secure_filename("../../../etc/passwd")
|
|
44
|
-
'etc_passwd'
|
|
45
|
-
>>> secure_filename('i contain cool \xfcml\xe4uts.txt')
|
|
46
|
-
'i_contain_cool_umlauts.txt'
|
|
47
|
-
|
|
48
|
-
The function might return an empty filename. It's your responsibility
|
|
49
|
-
to ensure that the filename is unique and that you abort or
|
|
50
|
-
generate a random filename if the function returned an empty one.
|
|
51
|
-
|
|
52
|
-
.. versionadded:: 0.5
|
|
53
|
-
|
|
54
|
-
:param filename: the filename to secure
|
|
55
|
-
"""
|
|
56
|
-
filename = unicodedata.normalize("NFKD", filename)
|
|
57
|
-
filename = filename.encode("ascii", "ignore").decode("ascii")
|
|
58
|
-
|
|
59
|
-
for sep in os.sep, os.path.altsep:
|
|
60
|
-
if sep:
|
|
61
|
-
filename = filename.replace(sep, " ")
|
|
62
|
-
filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip(
|
|
63
|
-
"._"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# on nt a couple of special files are present in each folder. We
|
|
67
|
-
# have to ensure that the target file is not such a filename. In
|
|
68
|
-
# this case we prepend an underline
|
|
69
|
-
if (
|
|
70
|
-
os.name == "nt"
|
|
71
|
-
and filename
|
|
72
|
-
and filename.split(".")[0].upper() in _windows_device_files
|
|
73
|
-
):
|
|
74
|
-
filename = f"_{filename}"
|
|
75
|
-
|
|
76
|
-
return filename
|
|
@@ -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,7 +1,7 @@
|
|
|
1
|
-
fastapi_rtk/__init__.py,sha256=
|
|
2
|
-
fastapi_rtk/_version.py,sha256=
|
|
1
|
+
fastapi_rtk/__init__.py,sha256=ULEV1ai_Hk5MU10uhzalnaX9DTzuNRP-tOd9spIRnHk,6511
|
|
2
|
+
fastapi_rtk/_version.py,sha256=3qPJsI_cs0SlJn5DWM459KiP-4DSaTWmW-7lyF1jPMo,23
|
|
3
3
|
fastapi_rtk/apis.py,sha256=6X_Lhl98m7lKrDRybg2Oe24pLFLJ29eCOQSwCAvpKhY,172
|
|
4
|
-
fastapi_rtk/config.py,sha256=
|
|
4
|
+
fastapi_rtk/config.py,sha256=7y5zsXlYwv4zv1CbiZsw0VvIZJv3IKAvLXNXGI4sios,14063
|
|
5
5
|
fastapi_rtk/const.py,sha256=sEj_cYeerj9pVwbCu0k5Sy1EYpdr1EHzUjqqbnporgc,4905
|
|
6
6
|
fastapi_rtk/db.py,sha256=BqWXj6zP_HZSSL_cFcF-TXwnI0pBqYp6nYV49-6ZmNs,24790
|
|
7
7
|
fastapi_rtk/decorators.py,sha256=HqAFSiO0l5_M0idWs0IcY24FdzbAcDQDQoifM_WgZAQ,14515
|
|
@@ -9,25 +9,24 @@ fastapi_rtk/dependencies.py,sha256=jlcsMrh83yrJsgXvpWJet_mjqwDP3nBZfPSg4Lq8KKE,7
|
|
|
9
9
|
fastapi_rtk/exceptions.py,sha256=P0qwd4VkeWFotgMVQHgmdT1NphFQaEznKLFIvJzW4Zs,2594
|
|
10
10
|
fastapi_rtk/fastapi_react_toolkit.py,sha256=lvtieBLzCAlozKQU9dkAUVoW7Ofi1ncri_hDyeHazF4,29803
|
|
11
11
|
fastapi_rtk/filters.py,sha256=weCH3suCxpGJQYmwhj9D1iAqMPiRnmbRiN7stK0rhoE,181
|
|
12
|
-
fastapi_rtk/globals.py,sha256=
|
|
12
|
+
fastapi_rtk/globals.py,sha256=VC_DyLpuoNvAQSVy8l3o8lHXNa8qH2tm4umqL4_rMn4,9842
|
|
13
13
|
fastapi_rtk/manager.py,sha256=F57aD0DApH98H7GPpnxrwKHz0nETuxQG2bax4FDfJgM,33737
|
|
14
14
|
fastapi_rtk/middlewares.py,sha256=ycdaAdLIUaNEZ7KotczTHS6OOqy98Mn1_O7Hpe7Sma8,10325
|
|
15
15
|
fastapi_rtk/mixins.py,sha256=78RqwrRJFOz6KAxf-GSNOmRdr7R3b-ws7OIENYlzRQQ,238
|
|
16
16
|
fastapi_rtk/models.py,sha256=lQSqe-r0H7jWBVdqXjEi_bNL1cGQr0YWnyI1ToS_coc,178
|
|
17
|
-
fastapi_rtk/routers.py,sha256=
|
|
17
|
+
fastapi_rtk/routers.py,sha256=k7rxDjkfn-92cGdH0M1HhTySws5KaCrpJ5becrMXxkI,20895
|
|
18
18
|
fastapi_rtk/schemas.py,sha256=mdIX93QRaQBLrQz-kBvG5wyKoi8H6OQpFuTBO9-mCK4,17894
|
|
19
19
|
fastapi_rtk/setting.py,sha256=mLIm-6DoVSEiXn2VUT-JByNVXujeIlyBFBUlPKTw7ys,19753
|
|
20
20
|
fastapi_rtk/types.py,sha256=-LPnTIbHvqJW81__gab3EWrhjNmznHhptz0BtXkEAHQ,3612
|
|
21
21
|
fastapi_rtk/version.py,sha256=D2cmQf2LNeHOiEfcNzVOOfcAmuLvPEmGEtZv5G54D0c,195
|
|
22
22
|
fastapi_rtk/api/__init__.py,sha256=MwFR7HHppnhbjZGg3sOdQ6nqy9uxnHHXvicpswNFMNA,245
|
|
23
23
|
fastapi_rtk/api/base_api.py,sha256=42I9v3b25lqxNAMDGEtajA5-btIDSyUWF0xMDgGkA8c,8078
|
|
24
|
-
fastapi_rtk/api/model_rest_api.py,sha256=
|
|
24
|
+
fastapi_rtk/api/model_rest_api.py,sha256=YNwIBDBm7jWNqN9KExZ5bLIxdLSNDuRsDpJOjLf6CkU,111821
|
|
25
25
|
fastapi_rtk/auth/__init__.py,sha256=iX7O41NivBYDfdomEaqm4lUx9KD17wI4g3EFLF6kUTw,336
|
|
26
26
|
fastapi_rtk/auth/auth.py,sha256=F2qZoR7go_7FnvVJrDxUCd6vtRz5XW8yyOv143MWPts,20664
|
|
27
27
|
fastapi_rtk/auth/hashers/__init__.py,sha256=uBThFj2VPPSMSioxYTktNiM4-mVgtDAjTpKA3ZzWxxs,110
|
|
28
|
-
fastapi_rtk/auth/hashers/pbkdf2.py,sha256=
|
|
29
|
-
fastapi_rtk/auth/hashers/scrypt.py,sha256=
|
|
30
|
-
fastapi_rtk/auth/hashers/utils.py,sha256=_fMQfTgzw8E2pvrWQDUTrpf2mE7Z3PS0PvUUwresWXY,4280
|
|
28
|
+
fastapi_rtk/auth/hashers/pbkdf2.py,sha256=_ss8PENIkRjenqhYqDAP-ewsoxkPKp0lTPAfOKacZpA,929
|
|
29
|
+
fastapi_rtk/auth/hashers/scrypt.py,sha256=cUcW_o49nrecpz6nysJE4hd3JqniEM67Ltx8TsdWxlg,912
|
|
31
30
|
fastapi_rtk/auth/password_helpers/__init__.py,sha256=sT4-Xe0B4kh593NPNmCYr6Sz2XZnhE9bK1tx9mgFUw4,68
|
|
32
31
|
fastapi_rtk/auth/password_helpers/fab.py,sha256=MjUlS-XpUmQUUI4EJ4uE7irRcBaUVSH-q4-Mb9dna1k,937
|
|
33
32
|
fastapi_rtk/auth/strategies/__init__.py,sha256=MQuIeNRzUXKo8-ZjZUQTzAxOj-krU9reIUu5EpEMkAI,87
|
|
@@ -60,7 +59,7 @@ fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py,sha256=L1WgO7UzWuAgs
|
|
|
60
59
|
fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py,sha256=sckNoxPE8ApKCLgBZzE_2dokXrM6mIXvMguHZvyJzIM,3891
|
|
61
60
|
fastapi_rtk/bases/__init__.py,sha256=Te9rcmi1AGG72iZCzGyvc5qEIVyIexN7ViFdb59a2RA,494
|
|
62
61
|
fastapi_rtk/bases/db.py,sha256=D27BhF89J0OaLHjALDCa85eNf35lBaTz6VV7EDa4wuM,18711
|
|
63
|
-
fastapi_rtk/bases/file_manager.py,sha256=
|
|
62
|
+
fastapi_rtk/bases/file_manager.py,sha256=sT49sYA-KFVZS4I4_uf4838QBmKBUPBlklFfHhz2M7Y,11603
|
|
64
63
|
fastapi_rtk/bases/filter.py,sha256=XmWTcLaIcBj9pKF1PMAKdwSnZNpdT8Df3uLeUIOGUDE,1840
|
|
65
64
|
fastapi_rtk/bases/interface.py,sha256=Cq9Duxa3w-tw342P424h88fc0_X1DoxCdTa3rAN-6jM,45380
|
|
66
65
|
fastapi_rtk/bases/model.py,sha256=nUZf0AVs0Mzqh2u_ALiRNYN1bfOU9PzYLvEFHDQ57Y0,1692
|
|
@@ -85,9 +84,9 @@ fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako,sha256=xm
|
|
|
85
84
|
fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py,sha256=KXb0a5c_3zutlTgyTP6MofCL0jinixLZMdG5aYd0E_U,7529
|
|
86
85
|
fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako,sha256=o02Ul3GmWdWkOiLZiKInBcAuCN3OjgxCzdj5Y1gvgVY,1324
|
|
87
86
|
fastapi_rtk/file_managers/__init__.py,sha256=RZzSSUDq_cIv4MeVePRBGR0mbEiJua7WKccfE6IAawk,198
|
|
88
|
-
fastapi_rtk/file_managers/file_manager.py,sha256=
|
|
87
|
+
fastapi_rtk/file_managers/file_manager.py,sha256=vXRp7e7i8bbLe3pSuvz7nJLNUJSguZhYcie_L6MU_rQ,2695
|
|
89
88
|
fastapi_rtk/file_managers/image_manager.py,sha256=4Tq1m0cdEYXGPJdjXJoEJWERs1vL6Ejfwyd_MBHp71U,628
|
|
90
|
-
fastapi_rtk/file_managers/s3_file_manager.py,sha256=
|
|
89
|
+
fastapi_rtk/file_managers/s3_file_manager.py,sha256=8lxmk889N0CQwi7HryfFp4YVPuwHoGEIMG6gHHQ9YBo,6962
|
|
91
90
|
fastapi_rtk/file_managers/s3_image_manager.py,sha256=FBwdGu9FWDPjrUQVTvTyZNnyZvS5-hd-fkxQLOsLa80,533
|
|
92
91
|
fastapi_rtk/lang/__init__.py,sha256=zJejpbIXx1O-nSuWwrCYgr5f5EtwjYW6kh8jRy_Yo0U,68
|
|
93
92
|
fastapi_rtk/lang/babel.cfg,sha256=ahkzg8wUKyl9G9UqkgAyWEtRlCnaWkjStM5c4FhKuUE,18
|
|
@@ -103,15 +102,15 @@ fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po,sha256=qxKDx7Q0xgKe48w9
|
|
|
103
102
|
fastapi_rtk/security/__init__.py,sha256=XkYZ_GO2opdyQS_LJwbCH1C6SUkNfLUvLkpFhcO7ZlI,86
|
|
104
103
|
fastapi_rtk/security/sqla/__init__.py,sha256=qKh1mGQezDtMzvRzxzfHZRyKVaks-O7PXrOx16ekSZA,88
|
|
105
104
|
fastapi_rtk/security/sqla/apis.py,sha256=8Qtd1G73i2TaA9Euo63naaB8d_I9r4EHL8DWtWNJEfs,10699
|
|
106
|
-
fastapi_rtk/security/sqla/models.py,sha256=
|
|
105
|
+
fastapi_rtk/security/sqla/models.py,sha256=u3raCpfawRggQnTgLgKUAzIWSFmrzkm_2RVwGTWWHfw,7942
|
|
107
106
|
fastapi_rtk/security/sqla/security_manager.py,sha256=ui0g2cH8Z-YjDuqszcstyj6Li8U4IpuVV4bAVaD69Lc,31395
|
|
108
|
-
fastapi_rtk/utils/__init__.py,sha256=
|
|
107
|
+
fastapi_rtk/utils/__init__.py,sha256=pIytlIp2c6r6vZqU-m_-DUoS0_lQcVKecByW0psw2n8,1782
|
|
109
108
|
fastapi_rtk/utils/async_task_runner.py,sha256=ISKDmF8SJS9sVAwrGEzvUwL7D_c0mmTiYi3GJ3J67kA,16473
|
|
110
109
|
fastapi_rtk/utils/class_factory.py,sha256=jlVw8yCh-tYsMnR5Hm8fgxtL0kvXwnhe6DPJA1sUh7k,598
|
|
111
110
|
fastapi_rtk/utils/csv_json_converter.py,sha256=7szrPiB7DrK5S-s2GaHVCmEP9_OXk9RFwbZmRtAKM5A,14036
|
|
112
111
|
fastapi_rtk/utils/deep_merge.py,sha256=PHtKJgXfCngOBGVliX9aVlEFcwCxr-GlzU-w6vjgAIs,2426
|
|
113
112
|
fastapi_rtk/utils/extender_mixin.py,sha256=eu22VAZJIf-r_uD-zVn_2IzvknfuUkmEHn9oo-0KU0k,1388
|
|
114
|
-
fastapi_rtk/utils/flask_appbuilder_utils.py,sha256
|
|
113
|
+
fastapi_rtk/utils/flask_appbuilder_utils.py,sha256=-wwRCn4eqROlTzuMby5NEq9xqO5aSI43KyPIetysx6w,373
|
|
115
114
|
fastapi_rtk/utils/formatter.py,sha256=bLMAo6-NmD7bp64VkkenoybWTAvbMyOHX55FRfINjVI,1224
|
|
116
115
|
fastapi_rtk/utils/hooks.py,sha256=iGE8HTfDDVfQm7yeD3WqPB8_I1FXFGpBdl3ngyjaRbY,901
|
|
117
116
|
fastapi_rtk/utils/lazy.py,sha256=SlVYQ-RHRcp6pGmcslVNc5lKs5GOSjqLcRsQsSWIB0s,10352
|
|
@@ -125,9 +124,8 @@ fastapi_rtk/utils/sqla.py,sha256=To4PhsO5orPJVqjdLh5C9y_xPgiy8-zhrJdSqhR_tsc,690
|
|
|
125
124
|
fastapi_rtk/utils/timezone.py,sha256=62S0pPWuDFFXxV1YTFCsc4uKiSP_Ba36Fv7S3gYjfhs,570
|
|
126
125
|
fastapi_rtk/utils/update_signature.py,sha256=PIzZgNpGEwvDNgQ3G51Zi-QhVV3mbxvUvSwDwf_-yYs,2209
|
|
127
126
|
fastapi_rtk/utils/use_default_when_none.py,sha256=H2HqhKy_8eYk3i1xijEjuaKak0oWkMIkrdz6T7DK9QU,469
|
|
128
|
-
fastapi_rtk/
|
|
129
|
-
fastapi_rtk-1.0.
|
|
130
|
-
fastapi_rtk-1.0.
|
|
131
|
-
fastapi_rtk-1.0.
|
|
132
|
-
fastapi_rtk-1.0.
|
|
133
|
-
fastapi_rtk-1.0.20.dist-info/RECORD,,
|
|
127
|
+
fastapi_rtk-1.0.22.dist-info/METADATA,sha256=BY2jPyBX2o7wdKs5swp8FJgPSVnw0FoqdZVRSQL4DyU,1333
|
|
128
|
+
fastapi_rtk-1.0.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
129
|
+
fastapi_rtk-1.0.22.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
|
|
130
|
+
fastapi_rtk-1.0.22.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
|
|
131
|
+
fastapi_rtk-1.0.22.dist-info/RECORD,,
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import hmac
|
|
3
|
-
import secrets
|
|
4
|
-
|
|
5
|
-
SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
6
|
-
DEFAULT_PBKDF2_ITERATIONS = 600000
|
|
7
|
-
|
|
8
|
-
__all__ = ["generate_password_hash", "check_password_hash"]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
|
|
12
|
-
method, *args = method.split(":")
|
|
13
|
-
salt_bytes = salt.encode()
|
|
14
|
-
password_bytes = password.encode()
|
|
15
|
-
|
|
16
|
-
if method == "scrypt":
|
|
17
|
-
if not args:
|
|
18
|
-
n = 2**15
|
|
19
|
-
r = 8
|
|
20
|
-
p = 1
|
|
21
|
-
else:
|
|
22
|
-
try:
|
|
23
|
-
n, r, p = map(int, args)
|
|
24
|
-
except ValueError:
|
|
25
|
-
raise ValueError("'scrypt' takes 3 arguments.") from None
|
|
26
|
-
|
|
27
|
-
maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
|
|
28
|
-
return (
|
|
29
|
-
hashlib.scrypt(
|
|
30
|
-
password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
|
|
31
|
-
).hex(),
|
|
32
|
-
f"scrypt:{n}:{r}:{p}",
|
|
33
|
-
)
|
|
34
|
-
elif method == "pbkdf2":
|
|
35
|
-
len_args = len(args)
|
|
36
|
-
|
|
37
|
-
if len_args == 0:
|
|
38
|
-
hash_name = "sha256"
|
|
39
|
-
iterations = DEFAULT_PBKDF2_ITERATIONS
|
|
40
|
-
elif len_args == 1:
|
|
41
|
-
hash_name = args[0]
|
|
42
|
-
iterations = DEFAULT_PBKDF2_ITERATIONS
|
|
43
|
-
elif len_args == 2:
|
|
44
|
-
hash_name = args[0]
|
|
45
|
-
iterations = int(args[1])
|
|
46
|
-
else:
|
|
47
|
-
raise ValueError("'pbkdf2' takes 2 arguments.")
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
hashlib.pbkdf2_hmac(
|
|
51
|
-
hash_name, password_bytes, salt_bytes, iterations
|
|
52
|
-
).hex(),
|
|
53
|
-
f"pbkdf2:{hash_name}:{iterations}",
|
|
54
|
-
)
|
|
55
|
-
else:
|
|
56
|
-
raise ValueError(f"Invalid hash method '{method}'.")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def gen_salt(length: int) -> str:
|
|
60
|
-
"""Generate a random string of SALT_CHARS with specified ``length``."""
|
|
61
|
-
if length <= 0:
|
|
62
|
-
raise ValueError("Salt length must be at least 1.")
|
|
63
|
-
|
|
64
|
-
return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def generate_password_hash(
|
|
68
|
-
password: str, method: str = "scrypt", salt_length: int = 16
|
|
69
|
-
) -> str:
|
|
70
|
-
"""Securely hash a password for storage. A password can be compared to a stored hash
|
|
71
|
-
using :func:`check_password_hash`.
|
|
72
|
-
|
|
73
|
-
The following methods are supported:
|
|
74
|
-
|
|
75
|
-
- ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
|
|
76
|
-
is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
|
|
77
|
-
- ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
|
|
78
|
-
the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
|
|
79
|
-
|
|
80
|
-
Default parameters may be updated to reflect current guidelines, and methods may be
|
|
81
|
-
deprecated and removed if they are no longer considered secure. To migrate old
|
|
82
|
-
hashes, you may generate a new hash when checking an old hash, or you may contact
|
|
83
|
-
users with a link to reset their password.
|
|
84
|
-
|
|
85
|
-
:param password: The plaintext password.
|
|
86
|
-
:param method: The key derivation function and parameters.
|
|
87
|
-
:param salt_length: The number of characters to generate for the salt.
|
|
88
|
-
|
|
89
|
-
.. versionchanged:: 2.3
|
|
90
|
-
Scrypt support was added.
|
|
91
|
-
|
|
92
|
-
.. versionchanged:: 2.3
|
|
93
|
-
The default iterations for pbkdf2 was increased to 600,000.
|
|
94
|
-
|
|
95
|
-
.. versionchanged:: 2.3
|
|
96
|
-
All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
|
|
97
|
-
"""
|
|
98
|
-
salt = gen_salt(salt_length)
|
|
99
|
-
h, actual_method = _hash_internal(method, salt, password)
|
|
100
|
-
return f"{actual_method}${salt}${h}"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def check_password_hash(pwhash: str, password: str) -> bool:
|
|
104
|
-
"""Securely check that the given stored password hash, previously generated using
|
|
105
|
-
:func:`generate_password_hash`, matches the given password.
|
|
106
|
-
|
|
107
|
-
Methods may be deprecated and removed if they are no longer considered secure. To
|
|
108
|
-
migrate old hashes, you may generate a new hash when checking an old hash, or you
|
|
109
|
-
may contact users with a link to reset their password.
|
|
110
|
-
|
|
111
|
-
:param pwhash: The hashed password.
|
|
112
|
-
:param password: The plaintext password.
|
|
113
|
-
|
|
114
|
-
.. versionchanged:: 2.3
|
|
115
|
-
All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
|
|
116
|
-
"""
|
|
117
|
-
try:
|
|
118
|
-
method, salt, hashval = pwhash.split("$", 2)
|
|
119
|
-
except ValueError:
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
|
fastapi_rtk/utils/werkzeug.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
__all__ = ["import_string", "ImportStringError"]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ImportStringError(ImportError):
|
|
7
|
-
"""
|
|
8
|
-
COPIED FROM WERKZEUG LIBRARY
|
|
9
|
-
|
|
10
|
-
Provides information about a failed :func:`import_string` attempt.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
#: String in dotted notation that failed to be imported.
|
|
14
|
-
import_name: str
|
|
15
|
-
#: Wrapped exception.
|
|
16
|
-
exception: BaseException
|
|
17
|
-
|
|
18
|
-
def __init__(self, import_name: str, exception: BaseException) -> None:
|
|
19
|
-
self.import_name = import_name
|
|
20
|
-
self.exception = exception
|
|
21
|
-
msg = import_name
|
|
22
|
-
name = ""
|
|
23
|
-
tracked = []
|
|
24
|
-
for part in import_name.replace(":", ".").split("."):
|
|
25
|
-
name = f"{name}.{part}" if name else part
|
|
26
|
-
imported = import_string(name, silent=True)
|
|
27
|
-
if imported:
|
|
28
|
-
tracked.append((name, getattr(imported, "__file__", None)))
|
|
29
|
-
else:
|
|
30
|
-
track = [f"- {n!r} found in {i!r}." for n, i in tracked]
|
|
31
|
-
track.append(f"- {name!r} not found.")
|
|
32
|
-
track_str = "\n".join(track)
|
|
33
|
-
msg = (
|
|
34
|
-
f"import_string() failed for {import_name!r}. Possible reasons"
|
|
35
|
-
f" are:\n\n"
|
|
36
|
-
"- missing __init__.py in a package;\n"
|
|
37
|
-
"- package or module path not included in sys.path;\n"
|
|
38
|
-
"- duplicated package or module name taking precedence in"
|
|
39
|
-
" sys.path;\n"
|
|
40
|
-
"- missing module, class, function or variable;\n\n"
|
|
41
|
-
f"Debugged import:\n\n{track_str}\n\n"
|
|
42
|
-
f"Original exception:\n\n{type(exception).__name__}: {exception}"
|
|
43
|
-
)
|
|
44
|
-
break
|
|
45
|
-
|
|
46
|
-
super().__init__(msg)
|
|
47
|
-
|
|
48
|
-
def __repr__(self) -> str:
|
|
49
|
-
return f"<{type(self).__name__}({self.import_name!r}, {self.exception!r})>"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def import_string(import_name: str, silent: bool = False):
|
|
53
|
-
"""
|
|
54
|
-
COPIED FROM WERKZEUG LIBRARY
|
|
55
|
-
|
|
56
|
-
Imports an object based on a string. This is useful if you want to
|
|
57
|
-
use import paths as endpoints or something similar. An import path can
|
|
58
|
-
be specified either in dotted notation (``xml.sax.saxutils.escape``)
|
|
59
|
-
or with a colon as object delimiter (``xml.sax.saxutils:escape``).
|
|
60
|
-
|
|
61
|
-
If `silent` is True the return value will be `None` if the import fails.
|
|
62
|
-
|
|
63
|
-
:param import_name: the dotted name for the object to import.
|
|
64
|
-
:param silent: if set to `True` import errors are ignored and
|
|
65
|
-
`None` is returned instead.
|
|
66
|
-
:return: imported object
|
|
67
|
-
"""
|
|
68
|
-
import_name = import_name.replace(":", ".")
|
|
69
|
-
try:
|
|
70
|
-
try:
|
|
71
|
-
__import__(import_name)
|
|
72
|
-
except ImportError:
|
|
73
|
-
if "." not in import_name:
|
|
74
|
-
raise
|
|
75
|
-
else:
|
|
76
|
-
return sys.modules[import_name]
|
|
77
|
-
|
|
78
|
-
module_name, obj_name = import_name.rsplit(".", 1)
|
|
79
|
-
module = __import__(module_name, globals(), locals(), [obj_name])
|
|
80
|
-
try:
|
|
81
|
-
return getattr(module, obj_name)
|
|
82
|
-
except AttributeError as e:
|
|
83
|
-
raise ImportError(e) from None
|
|
84
|
-
|
|
85
|
-
except ImportError as e:
|
|
86
|
-
if not silent:
|
|
87
|
-
raise ImportStringError(import_name, e).with_traceback(
|
|
88
|
-
sys.exc_info()[2]
|
|
89
|
-
) from None
|
|
90
|
-
|
|
91
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|