fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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.
Files changed (98) hide show
  1. fastapi_rtk/__init__.py +39 -35
  2. fastapi_rtk/_version.py +1 -0
  3. fastapi_rtk/api/model_rest_api.py +476 -221
  4. fastapi_rtk/auth/auth.py +0 -9
  5. fastapi_rtk/backends/generic/__init__.py +6 -0
  6. fastapi_rtk/backends/generic/column.py +21 -12
  7. fastapi_rtk/backends/generic/db.py +42 -7
  8. fastapi_rtk/backends/generic/filters.py +21 -16
  9. fastapi_rtk/backends/generic/interface.py +14 -8
  10. fastapi_rtk/backends/generic/model.py +19 -11
  11. fastapi_rtk/backends/sqla/__init__.py +1 -0
  12. fastapi_rtk/backends/sqla/db.py +77 -17
  13. fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
  14. fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
  15. fastapi_rtk/backends/sqla/filters.py +50 -21
  16. fastapi_rtk/backends/sqla/interface.py +96 -34
  17. fastapi_rtk/backends/sqla/model.py +56 -39
  18. fastapi_rtk/bases/__init__.py +20 -0
  19. fastapi_rtk/bases/db.py +94 -7
  20. fastapi_rtk/bases/file_manager.py +47 -3
  21. fastapi_rtk/bases/filter.py +22 -0
  22. fastapi_rtk/bases/interface.py +49 -5
  23. fastapi_rtk/bases/model.py +3 -0
  24. fastapi_rtk/bases/session.py +2 -0
  25. fastapi_rtk/cli/cli.py +62 -9
  26. fastapi_rtk/cli/commands/__init__.py +23 -0
  27. fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
  28. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
  29. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
  30. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
  31. fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
  32. fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
  33. fastapi_rtk/cli/commands/translate.py +299 -0
  34. fastapi_rtk/cli/decorators.py +9 -4
  35. fastapi_rtk/cli/utils.py +46 -0
  36. fastapi_rtk/config.py +41 -1
  37. fastapi_rtk/const.py +29 -1
  38. fastapi_rtk/db.py +76 -40
  39. fastapi_rtk/decorators.py +1 -1
  40. fastapi_rtk/dependencies.py +134 -62
  41. fastapi_rtk/exceptions.py +51 -1
  42. fastapi_rtk/fastapi_react_toolkit.py +186 -171
  43. fastapi_rtk/file_managers/file_manager.py +8 -6
  44. fastapi_rtk/file_managers/s3_file_manager.py +69 -33
  45. fastapi_rtk/globals.py +22 -12
  46. fastapi_rtk/lang/__init__.py +3 -0
  47. fastapi_rtk/lang/babel/__init__.py +4 -0
  48. fastapi_rtk/lang/babel/cli.py +40 -0
  49. fastapi_rtk/lang/babel/config.py +17 -0
  50. fastapi_rtk/lang/babel.cfg +1 -0
  51. fastapi_rtk/lang/lazy_text.py +120 -0
  52. fastapi_rtk/lang/messages.pot +238 -0
  53. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  54. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
  55. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  56. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
  57. fastapi_rtk/manager.py +355 -37
  58. fastapi_rtk/mixins.py +12 -0
  59. fastapi_rtk/routers.py +208 -72
  60. fastapi_rtk/schemas.py +142 -39
  61. fastapi_rtk/security/sqla/apis.py +39 -13
  62. fastapi_rtk/security/sqla/models.py +8 -23
  63. fastapi_rtk/security/sqla/security_manager.py +369 -11
  64. fastapi_rtk/setting.py +446 -88
  65. fastapi_rtk/types.py +94 -27
  66. fastapi_rtk/utils/__init__.py +8 -0
  67. fastapi_rtk/utils/async_task_runner.py +286 -61
  68. fastapi_rtk/utils/csv_json_converter.py +243 -40
  69. fastapi_rtk/utils/hooks.py +34 -0
  70. fastapi_rtk/utils/merge_schema.py +3 -3
  71. fastapi_rtk/utils/multiple_async_contexts.py +21 -0
  72. fastapi_rtk/utils/pydantic.py +46 -1
  73. fastapi_rtk/utils/run_utils.py +31 -1
  74. fastapi_rtk/utils/self_dependencies.py +1 -1
  75. fastapi_rtk/utils/use_default_when_none.py +1 -1
  76. fastapi_rtk/version.py +6 -1
  77. fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
  78. fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
  79. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
  80. fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
  81. fastapi_rtk/backends/gremlinpython/column.py +0 -208
  82. fastapi_rtk/backends/gremlinpython/db.py +0 -228
  83. fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
  84. fastapi_rtk/backends/gremlinpython/filters.py +0 -461
  85. fastapi_rtk/backends/gremlinpython/interface.py +0 -734
  86. fastapi_rtk/backends/gremlinpython/model.py +0 -364
  87. fastapi_rtk/backends/gremlinpython/session.py +0 -23
  88. fastapi_rtk/cli/commands.py +0 -295
  89. fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
  90. fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
  91. fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
  92. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
  93. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
  94. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
  95. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
  96. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
  97. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
  98. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -5,25 +5,37 @@ import os
5
5
  from contextlib import asynccontextmanager
6
6
  from typing import Awaitable, Callable
7
7
 
8
+ import fastapi
8
9
  import Secweb
9
10
  import Secweb.ContentSecurityPolicy
11
+ import starlette.types
10
12
  from fastapi import Depends, FastAPI, HTTPException, Request
11
13
  from fastapi.responses import HTMLResponse, StreamingResponse
12
14
  from fastapi.staticfiles import StaticFiles
13
15
  from fastapi.templating import Jinja2Templates
16
+ from fastapi_babel import BabelMiddleware
14
17
  from jinja2 import Environment, TemplateNotFound, select_autoescape
15
18
  from prometheus_fastapi_instrumentator import Instrumentator
16
- from sqlalchemy import and_, select
17
- from sqlalchemy.ext.asyncio import AsyncSession
18
- from sqlalchemy.orm import Session
19
19
  from starlette.routing import _DefaultLifespan
20
20
 
21
21
  from .api.model_rest_api import ModelRestApi
22
22
  from .auth import Auth
23
- from .cli.commands import upgrade
24
- from .const import BASE_APIS, DEFAULT_SECWEB_PARAMS, logger
23
+ from .backends.sqla.session import SQLASession
24
+ from .cli.commands.db import upgrade
25
+ from .cli.commands.translate import init_babel_cli
26
+ from .const import (
27
+ BASE_APIS,
28
+ DEFAULT_SECWEB_PARAMS,
29
+ INTERNAL_LANG_FOLDER,
30
+ ErrorCode,
31
+ logger,
32
+ )
25
33
  from .db import db
26
- from .dependencies import set_global_background_tasks, set_global_user
34
+ from .dependencies import (
35
+ set_global_background_tasks,
36
+ set_global_request,
37
+ set_global_user,
38
+ )
27
39
  from .globals import GlobalsMiddleware, g
28
40
  from .middlewares import (
29
41
  ProfilerMiddleware,
@@ -42,7 +54,7 @@ from .security.sqla.apis import (
42
54
  from .security.sqla.models import Api, Permission, PermissionApi, Role
43
55
  from .security.sqla.security_manager import SecurityManager
44
56
  from .setting import Setting
45
- from .utils import deep_merge, safe_call, smart_run
57
+ from .utils import deep_merge, lazy, multiple_async_contexts, safe_call, smart_run
46
58
  from .version import __version__
47
59
 
48
60
  __all__ = ["FastAPIReactToolkit"]
@@ -137,6 +149,7 @@ class FastAPIReactToolkit:
137
149
  exclude_apis: list[BASE_APIS] = None
138
150
  global_user_dependency = True
139
151
  global_background_tasks = True
152
+ global_request = True
140
153
  instrumentator: Instrumentator = None
141
154
  on_startup: (
142
155
  Callable[[FastAPI], None] | Awaitable[Callable[[FastAPI], None]] | None
@@ -148,7 +161,15 @@ class FastAPIReactToolkit:
148
161
  # Public attributes
149
162
  apis: list[ModelRestApi] = None
150
163
  initialized = False
151
- security: SecurityManager = None
164
+ sm: SecurityManager = None
165
+ security: SecurityManager = lazy(lambda self: self.sm)
166
+ """
167
+ Old attribute for `sm`, kept for backward compatibility.
168
+ """
169
+ started = False
170
+ """
171
+ Indicates whether the application has been started.
172
+ """
152
173
 
153
174
  # Private attributes
154
175
  _mounted = False
@@ -166,6 +187,7 @@ class FastAPIReactToolkit:
166
187
  exclude_apis: list[BASE_APIS] | None = None,
167
188
  global_user_dependency: bool = True,
168
189
  global_background_tasks: bool = True,
190
+ global_request: bool = True,
169
191
  instrumentator: Instrumentator | None = None,
170
192
  on_startup: (
171
193
  Callable[[FastAPI], None] | Awaitable[Callable[[FastAPI], None]] | None
@@ -173,6 +195,9 @@ class FastAPIReactToolkit:
173
195
  on_shutdown: (
174
196
  Callable[[FastAPI], None] | Awaitable[Callable[[FastAPI], None]] | None
175
197
  ) = None,
198
+ lifespans: starlette.types.Lifespan[fastapi.applications.AppType]
199
+ | list[starlette.types.Lifespan[fastapi.applications.AppType]]
200
+ | None = None,
176
201
  debug: bool = False,
177
202
  ):
178
203
  """
@@ -187,14 +212,16 @@ class FastAPIReactToolkit:
187
212
  exclude_apis (list[BASE_APIS] | None, optional): List of security APIs to be excluded when initializing the FastAPI application. Defaults to None.
188
213
  global_user_dependency (bool, optional): Whether to add the `set_global_user` dependency to the FastAPI application. This allows you to access the current user with the `g.user` object. Defaults to True.
189
214
  global_background_tasks (bool, optional): Whether to add the `set_global_background_tasks` dependency to the FastAPI application. This allows you to access the background tasks with the `g.background_tasks` object. Defaults to True.
215
+ global_request (bool, optional): Whether to add the `set_global_request` dependency to the FastAPI application. This allows you to access the current request with the `g.request` object. Defaults to True.
190
216
  instrumentator (Instrumentator | None, optional): The instrumentator to use for monitoring the FastAPI application. Defaults to `Instrumentator(**Setting.INSTRUMENTATOR_CONFIG)`.
191
217
  on_startup (Callable[[FastAPI], None] | Awaitable[Callable[[FastAPI], None]] | None, optional): Function to call when the app is starting up. Can either be a regular function or an async function. If the function takes a `FastAPI` instance as an argument, it will be passed to the function. Defaults to None.
192
218
  on_shutdown (Callable[[FastAPI], None] | Awaitable[Callable[[FastAPI], None]] | None, optional): Function to call when the app is shutting down. Can either be a regular function or an async function. If the function takes a `FastAPI` instance as an argument, it will be passed to the function. Defaults to None.
219
+ lifespans (starlette.types.Lifespan[fastapi.applications.AppType] | list[starlette.types.Lifespan[fastapi.applications.AppType]] | None, optional): Lifespan or list of lifespans to combine with the main lifespan. Defaults to None.
193
220
  debug (bool, optional): Whether to log debug messages. Defaults to False.
194
221
  """
195
222
  g.current_app = self
196
223
  self.apis = []
197
- self.security = SecurityManager(self)
224
+ self.sm = SecurityManager(self)
198
225
 
199
226
  # Database configuration
200
227
  self.create_tables = create_tables
@@ -204,9 +231,17 @@ class FastAPIReactToolkit:
204
231
  self.exclude_apis = exclude_apis or []
205
232
  self.global_user_dependency = global_user_dependency
206
233
  self.global_background_tasks = global_background_tasks
234
+ self.global_request = global_request
207
235
  self.instrumentator = instrumentator
208
236
  self.on_startup = on_startup
209
237
  self.on_shutdown = on_shutdown
238
+ self.lifespans = (
239
+ lifespans
240
+ if isinstance(lifespans, list)
241
+ else [lifespans]
242
+ if lifespans
243
+ else []
244
+ )
210
245
 
211
246
  if auth:
212
247
  for key, value in auth.items():
@@ -273,6 +308,16 @@ class FastAPIReactToolkit:
273
308
  self.app.router.dependencies.append(Depends(set_global_user()))
274
309
  if self.global_background_tasks:
275
310
  self.app.router.dependencies.append(Depends(set_global_background_tasks))
311
+ if self.global_request:
312
+ self.app.router.dependencies.append(Depends(set_global_request))
313
+
314
+ # Add the language middleware
315
+ try:
316
+ babel_cli = init_babel_cli(create_root_path_if_not_exists=False, log=False)
317
+ except FileNotFoundError:
318
+ # If the user does not have a lang folder, then use the internal one
319
+ babel_cli = init_babel_cli(root_path=INTERNAL_LANG_FOLDER, log=False)
320
+ self.app.add_middleware(BabelMiddleware, babel_configs=babel_cli.babel.config)
276
321
 
277
322
  # Initialize the instrumentator
278
323
  if not self.instrumentator:
@@ -375,171 +420,121 @@ class FastAPIReactToolkit:
375
420
 
376
421
  async with db.session() as session:
377
422
  logger.info("INITIALIZING DATABASE")
378
- await self._insert_permissions(session)
379
- await self._insert_apis(session)
380
- await self._insert_roles(session)
381
- await self._associate_permission_with_api(session)
382
- await self._associate_permission_api_with_role(session)
423
+ permissions = await self._insert_permissions(session)
424
+ apis = await self._insert_apis(session)
425
+ roles = await self._insert_roles(session)
426
+ assert permissions is not None, "Permissions should not be None"
427
+ assert apis is not None, "APIs should not be None"
428
+ permission_apis = await self._associate_permission_with_api(
429
+ session, permissions, apis
430
+ )
431
+ assert roles is not None, "Roles should not be None"
432
+ assert permission_apis is not None, "PermissionApis should not be None"
433
+ await self._associate_role_with_permission_api(
434
+ session, roles, permission_apis
435
+ )
383
436
  if self.cleanup:
384
- await self.security.cleanup()
437
+ await self.sm.cleanup()
385
438
  logger.info("DATABASE INITIALIZED")
386
439
 
387
- async def _insert_permissions(self, session: AsyncSession | Session):
388
- new_permissions = self.total_permissions()
389
- stmt = select(Permission).where(Permission.name.in_(new_permissions))
390
- result = await safe_call(session.scalars(stmt))
391
- existing_permissions = [permission.name for permission in result.all()]
392
- if len(new_permissions) == len(existing_permissions):
393
- return
394
-
395
- permission_objs = [
396
- Permission(name=permission)
397
- for permission in new_permissions
398
- if permission not in existing_permissions
440
+ async def _insert_permissions(self, session: SQLASession):
441
+ permissions = self.total_permissions() + [
442
+ x[1] for x in self.sm.get_api_permission_tuples_from_builtin_roles()
399
443
  ]
400
- for permission in permission_objs:
401
- logger.info(f"ADDING PERMISSION {permission}")
402
- session.add(permission)
403
- await safe_call(session.commit())
404
-
405
- async def _insert_apis(self, session: AsyncSession | Session):
406
- new_apis = [api.__class__.__name__ for api in self.apis]
407
- stmt = select(Api).where(Api.name.in_(new_apis))
408
- result = await safe_call(session.scalars(stmt))
409
- existing_apis = [api.name for api in result.all()]
410
- if len(new_apis) == len(existing_apis):
411
- return
412
-
413
- api_objs = [Api(name=api) for api in new_apis if api not in existing_apis]
414
- for api in api_objs:
415
- logger.info(f"ADDING API {api}")
416
- session.add(api)
417
- await safe_call(session.commit())
418
-
419
- async def _insert_roles(self, session: AsyncSession | Session):
420
- new_roles = [g.admin_role, g.public_role]
421
- stmt = select(Role).where(Role.name.in_(new_roles))
422
- result = await safe_call(session.scalars(stmt))
423
- existing_roles = [role.name for role in result.all()]
424
- if len(new_roles) == len(existing_roles):
425
- return
444
+ permissions = list(dict.fromkeys(permissions))
445
+ return await self.sm.create_permissions(permissions, session=session)
426
446
 
427
- role_objs = [
428
- Role(name=role) for role in new_roles if role not in existing_roles
447
+ async def _insert_apis(self, session: SQLASession):
448
+ apis = [api.__class__.__name__ for api in self.apis] + [
449
+ x[0] for x in self.sm.get_api_permission_tuples_from_builtin_roles()
429
450
  ]
430
- for role in role_objs:
431
- logger.info(f"ADDING ROLE {role}")
432
- session.add(role)
433
- await safe_call(session.commit())
451
+ apis = list(dict.fromkeys(apis))
452
+ return await self.sm.create_apis(apis, session=session)
453
+
454
+ async def _insert_roles(self, session: SQLASession):
455
+ roles = self.sm.get_roles_from_builtin_roles()
456
+ if g.admin_role and g.admin_role not in roles:
457
+ roles.append(g.admin_role)
458
+ if g.public_role and g.public_role not in roles:
459
+ roles.append(g.public_role)
460
+ return await self.sm.create_roles(roles, session=session)
461
+
462
+ async def _associate_permission_with_api(
463
+ self,
464
+ session: SQLASession,
465
+ permissions: list[Permission],
466
+ apis: list[Api],
467
+ ):
468
+ permission_map = {permission.name: permission for permission in permissions}
469
+ api_map = {api.name: api for api in apis}
470
+ permission_api_tuples = list[tuple[Permission, Api]]()
471
+ added_permission_api = set[tuple[str, str]]()
434
472
 
435
- async def _associate_permission_with_api(self, session: AsyncSession | Session):
436
473
  for api in self.apis:
437
- new_permissions = getattr(api, "permissions", [])
438
- if not new_permissions:
474
+ api_permissions = api.permissions
475
+ if not api_permissions:
439
476
  continue
440
477
 
441
- # Get the api object
442
- api_stmt = select(Api).where(Api.name == api.__class__.__name__)
443
- api_obj = await safe_call(session.scalar(api_stmt))
444
-
445
- if not api_obj:
446
- raise ValueError(f"API {api.__class__.__name__} not found")
447
-
448
- permission_stmt = select(Permission).where(
449
- and_(
450
- Permission.name.in_(new_permissions),
451
- ~Permission.id.in_([p.permission_id for p in api_obj.permissions]),
452
- )
453
- )
454
- permission_result = await safe_call(session.scalars(permission_stmt))
455
- new_permissions = permission_result.all()
456
-
457
- if not new_permissions:
478
+ for permission_name in api_permissions:
479
+ if (permission_name, api.__class__.__name__) in added_permission_api:
480
+ continue
481
+ permission = permission_map[permission_name]
482
+ api_obj = api_map[api.__class__.__name__]
483
+ permission_api_tuples.append((permission, api_obj))
484
+ added_permission_api.add((permission.name, api_obj.name))
485
+
486
+ for (
487
+ api_name,
488
+ perm_name,
489
+ ) in self.sm.get_api_permission_tuples_from_builtin_roles():
490
+ if (perm_name, api_name) in added_permission_api:
458
491
  continue
492
+ permission = permission_map[perm_name]
493
+ api_obj = api_map[api_name]
494
+ permission_api_tuples.append((permission, api_obj))
495
+ added_permission_api.add((permission.name, api_obj.name))
459
496
 
460
- for permission in new_permissions:
461
- session.add(
462
- PermissionApi(permission_id=permission.id, api_id=api_obj.id)
463
- )
464
- logger.info(f"ASSOCIATING PERMISSION {permission} WITH API {api_obj}")
465
- await safe_call(session.commit())
497
+ return await self.sm.associate_list_of_permission_with_api(
498
+ permission_api_tuples, session=session
499
+ )
466
500
 
467
- async def _associate_permission_api_with_role(
468
- self, session: AsyncSession | Session
501
+ async def _associate_role_with_permission_api(
502
+ self,
503
+ session: SQLASession,
504
+ roles: list[Role],
505
+ permission_apis: list[PermissionApi],
469
506
  ):
470
- # Read config based roles
471
- roles_dict = Setting.ROLES
472
-
473
- for role_name, role_permissions in roles_dict.items():
474
- role_stmt = select(Role).where(Role.name == role_name)
475
- role_result = await safe_call(session.scalars(role_stmt))
476
- role = role_result.first()
477
- if not role:
478
- role = Role(name=role_name)
479
- logger.info(f"ADDING ROLE {role}")
480
- session.add(role)
481
-
482
- for api_name, permission_name in role_permissions:
483
- permission_api_stmt = (
484
- select(PermissionApi)
485
- .where(
486
- and_(Api.name == api_name, Permission.name == permission_name)
487
- )
488
- .join(Permission)
489
- .join(Api)
490
- )
491
- permission_api = await safe_call(session.scalar(permission_api_stmt))
492
- if not permission_api:
493
- permission_stmt = select(Permission).where(
494
- Permission.name == permission_name
495
- )
496
- permission = await safe_call(session.scalar(permission_stmt))
497
- if not permission:
498
- permission = Permission(name=permission_name)
499
- logger.info(f"ADDING PERMISSION {permission}")
500
- session.add(permission)
501
-
502
- stmt = select(Api).where(Api.name == api_name)
503
- api = await safe_call(session.scalar(stmt))
504
- if not api:
505
- api = Api(name=api_name)
506
- logger.info(f"ADDING API {api}")
507
- session.add(api)
508
-
509
- permission_api = PermissionApi(permission=permission, api=api)
510
- logger.info(f"ADDING PERMISSION-API {permission_api}")
511
- session.add(permission_api)
512
-
513
- # Associate role with permission-api
514
- if role not in permission_api.roles:
515
- permission_api.roles.append(role)
516
- logger.info(
517
- f"ASSOCIATING {role} WITH PERMISSION-API {permission_api}"
518
- )
519
-
520
- await safe_call(session.commit())
521
-
522
- # Get admin role
523
- admin_role_stmt = select(Role).where(Role.name == g.admin_role)
524
- admin_role = await safe_call(session.scalar(admin_role_stmt))
525
-
526
- if admin_role:
527
- # Get list of permission-api.assoc_permission_api_id of the admin role
528
- stmt = (
529
- select(PermissionApi)
530
- .where(~PermissionApi.roles.contains(admin_role))
531
- .join(Api)
532
- )
533
- result = await safe_call(session.scalars(stmt))
534
- existing_assoc_permission_api_roles = result.all()
535
-
536
- # Add admin role to all permission-api objects
537
- for permission_api in existing_assoc_permission_api_roles:
538
- permission_api.roles.append(admin_role)
539
- logger.info(
540
- f"ASSOCIATING {admin_role} WITH PERMISSION-API {permission_api}"
541
- )
542
- await safe_call(session.commit())
507
+ role_map = {role.name: role for role in roles}
508
+ permission_api_map = {
509
+ (pa.permission.name, pa.api.name): pa for pa in permission_apis
510
+ }
511
+ permission_apis_to_exclude_from_admin = list[PermissionApi]()
512
+ role_permission_api_tuples = list[tuple[Role, PermissionApi]]()
513
+
514
+ for (
515
+ role_name,
516
+ role_api_permissions,
517
+ ) in self.sm.get_role_and_api_permission_tuples_from_builtin_roles():
518
+ for api_name, permission_name in role_api_permissions:
519
+ role = role_map[role_name]
520
+ permission_api = permission_api_map[(permission_name, api_name)]
521
+ role_permission_api_tuples.append((role, permission_api))
522
+ if "|" in permission_name and role_name != g.public_role:
523
+ # Exclude multi-tenant permissions from admin role if not explicitly set
524
+ permission_apis_to_exclude_from_admin.append(permission_api)
525
+
526
+ if g.admin_role:
527
+ admin_role = role_map[g.admin_role]
528
+ for permission_api in [
529
+ pa
530
+ for pa in permission_apis
531
+ if pa not in permission_apis_to_exclude_from_admin
532
+ ]:
533
+ role_permission_api_tuples.append((admin_role, permission_api))
534
+
535
+ await self.sm.associate_list_of_role_with_permission_api(
536
+ role_permission_api_tuples, session=session
537
+ )
543
538
 
544
539
  def _mount_static_folder(self):
545
540
  """
@@ -585,7 +580,9 @@ class FastAPIReactToolkit:
585
580
  },
586
581
  )
587
582
  except TemplateNotFound:
588
- raise HTTPException(status_code=404, detail="Not Found")
583
+ raise HTTPException(
584
+ fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.PAGE_NOT_FOUND
585
+ )
589
586
 
590
587
  """
591
588
  -----------------------------------------
@@ -624,14 +621,6 @@ class FastAPIReactToolkit:
624
621
  # Add the endpoint for the metrics
625
622
  self.instrumentator.expose(app, **Setting.INSTRUMENTATOR_EXPOSE_CONFIG)
626
623
 
627
- # Add the JS manifest route
628
- self._init_js_manifest()
629
-
630
- # Mount the static and template folders
631
- self._mounted = True
632
- self._mount_static_folder()
633
- self._mount_template_folder()
634
-
635
624
  await db.init_fastapi_rtk_tables()
636
625
 
637
626
  if self.upgrade_db:
@@ -678,13 +667,34 @@ class FastAPIReactToolkit:
678
667
  # Run when the app is shutting down
679
668
  await db.close()
680
669
 
670
+ @asynccontextmanager
671
+ async def mount_lifespan(app: FastAPI):
672
+ # Mount the js manifest, static, and template folders
673
+ self._init_js_manifest()
674
+ self._mount_static_folder()
675
+ self._mount_template_folder()
676
+ self._mounted = True
677
+ self.started = True
678
+ yield
679
+
680
+ # Combine with other lifespans
681
+ @asynccontextmanager
682
+ async def combined_lifespan(app: FastAPI):
683
+ async with multiple_async_contexts(
684
+ [
685
+ lifespan(app)
686
+ for lifespan in [lifespan] + self.lifespans + [mount_lifespan]
687
+ ]
688
+ ):
689
+ yield
690
+
681
691
  # Check whether lifespan is already set
682
692
  if not isinstance(self.app.router.lifespan_context, _DefaultLifespan):
683
693
  raise ValueError(
684
694
  "Lifespan already set, please do not set lifespan directly in the FastAPI app"
685
695
  )
686
696
 
687
- self.app.router.lifespan_context = lifespan
697
+ self.app.router.lifespan_context = combined_lifespan
688
698
 
689
699
  def _init_basic_apis(self):
690
700
  apis = [
@@ -707,7 +717,12 @@ class FastAPIReactToolkit:
707
717
  env = Environment(autoescape=select_autoescape(["html", "xml"]))
708
718
  template_string = "window.fab_react_config = {{ react_vars |tojson }}"
709
719
  template = env.from_string(template_string)
710
- rendered_string = template.render(react_vars=Setting.FAB_REACT_CONFIG)
720
+ react_vars = Setting.FAB_REACT_CONFIG
721
+ if Setting.TRANSLATIONS:
722
+ react_vars[Setting.TRANSLATIONS_KEY] = deep_merge(
723
+ react_vars.get(Setting.TRANSLATIONS_KEY, {}), Setting.TRANSLATIONS
724
+ )
725
+ rendered_string = template.render(react_vars=react_vars)
711
726
  content = rendered_string.encode("utf-8")
712
727
  scriptfile = io.BytesIO(content)
713
728
  return StreamingResponse(
@@ -4,7 +4,7 @@ import shutil
4
4
 
5
5
  from ..bases.file_manager import AbstractFileManager
6
6
  from ..setting import Setting
7
- from ..utils import lazy, secure_filename
7
+ from ..utils import lazy, secure_filename, smart_run
8
8
 
9
9
  __all__ = ["FileManager"]
10
10
 
@@ -20,6 +20,8 @@ class FileManager(AbstractFileManager):
20
20
  def post_init(self):
21
21
  if not self.base_path:
22
22
  raise ValueError("UPLOAD_FOLDER not set in config.")
23
+ if not op.exists(self.base_path):
24
+ os.makedirs(self.base_path, self.permission)
23
25
 
24
26
  def get_path(self, filename):
25
27
  return op.join(self.base_path, filename)
@@ -32,20 +34,20 @@ class FileManager(AbstractFileManager):
32
34
  async def stream_file(self, filename):
33
35
  _, path = self.generate_secure_filename(filename)
34
36
  with open(path, "rb") as f:
35
- while chunk := f.read(8192):
37
+ while chunk := await smart_run(f.read, 8192):
36
38
  yield chunk
37
39
 
38
40
  def save_file(self, file_data, filename):
39
- secured_filename, path = self.generate_secure_filename(filename)
41
+ _, path = self.generate_secure_filename(filename)
40
42
  with open(path, "wb") as buffer:
41
43
  shutil.copyfileobj(file_data.file, buffer)
42
- return secured_filename
44
+ return path
43
45
 
44
46
  def save_content_to_file(self, content, filename):
45
- secured_filename, path = self.generate_secure_filename(filename)
47
+ _, path = self.generate_secure_filename(filename)
46
48
  with open(path, "wb") as buffer:
47
49
  buffer.write(content)
48
- return secured_filename
50
+ return path
49
51
 
50
52
  def delete_file(self, filename):
51
53
  _, path = self.generate_secure_filename(filename)