alpha-python 0.5.0__py3-none-any.whl → 0.5.1__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.
alpha/__init__.py CHANGED
@@ -49,6 +49,7 @@ from alpha.repositories.models.repository_model import RepositoryModel
49
49
  from alpha.repositories.rest_api_repository import RestApiRepository
50
50
  from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
51
51
  from alpha.services.authentication_service import AuthenticationService
52
+ from alpha.services.user_lifecycle_management import UserLifecycleManagement
52
53
  from alpha.utils.is_attrs import is_attrs
53
54
  from alpha.utils.is_pydantic import is_pydantic
54
55
  from alpha.utils.logging_configurator import (
@@ -56,6 +57,7 @@ from alpha.utils.logging_configurator import (
56
57
  GunicornLogger,
57
58
  )
58
59
  from alpha.utils.logging_level_checker import logging_level_checker
60
+ from alpha.utils.request_headers import Headers
59
61
  from alpha.utils.response_object import create_response_object
60
62
  from alpha.utils.verify_identity import verify_identity
61
63
  from alpha.utils.version_checker import minor_version_gte
@@ -126,11 +128,13 @@ __all__ = [
126
128
  "RestApiRepository",
127
129
  "SqlAlchemyRepository",
128
130
  "AuthenticationService",
131
+ "UserLifecycleManagement",
129
132
  "is_attrs",
130
133
  "is_pydantic",
131
134
  "LoggingConfigurator",
132
135
  "GunicornLogger",
133
136
  "logging_level_checker",
137
+ "Headers",
134
138
  "create_response_object",
135
139
  "verify_identity",
136
140
  "minor_version_gte",
@@ -1,25 +1,89 @@
1
1
  from dataclasses import dataclass
2
2
  from datetime import datetime, timezone
3
+ from enum import Enum, auto
3
4
  from typing import Self, Sequence, cast
4
5
  from uuid import UUID
5
6
 
6
7
  from alpha.domain.models.base_model import BaseDomainModel, DomainModel
8
+ from alpha.domain.models.group import Group
7
9
  from alpha.domain.models.life_cycle_base import LifeCycleBase
8
10
 
9
11
  from alpha.providers.models.identity import Identity
10
12
 
11
13
 
14
+ class Role(Enum):
15
+ """Defines user roles with varying levels of permissions. The roles are
16
+ ordered from highest to lowest permissions. The comparison methods allow
17
+ for easy comparison of roles based on their hierarchy. The roles are
18
+ ordered on a scale from highest to lowest permissions.
19
+
20
+ Typical permissions are as follows:
21
+ - CREATE: Permission to create new content or data, but not modify existing
22
+ content.
23
+ - READ: Permission to read content or data.
24
+ - UPDATE: Permission to modify existing content or data, but not create new
25
+ content.
26
+ - DELETE: Permission to delete content or data.
27
+ - MANAGE_USERS: Permission to manage user accounts and permissions.
28
+ - MANAGE_SETTINGS: Permission to manage system settings and configurations.
29
+ - ALL: Permission to perform all actions, including user management and
30
+ system settings.
31
+
32
+ Roles:
33
+ - ADMIN: Role with permissions to manage users, content, and system
34
+ settings. Typically has the ALL permissions.
35
+ - SUPERUSER: Role with all permissions, including system settings and user
36
+ management. Typically has the ALL permissions, but may be used to denote a
37
+ special type of admin user with additional privileges or responsibilities.
38
+ - OWNER: Role with permissions to manage their own resources and users, but
39
+ not system settings. Typically has permissions similar to ADMIN, but
40
+ limited to their own scope of resources.
41
+ - MODERATOR: Role with permissions to manage content and users, but not
42
+ system settings. Typically has permissions to UPDATE and DELETE content,
43
+ and MANAGE_USERS, but not MANAGE_SETTINGS.
44
+ - EDITOR: Role with permissions to create and edit content, but not manage
45
+ users or settings. Typically has permissions to CREATE, READ, UPDATE, and
46
+ DELETE content, but not MANAGE_USERS or MANAGE_SETTINGS.
47
+ - USER: Default role with standard permissions. Typically has permissions
48
+ to CREATE, READ, and UPDATE their own content, but not DELETE content or
49
+ manage users or settings.
50
+ - VIEWER: Typical read-only role with limited permissions. Typically has
51
+ permission to READ content, but not CREATE, UPDATE, DELETE, or manage users
52
+ or settings.
53
+ """
54
+
55
+ ADMIN = auto()
56
+ SUPERUSER = auto()
57
+ OWNER = auto()
58
+ MODERATOR = auto()
59
+ EDITOR = auto()
60
+ USER = auto()
61
+ VIEWER = auto()
62
+
63
+ def __lt__(self, obj: Self) -> bool:
64
+ return self.value < obj.value
65
+
66
+ def __le__(self, obj: Self) -> bool:
67
+ return self.value <= obj.value
68
+
69
+ def __gt__(self, obj: Self) -> bool:
70
+ return self.value > obj.value
71
+
72
+ def __ge__(self, obj: Self) -> bool:
73
+ return self.value >= obj.value
74
+
75
+
12
76
  @dataclass(kw_only=True)
13
77
  class User(LifeCycleBase, BaseDomainModel):
14
78
  id: UUID | int | str | None = None
15
79
  username: str | None = None
16
80
  password: str | None = None
17
- role: str | None = None
81
+ role: str | Role | None = None
18
82
  email: str | None = None
19
83
  phone: str | None = None
20
84
  display_name: str | None = None
21
85
  permissions: Sequence[str] | None = None
22
- groups: Sequence[str] | None = None
86
+ groups: Sequence[str | Group] | None = None
23
87
  is_active: bool = True
24
88
  admin: bool = False
25
89
 
@@ -18,8 +18,12 @@ class JWTFactory:
18
18
  lifetime_hours: str | None = "12",
19
19
  issuer: str = "http://localhost",
20
20
  jwt_algorithm: str = "HS256",
21
+ options: dict[str, Any] | None = None,
21
22
  ) -> None:
22
- """Initialize the JWTFactory.
23
+ """Initialize the JWTFactory. This method sets up the necessary
24
+ configuration for creating and validating JWT tokens. It requires a
25
+ secret key for signing the tokens and allows optional configuration
26
+ for token lifetime, issuer, algorithm, and decoding options.
23
27
 
24
28
  Parameters
25
29
  ----------
@@ -31,6 +35,10 @@ class JWTFactory:
31
35
  The issuer of the JWT, by default "http://localhost"
32
36
  jwt_algorithm, optional
33
37
  The algorithm used to sign the JWT, by default "HS256"
38
+ options, optional
39
+ A dictionary of options to customize the decoding behavior, by
40
+ default None. If not provided, it defaults to requiring standard
41
+ claims and verifying the signature.
34
42
 
35
43
  Raises
36
44
  ------
@@ -46,6 +54,10 @@ class JWTFactory:
46
54
  self.JWT_ISSUER = issuer
47
55
  self.JWT_ALGORITHM = jwt_algorithm
48
56
  self.JWT_LIFETIME_SECONDS = 3600 * int(lifetime_hours)
57
+ self.JWT_OPTIONS = options or {
58
+ "require": ["exp", "iat", "nbf", "iss", "sub"],
59
+ "verify_signature": True,
60
+ }
49
61
 
50
62
  def create(
51
63
  self,
@@ -93,13 +105,19 @@ class JWTFactory:
93
105
  )
94
106
  return token
95
107
 
96
- def validate(self, token: str) -> bool:
97
- """Validate a JWT token.
108
+ def validate(
109
+ self, token: str, options: dict[str, Any] | None = None
110
+ ) -> bool:
111
+ """Validate a JWT token. This method checks the token's signature,
112
+ expiration, and issuer. If the token is invalid, it raises an
113
+ appropriate exception. If the token is valid, it returns True.
98
114
 
99
115
  Parameters
100
116
  ----------
101
117
  token
102
118
  The JWT token to be validated.
119
+ options
120
+ A dictionary of options to customize the decoding behavior.
103
121
 
104
122
  Returns
105
123
  -------
@@ -112,15 +130,62 @@ class JWTFactory:
112
130
  InvalidSignatureException
113
131
  If the token signature is invalid.
114
132
  """
133
+ if self._decode(token, options):
134
+ return True
135
+ return False
136
+
137
+ def get_payload(
138
+ self, token: str, options: dict[str, Any] | None = None
139
+ ) -> dict[str, Any]:
140
+ """Retrieve the payload from a JWT token. This method does not perform
141
+ validation of the token by default. It simply decodes the token and
142
+ extracts the payload.
143
+
144
+ If the `options` parameter is provided, it will be passed to the
145
+ `jwt.decode` function. This allows customization of the decoding
146
+ behavior, such as enabling or disabling signature verification.
147
+
148
+ Parameters
149
+ ----------
150
+ token
151
+ The JWT token from which to extract the payload.
152
+ options
153
+ A dictionary of options to customize the decoding behavior.
154
+
155
+ Returns
156
+ -------
157
+ A dictionary containing the payload data extracted from the token.
158
+ """
159
+ decoded = self._decode(token, options)
160
+ return decoded.get("payload", {})
161
+
162
+ def _decode(
163
+ self, token: str, options: dict[str, Any] | None = None
164
+ ) -> dict[str, Any]:
165
+ """Decode a JWT token without performing validation. This method is
166
+ intended for internal use and should not be exposed as part of the
167
+ public API.
168
+
169
+ Parameters
170
+ ----------
171
+ token
172
+ The JWT token to be decoded.
173
+ options
174
+ A dictionary of options to customize the decoding behavior.
175
+
176
+ Returns
177
+ -------
178
+ A dictionary containing the decoded token data.
179
+ """
115
180
  try:
116
- jwt.decode(
181
+ decoded: dict[str, Any] = jwt.decode(
117
182
  jwt=token,
118
183
  key=self.JWT_SECRET,
119
184
  algorithms=[self.JWT_ALGORITHM],
120
185
  issuer=self.JWT_ISSUER,
121
- verify=True,
186
+ options=options or self.JWT_OPTIONS,
122
187
  )
123
- return True
188
+ return decoded
124
189
  except jwt.ExpiredSignatureError as e:
125
190
  raise exceptions.TokenExpiredException(str(e)) from e
126
191
  except jwt.InvalidSignatureError as e:
@@ -129,23 +194,3 @@ class JWTFactory:
129
194
  raise exceptions.InvalidTokenException(
130
195
  f"Token is invalid: {str(e)}"
131
196
  ) from e
132
-
133
- def get_payload(self, token: str) -> dict[str, Any]:
134
- """Retrieve the payload from a JWT token.
135
-
136
- Parameters
137
- ----------
138
- token
139
- The JWT token from which to extract the payload.
140
-
141
- Returns
142
- -------
143
- A dictionary containing the payload data extracted from the token.
144
- """
145
- decoded: dict[str, Any] = jwt.decode(
146
- jwt=token,
147
- key=self.JWT_SECRET,
148
- algorithms=[self.JWT_ALGORITHM],
149
- issuer=self.JWT_ISSUER,
150
- )
151
- return decoded.get("payload", {})
@@ -11,6 +11,7 @@ from alpha import exceptions
11
11
  class PasswordFactory:
12
12
  """This class provides methods for hashing and verifying passwords using
13
13
  the argon2 library. It includes the following methods:
14
+
14
15
  - hash_password: Hashes a given password and returns the hashed value as a
15
16
  hexadecimal string.
16
17
  - verify_password: Verifies a given password against a provided hash and
@@ -11,6 +11,7 @@ from typing import List
11
11
  from alpha.factories.request_factory import RequestFactory
12
12
  from alpha.factories.response_factory import ResponseFactory
13
13
  from alpha.utils.response_object import create_response_object
14
+ from alpha.utils.request_headers import Headers
14
15
  from alpha.utils.verify_identity import verify_identity
15
16
  from alpha.utils._http_codes import http_codes_{{#languageCode}}{{languageCode}}{{/languageCode}}{{^languageCode}}en{{/languageCode}} as http_codes
16
17
  from alpha.exceptions import (
@@ -144,28 +145,36 @@ def {{operationId}}(
144
145
  {{response}}
145
146
  {{/isContainer}}
146
147
  {{/allParams}}
148
+ headers = Headers.from_headers(connexion.request.headers)
149
+
150
+ auth_token = headers.auth_token if headers.has_auth_token else None
151
+ refresh_token = headers.refresh_token
152
+ api_key = headers.api_key
147
153
 
148
- {{#authMethods}}{{#isBasicBearer}}
149
- if "Authorization" in connexion.request.headers:
150
- token = connexion.request.headers["Authorization"].split(" ").pop()
151
- else:
152
- token = None
153
- {{/isBasicBearer}}{{/authMethods}}
154
154
  try:
155
155
  # Objects used for authorization
156
156
  roles=[{{#vendorExtensions.x-alpha-verify-roles}}"{{.}}",{{/vendorExtensions.x-alpha-verify-roles}}]
157
157
  groups=[{{#vendorExtensions.x-alpha-verify-groups}}"{{.}}",{{/vendorExtensions.x-alpha-verify-groups}}]
158
158
  permissions=[{{#vendorExtensions.x-alpha-verify-permissions}}"{{.}}",{{/vendorExtensions.x-alpha-verify-permissions}}]
159
- {{#authMethods}}{{#isBasicBearer}}
160
- # validate token
161
- if token_factory.validate(token):
162
- identity = token_factory.get_payload(token)
163
- verify_identity(identity,
164
- roles=roles,
165
- groups=groups,
166
- permissions=permissions,
167
- )
168
- {{/isBasicBearer}}{{/authMethods}}
159
+ {{#authMethods}}
160
+ {{#isBasicBearer}}
161
+ # Validate authentication token
162
+ if auth_token is None:
163
+ raise UnauthorizedException('Missing authentication token')
164
+
165
+ token_factory.validate(auth_token)
166
+
167
+ identity = token_factory.get_payload(auth_token)
168
+ verify_identity(identity,
169
+ roles=roles,
170
+ groups=groups,
171
+ permissions=permissions,
172
+ )
173
+ {{/isBasicBearer}}
174
+ {{#isApiKey}}
175
+ # This needs to be replaced by code to authenticate with an API key
176
+ {{/isApiKey}}
177
+ {{/authMethods}}
169
178
  # Call method or use variable
170
179
  {{#vendorExtensions.x-alpha-request-factory}}
171
180
  {{#vendorExtensions.x-alpha-service-name}}
@@ -180,35 +189,34 @@ def {{operationId}}(
180
189
  {{/vendorExtensions.x-alpha-service-method}}
181
190
  {{/vendorExtensions.x-alpha-service-name}}
182
191
  {{^vendorExtensions.x-alpha-service-name}}
183
- {{#vendorExtensions.x-alpha-method}}
184
- result = {{vendorExtensions.x-alpha-method}}
185
- {{/vendorExtensions.x-alpha-method}}
192
+ {{#vendorExtensions.x-alpha-custom-response}}
193
+ result = {{vendorExtensions.x-alpha-custom-response}}
194
+ {{/vendorExtensions.x-alpha-custom-response}}
186
195
  {{/vendorExtensions.x-alpha-service-name}}
187
196
  {{/vendorExtensions.x-alpha-request-factory}}
188
197
  {{^vendorExtensions.x-alpha-request-factory}}
189
198
  {{^vendorExtensions.x-alpha-service-name}}
190
- {{^vendorExtensions.x-alpha-method}}
199
+ {{^vendorExtensions.x-alpha-custom-response}}
191
200
  raise ServerErrorException('Missing x-alpha-service-name in API specification')
192
- {{/vendorExtensions.x-alpha-method}}
201
+ {{/vendorExtensions.x-alpha-custom-response}}
193
202
  {{^vendorExtensions.x-alpha-service-method}}
194
- {{^vendorExtensions.x-alpha-method}}
203
+ {{^vendorExtensions.x-alpha-custom-response}}
195
204
  raise ServerErrorException('Missing x-alpha-service-method in API specification')
196
- {{/vendorExtensions.x-alpha-method}}
205
+ {{/vendorExtensions.x-alpha-custom-response}}
197
206
  {{/vendorExtensions.x-alpha-service-method}}
198
207
  {{/vendorExtensions.x-alpha-service-name}}
199
208
  {{#vendorExtensions.x-alpha-service-name}}
200
209
  {{#vendorExtensions.x-alpha-service-method}}
201
210
  result = {{vendorExtensions.x-alpha-service-name}}.{{vendorExtensions.x-alpha-service-method}}(
202
211
  {{#allParams}}{{paramName}}={{paramName}},{{/allParams}}
203
- {{#vendorExtensions.x-alpha-service-additional-parameters}}{{.}}={{.}},
204
- {{/vendorExtensions.x-alpha-service-additional-parameters}}
212
+ {{#vendorExtensions.x-alpha-service-additional-parameters}}{{.}}={{.}},{{/vendorExtensions.x-alpha-service-additional-parameters}}
205
213
  )
206
214
  {{/vendorExtensions.x-alpha-service-method}}
207
215
  {{/vendorExtensions.x-alpha-service-name}}
208
216
  {{^vendorExtensions.x-alpha-service-name}}
209
- {{#vendorExtensions.x-alpha-method}}
210
- result = {{vendorExtensions.x-alpha-method}}
211
- {{/vendorExtensions.x-alpha-method}}
217
+ {{#vendorExtensions.x-alpha-custom-response}}
218
+ result = {{vendorExtensions.x-alpha-custom-response}}
219
+ {{/vendorExtensions.x-alpha-custom-response}}
212
220
  {{/vendorExtensions.x-alpha-service-name}}
213
221
  {{/vendorExtensions.x-alpha-request-factory}}
214
222
 
@@ -48,13 +48,18 @@ class TokenFactory(Protocol):
48
48
  """
49
49
  ...
50
50
 
51
- def get_payload(self, token: str) -> dict[str, str]:
51
+ def get_payload(
52
+ self, token: str, options: dict[str, bool] | None
53
+ ) -> dict[str, str]:
52
54
  """Retrieve the payload from an authentication token.
53
55
 
54
56
  Parameters
55
57
  ----------
56
58
  token
57
59
  The authentication token from which to extract the payload.
60
+ options
61
+ A dictionary of options to customize the decoding behavior, such as
62
+ enabling or disabling signature verification.
58
63
 
59
64
  Returns
60
65
  -------
@@ -123,7 +123,7 @@ class Identity:
123
123
  username: str | None
124
124
  email: str | None
125
125
  display_name: str | None
126
- groups: Sequence[str]
126
+ groups: Sequence[str | Group]
127
127
  permissions: Sequence[str]
128
128
  claims: Mapping[str, Any]
129
129
  issued_at: datetime
@@ -30,5 +30,5 @@ class RepositoryModel(Generic[DomainModel]):
30
30
  name: str
31
31
  repository: Callable[..., object]
32
32
  default_model: type[DomainModel]
33
- interface: object | None
33
+ interface: object | None = None
34
34
  additional_config: dict[str, object] | None = None
@@ -1,5 +1,7 @@
1
1
  from alpha.services.authentication_service import AuthenticationService
2
+ from alpha.services.user_lifecycle_management import UserLifecycleManagement
2
3
 
3
4
  __all__ = [
4
5
  "AuthenticationService",
6
+ "UserLifecycleManagement",
5
7
  ]
@@ -43,8 +43,8 @@ class AuthenticationService:
43
43
  refresh_token_max_age: int = 3600 * 24 * 7,
44
44
  merge_with_database_users: bool = False,
45
45
  merge_with_database_groups: bool = False,
46
- user_id_attribute: str = "username",
47
- group_id_attribute: str = "name",
46
+ user_username_attribute: str = "username",
47
+ group_name_attribute: str = "name",
48
48
  uow: UnitOfWork | None = None,
49
49
  users_repository_name: str = "users",
50
50
  groups_repository_name: str = "groups",
@@ -106,10 +106,10 @@ class AuthenticationService:
106
106
  merge_with_database_groups, optional
107
107
  Whether to merge identity data with database group data,
108
108
  by default False
109
- user_id_attribute, optional
109
+ user_username_attribute, optional
110
110
  Attribute name in the user database to use as the unique
111
111
  identifier, by default "username"
112
- group_id_attribute, optional
112
+ group_name_attribute, optional
113
113
  Attribute name in the group database to use as the unique
114
114
  identifier, by default "name"
115
115
  uow, optional
@@ -176,8 +176,8 @@ class AuthenticationService:
176
176
  self._refresh_token_max_age = refresh_token_max_age
177
177
  self._merge_with_database_users = merge_with_database_users
178
178
  self._merge_with_database_groups = merge_with_database_groups
179
- self._user_id_attribute = user_id_attribute
180
- self._group_id_attribute = group_id_attribute
179
+ self._user_username_attribute = user_username_attribute
180
+ self._group_name_attribute = group_name_attribute
181
181
  self.uow = uow
182
182
  self._users_repository_name = users_repository_name
183
183
  self._groups_repository_name = groups_repository_name
@@ -368,8 +368,14 @@ class AuthenticationService:
368
368
  "configured, cannot retrieve identity from auth token."
369
369
  )
370
370
  try:
371
+ # Attempt to retrieve the identity from the auth token without
372
+ # validating the token, since it may be expired. The payload
373
+ # should still be retrievable if the token is expired, as long
374
+ # as the signature is valid. If the signature is invalid, an
375
+ # exception will be raised and caught, resulting in the
376
+ # identity remaining None.
371
377
  payload = self._identity_provider.token_factory.get_payload(
372
- token=auth_token
378
+ token=auth_token, options={"verify_exp": False}
373
379
  )
374
380
  identity = Identity.from_dict(payload)
375
381
  except Exception:
@@ -469,8 +475,8 @@ class AuthenticationService:
469
475
  )
470
476
 
471
477
  user = users.get_by_id(
472
- value=getattr(identity, self._user_id_attribute),
473
- attr=self._user_id_attribute,
478
+ value=getattr(identity, self._user_username_attribute),
479
+ attr=self._user_username_attribute,
474
480
  )
475
481
  if user:
476
482
  identity.update_from_user(user)
@@ -502,18 +508,23 @@ class AuthenticationService:
502
508
  self._raise_no_uow()
503
509
 
504
510
  with self.uow:
505
- groups: SqlRepository[Group] = getattr(
511
+ groups_repo: SqlRepository[Group] = getattr(
506
512
  self.uow, self._groups_repository_name
507
513
  )
508
514
 
515
+ groups = list(identity.groups)
516
+ for i, group in enumerate(groups):
517
+ if isinstance(group, Group):
518
+ groups[i] = getattr(group, self._group_name_attribute)
519
+
509
520
  filters = [
510
521
  SearchFilter(
511
- field=self._group_id_attribute,
522
+ field=self._group_name_attribute,
512
523
  op=Operator.IN,
513
- value=identity.groups,
524
+ value=groups,
514
525
  )
515
526
  ]
516
- user_groups = groups.select(filters=filters)
527
+ user_groups = groups_repo.select(filters=filters)
517
528
  identity.update_from_groups(user_groups)
518
529
 
519
530
  return identity
@@ -0,0 +1,331 @@
1
+ """Contains the UserLifecycleManagement class"""
2
+
3
+ from uuid import UUID
4
+
5
+ from alpha.domain.models.group import Group
6
+ from alpha.domain.models.user import User
7
+ from alpha.exceptions import BadRequestException, NotFoundException
8
+ from alpha.factories.password_factory import PasswordFactory
9
+ from alpha.interfaces.sql_repository import SqlRepository
10
+ from alpha.interfaces.unit_of_work import UnitOfWork
11
+ from alpha.providers.models.identity import Identity
12
+
13
+
14
+ class UserLifecycleManagement:
15
+ """Management service for User CRUD transactions"""
16
+
17
+ def __init__(
18
+ self,
19
+ uow: UnitOfWork,
20
+ users_repository_name: str = "users",
21
+ groups_repository_name: str = "groups",
22
+ password_support: bool = False,
23
+ password_factory: PasswordFactory | None = None,
24
+ user_username_attribute: str = "username",
25
+ user_password_attribute: str = "password",
26
+ ):
27
+ """Initializes the UserLifecycleManagement service. This service
28
+ provides methods for managing the lifecycle of User objects, including
29
+ creating, retrieving, updating, and deleting users. It interacts with a
30
+ Unit of Work to manage transactions and repositories for users and
31
+ groups. It also supports optional password hashing functionality when
32
+ enabled.
33
+
34
+ Parameters
35
+ ----------
36
+ uow
37
+ Unit of Work instance for managing transactions
38
+ users_repository_name, optional
39
+ Name of the users repository, by default "users"
40
+ groups_repository_name, optional
41
+ Name of the groups repository, by default "groups"
42
+ password_support, optional
43
+ Whether password support is enabled, by default False
44
+ password_factory, optional
45
+ Factory for creating password hashes, by default None. If None, a
46
+ default PasswordFactory will be used.
47
+ user_username_attribute, optional
48
+ Attribute name for the username, by default "username"
49
+ user_password_attribute, optional
50
+ Attribute name for the password, by default "password"
51
+ """
52
+ self.uow = uow
53
+ self._users_repository_name = users_repository_name
54
+ self._groups_repository_name = groups_repository_name
55
+ self._password_support = password_support
56
+ self._password_factory = password_factory or PasswordFactory()
57
+ self._user_username_attribute = user_username_attribute
58
+ self._user_password_attribute = user_password_attribute
59
+
60
+ def add_user(self, user: User, identity: Identity | None = None) -> User:
61
+ """Adds a new user object to the repository
62
+
63
+ Parameters
64
+ ----------
65
+ user
66
+ New user object
67
+ identity
68
+ The identity of the user making the changes, by default None. If
69
+ provided, the `created_by` attribute of the user will be updated
70
+ with the username from the identity.
71
+
72
+ Returns
73
+ -------
74
+ Created user object
75
+ """
76
+ if not hasattr(user, self._user_username_attribute) or not getattr(
77
+ user, self._user_username_attribute
78
+ ):
79
+ raise BadRequestException("Username of User attribute is empty")
80
+
81
+ if self._password_support:
82
+ if not hasattr(user, self._user_password_attribute) or not getattr(
83
+ user, self._user_password_attribute
84
+ ):
85
+ raise BadRequestException(
86
+ "Password of User attribute is empty"
87
+ )
88
+
89
+ setattr(
90
+ user,
91
+ self._user_password_attribute,
92
+ self._password_factory.hash_password(
93
+ password=getattr(user, self._user_password_attribute)
94
+ ),
95
+ )
96
+
97
+ if identity:
98
+ user.created_by = identity.username
99
+
100
+ with self.uow:
101
+ users: SqlRepository[User] = getattr(
102
+ self.uow, self._users_repository_name
103
+ )
104
+ user = users.add(user, raise_if_exists=True)
105
+ self.uow.commit()
106
+ return user
107
+
108
+ def add_group(
109
+ self, group: Group, identity: Identity | None = None
110
+ ) -> Group:
111
+ """Adds a new group object to the repository
112
+
113
+ Parameters
114
+ ----------
115
+ group
116
+ New group object
117
+ identity
118
+ The identity of the user making the changes, by default None. If
119
+ provided, the `created_by` attribute of the group will be updated
120
+ with the username from the identity.
121
+
122
+ Returns
123
+ -------
124
+ Created group object
125
+ """
126
+ if identity:
127
+ group.created_by = identity.username
128
+
129
+ with self.uow:
130
+ groups: SqlRepository[Group] = getattr(
131
+ self.uow, self._groups_repository_name
132
+ )
133
+ group = groups.add(group, raise_if_exists=True)
134
+ self.uow.commit()
135
+ return group
136
+
137
+ def get_user(self, user_id: str | int | UUID) -> User:
138
+ """Get an user object by id from the repository
139
+
140
+ Parameters
141
+ ----------
142
+ user_id
143
+ The id of the user object
144
+
145
+ Returns
146
+ -------
147
+ User object which corresponds to the id
148
+
149
+ Raises
150
+ ------
151
+ NotFoundException
152
+ When the object is not found in the repository
153
+ """
154
+ with self.uow:
155
+ users: SqlRepository[User] = getattr(
156
+ self.uow, self._users_repository_name
157
+ )
158
+ user = users.get_by_id(user_id)
159
+
160
+ if not user:
161
+ raise NotFoundException(
162
+ f"User with id '{user_id}' is not found"
163
+ )
164
+
165
+ return user
166
+
167
+ def get_group(self, group_id: str | int | UUID) -> Group:
168
+ """Get a group object by id from the repository
169
+
170
+ Parameters
171
+ ----------
172
+ group_id
173
+ The id of the group object
174
+
175
+ Returns
176
+ -------
177
+ Group object which corresponds to the id
178
+
179
+ Raises
180
+ ------
181
+ NotFoundException
182
+ When the object is not found in the repository
183
+ """
184
+ with self.uow:
185
+ groups: SqlRepository[Group] = getattr(
186
+ self.uow, self._groups_repository_name
187
+ )
188
+ group = groups.get_by_id(group_id)
189
+
190
+ if not group:
191
+ raise NotFoundException(
192
+ f"Group with id '{group_id}' is not found"
193
+ )
194
+
195
+ return group
196
+
197
+ def get_users(self) -> list[User]:
198
+ """Gets all user objects from the repository
199
+
200
+ Returns
201
+ -------
202
+ A collection of all the user objects
203
+ """
204
+ with self.uow:
205
+ users: SqlRepository[User] = getattr(
206
+ self.uow, self._users_repository_name
207
+ )
208
+ result = users.select()
209
+
210
+ return result
211
+
212
+ def get_groups(self) -> list[Group]:
213
+ """Gets all group objects from the repository
214
+
215
+ Returns
216
+ -------
217
+ A collection of all the group objects
218
+ """
219
+ with self.uow:
220
+ groups: SqlRepository[Group] = getattr(
221
+ self.uow, self._groups_repository_name
222
+ )
223
+ result = groups.select()
224
+
225
+ return result
226
+
227
+ def remove_user(self, user_id: str | int | UUID) -> None:
228
+ """Removes an user object from the repository
229
+
230
+ Parameters
231
+ ----------
232
+ user_id
233
+ The id of the user object
234
+ """
235
+ user = self.get_user(user_id=user_id)
236
+ with self.uow:
237
+ users: SqlRepository[User] = getattr(
238
+ self.uow, self._users_repository_name
239
+ )
240
+
241
+ users.remove(user)
242
+ self.uow.commit()
243
+
244
+ def remove_group(self, group_id: str | int | UUID) -> None:
245
+ """Removes a group object from the repository
246
+
247
+ Parameters
248
+ ----------
249
+ group_id
250
+ The id of the group object
251
+ """
252
+ group = self.get_group(group_id=group_id)
253
+ with self.uow:
254
+ groups: SqlRepository[Group] = getattr(
255
+ self.uow, self._groups_repository_name
256
+ )
257
+
258
+ groups.remove(group)
259
+ self.uow.commit()
260
+
261
+ def update_user(
262
+ self,
263
+ user_id: str | int | UUID,
264
+ user: User,
265
+ identity: Identity | None = None,
266
+ ) -> User:
267
+ """Updates an existing user object in the repository
268
+
269
+ Parameters
270
+ ----------
271
+ user
272
+ User object with changes
273
+ user_id
274
+ The id of the user object
275
+ identity
276
+ The identity of the user making the changes, by default None. If
277
+ provided, the `modified_by` attribute of the user will be updated
278
+ with the username from the identity.
279
+
280
+ Returns
281
+ -------
282
+ Updated user object
283
+ """
284
+ original_user = self.get_user(user_id=user_id)
285
+
286
+ with self.uow:
287
+ users: SqlRepository[User] = getattr(
288
+ self.uow, self._users_repository_name
289
+ )
290
+ updated_user = users.update(original_user, user)
291
+ if identity:
292
+ updated_user.modified_by = identity.username
293
+ self.uow.commit()
294
+
295
+ return updated_user
296
+
297
+ def update_group(
298
+ self,
299
+ group_id: str | int | UUID,
300
+ group: Group,
301
+ identity: Identity | None = None,
302
+ ) -> Group:
303
+ """Updates an existing group object in the repository
304
+
305
+ Parameters
306
+ ----------
307
+ group
308
+ Group object with changes
309
+ group_id
310
+ The id of the group object
311
+ identity
312
+ The identity of the user making the changes, by default None. If
313
+ provided, the `modified_by` attribute of the group will be updated
314
+ with the username from the identity.
315
+
316
+ Returns
317
+ -------
318
+ Updated group object
319
+ """
320
+ original_group = self.get_group(group_id=group_id)
321
+
322
+ with self.uow:
323
+ groups: SqlRepository[Group] = getattr(
324
+ self.uow, self._groups_repository_name
325
+ )
326
+ updated_group = groups.update(original_group, group)
327
+ if identity:
328
+ updated_group.modified_by = identity.username
329
+ self.uow.commit()
330
+
331
+ return updated_group
alpha/utils/__init__.py CHANGED
@@ -5,6 +5,7 @@ from alpha.utils.logging_configurator import (
5
5
  GunicornLogger,
6
6
  )
7
7
  from alpha.utils.logging_level_checker import logging_level_checker
8
+ from alpha.utils.request_headers import Headers
8
9
  from alpha.utils.response_object import create_response_object
9
10
  from alpha.utils.verify_identity import verify_identity
10
11
  from alpha.utils.version_checker import minor_version_gte
@@ -18,4 +19,5 @@ __all__ = [
18
19
  "create_response_object",
19
20
  "verify_identity",
20
21
  "minor_version_gte",
22
+ "Headers",
21
23
  ]
@@ -0,0 +1,108 @@
1
+ from dataclasses import dataclass
2
+ from http import cookies
3
+ from typing import Mapping, Self
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class Headers:
8
+ """A dataclass representing the headers of an HTTP request. This is
9
+ primarily used for extracting authentication tokens and API keys from the
10
+ request headers and cookies.
11
+
12
+ This class provides a convenient way to access authentication tokens and
13
+ API keys, regardless of whether they are provided in the standard headers
14
+ or in cookies. It provides properties to check the presence of these tokens
15
+ and keys.
16
+
17
+ This class is designed to be immutable and uses slots for memory
18
+ efficiency.
19
+ """
20
+
21
+ auth_token: str | None = None
22
+ auth_token_type: str | None = None
23
+ refresh_token: str | None = None
24
+ api_key: str | None = None
25
+
26
+ @classmethod
27
+ def from_headers(
28
+ cls,
29
+ headers: Mapping[str, str],
30
+ auth_token_cookie_name: str = "auth_token",
31
+ refresh_token_cookie_name: str = "refresh_token",
32
+ api_key_cookie_name: str = "api_key",
33
+ ) -> Self:
34
+ """Create a Headers instance from a mapping of headers.
35
+
36
+ For the auth token, the method first checks the "Authorization" header.
37
+ If present, it extracts the token and its type (e.g., "Bearer"). If it
38
+ is not present, it looks for a cookie with the name specified by
39
+ `auth_token_cookie_name`. The same logic applies to the refresh token
40
+ and API key, checking the "X-Refresh-Token" and "X-API-Key" headers. If
41
+ they are not present, it looks for cookies with the names specified by
42
+ `refresh_token_cookie_name` and `api_key_cookie_name`.
43
+
44
+ Parameters
45
+ ----------
46
+ headers
47
+ A mapping of header names to their values.
48
+ auth_token_cookie_name
49
+ The name of the cookie containing the auth token, default is
50
+ "auth_token".
51
+ refresh_token_cookie_name
52
+ The name of the cookie containing the refresh token, default is
53
+ "refresh_token".
54
+ api_key_cookie_name
55
+ The name of the cookie containing the API key, default is
56
+ "api_key".
57
+
58
+ Returns
59
+ -------
60
+ An instance of the Headers class.
61
+ """
62
+ auth_token = None
63
+ auth_token_type = None
64
+ authorization_header = headers.get("Authorization")
65
+ if authorization_header is not None:
66
+ parts = authorization_header.split()
67
+ if len(parts) == 2:
68
+ auth_token_type, auth_token = parts
69
+ if auth_token_type.lower() == "bearer":
70
+ auth_token_type = "Bearer"
71
+
72
+ refresh_token = headers.get("X-Refresh-Token")
73
+ api_key = headers.get("X-API-Key")
74
+ cookies_header = headers.get("Cookie")
75
+
76
+ if cookies_header is not None:
77
+ cookie_jar = cookies.SimpleCookie(cookies_header)
78
+
79
+ if not auth_token and auth_token_cookie_name in cookie_jar:
80
+ auth_token = cookie_jar[auth_token_cookie_name].value
81
+ auth_token_type = "Bearer"
82
+ if not refresh_token and refresh_token_cookie_name in cookie_jar:
83
+ refresh_token = cookie_jar[refresh_token_cookie_name].value
84
+ if not api_key and api_key_cookie_name in cookie_jar:
85
+ api_key = cookie_jar[api_key_cookie_name].value
86
+
87
+ return cls(
88
+ auth_token=auth_token,
89
+ auth_token_type=auth_token_type,
90
+ refresh_token=refresh_token,
91
+ api_key=api_key,
92
+ )
93
+
94
+ @property
95
+ def has_auth_token(self) -> bool:
96
+ if not self.auth_token:
97
+ return False
98
+ if not self.auth_token_type:
99
+ return False
100
+ return self.auth_token_type.lower() == "bearer"
101
+
102
+ @property
103
+ def has_refresh_token(self) -> bool:
104
+ return True if self.refresh_token else False
105
+
106
+ @property
107
+ def has_api_key(self) -> bool:
108
+ return True if self.api_key else False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpha-python
3
- Version: 0.5.0
3
+ Version: 0.5.1
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
  Requires-Python: >=3.11
@@ -1,4 +1,4 @@
1
- alpha/__init__.py,sha256=f-klxGrJmRBDMWJ2KC_G42RaLR_gMxHJGrl5MdaGN30,4755
1
+ alpha/__init__.py,sha256=2Dk-KZu8cM84sFeePENcFnxb4-CHl9OR7qBbYSetvnw,4926
2
2
  alpha/cli.py,sha256=YdBvz_nqvC1c4kA-Xb1KtS-1Or6rbDu2VcF3ClatP6o,5657
3
3
  alpha/encoder.py,sha256=ZKPHnG4DT6azne3Sh9tNOuPO9s8xnjlr64CQ0e2abjI,1924
4
4
  alpha/exceptions.py,sha256=AHoFMPyHvjj6j_1X2TS40dSaFBzCDtgzAmucimSZjfc,5793
@@ -13,17 +13,17 @@ alpha/domain/models/__init__.py,sha256=ZBL9sp-DL8qVvD5QQHOqoAGGZD7TneuiW7A_JPYtQ
13
13
  alpha/domain/models/base_model.py,sha256=VzPDUdizaDAydvv3PfSqWwkUtzaBSKv1et7yPcKEL44,837
14
14
  alpha/domain/models/group.py,sha256=8UeZrXDScAlfYf7qz8PzjFqUNdZkI49_oPPsAD-rYWI,1108
15
15
  alpha/domain/models/life_cycle_base.py,sha256=XMLAFoCxUeItmx9owyAxzUHJHN19s40TQKk_ZEDhexQ,246
16
- alpha/domain/models/user.py,sha256=nNdC4YHBHwhbm894dO2Cxffd-thIlYhgP4azKkfaxPE,1997
16
+ alpha/domain/models/user.py,sha256=9YXDn2l0S7TjobaUKNJsqLqpz3v43ObUo0jMqZR6DEk,4783
17
17
  alpha/factories/__init__.py,sha256=baxoq0UY7UYust7NiR9wT954nJw5bfgSmOWvLjBt2C4,278
18
18
  alpha/factories/_type_conversion_matrix.py,sha256=mhMYpus6OFE0sZB0gQ-b1ndIOhiA1ScprvDa9tLANAM,5055
19
19
  alpha/factories/_type_mapping.py,sha256=f8cRfu8KUfw1ggY0Txs6fEX2e6GaXrsNcc2SCeYFRHM,789
20
20
  alpha/factories/class_factories.py,sha256=MDUnWQC1a3OUE0HyKEZx6watdMes8BjjoZQaOzBWfVo,15582
21
21
  alpha/factories/default_field_factory.py,sha256=ZvPU6BdrdELmZNXIeTmRp_YOCIscWFC9W6s05qXyXrk,1758
22
22
  alpha/factories/field_iterator.py,sha256=_7Z5UIdGEHhVV2JYA-e7ayJb7wnvo6KVRgXwykOCbgA,6212
23
- alpha/factories/jwt_factory.py,sha256=S6RSbFGpB2LOs1rz_k5uPnqb_8pQ_04BNwCHdZ_Fozg,4354
23
+ alpha/factories/jwt_factory.py,sha256=zNRmGu874AYOHvRPrNQd1p-TAQPRx1lPgxusiut1JhM,6407
24
24
  alpha/factories/logging_handler_factory.py,sha256=H9gVS1rNt5Jln3C_2EbOvJG-gqS9ksTsYUIx2UK7gVk,3059
25
25
  alpha/factories/model_class_factory.py,sha256=40eiLr8gYXpJfbbn1wPzcgsys9gU5x9nB2zlVTbgU7E,5704
26
- alpha/factories/password_factory.py,sha256=ptj5BMcN_dYM5b0DzqvIBOYx_LaugWTA8LxXYz9SCs0,4186
26
+ alpha/factories/password_factory.py,sha256=dmpcKUG-R4CI68GpW5sxlnbuP5T09uzzRYeXbMn-QWk,4187
27
27
  alpha/factories/request_factory.py,sha256=271gFfiDf5ZgtDcB0CQYG4ppbj-XuKic2ENWVNMrg2s,6704
28
28
  alpha/factories/response_factory.py,sha256=QFtOq2n7cYkBeLoz-mZdBtHkVsJiQ2rHNF6SdP_nz_E,6399
29
29
  alpha/factories/type_factories.py,sha256=_cBJQQtccnbq-fThP402J1YuI5vM0BlmSJWGyZi0MTU,6017
@@ -47,7 +47,7 @@ alpha/handlers/templates/python-flask/__init__model.mustache,sha256=JRwyJmOCAD8n
47
47
  alpha/handlers/templates/python-flask/__init__test.mustache,sha256=wUxDXcyE4rPh3sDp8MvR5m8vqzoU25Fw1bo_xh6InnY,438
48
48
  alpha/handlers/templates/python-flask/__main__.mustache,sha256=GetXHmF7BXxrwVajfkI6OjLQV7ekFxD9SPYHjZLNNWY,2393
49
49
  alpha/handlers/templates/python-flask/base_model.mustache,sha256=B--jl9LmfiHACq9n1kDxm6YTi6tb1oSUFnZya_IKOhU,2184
50
- alpha/handlers/templates/python-flask/controller.mustache,sha256=uGontq1CAmb2h0vsqrl6ZtpS8i_yr3aSzrVNg_Oi2qg,13053
50
+ alpha/handlers/templates/python-flask/controller.mustache,sha256=zGVY4a5dGKH4gEYT6D5bXiTy3DEqRPKLKDSI3wucr64,13395
51
51
  alpha/handlers/templates/python-flask/controller_test.mustache,sha256=RX_IG_eaC4rmI696jDUkDcOhPE9CgS2YmgqOBIEn7GM,2849
52
52
  alpha/handlers/templates/python-flask/dockerignore.mustache,sha256=hqlHuqIMLuuwvVqK6xhIcfaezKoChriR3D7TvxtkjGo,906
53
53
  alpha/handlers/templates/python-flask/encoder.mustache,sha256=03rsvhN_QdqaWsQC2v3772UvrLEXna9WcvxQsl6mwiU,866
@@ -89,7 +89,7 @@ alpha/interfaces/pydantic_instance.py,sha256=S96jLEWRmDyvYV3VqofSqjiN-lcjfkjiNp7
89
89
  alpha/interfaces/sql_database.py,sha256=H52NR1ti2j9G9zJsibPtKmXL7WdHOzeSyPXO4Zq9wlA,996
90
90
  alpha/interfaces/sql_mapper.py,sha256=NyPYc4DuG9HBd9LO8ooYidD-rv1poSty5GmzW6jmE8g,465
91
91
  alpha/interfaces/sql_repository.py,sha256=PS_nIK9Yej15OlSVdLm_JNDPzhMEuCTT0C98lznIqJ8,8767
92
- alpha/interfaces/token_factory.py,sha256=lzrqU7Sx5w9UxHbBp17y0lnPKmDqMVCp4Z6xMdWqpXg,1644
92
+ alpha/interfaces/token_factory.py,sha256=8leGt3jIGiYQfQBFQWWgscU9uI3N_P4bkOSAwh15oMg,1845
93
93
  alpha/interfaces/unit_of_work.py,sha256=yQ0Zexj3gVOzH7pjwF_9tvAKUPvzCielvgdzzCgLuZA,870
94
94
  alpha/interfaces/updateable.py,sha256=7Vq6kzNa8hyTVk_vzjUdM8hUyRWclHt7L3erRlo7L2U,173
95
95
  alpha/mixins/__init__.py,sha256=4In0NQdfIFB8U-E4dzxHOJFJGUk6-1lQ_eJ7C1fKnvk,87
@@ -101,30 +101,32 @@ alpha/providers/ldap_provider.py,sha256=r6T8cMVrN1oMAw_a494n8P7ZmHQkNLVmcOwk4lCh
101
101
  alpha/providers/oidc_provider.py,sha256=sAu4kMepJ2YRao3d4LZo3-nX86k3Sn9cukQ7nEeCFhg,12717
102
102
  alpha/providers/models/__init__.py,sha256=SI-qTA3JMOxXx4o1h7frk8x3PRj0g_GSCEbLzyvw7CI,409
103
103
  alpha/providers/models/credentials.py,sha256=_nq60FcPVcWfRQ6UmK1Ug4ey6f-MWXGwKWHdIct2q9M,476
104
- alpha/providers/models/identity.py,sha256=L_BlFHlXNZaQ1DoQ_H1tI7ZXBzxOp_gWS2e5o9l2myw,14817
104
+ alpha/providers/models/identity.py,sha256=KostRfSm3vc0kTokdqPVdUZyBgEcNYK9hfpU_pLg2FQ,14825
105
105
  alpha/providers/models/token.py,sha256=jWFQSuXsPuy2xNpJRwpdzpOTtiMJ1xfUelQhQTxmykU,1698
106
106
  alpha/repositories/__init__.py,sha256=qN7iefBr93_fXo8cTn_RUFaz8PPhsUlzojKbYFM2m-c,306
107
107
  alpha/repositories/rest_api_repository.py,sha256=RWGa5KxvOAiS_g5WIx_qLTPAUd5HPtKgAY2CX3-W-t0,46019
108
108
  alpha/repositories/sql_alchemy_repository.py,sha256=k4WeK15w4a0XZ360UoUMtflLamGX8ycbySrxFpPwcio,19912
109
109
  alpha/repositories/models/__init__.py,sha256=PY3kFG0z5r5ALcwBFXVNO9GAVNn1tVTkvUp8XoZEMuY,109
110
- alpha/repositories/models/repository_model.py,sha256=IMkYC_u9m8R_BJi_7cYSZeau0jTriO5lPkWX_8VLavw,1031
111
- alpha/services/__init__.py,sha256=_S7-3YxzxXtqAgcxol_RnhjBOgEMcFaGECkqwuWyeLQ,116
112
- alpha/services/authentication_service.py,sha256=Bm1JGXPYq9_MWbSgj5pErzpp5rYrsLCQCT2cN8xMCzo,28978
110
+ alpha/repositories/models/repository_model.py,sha256=WpfW90HpDvwDK5jcqNcprIKugqxcMaQWoqEFEgr0FoA,1038
111
+ alpha/services/__init__.py,sha256=yeUbS-N0127DSejbCAv9jbdKuHRa_H5Ue0CtsWf4ghI,224
112
+ alpha/services/authentication_service.py,sha256=61x17xNI_D0MFhxf6D1ME0ceSfqfkn28bF6U8tDlQ5Y,29694
113
+ alpha/services/user_lifecycle_management.py,sha256=OHrzmRJ79yrYWB55Vp_9DDAM9V__nCUpgKCF5PwVT4E,10411
113
114
  alpha/services/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
115
  alpha/services/models/cookie.py,sha256=V0BTBG_AsvJEGoBv5_6n4LYWt36YJfGT2MVkTvWfxTU,1654
115
- alpha/utils/__init__.py,sha256=0ee9hUogCyBKBlaeQMrRzr_qf1noFm-PxfBx4tyKSZk,635
116
+ alpha/utils/__init__.py,sha256=QHentkiIMO_schZQ1k7XoTwZLfdwxliKlHExm51sMuc,698
116
117
  alpha/utils/_http_codes.py,sha256=QLU43JnjvYGsv6u_quRP7jzgg996boa4-a-yKcSw9Bo,6558
117
118
  alpha/utils/is_attrs.py,sha256=LlLozdSnKEjFtVwJm9AWf7NAIkBPotnpbhFmwz3f4AI,432
118
119
  alpha/utils/is_pydantic.py,sha256=qFxiR8UsHW612D_0e33ghtSZ4xDG-chhY6JU56lSzrg,635
119
120
  alpha/utils/logging_configurator.py,sha256=gnN1mlV9sPnSrWvR8jrGuBrc8pYWeQ2cyTpzCkWDOFM,3741
120
121
  alpha/utils/logging_level_checker.py,sha256=x3MrwV0aqmtd-YOb5VxZzNm6MKsX3YtaVxUQ6dT0E4E,669
122
+ alpha/utils/request_headers.py,sha256=ETaYLFuPdAYiFHBeb9HFSrpqM3lZsxueFwHqYOq_-wU,3993
121
123
  alpha/utils/response_object.py,sha256=AomSsUGHNjH-evtcFDlUbR56-B676Fs8hb94dhKELyY,4948
122
124
  alpha/utils/secret_generator.py,sha256=SOzhyC3e3epSLCpc-7TEKntmdsrNoU6SRD2mIxwlGe0,401
123
125
  alpha/utils/verify_identity.py,sha256=tnh9JAez8FlrqN4Bqh1bfTdnEOM2VA1ZAVLfZCqI-ZU,2229
124
126
  alpha/utils/version_checker.py,sha256=W8v69hDbrr0VvJ40gpFmXMtVawrDmUz_YGWssKUTmVE,380
125
- alpha_python-0.5.0.dist-info/licenses/LICENSE,sha256=5KwEqC3KUoH4lVXgZ9tGriKOl-LGxHkXBWo16mFmAYM,1070
126
- alpha_python-0.5.0.dist-info/METADATA,sha256=RUwU7vjZOMr6NkUhqVIhKQ_JkK0GJ0Bo3-qPedzjBnE,3783
127
- alpha_python-0.5.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
128
- alpha_python-0.5.0.dist-info/entry_points.txt,sha256=LBEXdcofOugYYdZ46nz5Dxj_aj1QbRBkumfPGhy-GXI,41
129
- alpha_python-0.5.0.dist-info/top_level.txt,sha256=tqmNnOmi2RSSiPo99C03fD5Cc3r9za9xTjPAoQC1EGA,6
130
- alpha_python-0.5.0.dist-info/RECORD,,
127
+ alpha_python-0.5.1.dist-info/licenses/LICENSE,sha256=5KwEqC3KUoH4lVXgZ9tGriKOl-LGxHkXBWo16mFmAYM,1070
128
+ alpha_python-0.5.1.dist-info/METADATA,sha256=Gw4-1tWzAB-lx_hBAxLryd6Lt2kGoDQXvZdd6DTfWo4,3783
129
+ alpha_python-0.5.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
130
+ alpha_python-0.5.1.dist-info/entry_points.txt,sha256=LBEXdcofOugYYdZ46nz5Dxj_aj1QbRBkumfPGhy-GXI,41
131
+ alpha_python-0.5.1.dist-info/top_level.txt,sha256=tqmNnOmi2RSSiPo99C03fD5Cc3r9za9xTjPAoQC1EGA,6
132
+ alpha_python-0.5.1.dist-info/RECORD,,