alpha-python 0.7.4__tar.gz → 0.7.6__tar.gz

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 (159) hide show
  1. {alpha_python-0.7.4 → alpha_python-0.7.6}/PKG-INFO +2 -2
  2. {alpha_python-0.7.4 → alpha_python-0.7.6}/pyproject.toml +2 -2
  3. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/__init__.py +2 -0
  4. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/adapters/sqla_unit_of_work.py +11 -3
  5. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/__init__.py +2 -0
  6. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/models/__init__.py +2 -0
  7. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/models/group.py +41 -1
  8. alpha_python-0.7.6/src/alpha/domain/models/permission.py +110 -0
  9. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/models/user.py +66 -2
  10. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/mixins/jwt_provider.py +4 -1
  11. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/database_provider.py +8 -2
  12. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/ldap_provider.py +18 -1
  13. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/models/identity.py +52 -12
  14. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/oidc_provider.py +20 -2
  15. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/services/authentication_service.py +7 -2
  16. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha_python.egg-info/PKG-INFO +2 -2
  17. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha_python.egg-info/SOURCES.txt +1 -0
  18. {alpha_python-0.7.4 → alpha_python-0.7.6}/LICENSE +0 -0
  19. {alpha_python-0.7.4 → alpha_python-0.7.6}/README.md +0 -0
  20. {alpha_python-0.7.4 → alpha_python-0.7.6}/setup.cfg +0 -0
  21. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/adapters/__init__.py +0 -0
  22. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/adapters/rest_api_unit_of_work.py +0 -0
  23. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/cli.py +0 -0
  24. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/containers/__init__.py +0 -0
  25. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/containers/container.py +0 -0
  26. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/models/base_model.py +0 -0
  27. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/models/life_cycle_base.py +0 -0
  28. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/domain/models/role.py +0 -0
  29. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/encoder.py +0 -0
  30. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/exceptions.py +0 -0
  31. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/__init__.py +0 -0
  32. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/_type_conversion_matrix.py +0 -0
  33. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/_type_mapping.py +0 -0
  34. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/class_factories.py +0 -0
  35. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/default_field_factory.py +0 -0
  36. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/field_iterator.py +0 -0
  37. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/jwt_factory.py +0 -0
  38. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/logging_handler_factory.py +0 -0
  39. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/model_class_factory.py +0 -0
  40. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/models/__init__.py +0 -0
  41. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/models/factory_classes.py +0 -0
  42. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/password_factory.py +0 -0
  43. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/request_factory.py +0 -0
  44. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/response_factory.py +0 -0
  45. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/factories/type_factories.py +0 -0
  46. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/__init__.py +0 -0
  47. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/api_generate_handler.py +0 -0
  48. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/api_run_handler.py +0 -0
  49. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/base_handler.py +0 -0
  50. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/gen-code.sh +0 -0
  51. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/models/__init__.py +0 -0
  52. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/models/argument.py +0 -0
  53. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/models/command.py +0 -0
  54. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/models/section.py +0 -0
  55. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/models/subparser.py +0 -0
  56. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/run-api.sh +0 -0
  57. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/__init__.py +0 -0
  58. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/Dockerfile.mustache +0 -0
  59. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/README.mustache +0 -0
  60. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/__init__model.mustache +0 -0
  61. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/__init__test.mustache +0 -0
  62. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/__main__.mustache +0 -0
  63. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/base_model.mustache +0 -0
  64. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/controller.mustache +0 -0
  65. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/controller_test.mustache +0 -0
  66. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/dockerignore.mustache +0 -0
  67. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/encoder.mustache +0 -0
  68. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/git_push.sh.mustache +0 -0
  69. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/gitignore.mustache +0 -0
  70. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/model.mustache +0 -0
  71. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/openapi.mustache +0 -0
  72. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/param_type.mustache +0 -0
  73. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/requirements.mustache +0 -0
  74. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/security_controller_.mustache +0 -0
  75. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/setup.mustache +0 -0
  76. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/test-requirements.mustache +0 -0
  77. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/tox.mustache +0 -0
  78. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/travis.mustache +0 -0
  79. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/typing_utils.mustache +0 -0
  80. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/handlers/templates/python-flask/util.mustache +0 -0
  81. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/__init__.py +0 -0
  82. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/connectors/__init__.py +0 -0
  83. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/connectors/ldap_connector.py +0 -0
  84. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/connectors/oidc_connector.py +0 -0
  85. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/connectors/sql_alchemy.py +0 -0
  86. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/databases/__init__.py +0 -0
  87. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/databases/sql_alchemy.py +0 -0
  88. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/models/__init__.py +0 -0
  89. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/models/filter_operators.py +0 -0
  90. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/models/json_patch.py +0 -0
  91. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/models/order_by.py +0 -0
  92. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/models/query_clause.py +0 -0
  93. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/infra/models/search_filter.py +0 -0
  94. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/__init__.py +0 -0
  95. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/api_repository.py +0 -0
  96. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/attrs_instance.py +0 -0
  97. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/dataclass_instance.py +0 -0
  98. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/factories.py +0 -0
  99. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/handler.py +0 -0
  100. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/http_client.py +0 -0
  101. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/openapi_model.py +0 -0
  102. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/patchable.py +0 -0
  103. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/providers.py +0 -0
  104. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/pydantic_instance.py +0 -0
  105. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/refresh_repository.py +0 -0
  106. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/sql_database.py +0 -0
  107. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/sql_mapper.py +0 -0
  108. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/sql_repository.py +0 -0
  109. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/token_factory.py +0 -0
  110. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/unit_of_work.py +0 -0
  111. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/interfaces/updatable.py +0 -0
  112. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/mixins/__init__.py +0 -0
  113. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/mixins/group_lifecycle.py +0 -0
  114. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/mixins/user_lifecycle.py +0 -0
  115. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/__init__.py +0 -0
  116. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/api_key_provider.py +0 -0
  117. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/models/__init__.py +0 -0
  118. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/models/credentials.py +0 -0
  119. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/providers/models/token.py +0 -0
  120. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/py.typed +0 -0
  121. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/__init__.py +0 -0
  122. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/models/__init__.py +0 -0
  123. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/models/repository_model.py +0 -0
  124. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/refresh/__init__.py +0 -0
  125. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/refresh/cache_repository.py +0 -0
  126. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/refresh/database_repository.py +0 -0
  127. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/refresh/file_repository.py +0 -0
  128. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/refresh/memory_repository.py +0 -0
  129. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/rest_api_repository.py +0 -0
  130. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/repositories/sql_alchemy_repository.py +0 -0
  131. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/services/__init__.py +0 -0
  132. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/services/models/__init__.py +0 -0
  133. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/services/models/cookie.py +0 -0
  134. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/services/user_lifecycle_management.py +0 -0
  135. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/__init__.py +0 -0
  136. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/_http_codes.py +0 -0
  137. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/cookie.py +0 -0
  138. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/is_attrs.py +0 -0
  139. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/is_pydantic.py +0 -0
  140. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/logging_configurator.py +0 -0
  141. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/logging_level_checker.py +0 -0
  142. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/__init__.py +0 -0
  143. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/container.py +0 -0
  144. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/exceptions.py +0 -0
  145. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/models.py +0 -0
  146. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/orm.py +0 -0
  147. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/response.py +0 -0
  148. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/openapi_test/service.py +0 -0
  149. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/request_headers.py +0 -0
  150. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/response_object.py +0 -0
  151. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/secret_generator.py +0 -0
  152. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/verify_identity.py +0 -0
  153. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha/utils/version_checker.py +0 -0
  154. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha_python.egg-info/dependency_links.txt +0 -0
  155. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha_python.egg-info/entry_points.txt +0 -0
  156. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha_python.egg-info/requires.txt +0 -0
  157. {alpha_python-0.7.4 → alpha_python-0.7.6}/src/alpha_python.egg-info/top_level.txt +0 -0
  158. {alpha_python-0.7.4 → alpha_python-0.7.6}/tests/test_cli.py +0 -0
  159. {alpha_python-0.7.4 → alpha_python-0.7.6}/tests/test_encoder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpha-python
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction.
5
5
  Author-email: Bart Reijling <bart@reijling.eu>
6
6
  License-Expression: MIT
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
- Requires-Python: <3.15,>=3.11
19
+ Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: attrs>=25.4.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alpha-python"
3
- version = "0.7.4"
3
+ version = "0.7.6"
4
4
  description = "Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -18,7 +18,7 @@ classifiers = [
18
18
  ]
19
19
  license = "MIT"
20
20
  license-files = ['LICENSE']
21
- requires-python = ">=3.11, <3.15"
21
+ requires-python = ">=3.11"
22
22
  dependencies = [
23
23
  "attrs>=25.4.0",
24
24
  "gunicorn>=23.0.0",
@@ -7,6 +7,7 @@ from alpha.factories.logging_handler_factory import LoggingHandlerFactory
7
7
  from alpha.factories.model_class_factory import ModelClassFactory
8
8
  from alpha.domain.models.user import User
9
9
  from alpha.domain.models.group import Group
10
+ from alpha.domain.models.permission import Permission
10
11
  from alpha.domain.models.role import Role
11
12
  from alpha.domain.models.base_model import (
12
13
  BaseDomainModel,
@@ -123,6 +124,7 @@ __all__ = [
123
124
  "LifeCycleBase",
124
125
  "User",
125
126
  "Group",
127
+ "Permission",
126
128
  "Role",
127
129
  "OIDCConnector",
128
130
  "KeyCloakOIDCConnector",
@@ -78,14 +78,22 @@ class SqlAlchemyUnitOfWork:
78
78
 
79
79
  return self
80
80
 
81
- def __exit__(self, *args: Any) -> None:
82
- """Finalize the Unit of Work context."""
81
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
82
+ """Finalize the Unit of Work context.
83
+
84
+ Roll back only when leaving the context because of an exception.
85
+ Rolling back after a successful commit can expire ORM state and may
86
+ trigger DetachedInstanceError once the session is closed/detached.
87
+ """
83
88
  if not self._session:
84
89
  raise exceptions.DatabaseSessionError(
85
90
  "No active database session is defined"
86
91
  )
92
+
93
+ if exc_type is not None:
94
+ self._session.rollback()
95
+
87
96
  self._session.close()
88
- self.rollback()
89
97
  self._session = None # type: ignore
90
98
 
91
99
  def commit(self) -> None:
@@ -1,5 +1,6 @@
1
1
  from alpha.domain.models.user import User
2
2
  from alpha.domain.models.group import Group
3
+ from alpha.domain.models.permission import Permission
3
4
  from alpha.domain.models.role import Role
4
5
  from alpha.domain.models.base_model import (
5
6
  BaseDomainModel,
@@ -17,5 +18,6 @@ __all__ = [
17
18
  "LifeCycleBase",
18
19
  "User",
19
20
  "Group",
21
+ "Permission",
20
22
  "Role",
21
23
  ]
@@ -1,5 +1,6 @@
1
1
  from alpha.domain.models.user import User
2
2
  from alpha.domain.models.group import Group
3
+ from alpha.domain.models.permission import Permission
3
4
  from alpha.domain.models.role import Role
4
5
  from alpha.domain.models.base_model import (
5
6
  BaseDomainModel,
@@ -17,5 +18,6 @@ __all__ = [
17
18
  "LifeCycleBase",
18
19
  "User",
19
20
  "Group",
21
+ "Permission",
20
22
  "Role",
21
23
  ]
@@ -39,6 +39,20 @@ class Group(LifeCycleBase, BaseDomainModel):
39
39
  permissions: list[str] = field(default_factory=list) # type: ignore
40
40
  is_active: bool = True
41
41
 
42
+ def __str__(self) -> str:
43
+ """Return the name-based representation of the Group instance."""
44
+ return self.name or ""
45
+
46
+ def __repr__(self) -> str:
47
+ """Return the official string representation of the Group instance."""
48
+ return (
49
+ "Group("
50
+ f"id={self.id!r}, "
51
+ f"name={self.name!r}, "
52
+ f"description={self.description!r}"
53
+ ")"
54
+ )
55
+
42
56
  def to_dict(self) -> dict[str, Any]:
43
57
  """Convert the Group instance to a dictionary.
44
58
 
@@ -47,12 +61,38 @@ class Group(LifeCycleBase, BaseDomainModel):
47
61
  dict[str, Any]
48
62
  A dictionary representation of the Group instance.
49
63
  """
64
+ permissions = cast(
65
+ list[str | dict[str, Any]],
66
+ [
67
+ p.to_dict() if hasattr(p, "to_dict") else p # type: ignore
68
+ for p in self.permissions
69
+ ],
70
+ )
71
+ created_at = (
72
+ self.created_at.isoformat()
73
+ if hasattr(self, "created_at") and self.created_at is not None
74
+ else None
75
+ )
76
+ modified_at = (
77
+ self.modified_at.isoformat()
78
+ if hasattr(self, "modified_at") and self.modified_at is not None
79
+ else None
80
+ )
81
+
50
82
  return {
51
83
  "id": self.id,
52
84
  "name": self.name,
53
85
  "description": self.description,
54
- "permissions": self.permissions,
86
+ "permissions": permissions,
55
87
  "is_active": self.is_active,
88
+ "created_by": self.created_by
89
+ if hasattr(self, "created_by") and self.created_by is not None
90
+ else None,
91
+ "created_at": created_at,
92
+ "modified_by": self.modified_by
93
+ if hasattr(self, "modified_by") and self.modified_by is not None
94
+ else None,
95
+ "modified_at": modified_at,
56
96
  }
57
97
 
58
98
  def update(self, obj: DomainModel) -> DomainModel:
@@ -0,0 +1,110 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime, timezone
3
+ from typing import Any, cast
4
+ from uuid import UUID
5
+
6
+ from alpha.domain.models.base_model import BaseDomainModel, DomainModel
7
+ from alpha.domain.models.life_cycle_base import LifeCycleBase
8
+
9
+
10
+ @dataclass(kw_only=True)
11
+ class Permission(LifeCycleBase, BaseDomainModel):
12
+ """Permission class representing a system permission. This class is used to
13
+ define and manage permissions. User and group permissions are handled
14
+ through this class.
15
+
16
+ Attributes
17
+ ----------
18
+ id
19
+ Unique identifier for the permission. Can be a UUID, integer, or
20
+ string.
21
+ name
22
+ Name of the permission.
23
+ description
24
+ Description for the purpose of the permission.
25
+ """
26
+
27
+ id: UUID | int | str | None = None
28
+ name: str
29
+ description: str = ""
30
+
31
+ def __str__(self) -> str:
32
+ """Return the name of the permission.
33
+
34
+ Returns
35
+ -------
36
+ self.name value
37
+ """
38
+ return self.name
39
+
40
+ def __repr__(self) -> str:
41
+ """Return the official string representation of the object.
42
+
43
+ Returns
44
+ -------
45
+ A string representing the Permission instance.
46
+ """
47
+ return (
48
+ f"Permission(name={self.name!r}, description={self.description!r})"
49
+ )
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ """Convert the Permission instance to a dictionary.
53
+
54
+ Returns
55
+ -------
56
+ dict[str, Any]
57
+ A dictionary representation of the Permission instance.
58
+ """
59
+ created_at = (
60
+ self.created_at.isoformat()
61
+ if hasattr(self, "created_at") and self.created_at is not None
62
+ else None
63
+ )
64
+ modified_at = (
65
+ self.modified_at.isoformat()
66
+ if hasattr(self, "modified_at") and self.modified_at is not None
67
+ else None
68
+ )
69
+
70
+ return {
71
+ "id": self.id,
72
+ "name": self.name,
73
+ "description": self.description,
74
+ "created_by": self.created_by
75
+ if hasattr(self, "created_by") and self.created_by is not None
76
+ else None,
77
+ "created_at": created_at,
78
+ "modified_by": self.modified_by
79
+ if hasattr(self, "modified_by") and self.modified_by is not None
80
+ else None,
81
+ "modified_at": modified_at,
82
+ }
83
+
84
+ def update(self, obj: DomainModel) -> DomainModel:
85
+ """Update the Permission instance with data from another Permission
86
+ instance.
87
+
88
+ Parameters
89
+ ----------
90
+ obj
91
+ Permission object to update from.
92
+
93
+ Returns
94
+ -------
95
+ DomainModel
96
+ The updated instance of the Permission.
97
+
98
+ Raises
99
+ ------
100
+ TypeError
101
+ If the provided object is not a Permission instance.
102
+ """
103
+ if not isinstance(obj, Permission):
104
+ raise TypeError("Permission.update expects a Permission instance.")
105
+
106
+ self.name = obj.name
107
+ self.description = obj.description
108
+ self.modified_at = datetime.now(tz=timezone.utc)
109
+
110
+ return cast(DomainModel, self)
@@ -72,6 +72,32 @@ class User(LifeCycleBase, BaseDomainModel):
72
72
  is_active: bool = True
73
73
  admin: bool = False
74
74
 
75
+ def __str__(self) -> str:
76
+ """Return the username of the user.
77
+
78
+ Returns
79
+ -------
80
+ self.username value
81
+ """
82
+ return self.username or ""
83
+
84
+ def __repr__(self) -> str:
85
+ """Return the official string representation of the object.
86
+
87
+ Returns
88
+ -------
89
+ A string representing the User instance.
90
+ """
91
+ return (
92
+ "User("
93
+ f"id={self.id}, "
94
+ f"username={self.username!r}, "
95
+ f"display_name={self.display_name!r}, "
96
+ f"permissions={self.permissions!r}, "
97
+ f"groups={self.groups!r}"
98
+ ")"
99
+ )
100
+
75
101
  @classmethod
76
102
  def from_identity(cls, identity: Identity) -> Self:
77
103
  """Create a User instance from an Identity instance.
@@ -101,6 +127,35 @@ class User(LifeCycleBase, BaseDomainModel):
101
127
  dict[str, Any]
102
128
  A dictionary representation of the User instance.
103
129
  """
130
+ permissions = cast(
131
+ list[str | dict[str, Any]],
132
+ [
133
+ permission.to_dict() # type: ignore
134
+ if hasattr(permission, "to_dict")
135
+ else permission
136
+ for permission in self.permissions
137
+ ],
138
+ )
139
+ groups = cast(
140
+ list[str | dict[str, Any]],
141
+ [
142
+ group.to_dict() # type: ignore
143
+ if hasattr(group, "to_dict")
144
+ else group
145
+ for group in self.groups
146
+ ],
147
+ )
148
+ created_at = (
149
+ self.created_at.isoformat()
150
+ if hasattr(self, "created_at") and self.created_at is not None
151
+ else None
152
+ )
153
+ modified_at = (
154
+ self.modified_at.isoformat()
155
+ if hasattr(self, "modified_at") and self.modified_at is not None
156
+ else None
157
+ )
158
+
104
159
  return {
105
160
  "id": self.id,
106
161
  "username": self.username,
@@ -109,10 +164,18 @@ class User(LifeCycleBase, BaseDomainModel):
109
164
  "email": self.email,
110
165
  "phone": self.phone,
111
166
  "display_name": self.display_name,
112
- "permissions": self.permissions,
113
- "groups": self.groups,
167
+ "permissions": permissions,
168
+ "groups": groups,
114
169
  "is_active": self.is_active,
115
170
  "admin": self.admin,
171
+ "created_by": self.created_by
172
+ if hasattr(self, "created_by") and self.created_by is not None
173
+ else None,
174
+ "created_at": created_at,
175
+ "modified_by": self.modified_by
176
+ if hasattr(self, "modified_by") and self.modified_by is not None
177
+ else None,
178
+ "modified_at": modified_at,
116
179
  }
117
180
 
118
181
  def update(self, obj: DomainModel) -> DomainModel:
@@ -140,4 +203,5 @@ class User(LifeCycleBase, BaseDomainModel):
140
203
  self.modified_at = datetime.now(tz=timezone.utc)
141
204
  self.is_active = obj.is_active
142
205
  self.admin = obj.admin
206
+
143
207
  return cast(DomainModel, self)
@@ -12,6 +12,7 @@ class JWTProviderMixin:
12
12
  """
13
13
 
14
14
  token_factory: TokenFactory | None = None
15
+ _identity_model: type[Identity] = Identity
15
16
 
16
17
  def validate(self, token: Token) -> Identity:
17
18
  """Validate a token and return the associated identity
@@ -43,7 +44,7 @@ class JWTProviderMixin:
43
44
  "Token payload does not contain mandatory 'subject' field"
44
45
  )
45
46
 
46
- return Identity.from_dict(payload)
47
+ return self._identity_model.from_dict(payload)
47
48
 
48
49
  def issue_token(self, identity: Identity) -> Token:
49
50
  """Issue a token for the given identity
@@ -57,6 +58,8 @@ class JWTProviderMixin:
57
58
  -------
58
59
  Token object
59
60
  """
61
+ identity = self._identity_model.from_dict(identity.to_dict())
62
+
60
63
  if not self.token_factory:
61
64
  raise exceptions.MissingDependencyException(
62
65
  "Token factory is not configured"
@@ -18,12 +18,14 @@ class DatabaseProvider(JWTProviderMixin):
18
18
 
19
19
  protocol = "database"
20
20
  token_factory: TokenFactory | None = None
21
+ _identity_model: type[Identity] = Identity
21
22
 
22
23
  def __init__(
23
24
  self,
24
25
  uow: UnitOfWork,
25
26
  token_factory: TokenFactory | None = None,
26
27
  password_factory: PasswordFactory | None = None,
28
+ identity_model: type[Identity] = Identity,
27
29
  user_name_attribute: str = "username",
28
30
  users_repository_name: str = "users",
29
31
  ) -> None:
@@ -42,6 +44,9 @@ class DatabaseProvider(JWTProviderMixin):
42
44
  Password factory instance to handle password hashing and
43
45
  verification, by default None. If None, a default PasswordFactory
44
46
  will be used.
47
+ identity_model
48
+ Identity model class to use for representing users, by default
49
+ Identity
45
50
  user_name_attribute
46
51
  Attribute name to identify the user, by default "username"
47
52
  users_repository_name
@@ -52,6 +57,7 @@ class DatabaseProvider(JWTProviderMixin):
52
57
  self._password_factory = password_factory or PasswordFactory()
53
58
  self._user_name_attribute = user_name_attribute
54
59
  self._users_repository_name = users_repository_name
60
+ self._identity_model = identity_model
55
61
 
56
62
  def authenticate(self, credentials: PasswordCredentials) -> Identity:
57
63
  """Authenticate a user using their credentials.
@@ -73,7 +79,7 @@ class DatabaseProvider(JWTProviderMixin):
73
79
  credentials=credentials, user_repository=users
74
80
  )
75
81
 
76
- return Identity.from_user(user)
82
+ return self._identity_model.from_user(user)
77
83
 
78
84
  def get_user(self, subject: str) -> Identity:
79
85
  """Retrieve a user by their subject identifier.
@@ -95,7 +101,7 @@ class DatabaseProvider(JWTProviderMixin):
95
101
  username=subject, user_repository=users, attribute_name="id"
96
102
  )
97
103
 
98
- return Identity.from_user(user)
104
+ return self._identity_model.from_user(user)
99
105
 
100
106
  def change_password(
101
107
  self, credentials: PasswordCredentials, new_password: str
@@ -33,6 +33,7 @@ class LDAPProvider(JWTProviderMixin):
33
33
 
34
34
  protocol = "ldap"
35
35
  token_factory: TokenFactory | None = None
36
+ _identity_model: type[Identity] = Identity
36
37
 
37
38
  def __init__(
38
39
  self,
@@ -45,8 +46,10 @@ class LDAPProvider(JWTProviderMixin):
45
46
  populate_groups: bool = True,
46
47
  populate_permissions: bool = False,
47
48
  populate_claims: bool = True,
49
+ flatten_claims: bool = True,
48
50
  auto_connect: bool = True,
49
51
  change_password_supported: bool = False,
52
+ identity_model: type[Identity] = Identity,
50
53
  additional_connector_params: dict[str, Any] | None = None,
51
54
  ) -> None:
52
55
  """Initialize LDAPProvider.
@@ -71,11 +74,17 @@ class LDAPProvider(JWTProviderMixin):
71
74
  Whether to populate permissions in the Identity, by default False
72
75
  populate_claims
73
76
  Whether to populate claims in the Identity, by default True
77
+ flatten_claims
78
+ Whether to flatten single-value claims in the Identity, by default
79
+ True
74
80
  auto_connect
75
81
  Whether to automatically connect using the connector, by default
76
82
  True
77
83
  change_password_supported
78
84
  Whether the provider supports changing passwords, by default False
85
+ identity_model
86
+ Identity model class to use for representing users, by default
87
+ Identity
79
88
  additional_connector_params
80
89
  Additional parameters to pass to the LDAP connection, by default
81
90
  {"receive_timeout": 5}
@@ -89,8 +98,10 @@ class LDAPProvider(JWTProviderMixin):
89
98
  self._populate_groups = populate_groups
90
99
  self._populate_permissions = populate_permissions
91
100
  self._populate_claims = populate_claims
101
+ self._flatten_claims = flatten_claims
92
102
  self._auto_connect = auto_connect
93
103
  self._change_password_supported = change_password_supported
104
+ self._identity_model = identity_model
94
105
  self._additional_connector_params = additional_connector_params or {
95
106
  "receive_timeout": 5
96
107
  }
@@ -291,12 +302,13 @@ class LDAPProvider(JWTProviderMixin):
291
302
  Identity object
292
303
  """
293
304
  entry_dict = cast(dict[str, Any], entry.entry_attributes_as_dict) # type: ignore
294
- identity = Identity.from_ldap_dict(
305
+ identity = self._identity_model.from_ldap_dict(
295
306
  entry=entry_dict,
296
307
  mappings=self._identity_mappings,
297
308
  populate_claims=self._populate_claims,
298
309
  populate_groups=self._populate_groups,
299
310
  populate_permissions=self._populate_permissions,
311
+ flatten_claims=self._flatten_claims,
300
312
  )
301
313
  return identity
302
314
 
@@ -324,6 +336,7 @@ class ADProvider(LDAPProvider):
324
336
  populate_groups: bool = True,
325
337
  populate_permissions: bool = False,
326
338
  populate_claims: bool = True,
339
+ flatten_claims: bool = True,
327
340
  auto_connect: bool = True,
328
341
  change_password_supported: bool = False,
329
342
  ) -> None:
@@ -356,6 +369,9 @@ class ADProvider(LDAPProvider):
356
369
  populate_claims
357
370
  Whether to populate claims on the :class:`Identity`, by default
358
371
  True.
372
+ flatten_claims
373
+ Whether to flatten single-value claims in the Identity, by default
374
+ True
359
375
  auto_connect
360
376
  Whether to automatically open the LDAP connection on first use, by
361
377
  default True.
@@ -373,6 +389,7 @@ class ADProvider(LDAPProvider):
373
389
  populate_groups=populate_groups,
374
390
  populate_permissions=populate_permissions,
375
391
  populate_claims=populate_claims,
392
+ flatten_claims=flatten_claims,
376
393
  auto_connect=auto_connect,
377
394
  change_password_supported=change_password_supported,
378
395
  )
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Mapping, Any, Self, Sequence, TYPE_CHECKING
4
+ from typing import Mapping, Any, Self, Sequence, TYPE_CHECKING, cast
5
5
  from datetime import datetime, timezone
6
6
 
7
7
  from alpha.domain.models.group import Group
@@ -125,7 +125,7 @@ class Identity:
125
125
  username: str | None
126
126
  email: str | None
127
127
  display_name: str | None
128
- groups: Sequence[str | Group]
128
+ groups: Sequence[str]
129
129
  permissions: Sequence[str]
130
130
  claims: Mapping[str, Any]
131
131
  issued_at: datetime
@@ -160,6 +160,7 @@ class Identity:
160
160
  populate_groups: bool = True,
161
161
  populate_permissions: bool = False,
162
162
  populate_claims: bool = True,
163
+ flatten_claims: bool = False,
163
164
  ) -> Identity:
164
165
  """Instantiate an Identity from an LDAP entry dictionary.
165
166
 
@@ -178,6 +179,9 @@ class Identity:
178
179
  populate_claims
179
180
  Whether to populate the claims dictionary from the LDAP entry, by
180
181
  default True
182
+ flatten_claims
183
+ Whether to replace claim lists containing a single value with that
184
+ value, by default False
181
185
 
182
186
  Returns
183
187
  -------
@@ -188,6 +192,12 @@ class Identity:
188
192
  if not username:
189
193
  username = cls._get_key(entry, mappings["subject"])
190
194
 
195
+ claims = (
196
+ cls._remove_password_from_claims(entry) if populate_claims else {}
197
+ )
198
+ if populate_claims and flatten_claims:
199
+ claims = cls.flatten_single_value_claims(claims)
200
+
191
201
  return cls(
192
202
  subject=cls._get_key(entry, mappings["subject"], ""),
193
203
  username=username,
@@ -204,11 +214,7 @@ class Identity:
204
214
  if populate_permissions
205
215
  else []
206
216
  ),
207
- claims=(
208
- cls._remove_password_from_claims(entry)
209
- if populate_claims
210
- else {}
211
- ),
217
+ claims=claims,
212
218
  issued_at=datetime.now(tz=timezone.utc),
213
219
  )
214
220
 
@@ -268,13 +274,16 @@ class Identity:
268
274
  An Identity instance populated with data from the User object.
269
275
  """
270
276
  subject = str(user.id) if user.id else user.username
277
+
271
278
  return cls(
272
279
  subject=subject, # type: ignore
273
280
  username=user.username,
274
281
  email=user.email,
275
282
  display_name=user.display_name,
276
- groups=user.groups or [],
277
- permissions=user.permissions or [],
283
+ groups=[str(group) for group in user.groups or []],
284
+ permissions=[
285
+ str(permission) for permission in user.permissions or []
286
+ ],
278
287
  claims={},
279
288
  issued_at=datetime.now(tz=timezone.utc),
280
289
  role=user.role, # type: ignore
@@ -318,10 +327,10 @@ class Identity:
318
327
  self.display_name = user.display_name
319
328
  for permission in user.permissions or []:
320
329
  self.permissions = self._append_on_sequence(
321
- self.permissions, permission
330
+ self.permissions, str(permission)
322
331
  )
323
332
  for group in user.groups or []:
324
- self.groups = self._append_on_sequence(self.groups, group)
333
+ self.groups = self._append_on_sequence(self.groups, str(group))
325
334
  self.role = user.role # type: ignore
326
335
  self.admin = user.admin
327
336
 
@@ -337,7 +346,7 @@ class Identity:
337
346
  for group in groups:
338
347
  for permission in group.permissions or []:
339
348
  self.permissions = self._append_on_sequence(
340
- self.permissions, permission
349
+ self.permissions, str(permission)
341
350
  )
342
351
 
343
352
  def to_dict(self) -> dict[str, Any]:
@@ -430,6 +439,37 @@ class Identity:
430
439
  }
431
440
  return filtered_claims
432
441
 
442
+ @staticmethod
443
+ def flatten_single_value_claims(
444
+ claims: Mapping[str, Any],
445
+ ) -> Mapping[str, Any]:
446
+ """Replace single-value claim lists with their contained value.
447
+
448
+ Password-related claims (keys containing "password", case-insensitive)
449
+ are removed before flattening.
450
+
451
+ Parameters
452
+ ----------
453
+ claims
454
+ Original claims dictionary.
455
+
456
+ Returns
457
+ -------
458
+ Mapping[str, Any]
459
+ A new claims dictionary with password-related keys removed and
460
+ single-value lists replaced by their contained value.
461
+ """
462
+ claims = Identity._remove_password_from_claims(claims)
463
+
464
+ return {
465
+ key: (
466
+ value[0]
467
+ if isinstance(value, list) and len(cast(list[Any], value)) == 1
468
+ else value
469
+ )
470
+ for key, value in claims.items()
471
+ }
472
+
433
473
  @staticmethod
434
474
  def _extract_groups(
435
475
  entry: Mapping[str, Any],