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
@@ -3,6 +3,8 @@ import typing
3
3
  from datetime import datetime
4
4
 
5
5
  import fastapi_users.exceptions
6
+ import sqlalchemy
7
+ import sqlalchemy.orm
6
8
  from pydantic import BaseModel, Field
7
9
  from sqlalchemy import select
8
10
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,7 +13,7 @@ from sqlalchemy.orm import Session
11
13
  from ...const import logger
12
14
  from ...globals import g
13
15
  from ...setting import Setting
14
- from ...utils import merge_schema, safe_call
16
+ from ...utils import T, lazy, merge_schema, safe_call
15
17
  from .models import Api, Permission, PermissionApi, Role, User
16
18
 
17
19
  __all__ = ["SecurityManager"]
@@ -28,6 +30,21 @@ class SecurityManager:
28
30
  """
29
31
 
30
32
  toolkit: typing.Optional["FastAPIReactToolkit"]
33
+ builtin_roles = lazy(lambda: Setting.ROLES)
34
+ """
35
+ The built-in roles defined in the settings.
36
+
37
+ Format:
38
+ ```python
39
+ {
40
+ "role_name": [
41
+ (api_name1|api_name2|..., permission_name1|permission_name2|...),
42
+ ...
43
+ ],
44
+ ...
45
+ }
46
+ ```
47
+ """
31
48
 
32
49
  def __init__(self, toolkit: typing.Optional["FastAPIReactToolkit"] = None) -> None:
33
50
  self.toolkit = toolkit
@@ -385,6 +402,273 @@ class SecurityManager:
385
402
  -----------------------------------------
386
403
  """
387
404
 
405
+ def has_access_in_builtin_roles(
406
+ self, role_name: str, api_name: str, permission_name: str
407
+ ):
408
+ """
409
+ Checks if the given role has access to the specified API and permission in the built-in roles.
410
+
411
+ Args:
412
+ role_name (str): The name of the role to check.
413
+ api_name (str): The name of the API to check.
414
+ permission_name (str): The name of the permission to check.
415
+
416
+ Returns:
417
+ bool: True if the role has access, False otherwise.
418
+ """
419
+ if role_name not in self.builtin_roles:
420
+ return False
421
+
422
+ for api, perm in self.builtin_roles[role_name]:
423
+ api_names = api.split("|")
424
+ perm_names = perm.split("|")
425
+ if api_name in api_names and permission_name in perm_names:
426
+ return True
427
+ return False
428
+
429
+ def get_roles_from_builtin_roles(self):
430
+ """
431
+ Retrieves the names of the built-in roles.
432
+
433
+ Returns:
434
+ list[str]: The list of built-in role names.
435
+ """
436
+ return list(self.builtin_roles.keys())
437
+
438
+ def get_api_permission_tuples_from_builtin_roles(self, role: str | None = None):
439
+ """
440
+ Retrieves the API-permission tuples from the built-in roles.
441
+
442
+ Args:
443
+ role (str | None, optional): The name of the role to filter by. If None, retrieves from all roles. Defaults to None.
444
+
445
+ Returns:
446
+ list[tuple[str, str]]: The list of API-permission tuples.
447
+ """
448
+ api_permission_tuples = list[tuple[str, str]]()
449
+ for role_name, role_api_permission_list in self.builtin_roles.items():
450
+ if role is not None and role != role_name:
451
+ continue
452
+
453
+ for api, perm in role_api_permission_list:
454
+ api_permission_tuples.append((api, perm))
455
+ return api_permission_tuples
456
+
457
+ def get_role_and_api_permission_tuples_from_builtin_roles(self):
458
+ """
459
+ Retrieves the role and API-permission tuples from the built-in roles.
460
+
461
+ Returns:
462
+ list[tuple[str, list[tuple[str, str]]]]: The list of role and API-permission tuples.
463
+ """
464
+ role_api_permission_tuples = list[tuple[str, list[tuple[str, str]]]]()
465
+ for role_name, role_api_permission_list in self.builtin_roles.items():
466
+ api_permission_list = list[tuple[str, str]]()
467
+ for api, perm in role_api_permission_list:
468
+ api_permission_list.append((api, perm))
469
+ role_api_permission_tuples.append((role_name, api_permission_list))
470
+ return role_api_permission_tuples
471
+
472
+ async def create_roles(
473
+ self,
474
+ roles: list[str],
475
+ *,
476
+ session: AsyncSession | Session = None,
477
+ raise_exception: bool = True,
478
+ ):
479
+ """
480
+ Creates new roles with the given names. Existing roles are not duplicated.
481
+
482
+ Args:
483
+ roles (list[str]): The names of the roles to create.
484
+ session (AsyncSession | Session, optional): The database session to use. If not given, a new session will be created. Defaults to None.
485
+ raise_exception (bool, optional): When set to True, raises an exception if an error occurs, otherwise returns None. Defaults to True.
486
+
487
+ Returns:
488
+ list[Role] | None: The created role objects if successful, else None.
489
+
490
+ Raises:
491
+ SomeException: Description of the exception raised, if any.
492
+ """
493
+ return await self._create_entities(
494
+ Role,
495
+ roles,
496
+ session=session,
497
+ raise_exception=raise_exception,
498
+ on_after_create=lambda role: logger.info(f"ADDING ROLE {role}"),
499
+ )
500
+
501
+ async def create_permissions(
502
+ self,
503
+ permissions: list[str],
504
+ *,
505
+ session: AsyncSession | Session = None,
506
+ raise_exception: bool = True,
507
+ ):
508
+ """
509
+ Creates new permissions with the given names. Existing permissions are not duplicated.
510
+
511
+ Args:
512
+ permissions (list[str]): The names of the permissions to create.
513
+ session (AsyncSession | Session, optional): The database session to use. If not given, a new session will be created. Defaults to None.
514
+ raise_exception (bool, optional): When set to True, raises an exception if an error occurs, otherwise returns None. Defaults to True.
515
+
516
+ Returns:
517
+ list[Permission] | None: The created permission objects if successful, else None.
518
+
519
+ Raises:
520
+ SomeException: Description of the exception raised, if any.
521
+ """
522
+ return await self._create_entities(
523
+ Permission,
524
+ permissions,
525
+ session=session,
526
+ raise_exception=raise_exception,
527
+ on_after_create=lambda permission: logger.info(
528
+ f"ADDING PERMISSION {permission}"
529
+ ),
530
+ )
531
+
532
+ async def create_apis(
533
+ self,
534
+ apis: list[str],
535
+ *,
536
+ session: AsyncSession | Session = None,
537
+ raise_exception: bool = True,
538
+ ):
539
+ """
540
+ Creates new APIs with the given names. Existing APIs are not duplicated.
541
+
542
+ Args:
543
+ apis (list[str]): The names of the APIs to create.
544
+ session (AsyncSession | Session, optional): The database session to use. If not given, a new session will be created. Defaults to None.
545
+ raise_exception (bool, optional): When set to True, raises an exception if an error occurs, otherwise returns None. Defaults to True.
546
+
547
+ Returns:
548
+ list[Api] | None: The created API objects if successful, else None.
549
+
550
+ Raises:
551
+ SomeException: Description of the exception raised, if any.
552
+ """
553
+ return await self._create_entities(
554
+ Api,
555
+ apis,
556
+ session=session,
557
+ raise_exception=raise_exception,
558
+ on_after_create=lambda api: logger.info(f"ADDING API {api}"),
559
+ )
560
+
561
+ async def associate_list_of_permission_with_api(
562
+ self,
563
+ permission_api_tuples: list[tuple[Permission, Api]],
564
+ *,
565
+ session: AsyncSession | Session = None,
566
+ raise_exception: bool = True,
567
+ ):
568
+ """
569
+ Associates a list of permissions with APIs. Existing associations are not duplicated.
570
+
571
+ Args:
572
+ permission_api_tuples (list[tuple[Permission, Api]]): A list of tuples containing Permission and Api objects to associate.
573
+ session (AsyncSession | Session, optional): The database session to use. If not given, a new session will be created. Defaults to None.
574
+ raise_exception (bool, optional): When set to True, raises an exception if an error occurs, otherwise returns None. Defaults to True.
575
+
576
+ Raises:
577
+ SomeException: Description of the exception raised, if any.
578
+ """
579
+ try:
580
+ if not session:
581
+ async with db.session() as session:
582
+ await self.associate_list_of_permission_with_api(
583
+ permission_api_tuples,
584
+ session=session,
585
+ raise_exception=raise_exception,
586
+ )
587
+ return
588
+
589
+ conditions = []
590
+ query = select(PermissionApi).options(
591
+ sqlalchemy.orm.joinedload(PermissionApi.api),
592
+ sqlalchemy.orm.joinedload(PermissionApi.permission),
593
+ sqlalchemy.orm.selectinload(PermissionApi.roles),
594
+ )
595
+ for permission, api in permission_api_tuples:
596
+ conditions.append(
597
+ sqlalchemy.and_(
598
+ PermissionApi.permission_id == permission.id,
599
+ PermissionApi.api_id == api.id,
600
+ )
601
+ )
602
+ query = query.where(sqlalchemy.or_(*conditions))
603
+ result = await safe_call(session.scalars(query))
604
+ existing_permission_apis = list(result.all())
605
+
606
+ new_tuples = list[tuple[Permission, Api]]()
607
+ for permission, api in permission_api_tuples:
608
+ exists = False
609
+ for permission_api in existing_permission_apis:
610
+ if (
611
+ permission_api.permission.id == permission.id
612
+ and permission_api.api.id == api.id
613
+ ):
614
+ exists = True
615
+ break
616
+ if not exists:
617
+ new_tuples.append((permission, api))
618
+
619
+ new_permission_apis = list[PermissionApi]()
620
+ for permission, api in new_tuples:
621
+ permission_api = PermissionApi(permission=permission, api=api, roles=[])
622
+ session.add(permission_api)
623
+ new_permission_apis.append(permission_api)
624
+ logger.info(f"ASSOCIATING PERMISSION {permission} WITH API {api}")
625
+ await safe_call(session.commit())
626
+ return existing_permission_apis + new_permission_apis
627
+ except Exception as e:
628
+ if not raise_exception:
629
+ return
630
+ raise e
631
+
632
+ async def associate_list_of_role_with_permission_api(
633
+ self,
634
+ role_permission_api_tuples: list[tuple[Role, PermissionApi]],
635
+ *,
636
+ session: AsyncSession | Session = None,
637
+ raise_exception: bool = True,
638
+ ):
639
+ """
640
+ Associates a list of roles with permission APIs. Existing associations are not duplicated.
641
+
642
+ Args:
643
+ role_permission_api_tuples (list[tuple[Role, PermissionApi]]): A list of tuples containing Role and PermissionApi objects to associate.
644
+ session (AsyncSession | Session, optional): The database session to use. If not given, a new session will be created. Defaults to None.
645
+ raise_exception (bool, optional): When set to True, raises an exception if an error occurs, otherwise returns None. Defaults to True.
646
+
647
+ Raises:
648
+ SomeException: Description of the exception raised, if any.
649
+ """
650
+ try:
651
+ if not session:
652
+ async with db.session() as session:
653
+ await self.associate_list_of_role_with_permission_api(
654
+ role_permission_api_tuples,
655
+ session=session,
656
+ raise_exception=raise_exception,
657
+ )
658
+ return
659
+
660
+ for role, permission_api in role_permission_api_tuples:
661
+ if role not in permission_api.roles:
662
+ permission_api.roles.append(role)
663
+ logger.info(
664
+ f"ASSOCIATING ROLE {role} WITH PERMISSION API {permission_api}"
665
+ )
666
+ await safe_call(session.commit())
667
+ except Exception as e:
668
+ if not raise_exception:
669
+ return
670
+ raise e
671
+
388
672
  async def cleanup(self, *, session: AsyncSession | Session = None):
389
673
  """
390
674
  Cleanup unused permissions from apis and roles.
@@ -404,13 +688,12 @@ class SecurityManager:
404
688
  "FastAPIReactToolkit instance not provided, you must provide it to use this function."
405
689
  )
406
690
 
407
- api_permission_tuples = (Setting.ROLES).values()
691
+ api_permission_tuples = self.get_api_permission_tuples_from_builtin_roles()
408
692
  apis = [api.__class__.__name__ for api in self.toolkit.apis]
409
693
  permissions = self.toolkit.total_permissions()
410
- for api_permission_tuple in api_permission_tuples:
411
- for api, permission in api_permission_tuple:
412
- apis.append(api)
413
- permissions.append(permission)
694
+ for api, permission in api_permission_tuples:
695
+ apis.append(api)
696
+ permissions.append(permission)
414
697
 
415
698
  # Clean up unused permissions
416
699
  unused_permissions = await safe_call(
@@ -428,13 +711,20 @@ class SecurityManager:
428
711
  logger.info(f"DELETING API {api} AND ITS ASSOCIATIONS")
429
712
  await safe_call(session.delete(api))
430
713
 
431
- roles = Setting.ROLES.keys()
432
- roles = list(roles) + [g.admin_role]
714
+ roles = self.get_roles_from_builtin_roles()
715
+ if g.admin_role is not None:
716
+ roles.append(g.admin_role)
433
717
 
434
718
  # Clean up existing permission-apis, that are no longer connected to any roles
435
- unused_permission_apis = await safe_call(session.scalars(select(PermissionApi)))
436
- for permission_api in unused_permission_apis:
437
- for role in list(permission_api.roles) or []:
719
+ permission_apis_in_db = await safe_call(
720
+ session.scalars(
721
+ select(PermissionApi).options(
722
+ sqlalchemy.orm.selectinload(PermissionApi.roles)
723
+ )
724
+ )
725
+ )
726
+ for permission_api in permission_apis_in_db:
727
+ for role in permission_api.roles:
438
728
  if role.name not in roles:
439
729
  permission_api.roles.remove(role)
440
730
  logger.info(
@@ -442,3 +732,71 @@ class SecurityManager:
442
732
  )
443
733
 
444
734
  await safe_call(session.commit())
735
+
736
+ """
737
+ -----------------------------------------
738
+ HELPER FUNCTIONS
739
+ -----------------------------------------
740
+ """
741
+
742
+ async def _create_entities(
743
+ self,
744
+ entity_class: typing.Type[T],
745
+ names: list[str],
746
+ *,
747
+ session: AsyncSession | Session = None,
748
+ raise_exception: bool = True,
749
+ on_after_create: typing.Optional[
750
+ typing.Callable[[T], typing.Awaitable[None] | None]
751
+ ] = None,
752
+ ):
753
+ """
754
+ Helper function to create new entities with the given names.
755
+ Existing entities are not duplicated.
756
+
757
+ Args:
758
+ entity_class (typing.Type[T]): The entity class to create.
759
+ names (list[str]): The names of the entities to create.
760
+ session (AsyncSession | Session, optional): The database session to use. If not given, a new session will be created. Defaults to None.
761
+ raise_exception (bool, optional): When set to True, raises an exception if an error occurs, otherwise returns None. Defaults to True.
762
+ on_after_create (Optional[Callable[[T], Awaitable[None] | None]], optional): An optional callback function to be called after each entity is created. Defaults to None.
763
+
764
+ Returns:
765
+ list[T] | None: The created entity objects if successful, else None.
766
+
767
+ Raises:
768
+ Exception: If an error occurs and raise_exception is True.
769
+ """
770
+ try:
771
+ if not session:
772
+ async with db.session() as session:
773
+ return await self._create_entities(
774
+ entity_class,
775
+ names,
776
+ session=session,
777
+ raise_exception=raise_exception,
778
+ )
779
+
780
+ # Check for existing entities
781
+ result = await safe_call(
782
+ session.scalars(
783
+ select(entity_class).where(entity_class.name.in_(names))
784
+ )
785
+ )
786
+ existing_entities = list(result.all())
787
+ existing_names = [entity.name for entity in existing_entities]
788
+
789
+ new_names = [name for name in names if name not in existing_names]
790
+ new_entities = list[T]()
791
+ for name in new_names:
792
+ entity = entity_class(name=name)
793
+ session.add(entity)
794
+ new_entities.append(entity)
795
+ if on_after_create:
796
+ await safe_call(on_after_create(entity))
797
+ await safe_call(session.commit())
798
+ return existing_entities + new_entities
799
+ except Exception as e:
800
+ if not raise_exception:
801
+ return
802
+ raise e