fastapi-rtk 0.2.60__py3-none-any.whl → 1.0.18__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 +0 -1
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +182 -87
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/sqla/db.py +32 -7
- fastapi_rtk/backends/sqla/filters.py +16 -0
- fastapi_rtk/backends/sqla/interface.py +11 -62
- fastapi_rtk/backends/sqla/model.py +16 -1
- fastapi_rtk/bases/db.py +20 -2
- fastapi_rtk/bases/file_manager.py +12 -0
- fastapi_rtk/bases/filter.py +1 -1
- fastapi_rtk/cli/cli.py +61 -0
- fastapi_rtk/cli/commands/security.py +6 -6
- fastapi_rtk/const.py +1 -1
- fastapi_rtk/db.py +3 -0
- fastapi_rtk/dependencies.py +110 -64
- fastapi_rtk/fastapi_react_toolkit.py +123 -172
- fastapi_rtk/file_managers/s3_file_manager.py +63 -32
- fastapi_rtk/lang/messages.pot +12 -12
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +12 -12
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +12 -12
- fastapi_rtk/manager.py +10 -14
- fastapi_rtk/schemas.py +6 -4
- fastapi_rtk/security/sqla/apis.py +20 -5
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +367 -10
- fastapi_rtk/utils/async_task_runner.py +119 -30
- fastapi_rtk/utils/csv_json_converter.py +242 -39
- fastapi_rtk/utils/hooks.py +7 -4
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.18.dist-info/METADATA +28 -0
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/RECORD +38 -38
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/WHEEL +1 -2
- fastapi_rtk-0.2.60.dist-info/METADATA +0 -25
- fastapi_rtk-0.2.60.dist-info/top_level.txt +0 -1
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,13 +16,11 @@ from fastapi.templating import Jinja2Templates
|
|
|
16
16
|
from fastapi_babel import BabelMiddleware
|
|
17
17
|
from jinja2 import Environment, TemplateNotFound, select_autoescape
|
|
18
18
|
from prometheus_fastapi_instrumentator import Instrumentator
|
|
19
|
-
from sqlalchemy import and_, select
|
|
20
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
21
|
-
from sqlalchemy.orm import Session
|
|
22
19
|
from starlette.routing import _DefaultLifespan
|
|
23
20
|
|
|
24
21
|
from .api.model_rest_api import ModelRestApi
|
|
25
22
|
from .auth import Auth
|
|
23
|
+
from .backends.sqla.session import SQLASession
|
|
26
24
|
from .cli.commands.db import upgrade
|
|
27
25
|
from .cli.commands.translate import init_babel_cli
|
|
28
26
|
from .const import (
|
|
@@ -56,7 +54,7 @@ from .security.sqla.apis import (
|
|
|
56
54
|
from .security.sqla.models import Api, Permission, PermissionApi, Role
|
|
57
55
|
from .security.sqla.security_manager import SecurityManager
|
|
58
56
|
from .setting import Setting
|
|
59
|
-
from .utils import deep_merge, multiple_async_contexts, safe_call, smart_run
|
|
57
|
+
from .utils import deep_merge, lazy, multiple_async_contexts, safe_call, smart_run
|
|
60
58
|
from .version import __version__
|
|
61
59
|
|
|
62
60
|
__all__ = ["FastAPIReactToolkit"]
|
|
@@ -163,7 +161,11 @@ class FastAPIReactToolkit:
|
|
|
163
161
|
# Public attributes
|
|
164
162
|
apis: list[ModelRestApi] = None
|
|
165
163
|
initialized = False
|
|
166
|
-
|
|
164
|
+
sm: SecurityManager = None
|
|
165
|
+
security: SecurityManager = lazy(lambda self: self.sm)
|
|
166
|
+
"""
|
|
167
|
+
Old attribute for `sm`, kept for backward compatibility.
|
|
168
|
+
"""
|
|
167
169
|
started = False
|
|
168
170
|
"""
|
|
169
171
|
Indicates whether the application has been started.
|
|
@@ -219,7 +221,7 @@ class FastAPIReactToolkit:
|
|
|
219
221
|
"""
|
|
220
222
|
g.current_app = self
|
|
221
223
|
self.apis = []
|
|
222
|
-
self.
|
|
224
|
+
self.sm = SecurityManager(self)
|
|
223
225
|
|
|
224
226
|
# Database configuration
|
|
225
227
|
self.create_tables = create_tables
|
|
@@ -418,175 +420,121 @@ class FastAPIReactToolkit:
|
|
|
418
420
|
|
|
419
421
|
async with db.session() as session:
|
|
420
422
|
logger.info("INITIALIZING DATABASE")
|
|
421
|
-
await self._insert_permissions(session)
|
|
422
|
-
await self._insert_apis(session)
|
|
423
|
-
await self._insert_roles(session)
|
|
424
|
-
|
|
425
|
-
|
|
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
|
+
)
|
|
426
436
|
if self.cleanup:
|
|
427
|
-
await self.
|
|
437
|
+
await self.sm.cleanup()
|
|
428
438
|
logger.info("DATABASE INITIALIZED")
|
|
429
439
|
|
|
430
|
-
async def _insert_permissions(self, session:
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
result = await safe_call(session.scalars(stmt))
|
|
434
|
-
existing_permissions = [permission.name for permission in result.all()]
|
|
435
|
-
if len(new_permissions) == len(existing_permissions):
|
|
436
|
-
return
|
|
437
|
-
|
|
438
|
-
permission_objs = [
|
|
439
|
-
Permission(name=permission)
|
|
440
|
-
for permission in new_permissions
|
|
441
|
-
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()
|
|
442
443
|
]
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
session.add(permission)
|
|
446
|
-
await safe_call(session.commit())
|
|
447
|
-
|
|
448
|
-
async def _insert_apis(self, session: AsyncSession | Session):
|
|
449
|
-
new_apis = [api.__class__.__name__ for api in self.apis]
|
|
450
|
-
stmt = select(Api).where(Api.name.in_(new_apis))
|
|
451
|
-
result = await safe_call(session.scalars(stmt))
|
|
452
|
-
existing_apis = [api.name for api in result.all()]
|
|
453
|
-
if len(new_apis) == len(existing_apis):
|
|
454
|
-
return
|
|
455
|
-
|
|
456
|
-
api_objs = [Api(name=api) for api in new_apis if api not in existing_apis]
|
|
457
|
-
for api in api_objs:
|
|
458
|
-
logger.info(f"ADDING API {api}")
|
|
459
|
-
session.add(api)
|
|
460
|
-
await safe_call(session.commit())
|
|
461
|
-
|
|
462
|
-
async def _insert_roles(self, session: AsyncSession | Session):
|
|
463
|
-
new_roles = [x for x in [g.admin_role, g.public_role] if x is not None]
|
|
464
|
-
stmt = select(Role).where(Role.name.in_(new_roles))
|
|
465
|
-
result = await safe_call(session.scalars(stmt))
|
|
466
|
-
existing_roles = [role.name for role in result.all()]
|
|
467
|
-
if len(new_roles) == len(existing_roles):
|
|
468
|
-
return
|
|
444
|
+
permissions = list(dict.fromkeys(permissions))
|
|
445
|
+
return await self.sm.create_permissions(permissions, session=session)
|
|
469
446
|
|
|
470
|
-
|
|
471
|
-
|
|
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()
|
|
472
450
|
]
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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]]()
|
|
477
472
|
|
|
478
|
-
async def _associate_permission_with_api(self, session: AsyncSession | Session):
|
|
479
473
|
for api in self.apis:
|
|
480
|
-
|
|
481
|
-
if not
|
|
474
|
+
api_permissions = api.permissions
|
|
475
|
+
if not api_permissions:
|
|
482
476
|
continue
|
|
483
477
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
)
|
|
497
|
-
permission_result = await safe_call(session.scalars(permission_stmt))
|
|
498
|
-
new_permissions = permission_result.all()
|
|
499
|
-
|
|
500
|
-
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:
|
|
501
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))
|
|
502
496
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
507
|
-
logger.info(f"ASSOCIATING PERMISSION {permission} WITH API {api_obj}")
|
|
508
|
-
await safe_call(session.commit())
|
|
497
|
+
return await self.sm.associate_list_of_permission_with_api(
|
|
498
|
+
permission_api_tuples, session=session
|
|
499
|
+
)
|
|
509
500
|
|
|
510
|
-
async def
|
|
511
|
-
self,
|
|
501
|
+
async def _associate_role_with_permission_api(
|
|
502
|
+
self,
|
|
503
|
+
session: SQLASession,
|
|
504
|
+
roles: list[Role],
|
|
505
|
+
permission_apis: list[PermissionApi],
|
|
512
506
|
):
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
stmt = select(Api).where(Api.name == api_name)
|
|
546
|
-
api = await safe_call(session.scalar(stmt))
|
|
547
|
-
if not api:
|
|
548
|
-
api = Api(name=api_name)
|
|
549
|
-
logger.info(f"ADDING API {api}")
|
|
550
|
-
session.add(api)
|
|
551
|
-
|
|
552
|
-
permission_api = PermissionApi(permission=permission, api=api)
|
|
553
|
-
logger.info(f"ADDING PERMISSION-API {permission_api}")
|
|
554
|
-
session.add(permission_api)
|
|
555
|
-
|
|
556
|
-
# Associate role with permission-api
|
|
557
|
-
if role not in permission_api.roles:
|
|
558
|
-
permission_api.roles.append(role)
|
|
559
|
-
logger.info(
|
|
560
|
-
f"ASSOCIATING {role} WITH PERMISSION-API {permission_api}"
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
await safe_call(session.commit())
|
|
564
|
-
|
|
565
|
-
# Get admin role
|
|
566
|
-
if g.admin_role is None:
|
|
567
|
-
logger.warning("Admin role is not set, skipping admin role association")
|
|
568
|
-
return
|
|
569
|
-
|
|
570
|
-
admin_role_stmt = select(Role).where(Role.name == g.admin_role)
|
|
571
|
-
admin_role = await safe_call(session.scalar(admin_role_stmt))
|
|
572
|
-
|
|
573
|
-
if admin_role:
|
|
574
|
-
# Get list of permission-api.assoc_permission_api_id of the admin role
|
|
575
|
-
stmt = (
|
|
576
|
-
select(PermissionApi)
|
|
577
|
-
.where(~PermissionApi.roles.contains(admin_role))
|
|
578
|
-
.join(Api)
|
|
579
|
-
)
|
|
580
|
-
result = await safe_call(session.scalars(stmt))
|
|
581
|
-
existing_assoc_permission_api_roles = result.all()
|
|
582
|
-
|
|
583
|
-
# Add admin role to all permission-api objects
|
|
584
|
-
for permission_api in existing_assoc_permission_api_roles:
|
|
585
|
-
permission_api.roles.append(admin_role)
|
|
586
|
-
logger.info(
|
|
587
|
-
f"ASSOCIATING {admin_role} WITH PERMISSION-API {permission_api}"
|
|
588
|
-
)
|
|
589
|
-
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
|
+
)
|
|
590
538
|
|
|
591
539
|
def _mount_static_folder(self):
|
|
592
540
|
"""
|
|
@@ -673,14 +621,6 @@ class FastAPIReactToolkit:
|
|
|
673
621
|
# Add the endpoint for the metrics
|
|
674
622
|
self.instrumentator.expose(app, **Setting.INSTRUMENTATOR_EXPOSE_CONFIG)
|
|
675
623
|
|
|
676
|
-
# Add the JS manifest route
|
|
677
|
-
self._init_js_manifest()
|
|
678
|
-
|
|
679
|
-
# Mount the static and template folders
|
|
680
|
-
self._mounted = True
|
|
681
|
-
self._mount_static_folder()
|
|
682
|
-
self._mount_template_folder()
|
|
683
|
-
|
|
684
624
|
await db.init_fastapi_rtk_tables()
|
|
685
625
|
|
|
686
626
|
if self.upgrade_db:
|
|
@@ -707,8 +647,6 @@ class FastAPIReactToolkit:
|
|
|
707
647
|
if hasattr(api, "datamodel"):
|
|
708
648
|
await safe_call(api.datamodel.on_startup())
|
|
709
649
|
|
|
710
|
-
self.started = True
|
|
711
|
-
|
|
712
650
|
yield
|
|
713
651
|
|
|
714
652
|
# On shutdown
|
|
@@ -729,11 +667,24 @@ class FastAPIReactToolkit:
|
|
|
729
667
|
# Run when the app is shutting down
|
|
730
668
|
await db.close()
|
|
731
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
|
+
|
|
732
680
|
# Combine with other lifespans
|
|
733
681
|
@asynccontextmanager
|
|
734
682
|
async def combined_lifespan(app: FastAPI):
|
|
735
683
|
async with multiple_async_contexts(
|
|
736
|
-
[
|
|
684
|
+
[
|
|
685
|
+
lifespan(app)
|
|
686
|
+
for lifespan in [lifespan] + self.lifespans + [mount_lifespan]
|
|
687
|
+
]
|
|
737
688
|
):
|
|
738
689
|
yield
|
|
739
690
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
1
3
|
from ..bases.file_manager import AbstractFileManager
|
|
2
4
|
from ..const import logger
|
|
3
5
|
from ..utils import smart_run
|
|
@@ -12,20 +14,58 @@ class S3FileManager(AbstractFileManager):
|
|
|
12
14
|
|
|
13
15
|
def __init__(
|
|
14
16
|
self,
|
|
15
|
-
base_path=None,
|
|
16
|
-
allowed_extensions=None,
|
|
17
|
-
namegen=None,
|
|
18
|
-
permission=None,
|
|
19
|
-
bucket_name=None,
|
|
20
|
-
bucket_subfolder=None,
|
|
21
|
-
access_key=None,
|
|
22
|
-
secret_key=None,
|
|
17
|
+
base_path: str | None = None,
|
|
18
|
+
allowed_extensions: list[str] | None = None,
|
|
19
|
+
namegen: typing.Callable[[str], str] | None = None,
|
|
20
|
+
permission: int | None = None,
|
|
21
|
+
bucket_name: str | None = None,
|
|
22
|
+
bucket_subfolder: str | None = None,
|
|
23
|
+
access_key: str | None = None,
|
|
24
|
+
secret_key: str | None = None,
|
|
25
|
+
open_params: dict[str, typing.Any] | None = None,
|
|
26
|
+
boto3_client: typing.Any | None = None,
|
|
23
27
|
):
|
|
28
|
+
"""
|
|
29
|
+
Initializes the S3FileManager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
base_path (str | None, optional): URL path to the S3 bucket. Defaults to None.
|
|
33
|
+
allowed_extensions (list[str] | None, optional): Allowed file extensions. Defaults to None.
|
|
34
|
+
namegen (typing.Callable[[str], str] | None, optional): Callable for generating file names. Defaults to None.
|
|
35
|
+
permission (int | None, optional): File permission settings. Defaults to None.
|
|
36
|
+
bucket_name (str | None, optional): Name of the S3 bucket. Defaults to None.
|
|
37
|
+
bucket_subfolder (str | None, optional): Subfolder within the S3 bucket. Defaults to None.
|
|
38
|
+
access_key (str | None, optional): AWS access key. Needed for default boto3 client in order to delete files. Defaults to None.
|
|
39
|
+
secret_key (str | None, optional): AWS secret key. Needed for default boto3 client in order to delete files. Defaults to None.
|
|
40
|
+
open_params (dict[str, typing.Any] | None, optional): Parameters for opening files. Defaults to None.
|
|
41
|
+
boto3_client (typing.Any | None, optional): Boto3 client instance. If None, a new client will be created. Defaults to None.
|
|
42
|
+
Raises:
|
|
43
|
+
ImportError: If required libraries are not installed.
|
|
44
|
+
"""
|
|
24
45
|
super().__init__(base_path, allowed_extensions, namegen, permission)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import boto3
|
|
49
|
+
import smart_open
|
|
50
|
+
|
|
51
|
+
self.smart_open = smart_open
|
|
52
|
+
self.boto3 = boto3
|
|
53
|
+
except ImportError:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"smart_open is required for S3FileManager. "
|
|
56
|
+
"Please install it with 'pip install smart_open[s3]'."
|
|
57
|
+
)
|
|
58
|
+
|
|
25
59
|
self.bucket_name = bucket_name
|
|
26
60
|
self.bucket_subfolder = bucket_subfolder
|
|
27
61
|
self.access_key = access_key
|
|
28
62
|
self.secret_key = secret_key
|
|
63
|
+
self.open_params = open_params or {}
|
|
64
|
+
self.boto3_client = boto3_client or self.boto3.client(
|
|
65
|
+
"s3",
|
|
66
|
+
aws_access_key_id=self.access_key,
|
|
67
|
+
aws_secret_access_key=self.secret_key,
|
|
68
|
+
)
|
|
29
69
|
|
|
30
70
|
if not self.bucket_name:
|
|
31
71
|
logger.warning(
|
|
@@ -39,52 +79,41 @@ class S3FileManager(AbstractFileManager):
|
|
|
39
79
|
"Files may not be able to be deleted"
|
|
40
80
|
)
|
|
41
81
|
|
|
42
|
-
try:
|
|
43
|
-
import boto3
|
|
44
|
-
import smart_open
|
|
45
|
-
|
|
46
|
-
self.smart_open = smart_open
|
|
47
|
-
self.boto3 = boto3
|
|
48
|
-
except ImportError:
|
|
49
|
-
raise ImportError(
|
|
50
|
-
"smart_open is required for S3FileManager. "
|
|
51
|
-
"Please install it with 'pip install smart_open[s3]'."
|
|
52
|
-
)
|
|
53
|
-
|
|
54
82
|
def get_path(self, filename):
|
|
55
83
|
return self.base_path + "/" + filename
|
|
56
84
|
|
|
57
85
|
def get_file(self, filename):
|
|
58
|
-
with self.smart_open.open(
|
|
86
|
+
with self.smart_open.open(
|
|
87
|
+
self.get_path(filename), "rb", **self.open_params
|
|
88
|
+
) as f:
|
|
59
89
|
return f.read()
|
|
60
90
|
|
|
61
91
|
async def stream_file(self, filename):
|
|
62
|
-
with self.smart_open.open(
|
|
92
|
+
with self.smart_open.open(
|
|
93
|
+
self.get_path(filename), "rb", **self.open_params
|
|
94
|
+
) as f:
|
|
63
95
|
while chunk := await smart_run(f.read, 8192):
|
|
64
96
|
yield chunk
|
|
65
97
|
|
|
66
98
|
def save_file(self, file_data, filename):
|
|
67
99
|
path = self.get_path(filename)
|
|
68
|
-
with self.smart_open.open(path, "wb") as f:
|
|
100
|
+
with self.smart_open.open(path, "wb", **self.open_params) as f:
|
|
69
101
|
f.write(file_data.file.read())
|
|
70
102
|
return path
|
|
71
103
|
|
|
72
104
|
def save_content_to_file(self, content, filename):
|
|
73
105
|
path = self.get_path(filename)
|
|
74
|
-
with self.smart_open.open(path, "wb") as f:
|
|
106
|
+
with self.smart_open.open(path, "wb", **self.open_params) as f:
|
|
75
107
|
f.write(content)
|
|
76
108
|
return path
|
|
77
109
|
|
|
78
110
|
def delete_file(self, filename):
|
|
79
111
|
path = self.get_path(filename)
|
|
80
112
|
try:
|
|
81
|
-
self.smart_open.open(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
aws_secret_access_key=self.secret_key,
|
|
86
|
-
)
|
|
87
|
-
s3.delete_object(
|
|
113
|
+
self.smart_open.open(
|
|
114
|
+
path, "rb", **self.open_params
|
|
115
|
+
).close() # Check if file exists
|
|
116
|
+
self.boto3_client.delete_object(
|
|
88
117
|
Bucket=self.bucket_name,
|
|
89
118
|
Key=f"{self.bucket_subfolder}/{filename}"
|
|
90
119
|
if self.bucket_subfolder
|
|
@@ -96,7 +125,7 @@ class S3FileManager(AbstractFileManager):
|
|
|
96
125
|
def file_exists(self, filename):
|
|
97
126
|
path = self.get_path(filename)
|
|
98
127
|
try:
|
|
99
|
-
with self.smart_open.open(path, "rb"):
|
|
128
|
+
with self.smart_open.open(path, "rb", **self.open_params):
|
|
100
129
|
return True
|
|
101
130
|
except FileNotFoundError:
|
|
102
131
|
return False
|
|
@@ -110,6 +139,8 @@ class S3FileManager(AbstractFileManager):
|
|
|
110
139
|
else subfolder,
|
|
111
140
|
access_key=self.access_key,
|
|
112
141
|
secret_key=self.secret_key,
|
|
142
|
+
open_params=self.open_params,
|
|
143
|
+
boto3_client=self.boto3_client,
|
|
113
144
|
*args,
|
|
114
145
|
**kwargs,
|
|
115
146
|
)
|
fastapi_rtk/lang/messages.pot
CHANGED
|
@@ -8,7 +8,7 @@ msgid ""
|
|
|
8
8
|
msgstr ""
|
|
9
9
|
"Project-Id-Version: PROJECT VERSION\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
|
11
|
-
"POT-Creation-Date: 2025-10-
|
|
11
|
+
"POT-Creation-Date: 2025-10-30 10:07+0100\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
@@ -53,52 +53,52 @@ msgstr ""
|
|
|
53
53
|
msgid "Invalid opr_filters: Not a valid JSON string"
|
|
54
54
|
msgstr ""
|
|
55
55
|
|
|
56
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
56
|
+
#: fastapi_rtk/api/model_rest_api.py:476
|
|
57
57
|
#, python-brace-format
|
|
58
58
|
msgid "List {ModelName}"
|
|
59
59
|
msgstr ""
|
|
60
60
|
|
|
61
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
61
|
+
#: fastapi_rtk/api/model_rest_api.py:485
|
|
62
62
|
#, python-brace-format
|
|
63
63
|
msgid "Show {ModelName}"
|
|
64
64
|
msgstr ""
|
|
65
65
|
|
|
66
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
66
|
+
#: fastapi_rtk/api/model_rest_api.py:494
|
|
67
67
|
#, python-brace-format
|
|
68
68
|
msgid "Add {ModelName}"
|
|
69
69
|
msgstr ""
|
|
70
70
|
|
|
71
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
71
|
+
#: fastapi_rtk/api/model_rest_api.py:503
|
|
72
72
|
#, python-brace-format
|
|
73
73
|
msgid "Edit {ModelName}"
|
|
74
74
|
msgstr ""
|
|
75
75
|
|
|
76
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
76
|
+
#: fastapi_rtk/api/model_rest_api.py:1395
|
|
77
77
|
msgid "OK"
|
|
78
78
|
msgstr ""
|
|
79
79
|
|
|
80
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
81
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
80
|
+
#: fastapi_rtk/api/model_rest_api.py:2411
|
|
81
|
+
#: fastapi_rtk/api/model_rest_api.py:2810
|
|
82
82
|
#, python-brace-format
|
|
83
83
|
msgid "Number of items in '{column}' does not match the number of items found."
|
|
84
84
|
msgstr ""
|
|
85
85
|
|
|
86
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
86
|
+
#: fastapi_rtk/api/model_rest_api.py:2432
|
|
87
87
|
#, python-brace-format
|
|
88
88
|
msgid "Could not find related item for column '{column}'"
|
|
89
89
|
msgstr ""
|
|
90
90
|
|
|
91
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
91
|
+
#: fastapi_rtk/api/model_rest_api.py:2464
|
|
92
92
|
#, python-brace-format
|
|
93
93
|
msgid "File type from '{filename}' is not allowed."
|
|
94
94
|
msgstr ""
|
|
95
95
|
|
|
96
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
96
|
+
#: fastapi_rtk/api/model_rest_api.py:2872
|
|
97
97
|
#, python-brace-format
|
|
98
98
|
msgid "Invalid column: {column}"
|
|
99
99
|
msgstr ""
|
|
100
100
|
|
|
101
|
-
#: fastapi_rtk/api/model_rest_api.py:
|
|
101
|
+
#: fastapi_rtk/api/model_rest_api.py:2882
|
|
102
102
|
#, python-brace-format
|
|
103
103
|
msgid "Invalid filter: {column}"
|
|
104
104
|
msgstr ""
|
|
Binary file
|