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 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.20"
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,
fastapi_rtk/config.py CHANGED
@@ -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
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: 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")
@@ -1,19 +1,6 @@
1
- import os
2
- import re
3
- import unicodedata
4
1
  import uuid
5
2
 
6
- __all__ = ["uuid_namegen", "secure_filename"]
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.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,7 +1,7 @@
1
- fastapi_rtk/__init__.py,sha256=TGDmH62gymFCC6yFdAsiVHcdOvy4_tdDCzAK4WzEHH8,6264
2
- fastapi_rtk/_version.py,sha256=D3FIR1OP9p54JyJSpU5wDfDWvaAjk2k-IH5FzjX_kCA,23
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=9PZF9E5i1gxmnsZEprZZKxVHSk0dFEklJSplX9NEqdo,14036
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=TcoMHCueM7sFwZ8iYorUe4q-3KpVFfV04RAPuMTYFKY,8863
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=5gMGyH4Any9TwB3Fz23QDkmrX8MouzCxVgoAIcBqnTI,18988
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=B29CA1pOoCyoyKbknPmnIfgbYBLAWKD8pRdBlPh0RA4,111332
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=EeusoypVDHVuxneoPP2aXOPpyCwxx_5O22qMeDW51b0,919
29
- fastapi_rtk/auth/hashers/scrypt.py,sha256=CPGe6vzj7hlKTl_MKDGSgKQW927EpPkgpGM7mVO5MW0,902
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=A5Tt_BYQQc3cNYzWxJ5tCKcQe3HOxhXcrBoyWx6wtnc,11557
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=eEoaoh1pw0negbNNo7z8adijwCR_Y6ZWlbIeWK-9ZB0,2668
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=Rb5rEet2R72VFUHMPITJugVLhsTjxVh4oqrL7TjKZVo,6424
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=rEXpbljTMlTkiDp0VIxRXant8xN-WTjpqgQkr-ElXeM,7865
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=_FbWJg5OwHqHdoDfJnGcp54oVKIvAMwEkfOOrsv0-Tw,1891
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=nPLQIczDZgKElMtqBRSo_aPJZMOnPs7fRyjqKUtPDbo,2276
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/utils/werkzeug.py,sha256=1Gv-oyqSmhVGrmNbB9fDqpqJzPpANOzWf4zMMrhW9UA,3245
129
- fastapi_rtk-1.0.20.dist-info/METADATA,sha256=3HSGbdKbJ8IG24nA3D8HxptAVRj9NtYieA-3tF5GSZY,1302
130
- fastapi_rtk-1.0.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
131
- fastapi_rtk-1.0.20.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
132
- fastapi_rtk-1.0.20.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
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)
@@ -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