alpha-python 0.7.3__tar.gz → 0.7.5__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 (158) hide show
  1. {alpha_python-0.7.3 → alpha_python-0.7.5}/PKG-INFO +1 -1
  2. {alpha_python-0.7.3 → alpha_python-0.7.5}/pyproject.toml +1 -1
  3. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/adapters/rest_api_unit_of_work.py +14 -10
  4. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/adapters/sqla_unit_of_work.py +11 -3
  5. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/models/group.py +3 -3
  6. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/models/user.py +4 -4
  7. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/request_factory.py +10 -0
  8. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/controller.mustache +1 -0
  9. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/api_repository.py +21 -0
  10. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/sql_repository.py +13 -2
  11. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/mixins/user_lifecycle.py +11 -11
  12. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/ldap_provider.py +11 -0
  13. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/models/identity.py +48 -9
  14. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/oidc_provider.py +11 -1
  15. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/refresh/database_repository.py +2 -2
  16. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/request_headers.py +50 -2
  17. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha_python.egg-info/PKG-INFO +1 -1
  18. {alpha_python-0.7.3 → alpha_python-0.7.5}/LICENSE +0 -0
  19. {alpha_python-0.7.3 → alpha_python-0.7.5}/README.md +0 -0
  20. {alpha_python-0.7.3 → alpha_python-0.7.5}/setup.cfg +0 -0
  21. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/__init__.py +0 -0
  22. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/adapters/__init__.py +0 -0
  23. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/cli.py +0 -0
  24. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/containers/__init__.py +0 -0
  25. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/containers/container.py +0 -0
  26. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/__init__.py +0 -0
  27. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/models/__init__.py +0 -0
  28. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/models/base_model.py +0 -0
  29. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/models/life_cycle_base.py +0 -0
  30. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/domain/models/role.py +0 -0
  31. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/encoder.py +0 -0
  32. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/exceptions.py +0 -0
  33. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/__init__.py +0 -0
  34. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/_type_conversion_matrix.py +0 -0
  35. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/_type_mapping.py +0 -0
  36. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/class_factories.py +0 -0
  37. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/default_field_factory.py +0 -0
  38. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/field_iterator.py +0 -0
  39. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/jwt_factory.py +0 -0
  40. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/logging_handler_factory.py +0 -0
  41. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/model_class_factory.py +0 -0
  42. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/models/__init__.py +0 -0
  43. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/models/factory_classes.py +0 -0
  44. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/password_factory.py +0 -0
  45. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/response_factory.py +0 -0
  46. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/factories/type_factories.py +0 -0
  47. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/__init__.py +0 -0
  48. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/api_generate_handler.py +0 -0
  49. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/api_run_handler.py +0 -0
  50. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/base_handler.py +0 -0
  51. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/gen-code.sh +0 -0
  52. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/models/__init__.py +0 -0
  53. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/models/argument.py +0 -0
  54. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/models/command.py +0 -0
  55. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/models/section.py +0 -0
  56. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/models/subparser.py +0 -0
  57. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/run-api.sh +0 -0
  58. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/__init__.py +0 -0
  59. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/Dockerfile.mustache +0 -0
  60. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/README.mustache +0 -0
  61. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/__init__model.mustache +0 -0
  62. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/__init__test.mustache +0 -0
  63. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/__main__.mustache +0 -0
  64. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/base_model.mustache +0 -0
  65. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/controller_test.mustache +0 -0
  66. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/dockerignore.mustache +0 -0
  67. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/encoder.mustache +0 -0
  68. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/git_push.sh.mustache +0 -0
  69. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/gitignore.mustache +0 -0
  70. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/model.mustache +0 -0
  71. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/openapi.mustache +0 -0
  72. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/param_type.mustache +0 -0
  73. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/requirements.mustache +0 -0
  74. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/security_controller_.mustache +0 -0
  75. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/setup.mustache +0 -0
  76. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/test-requirements.mustache +0 -0
  77. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/tox.mustache +0 -0
  78. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/travis.mustache +0 -0
  79. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/typing_utils.mustache +0 -0
  80. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/handlers/templates/python-flask/util.mustache +0 -0
  81. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/__init__.py +0 -0
  82. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/connectors/__init__.py +0 -0
  83. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/connectors/ldap_connector.py +0 -0
  84. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/connectors/oidc_connector.py +0 -0
  85. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/connectors/sql_alchemy.py +0 -0
  86. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/databases/__init__.py +0 -0
  87. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/databases/sql_alchemy.py +0 -0
  88. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/models/__init__.py +0 -0
  89. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/models/filter_operators.py +0 -0
  90. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/models/json_patch.py +0 -0
  91. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/models/order_by.py +0 -0
  92. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/models/query_clause.py +0 -0
  93. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/infra/models/search_filter.py +0 -0
  94. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/__init__.py +0 -0
  95. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/attrs_instance.py +0 -0
  96. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/dataclass_instance.py +0 -0
  97. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/factories.py +0 -0
  98. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/handler.py +0 -0
  99. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/http_client.py +0 -0
  100. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/openapi_model.py +0 -0
  101. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/patchable.py +0 -0
  102. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/providers.py +0 -0
  103. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/pydantic_instance.py +0 -0
  104. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/refresh_repository.py +0 -0
  105. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/sql_database.py +0 -0
  106. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/sql_mapper.py +0 -0
  107. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/token_factory.py +0 -0
  108. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/unit_of_work.py +0 -0
  109. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/interfaces/updatable.py +0 -0
  110. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/mixins/__init__.py +0 -0
  111. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/mixins/group_lifecycle.py +0 -0
  112. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/mixins/jwt_provider.py +0 -0
  113. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/__init__.py +0 -0
  114. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/api_key_provider.py +0 -0
  115. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/database_provider.py +0 -0
  116. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/models/__init__.py +0 -0
  117. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/models/credentials.py +0 -0
  118. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/providers/models/token.py +0 -0
  119. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/py.typed +0 -0
  120. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/__init__.py +0 -0
  121. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/models/__init__.py +0 -0
  122. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/models/repository_model.py +0 -0
  123. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/refresh/__init__.py +0 -0
  124. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/refresh/cache_repository.py +0 -0
  125. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/refresh/file_repository.py +0 -0
  126. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/refresh/memory_repository.py +0 -0
  127. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/rest_api_repository.py +0 -0
  128. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/repositories/sql_alchemy_repository.py +0 -0
  129. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/services/__init__.py +0 -0
  130. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/services/authentication_service.py +0 -0
  131. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/services/models/__init__.py +0 -0
  132. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/services/models/cookie.py +0 -0
  133. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/services/user_lifecycle_management.py +0 -0
  134. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/__init__.py +0 -0
  135. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/_http_codes.py +0 -0
  136. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/cookie.py +0 -0
  137. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/is_attrs.py +0 -0
  138. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/is_pydantic.py +0 -0
  139. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/logging_configurator.py +0 -0
  140. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/logging_level_checker.py +0 -0
  141. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/__init__.py +0 -0
  142. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/container.py +0 -0
  143. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/exceptions.py +0 -0
  144. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/models.py +0 -0
  145. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/orm.py +0 -0
  146. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/response.py +0 -0
  147. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/openapi_test/service.py +0 -0
  148. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/response_object.py +0 -0
  149. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/secret_generator.py +0 -0
  150. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/verify_identity.py +0 -0
  151. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha/utils/version_checker.py +0 -0
  152. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha_python.egg-info/SOURCES.txt +0 -0
  153. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha_python.egg-info/dependency_links.txt +0 -0
  154. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha_python.egg-info/entry_points.txt +0 -0
  155. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha_python.egg-info/requires.txt +0 -0
  156. {alpha_python-0.7.3 → alpha_python-0.7.5}/src/alpha_python.egg-info/top_level.txt +0 -0
  157. {alpha_python-0.7.3 → alpha_python-0.7.5}/tests/test_cli.py +0 -0
  158. {alpha_python-0.7.3 → alpha_python-0.7.5}/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.3
3
+ Version: 0.7.5
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alpha-python"
3
- version = "0.7.3"
3
+ version = "0.7.5"
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 = [
@@ -1,9 +1,10 @@
1
1
  """Contains the REST API Unit of Work implementation."""
2
2
 
3
- from typing import Any, TypeVar
3
+ from typing import Any, TypeVar, cast
4
4
 
5
5
  import requests
6
6
 
7
+ from alpha.interfaces.http_client import HTTPClient
7
8
  from alpha.repositories.models.repository_model import RepositoryModel
8
9
 
9
10
 
@@ -22,7 +23,7 @@ class RestApiUnitOfWork:
22
23
  def __init__(
23
24
  self,
24
25
  repos: list[RepositoryModel[Any]],
25
- session: requests.sessions.Session | None = None,
26
+ session: HTTPClient | None = None,
26
27
  ) -> None:
27
28
  """Initialize the Unit of Work with repositories.
28
29
 
@@ -44,11 +45,12 @@ class RestApiUnitOfWork:
44
45
 
45
46
  def __enter__(self: UOW) -> UOW:
46
47
  """Enter the REST API Unit of Work context.
47
- Initializes a :class:`requests.sessions.Session` if one was not
48
- provided and attaches the configured repositories as attributes on the
49
- unit of work instance. Each repository is constructed using the shared
50
- session and its associated configuration, and optionally validated
51
- against a declared interface.
48
+
49
+ Initializes an `HTTPClient` session if one was not provided and attaches
50
+ the configured repositories as attributes on the unit of work instance.
51
+ Each repository is constructed using the shared session and its
52
+ associated configuration, and optionally validated against a declared
53
+ interface.
52
54
 
53
55
  Returns
54
56
  -------
@@ -56,7 +58,9 @@ class RestApiUnitOfWork:
56
58
  The configured :class:`RestApiUnitOfWork` instance to be used
57
59
  within the context manager.
58
60
  """
59
- self._session = self._session or requests.sessions.Session()
61
+ self._session = cast(
62
+ HTTPClient, self._session or requests.sessions.Session()
63
+ )
60
64
 
61
65
  for repo in self._repositories:
62
66
  name: str = repo.name
@@ -100,12 +104,12 @@ class RestApiUnitOfWork:
100
104
  raise NotImplementedError("RestApiUnitOfWork does not support refresh")
101
105
 
102
106
  @property
103
- def session(self) -> requests.sessions.Session | None:
107
+ def session(self) -> HTTPClient | None:
104
108
  """Get the current session.
105
109
 
106
110
  Returns
107
111
  -------
108
- requests.sessions.Session | None
112
+ HTTPClient | None
109
113
  The current session used for API interactions.
110
114
  """
111
115
  return self._session
@@ -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,6 +1,6 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from datetime import datetime, timezone
3
- from typing import Any, Sequence, cast
3
+ from typing import Any, cast
4
4
  from uuid import UUID
5
5
 
6
6
  from alpha.domain.models.base_model import BaseDomainModel, DomainModel
@@ -36,7 +36,7 @@ class Group(LifeCycleBase, BaseDomainModel):
36
36
  id: UUID | int | str | None = None
37
37
  name: str | None = None
38
38
  description: str | None = None
39
- permissions: Sequence[str] | None = None
39
+ permissions: list[str] = field(default_factory=list) # type: ignore
40
40
  is_active: bool = True
41
41
 
42
42
  def to_dict(self) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from datetime import datetime, timezone
3
- from typing import Any, Self, Sequence, cast
3
+ from typing import Any, Self, cast
4
4
  from uuid import UUID
5
5
 
6
6
  from alpha.domain.models.base_model import BaseDomainModel, DomainModel
@@ -67,8 +67,8 @@ class User(LifeCycleBase, BaseDomainModel):
67
67
  email: str | None = None
68
68
  phone: str | None = None
69
69
  display_name: str | None = None
70
- permissions: Sequence[str] | None = None
71
- groups: Sequence[str | Group] | None = None
70
+ permissions: list[str] = field(default_factory=list) # type: ignore
71
+ groups: list[str | Group] = field(default_factory=list) # type: ignore
72
72
  is_active: bool = True
73
73
  admin: bool = False
74
74
 
@@ -25,6 +25,8 @@ from alpha.interfaces.factories import (
25
25
  )
26
26
  from alpha.interfaces.openapi_model import OpenAPIModel
27
27
  from alpha.interfaces.pydantic_instance import PydanticInstance
28
+ from alpha.providers.models.identity import Identity
29
+ from alpha.utils.request_headers import Headers
28
30
 
29
31
 
30
32
  class RequestFactory:
@@ -184,6 +186,8 @@ class RequestFactory:
184
186
  DataclassInstance
185
187
  | AttrsInstance
186
188
  | PydanticInstance
189
+ | Identity
190
+ | Headers
187
191
  | None
188
192
  | OpenAPIModel
189
193
  ):
@@ -202,6 +206,9 @@ class RequestFactory:
202
206
  -------
203
207
  DataclassInstance | AttrsInstance | PydanticInstance | None
204
208
  Dataclass, attrs, or pydantic instance
209
+ Identity | Headers
210
+ If the value is an instance of Identity or Headers, it will be
211
+ returned as is.
205
212
  OpenAPIModel
206
213
  If the `use_model_class_factory` parameter is set to False, the
207
214
  value will be returned as an OpenAPIModel instance.
@@ -211,6 +218,9 @@ class RequestFactory:
211
218
  TypeError
212
219
  The value is not an instance of the OpenAPI (Base)Model
213
220
  """
221
+ if isinstance(value, (Identity, Headers)):
222
+ return value
223
+
214
224
  if not isinstance(value, OpenAPIModel):
215
225
  raise TypeError(f"Unable to map {type(value)} on dataclass model")
216
226
 
@@ -198,6 +198,7 @@ def {{operationId}}(
198
198
  auth_token = headers.auth_token if headers.has_auth_token else None
199
199
  refresh_token = headers.refresh_token
200
200
  api_key = headers.api_key
201
+ cookies = headers.cookies
201
202
 
202
203
  {{#vendorExtensions.x-alpha-factory}}
203
204
  factory = {{vendorExtensions.x-alpha-factory}}
@@ -11,6 +11,27 @@ from alpha.infra.models.json_patch import JsonPatch
11
11
  class ApiRepository(Protocol[DomainModel]):
12
12
  """Repository contract for remote API resources."""
13
13
 
14
+ def request(
15
+ self,
16
+ method: str,
17
+ url: str,
18
+ **kwargs: Any,
19
+ ) -> Any | None:
20
+ """Make a custom API request.
21
+
22
+ Parameters
23
+ ----------
24
+ method
25
+ The HTTP method to use for the request (e.g., 'GET', 'POST',
26
+ 'PATCH', 'PUT', 'DELETE').
27
+ url
28
+ The URL to which the request should be made.
29
+ **kwargs
30
+ Additional parameters to include in the API request, such as
31
+ headers, authentication tokens, or other request options.
32
+ """
33
+ ...
34
+
14
35
  @overload
15
36
  def add(
16
37
  self,
@@ -3,7 +3,14 @@ SQLAlchemy ORM repository operations.
3
3
  """
4
4
 
5
5
  from enum import Enum
6
- from typing import Any, Literal, Protocol, overload, runtime_checkable
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ Literal,
10
+ Protocol,
11
+ overload,
12
+ runtime_checkable,
13
+ )
7
14
  from uuid import UUID
8
15
 
9
16
  from sqlalchemy.orm import Session
@@ -12,7 +19,11 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute
12
19
  from alpha.domain.models.base_model import BaseDomainModel, DomainModel
13
20
  from alpha.infra.models.json_patch import JsonPatch
14
21
  from alpha.interfaces.patchable import Patchable
15
- from alpha.interfaces.updatable import Updatable
22
+
23
+ if TYPE_CHECKING:
24
+ from alpha.interfaces.updatable import Updatable
25
+ else:
26
+ Updatable = Any
16
27
 
17
28
 
18
29
  @runtime_checkable
@@ -1,6 +1,6 @@
1
1
  """Contains the UserLifecycleMixin class"""
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any
4
4
  from uuid import UUID
5
5
 
6
6
  from alpha.exceptions import BadRequestException, NotFoundException
@@ -11,6 +11,8 @@ from alpha.providers.models.identity import Identity
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from alpha.domain.models.user import User
14
+ else:
15
+ User = Any
14
16
 
15
17
 
16
18
  class UserLifecycleMixin:
@@ -20,15 +22,13 @@ class UserLifecycleMixin:
20
22
 
21
23
  uow: UnitOfWork
22
24
  _users_repository_name: str
23
- _user_model: type["User"]
25
+ _user_model: type[User]
24
26
  _password_support: bool
25
27
  _password_factory: PasswordFactory
26
28
  _user_username_attribute: str
27
29
  _user_password_attribute: str
28
30
 
29
- def add_user(
30
- self, user: "User", identity: Identity | None = None
31
- ) -> "User":
31
+ def add_user(self, user: User, identity: Identity | None = None) -> User:
32
32
  """Adds a new user object to the repository
33
33
 
34
34
  Parameters
@@ -79,7 +79,7 @@ class UserLifecycleMixin:
79
79
  self.uow.commit()
80
80
  return user
81
81
 
82
- def get_user(self, user_id: str | int | UUID) -> "User":
82
+ def get_user(self, user_id: str | int | UUID) -> User:
83
83
  """Get an user object by id from the repository
84
84
 
85
85
  Parameters
@@ -110,7 +110,7 @@ class UserLifecycleMixin:
110
110
 
111
111
  return user
112
112
 
113
- def get_users(self) -> list["User"]:
113
+ def get_users(self) -> list[User]:
114
114
  """Gets all user objects from the repository
115
115
 
116
116
  Returns
@@ -119,7 +119,7 @@ class UserLifecycleMixin:
119
119
  A collection of all the user objects
120
120
  """
121
121
  with self.uow:
122
- users: SqlRepository["User"] = getattr(
122
+ users: SqlRepository[User] = getattr(
123
123
  self.uow, self._users_repository_name
124
124
  )
125
125
  result = users.select()
@@ -136,7 +136,7 @@ class UserLifecycleMixin:
136
136
  """
137
137
  user = self.get_user(user_id=user_id)
138
138
  with self.uow:
139
- users: SqlRepository["User"] = getattr(
139
+ users: SqlRepository[User] = getattr(
140
140
  self.uow, self._users_repository_name
141
141
  )
142
142
 
@@ -146,9 +146,9 @@ class UserLifecycleMixin:
146
146
  def update_user(
147
147
  self,
148
148
  user_id: str | int | UUID,
149
- user: "User",
149
+ user: User,
150
150
  identity: Identity | None = None,
151
- ) -> "User":
151
+ ) -> User:
152
152
  """Updates an existing user object in the repository
153
153
 
154
154
  Parameters
@@ -45,6 +45,7 @@ class LDAPProvider(JWTProviderMixin):
45
45
  populate_groups: bool = True,
46
46
  populate_permissions: bool = False,
47
47
  populate_claims: bool = True,
48
+ flatten_claims: bool = True,
48
49
  auto_connect: bool = True,
49
50
  change_password_supported: bool = False,
50
51
  additional_connector_params: dict[str, Any] | None = None,
@@ -71,6 +72,9 @@ class LDAPProvider(JWTProviderMixin):
71
72
  Whether to populate permissions in the Identity, by default False
72
73
  populate_claims
73
74
  Whether to populate claims in the Identity, by default True
75
+ flatten_claims
76
+ Whether to flatten single-value claims in the Identity, by default
77
+ True
74
78
  auto_connect
75
79
  Whether to automatically connect using the connector, by default
76
80
  True
@@ -89,6 +93,7 @@ class LDAPProvider(JWTProviderMixin):
89
93
  self._populate_groups = populate_groups
90
94
  self._populate_permissions = populate_permissions
91
95
  self._populate_claims = populate_claims
96
+ self._flatten_claims = flatten_claims
92
97
  self._auto_connect = auto_connect
93
98
  self._change_password_supported = change_password_supported
94
99
  self._additional_connector_params = additional_connector_params or {
@@ -297,6 +302,7 @@ class LDAPProvider(JWTProviderMixin):
297
302
  populate_claims=self._populate_claims,
298
303
  populate_groups=self._populate_groups,
299
304
  populate_permissions=self._populate_permissions,
305
+ flatten_claims=self._flatten_claims,
300
306
  )
301
307
  return identity
302
308
 
@@ -324,6 +330,7 @@ class ADProvider(LDAPProvider):
324
330
  populate_groups: bool = True,
325
331
  populate_permissions: bool = False,
326
332
  populate_claims: bool = True,
333
+ flatten_claims: bool = True,
327
334
  auto_connect: bool = True,
328
335
  change_password_supported: bool = False,
329
336
  ) -> None:
@@ -356,6 +363,9 @@ class ADProvider(LDAPProvider):
356
363
  populate_claims
357
364
  Whether to populate claims on the :class:`Identity`, by default
358
365
  True.
366
+ flatten_claims
367
+ Whether to flatten single-value claims in the Identity, by default
368
+ True
359
369
  auto_connect
360
370
  Whether to automatically open the LDAP connection on first use, by
361
371
  default True.
@@ -373,6 +383,7 @@ class ADProvider(LDAPProvider):
373
383
  populate_groups=populate_groups,
374
384
  populate_permissions=populate_permissions,
375
385
  populate_claims=populate_claims,
386
+ flatten_claims=flatten_claims,
376
387
  auto_connect=auto_connect,
377
388
  change_password_supported=change_password_supported,
378
389
  )
@@ -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
@@ -9,6 +9,8 @@ from alpha.domain.models.group import Group
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from alpha.domain.models.user import User
12
+ else:
13
+ User = Any
12
14
 
13
15
  DEFAULT_LDAP_MAPPINGS = {
14
16
  "subject": "uid",
@@ -158,7 +160,8 @@ class Identity:
158
160
  populate_groups: bool = True,
159
161
  populate_permissions: bool = False,
160
162
  populate_claims: bool = True,
161
- ) -> "Identity":
163
+ flatten_claims: bool = False,
164
+ ) -> Identity:
162
165
  """Instantiate an Identity from an LDAP entry dictionary.
163
166
 
164
167
  Parameters
@@ -176,6 +179,9 @@ class Identity:
176
179
  populate_claims
177
180
  Whether to populate the claims dictionary from the LDAP entry, by
178
181
  default True
182
+ flatten_claims
183
+ Whether to replace claim lists containing a single value with that
184
+ value, by default False
179
185
 
180
186
  Returns
181
187
  -------
@@ -186,6 +192,12 @@ class Identity:
186
192
  if not username:
187
193
  username = cls._get_key(entry, mappings["subject"])
188
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
+
189
201
  return cls(
190
202
  subject=cls._get_key(entry, mappings["subject"], ""),
191
203
  username=username,
@@ -202,16 +214,12 @@ class Identity:
202
214
  if populate_permissions
203
215
  else []
204
216
  ),
205
- claims=(
206
- cls._remove_password_from_claims(entry)
207
- if populate_claims
208
- else {}
209
- ),
217
+ claims=claims,
210
218
  issued_at=datetime.now(tz=timezone.utc),
211
219
  )
212
220
 
213
221
  @classmethod
214
- def from_dict(cls, data: Mapping[str, Any]) -> "Identity":
222
+ def from_dict(cls, data: Mapping[str, Any]) -> Identity:
215
223
  """Instantiate an Identity from a generic dictionary.
216
224
 
217
225
  Parameters
@@ -252,7 +260,7 @@ class Identity:
252
260
  )
253
261
 
254
262
  @classmethod
255
- def from_user(cls, user: User) -> "Identity":
263
+ def from_user(cls, user: User) -> Identity:
256
264
  """Instantiate an Identity from a User instance.
257
265
 
258
266
  Parameters
@@ -428,6 +436,37 @@ class Identity:
428
436
  }
429
437
  return filtered_claims
430
438
 
439
+ @staticmethod
440
+ def flatten_single_value_claims(
441
+ claims: Mapping[str, Any],
442
+ ) -> Mapping[str, Any]:
443
+ """Replace single-value claim lists with their contained value.
444
+
445
+ Password-related claims (keys containing "password", case-insensitive)
446
+ are removed before flattening.
447
+
448
+ Parameters
449
+ ----------
450
+ claims
451
+ Original claims dictionary.
452
+
453
+ Returns
454
+ -------
455
+ Mapping[str, Any]
456
+ A new claims dictionary with password-related keys removed and
457
+ single-value lists replaced by their contained value.
458
+ """
459
+ claims = Identity._remove_password_from_claims(claims)
460
+
461
+ return {
462
+ key: (
463
+ value[0]
464
+ if isinstance(value, list) and len(cast(list[Any], value)) == 1
465
+ else value
466
+ )
467
+ for key, value in claims.items()
468
+ }
469
+
431
470
  @staticmethod
432
471
  def _extract_groups(
433
472
  entry: Mapping[str, Any],
@@ -57,6 +57,7 @@ class OIDCProvider(JWTProviderMixin):
57
57
  populate_groups: bool = True,
58
58
  populate_permissions: bool = False,
59
59
  populate_claims: bool = False,
60
+ flatten_claims: bool = True,
60
61
  change_password_supported: bool = False,
61
62
  ) -> None:
62
63
  """Initialize OIDCProvider.
@@ -90,6 +91,7 @@ class OIDCProvider(JWTProviderMixin):
90
91
  self._populate_groups = populate_groups
91
92
  self._populate_permissions = populate_permissions
92
93
  self._populate_claims = populate_claims
94
+ self._flatten_claims = flatten_claims
93
95
  self._change_password_supported = change_password_supported
94
96
 
95
97
  def authenticate(self, credentials: PasswordCredentials) -> Identity:
@@ -237,6 +239,9 @@ class OIDCProvider(JWTProviderMixin):
237
239
 
238
240
  audience = self._extract_audience(claims)
239
241
 
242
+ if self._populate_claims and self._flatten_claims:
243
+ claims = Identity.flatten_single_value_claims(claims)
244
+
240
245
  identity = Identity(
241
246
  subject=str(subject),
242
247
  username=username,
@@ -244,7 +249,7 @@ class OIDCProvider(JWTProviderMixin):
244
249
  display_name=self._get_claim(claims, "display_name"),
245
250
  groups=groups,
246
251
  permissions=permissions,
247
- claims=dict(claims) if self._populate_claims else {},
252
+ claims=claims if self._populate_claims else {},
248
253
  issued_at=issued_at,
249
254
  audience=audience,
250
255
  role=self._get_claim(claims, "role"),
@@ -403,6 +408,7 @@ class KeyCloakProvider(OIDCProvider):
403
408
  populate_groups: bool = True,
404
409
  populate_permissions: bool = False,
405
410
  populate_claims: bool = False,
411
+ flatten_claims: bool = True,
406
412
  change_password_supported: bool = False,
407
413
  ) -> None:
408
414
  """Initialize KeyCloakProvider.
@@ -427,6 +433,9 @@ class KeyCloakProvider(OIDCProvider):
427
433
  Whether to populate permissions on the Identity.
428
434
  populate_claims
429
435
  Whether to include raw claims on the Identity.
436
+ flatten_claims
437
+ Whether to flatten single-value claims when populating raw claims
438
+ on the Identity.
430
439
  change_password_supported
431
440
  Whether this provider supports changing passwords.
432
441
  """
@@ -437,5 +446,6 @@ class KeyCloakProvider(OIDCProvider):
437
446
  populate_groups=populate_groups,
438
447
  populate_permissions=populate_permissions,
439
448
  populate_claims=populate_claims,
449
+ flatten_claims=flatten_claims,
440
450
  change_password_supported=change_password_supported,
441
451
  )
@@ -1,5 +1,5 @@
1
1
  from alpha import exceptions
2
- from alpha.infra.connectors.sql_alchemy import SqlAlchemyDatabase
2
+ from alpha.interfaces.sql_database import SqlDatabase
3
3
  from alpha.providers.models.token import Token
4
4
 
5
5
 
@@ -15,7 +15,7 @@ class DatabaseRefreshRepository:
15
15
 
16
16
  def __init__(
17
17
  self,
18
- database_connector: SqlAlchemyDatabase,
18
+ database_connector: SqlDatabase,
19
19
  token_model: type[Token] = Token,
20
20
  token_max_age_seconds: int = 7 * 24 * 3600,
21
21
  token_length: int = 32,
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from http import cookies
2
+ from http.cookies import SimpleCookie
3
3
  from typing import Mapping, Self
4
4
 
5
5
 
@@ -16,12 +16,28 @@ class Headers:
16
16
 
17
17
  This class is designed to be immutable and uses slots for memory
18
18
  efficiency.
19
+
20
+ Attributes
21
+ ----------
22
+ auth_token
23
+ The authentication token, typically a JWT, extracted from the
24
+ "Authorization" header or from cookies.
25
+ auth_token_type
26
+ The type of the authentication token, typically "Bearer".
27
+ refresh_token
28
+ The refresh token, extracted from the "X-Refresh-Token" header or from
29
+ cookies.
30
+ api_key
31
+ The API key, extracted from the "X-API-Key" header or from cookies.
32
+ cookies
33
+ A mapping of cookie names to their values, if any cookies are present.
19
34
  """
20
35
 
21
36
  auth_token: str | None = None
22
37
  auth_token_type: str | None = None
23
38
  refresh_token: str | None = None
24
39
  api_key: str | None = None
40
+ _cookie_jar: SimpleCookie | None = None
25
41
 
26
42
  @classmethod
27
43
  def from_headers(
@@ -73,8 +89,9 @@ class Headers:
73
89
  api_key = headers.get("X-API-Key")
74
90
  cookies_header = headers.get("Cookie")
75
91
 
92
+ cookie_jar: SimpleCookie | None = None
76
93
  if cookies_header is not None:
77
- cookie_jar = cookies.SimpleCookie(cookies_header)
94
+ cookie_jar = SimpleCookie(cookies_header)
78
95
 
79
96
  if not auth_token and auth_token_cookie_name in cookie_jar:
80
97
  auth_token = cookie_jar[auth_token_cookie_name].value
@@ -89,6 +106,7 @@ class Headers:
89
106
  auth_token_type=auth_token_type,
90
107
  refresh_token=refresh_token,
91
108
  api_key=api_key,
109
+ _cookie_jar=cookie_jar,
92
110
  )
93
111
 
94
112
  @property
@@ -107,6 +125,20 @@ class Headers:
107
125
  def has_api_key(self) -> bool:
108
126
  return True if self.api_key else False
109
127
 
128
+ @property
129
+ def cookies(self) -> dict[str, str]:
130
+ """Return a dictionary of cookies extracted from the request headers.
131
+
132
+ Returns
133
+ -------
134
+ dict[str, str]
135
+ A dictionary mapping cookie names to their values. If no cookies
136
+ are present, an empty dictionary is returned.
137
+ """
138
+ if self._cookie_jar is None:
139
+ return {}
140
+ return self._cookie_jar_to_dict(self._cookie_jar)
141
+
110
142
  def __repr__(self) -> str:
111
143
  return (
112
144
  f"Headers(auth_token={'***' if self.auth_token else None}, "
@@ -114,3 +146,19 @@ class Headers:
114
146
  f"refresh_token={'***' if self.refresh_token else None}, "
115
147
  f"api_key={'***' if self.api_key else None})"
116
148
  )
149
+
150
+ @staticmethod
151
+ def _cookie_jar_to_dict(cookie_jar: SimpleCookie) -> dict[str, str]:
152
+ """Convert a SimpleCookie object to a dictionary.
153
+
154
+ Parameters
155
+ ----------
156
+ cookie_jar
157
+ A SimpleCookie object containing the cookies.
158
+
159
+ Returns
160
+ -------
161
+ dict[str, str]
162
+ A dictionary mapping cookie names to their values.
163
+ """
164
+ return {key: morsel.value for key, morsel in cookie_jar.items()}