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.
Files changed (40) hide show
  1. fastapi_rtk/__init__.py +0 -1
  2. fastapi_rtk/_version.py +1 -0
  3. fastapi_rtk/api/model_rest_api.py +182 -87
  4. fastapi_rtk/auth/auth.py +0 -9
  5. fastapi_rtk/backends/sqla/db.py +32 -7
  6. fastapi_rtk/backends/sqla/filters.py +16 -0
  7. fastapi_rtk/backends/sqla/interface.py +11 -62
  8. fastapi_rtk/backends/sqla/model.py +16 -1
  9. fastapi_rtk/bases/db.py +20 -2
  10. fastapi_rtk/bases/file_manager.py +12 -0
  11. fastapi_rtk/bases/filter.py +1 -1
  12. fastapi_rtk/cli/cli.py +61 -0
  13. fastapi_rtk/cli/commands/security.py +6 -6
  14. fastapi_rtk/const.py +1 -1
  15. fastapi_rtk/db.py +3 -0
  16. fastapi_rtk/dependencies.py +110 -64
  17. fastapi_rtk/fastapi_react_toolkit.py +123 -172
  18. fastapi_rtk/file_managers/s3_file_manager.py +63 -32
  19. fastapi_rtk/lang/messages.pot +12 -12
  20. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  21. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +12 -12
  22. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  23. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +12 -12
  24. fastapi_rtk/manager.py +10 -14
  25. fastapi_rtk/schemas.py +6 -4
  26. fastapi_rtk/security/sqla/apis.py +20 -5
  27. fastapi_rtk/security/sqla/models.py +8 -23
  28. fastapi_rtk/security/sqla/security_manager.py +367 -10
  29. fastapi_rtk/utils/async_task_runner.py +119 -30
  30. fastapi_rtk/utils/csv_json_converter.py +242 -39
  31. fastapi_rtk/utils/hooks.py +7 -4
  32. fastapi_rtk/utils/self_dependencies.py +1 -1
  33. fastapi_rtk/version.py +6 -1
  34. fastapi_rtk-1.0.18.dist-info/METADATA +28 -0
  35. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/RECORD +38 -38
  36. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/WHEEL +1 -2
  37. fastapi_rtk-0.2.60.dist-info/METADATA +0 -25
  38. fastapi_rtk-0.2.60.dist-info/top_level.txt +0 -1
  39. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/entry_points.txt +0 -0
  40. {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
- 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
+ """
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.security = SecurityManager(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
- await self._associate_permission_with_api(session)
425
- 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
+ )
426
436
  if self.cleanup:
427
- await self.security.cleanup()
437
+ await self.sm.cleanup()
428
438
  logger.info("DATABASE INITIALIZED")
429
439
 
430
- async def _insert_permissions(self, session: AsyncSession | Session):
431
- new_permissions = self.total_permissions()
432
- stmt = select(Permission).where(Permission.name.in_(new_permissions))
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
- for permission in permission_objs:
444
- logger.info(f"ADDING PERMISSION {permission}")
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
- role_objs = [
471
- 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()
472
450
  ]
473
- for role in role_objs:
474
- logger.info(f"ADDING ROLE {role}")
475
- session.add(role)
476
- 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]]()
477
472
 
478
- async def _associate_permission_with_api(self, session: AsyncSession | Session):
479
473
  for api in self.apis:
480
- new_permissions = getattr(api, "permissions", [])
481
- if not new_permissions:
474
+ api_permissions = api.permissions
475
+ if not api_permissions:
482
476
  continue
483
477
 
484
- # Get the api object
485
- api_stmt = select(Api).where(Api.name == api.__class__.__name__)
486
- api_obj = await safe_call(session.scalar(api_stmt))
487
-
488
- if not api_obj:
489
- raise ValueError(f"API {api.__class__.__name__} not found")
490
-
491
- permission_stmt = select(Permission).where(
492
- and_(
493
- Permission.name.in_(new_permissions),
494
- ~Permission.id.in_([p.permission_id for p in api_obj.permissions]),
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
- for permission in new_permissions:
504
- session.add(
505
- PermissionApi(permission_id=permission.id, api_id=api_obj.id)
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 _associate_permission_api_with_role(
511
- 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],
512
506
  ):
513
- # Read config based roles
514
- roles_dict = Setting.ROLES
515
-
516
- for role_name, role_permissions in roles_dict.items():
517
- role_stmt = select(Role).where(Role.name == role_name)
518
- role_result = await safe_call(session.scalars(role_stmt))
519
- role = role_result.first()
520
- if not role:
521
- role = Role(name=role_name)
522
- logger.info(f"ADDING ROLE {role}")
523
- session.add(role)
524
-
525
- for api_name, permission_name in role_permissions:
526
- permission_api_stmt = (
527
- select(PermissionApi)
528
- .where(
529
- and_(Api.name == api_name, Permission.name == permission_name)
530
- )
531
- .join(Permission)
532
- .join(Api)
533
- )
534
- permission_api = await safe_call(session.scalar(permission_api_stmt))
535
- if not permission_api:
536
- permission_stmt = select(Permission).where(
537
- Permission.name == permission_name
538
- )
539
- permission = await safe_call(session.scalar(permission_stmt))
540
- if not permission:
541
- permission = Permission(name=permission_name)
542
- logger.info(f"ADDING PERMISSION {permission}")
543
- session.add(permission)
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
- [lifespan(app) for lifespan in [lifespan] + self.lifespans]
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(self.get_path(filename), "rb") as f:
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(self.get_path(filename), "rb") as f:
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(path, "rb").close() # Check if file exists
82
- s3 = self.boto3.client(
83
- "s3",
84
- aws_access_key_id=self.access_key,
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
  )
@@ -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-16 13:49+0200\n"
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:461
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:470
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:479
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:488
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:1338
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:2354
81
- #: fastapi_rtk/api/model_rest_api.py:2753
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:2375
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:2407
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:2815
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:2825
101
+ #: fastapi_rtk/api/model_rest_api.py:2882
102
102
  #, python-brace-format
103
103
  msgid "Invalid filter: {column}"
104
104
  msgstr ""