alpha-python 0.6.2__py3-none-any.whl → 0.7.0__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
@@ -64,6 +64,18 @@ from alpha.providers.oidc_provider import (
64
64
  from alpha.repositories.models.repository_model import RepositoryModel
65
65
  from alpha.repositories.rest_api_repository import RestApiRepository
66
66
  from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
67
+ from alpha.repositories.refresh.cache_repository import (
68
+ CacheRefreshRepository,
69
+ )
70
+ from alpha.repositories.refresh.database_repository import (
71
+ DatabaseRefreshRepository,
72
+ )
73
+ from alpha.repositories.refresh.file_repository import (
74
+ FileRefreshRepository,
75
+ )
76
+ from alpha.repositories.refresh.memory_repository import (
77
+ MemoryRefreshRepository,
78
+ )
67
79
  from alpha.services.authentication_service import AuthenticationService
68
80
  from alpha.services.user_lifecycle_management import UserLifecycleManagement
69
81
  from alpha.utils.is_attrs import is_attrs
@@ -156,6 +168,10 @@ __all__ = [
156
168
  "RepositoryModel",
157
169
  "RestApiRepository",
158
170
  "SqlAlchemyRepository",
171
+ "CacheRefreshRepository",
172
+ "DatabaseRefreshRepository",
173
+ "FileRefreshRepository",
174
+ "MemoryRefreshRepository",
159
175
  "AuthenticationService",
160
176
  "UserLifecycleManagement",
161
177
  "is_attrs",
alpha/encoder.py CHANGED
@@ -4,6 +4,7 @@ complex types into JSON format.
4
4
 
5
5
  import json
6
6
  from dataclasses import asdict, is_dataclass
7
+ from attrs import asdict as attrs_asdict
7
8
  from datetime import date, datetime, time
8
9
  from enum import Enum
9
10
  from json import encoder
@@ -14,8 +15,9 @@ import numpy as np # type: ignore[import-untyped]
14
15
  import pandas as pd # type: ignore[import-untyped]
15
16
  import six # type: ignore[import-untyped]
16
17
 
17
-
18
18
  from alpha.interfaces.openapi_model import OpenAPIModel
19
+ from alpha.utils.is_attrs import is_attrs
20
+ from alpha.utils.is_pydantic import is_pydantic
19
21
 
20
22
 
21
23
  class JSONEncoder(encoder.JSONEncoder):
@@ -72,11 +74,15 @@ class JSONEncoder(encoder.JSONEncoder):
72
74
  return o.isoformat()
73
75
  if isinstance(o, time):
74
76
  return o.isoformat()
77
+ if isinstance(o, type):
78
+ cls = getattr(o, "__name__", None)
79
+ return cls if cls is not None else str(o)
75
80
  if is_dataclass(o):
76
- if isinstance(o, type):
77
- cls = getattr(o, "__class__")
78
- return cls.__name__
79
- return asdict(o)
81
+ return asdict(o) # type: ignore
82
+ if is_attrs(o):
83
+ return attrs_asdict(o)
84
+ if is_pydantic(o):
85
+ return o.model_dump()
80
86
 
81
87
  try:
82
88
  return json.JSONEncoder.default(self, o)
@@ -3,12 +3,14 @@
3
3
  import json
4
4
  from dataclasses import MISSING, is_dataclass
5
5
  from enum import Enum
6
- from typing import Any, Iterable, get_args, get_origin
6
+ from typing import Any, Sequence, get_args, get_origin
7
7
 
8
8
  from alpha import exceptions
9
9
  from alpha.encoder import JSONEncoder
10
+ from alpha.interfaces.attrs_instance import AttrsInstance
10
11
  from alpha.interfaces.dataclass_instance import DataclassInstance
11
12
  from alpha.interfaces.openapi_model import OpenAPIModel
13
+ from alpha.interfaces.pydantic_instance import PydanticInstance
12
14
  from alpha.utils.is_attrs import is_attrs
13
15
  from alpha.utils.is_pydantic import is_pydantic
14
16
 
@@ -18,8 +20,13 @@ class ResponseFactory:
18
20
 
19
21
  def process(
20
22
  self,
21
- response: DataclassInstance | Iterable[DataclassInstance],
22
- cls: OpenAPIModel | Iterable[OpenAPIModel],
23
+ response: DataclassInstance
24
+ | Sequence[DataclassInstance]
25
+ | AttrsInstance
26
+ | Sequence[AttrsInstance]
27
+ | PydanticInstance
28
+ | Sequence[PydanticInstance],
29
+ cls: OpenAPIModel | Sequence[OpenAPIModel],
23
30
  ) -> object:
24
31
  """Mapping a dataclass instance or a collection of instances to an
25
32
  OpenAPI model
@@ -54,7 +61,7 @@ class ResponseFactory:
54
61
 
55
62
  # When the source instance and target class are of an iterable type
56
63
  if cls_origin in [list, tuple, set]:
57
- if isinstance(response, Iterable):
64
+ if isinstance(response, Sequence):
58
65
  arg = get_args(cls)[0]
59
66
  return [
60
67
  self.process(response=obj, cls=arg) for obj in response
@@ -12,6 +12,7 @@ from typing import List
12
12
  from alpha.factories.request_factory import RequestFactory
13
13
  from alpha.factories.response_factory import ResponseFactory
14
14
  from alpha.encoder import JSONEncoder
15
+ from alpha.providers.models.identity import Identity
15
16
  from alpha.utils.logging_level_checker import logging_level_checker
16
17
  from alpha.utils.response_object import create_response_object
17
18
  from alpha.utils.request_headers import Headers
@@ -227,7 +228,8 @@ def {{operationId}}(
227
228
  token_factory.validate(auth_token)
228
229
 
229
230
  # Get auth token payload and verify identity
230
- identity = token_factory.get_payload(auth_token)
231
+ payload = token_factory.get_payload(auth_token)
232
+ identity = Identity.from_dict(payload)
231
233
  verify_identity(identity,
232
234
  roles=roles,
233
235
  groups=groups,
@@ -307,8 +309,9 @@ def {{operationId}}(
307
309
  status_code={{code}},
308
310
  status_message='{{message}}',
309
311
  data=result,
310
- {{#vendorExtensions.x-content-type}}data_type='{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}
311
- {{#vendorExtensions.x-alpha-cookie-support}}response_type='flask'{{/vendorExtensions.x-alpha-cookie-support}}
312
+ accept_header={{#vendorExtensions.x-content-type}}'{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}{{^vendorExtensions.x-content-type}}connexion.request.headers.get("Accept", None){{/vendorExtensions.x-content-type}},
313
+ supported_accept_headers=[{{#produces}}'{{mediaType}}',{{/produces}}{{^produces}}{{#vendorExtensions.x-preferred-produce}}'{{mediaType}}',{{/vendorExtensions.x-preferred-produce}}{{/produces}}],
314
+ {{#vendorExtensions.x-alpha-cookie-support}}response_format='flask'{{/vendorExtensions.x-alpha-cookie-support}}
312
315
  )
313
316
  {{/returnType}}
314
317
  {{^returnType}}
@@ -316,19 +319,20 @@ def {{operationId}}(
316
319
  http_codes=http_codes,
317
320
  status_code=204,
318
321
  status_message='{{message}}',
319
- {{#vendorExtensions.x-alpha-cookie-support}}data=result, response_type='flask'{{/vendorExtensions.x-alpha-cookie-support}}
322
+ {{#vendorExtensions.x-alpha-cookie-support}}data=result, response_format='flask'{{/vendorExtensions.x-alpha-cookie-support}}
320
323
  )
321
324
  {{/returnType}}
322
325
  {{/vendorExtensions.x-alpha-raw-response}}
323
326
  {{/is2xx}}
324
-
325
327
  {{#is4xx}}
326
328
  {{#vendorExtensions.x-alpha-exception}}
327
329
  except {{vendorExtensions.x-alpha-exception}} as exc:
328
330
  return response_object_function(
329
331
  http_codes=http_codes,
330
332
  status_code={{code}},
331
- status_message=f'{exc}'
333
+ status_message=f'{exc}',
334
+ accept_header={{#vendorExtensions.x-content-type}}'{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}{{^vendorExtensions.x-content-type}}connexion.request.headers.get("Accept", None){{/vendorExtensions.x-content-type}},
335
+ supported_accept_headers=[{{#produces}}'{{mediaType}}',{{/produces}}{{^produces}}{{#vendorExtensions.x-preferred-produce}}'{{mediaType}}',{{/vendorExtensions.x-preferred-produce}}{{/produces}}],
332
336
  )
333
337
  {{/vendorExtensions.x-alpha-exception}}
334
338
  {{/is4xx}}
@@ -339,6 +343,8 @@ def {{operationId}}(
339
343
  http_codes=http_codes,
340
344
  status_code={{code}},
341
345
  status_message=f'{exc}'
346
+ accept_header={{#vendorExtensions.x-content-type}}'{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}{{^vendorExtensions.x-content-type}}connexion.request.headers.get("Accept", None){{/vendorExtensions.x-content-type}},
347
+ supported_accept_headers=[{{#produces}}'{{mediaType}}',{{/produces}}{{^produces}}{{#vendorExtensions.x-preferred-produce}}'{{mediaType}}',{{/vendorExtensions.x-preferred-produce}}{{/produces}}],
342
348
  )
343
349
  {{/vendorExtensions.x-alpha-exception}}
344
350
  {{/is5xx}}
@@ -31,6 +31,9 @@ class LDAPConnector:
31
31
  server_port: int = 636,
32
32
  use_tls: bool = True,
33
33
  client_strategy: ClientStrategyType = SYNC,
34
+ connect_timeout: float | None = 5.0,
35
+ additional_connector_params: dict[str, Any] | None = None,
36
+ additional_server_params: dict[str, Any] | None = None,
34
37
  ) -> None:
35
38
  """
36
39
  Parameters
@@ -59,12 +62,29 @@ class LDAPConnector:
59
62
  - 'MOCK_SYNC': Mock synchronous strategy.
60
63
  - 'MOCK_ASYNC': Mock asynchronous strategy.
61
64
  - 'ASYNC_STREAM': Asynchronous stream strategy.
65
+ connect_timeout
66
+ Maximum number of seconds to wait while opening the socket.
67
+ additional_connector_params
68
+ Additional parameters to pass to the LDAP connection, by default
69
+ {"receive_timeout": 5}
70
+ additional_server_params
71
+ Additional parameters to pass to the LDAP server, by default None
62
72
  """
63
73
  self._server_url = server_url
64
74
  self._bind_dn = bind_dn
65
75
  self._bind_password = bind_password
66
76
  self._client_strategy = client_strategy
67
-
77
+ self._connect_timeout = connect_timeout
78
+ self._additional_connector_params: dict[str, Any] = (
79
+ {"receive_timeout": 5}
80
+ if additional_connector_params is None
81
+ else dict(additional_connector_params)
82
+ )
83
+ self._additional_server_params: dict[str, Any] = (
84
+ {}
85
+ if additional_server_params is None
86
+ else dict(additional_server_params)
87
+ )
68
88
  tls = None
69
89
  if use_tls:
70
90
  tls = Tls(
@@ -78,6 +98,8 @@ class LDAPConnector:
78
98
  use_ssl=use_tls,
79
99
  tls=tls,
80
100
  get_info=ALL,
101
+ connect_timeout=self._connect_timeout,
102
+ **self._additional_server_params,
81
103
  )
82
104
  self._connection: Connection | None = None
83
105
 
@@ -97,8 +119,9 @@ class LDAPConnector:
97
119
  self._server,
98
120
  user=self._bind_dn,
99
121
  password=self._bind_password,
100
- auto_bind=True,
101
122
  client_strategy=self._client_strategy, # type: ignore
123
+ auto_bind=True,
124
+ **self._additional_connector_params,
102
125
  )
103
126
 
104
127
  def disconnect(self) -> None:
@@ -9,6 +9,7 @@ from alpha.interfaces.patchable import Patchable
9
9
  # import all repository related interfaces
10
10
  from alpha.interfaces.api_repository import ApiRepository
11
11
  from alpha.interfaces.sql_repository import SqlRepository
12
+ from alpha.interfaces.refresh_repository import RefreshRepository
12
13
 
13
14
  # import all database related interfaces
14
15
  from alpha.interfaces.sql_mapper import SqlMapper
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "Patchable",
36
37
  "ApiRepository",
37
38
  "SqlRepository",
39
+ "RefreshRepository",
38
40
  "SqlMapper",
39
41
  "SqlDatabase",
40
42
  "UnitOfWork",
@@ -0,0 +1,26 @@
1
+ from typing import Protocol, Any, runtime_checkable
2
+
3
+
4
+ @runtime_checkable
5
+ class HTTPClient(Protocol):
6
+ """Interface for HTTP clients like requests, httpx or a custom
7
+ implementation.
8
+
9
+ This interface is compatible with the popular synchronous HTTP client
10
+ libraries, for example, the `requests` library, `httpx` library or any
11
+ custom implementation that follows the same method signatures.
12
+
13
+ This interface defines the methods that an HTTP client should implement to
14
+ be compatible with the REST API repository. It includes methods for making
15
+ HTTP requests (POST, GET, DELETE, PUT, PATCH) and allows for additional
16
+ parameters to be passed as needed.
17
+ """
18
+
19
+ cookies: Any
20
+ headers: Any
21
+
22
+ def post(self, url: str, json: Any = None, **kwargs: Any) -> Any: ...
23
+ def get(self, url: str, **kwargs: Any) -> Any: ...
24
+ def delete(self, url: str, **kwargs: Any) -> Any: ...
25
+ def put(self, url: str, json: Any = None, **kwargs: Any) -> Any: ...
26
+ def patch(self, url: str, json: Any = None, **kwargs: Any) -> Any: ...
@@ -1,11 +1,15 @@
1
1
  """This module contains interfaces for various types of identity providers."""
2
2
 
3
- from typing import ClassVar, Protocol, runtime_checkable
3
+ from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable, Any
4
4
 
5
5
  from alpha.interfaces.token_factory import TokenFactory
6
6
  from alpha.providers.models.credentials import PasswordCredentials
7
7
  from alpha.providers.models.identity import Identity
8
- from alpha.providers.models.token import Token
8
+
9
+ if TYPE_CHECKING:
10
+ from alpha.providers.models.token import Token
11
+ else:
12
+ Token = Any
9
13
 
10
14
 
11
15
  @runtime_checkable
@@ -0,0 +1,60 @@
1
+ from typing import TYPE_CHECKING, Protocol, Any
2
+
3
+ if TYPE_CHECKING:
4
+ from alpha.providers.models.token import Token
5
+ else:
6
+ Token = Any
7
+
8
+
9
+ class RefreshRepository(Protocol):
10
+ """Repository interface for managing refresh tokens."""
11
+
12
+ def get(self, token: str) -> Token:
13
+ """Get a token by its value.
14
+
15
+ Parameters
16
+ ----------
17
+ token
18
+ Token value.
19
+
20
+ Returns
21
+ -------
22
+ Token
23
+ Token object with the given token value.
24
+ """
25
+ ...
26
+
27
+ def create(self, subject: str) -> Token:
28
+ """Create a new token for a given subject.
29
+
30
+ Parameters
31
+ ----------
32
+ subject
33
+ Subject identifier
34
+
35
+ Returns
36
+ -------
37
+ Token
38
+ Newly created token object.
39
+ """
40
+ ...
41
+
42
+ def delete(self, token: str) -> None:
43
+ """Delete a token by its value.
44
+
45
+ Parameters
46
+ ----------
47
+ token
48
+ Token value.
49
+ """
50
+ ...
51
+
52
+ def delete_all(self, subject: str) -> None:
53
+ """Delete all tokens for a given subject.
54
+
55
+ Parameters
56
+ ----------
57
+ subject
58
+ Subject identifier.
59
+ """
60
+ ...
@@ -1,9 +1,10 @@
1
1
  from uuid import UUID
2
- from datetime import datetime
2
+ from datetime import datetime, timedelta, timezone
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, Literal
5
5
 
6
6
  from alpha.domain.models.base_model import BaseDomainModel
7
+ from alpha.utils.secret_generator import generate_secret
7
8
 
8
9
 
9
10
  @dataclass
@@ -79,6 +80,40 @@ class Token(BaseDomainModel):
79
80
  ),
80
81
  )
81
82
 
83
+ @classmethod
84
+ def create_refresh(
85
+ cls,
86
+ subject: str,
87
+ max_age_seconds: int = 7 * 24 * 3600,
88
+ token_length: int = 32,
89
+ ) -> "Token":
90
+ """Factory method to create a new Refresh Token instance.
91
+
92
+ Parameters
93
+ ----------
94
+ subject
95
+ The subject or user associated with the token.
96
+ max_age_seconds, optional
97
+ Optional maximum age of the token in seconds. Defaults to 7 days.
98
+ If provided, the expires_at will be set to created_at +
99
+ max_age_seconds.
100
+ token_length, optional
101
+ Optional length of the token value. Defaults to 32.
102
+
103
+ Returns
104
+ -------
105
+ Token
106
+ A new Refresh Token instance with the provided attributes and generated id and created_at.
107
+ """
108
+ return cls(
109
+ value=generate_secret(token_length),
110
+ token_type="Refresh",
111
+ subject=subject,
112
+ created_at=datetime.now(tz=timezone.utc),
113
+ expires_at=datetime.now(tz=timezone.utc)
114
+ + timedelta(seconds=max_age_seconds),
115
+ )
116
+
82
117
  def to_dict(self) -> dict[str, str | None]:
83
118
  """Converts the Token instance to a dictionary.
84
119
 
@@ -97,15 +132,18 @@ class Token(BaseDomainModel):
97
132
  - "expires_at": ISO format datetime string for when the token
98
133
  expires or None.
99
134
  """
100
- return {
101
- "id": str(self.id) if self.id else None,
135
+
136
+ obj: dict[str, str | None] = {
102
137
  "value": self.value,
103
138
  "subject": self.subject,
104
139
  "token_type": self.token_type,
105
- "created_at": (
106
- self.created_at.isoformat() if self.created_at else None
107
- ),
108
- "expires_at": (
109
- self.expires_at.isoformat() if self.expires_at else None
110
- ),
111
140
  }
141
+
142
+ if self.id:
143
+ obj["id"] = str(self.id)
144
+ if self.created_at:
145
+ obj["created_at"] = self.created_at.isoformat()
146
+ if self.expires_at:
147
+ obj["expires_at"] = self.expires_at.isoformat()
148
+
149
+ return obj
@@ -1,4 +1,16 @@
1
1
  from alpha.repositories.models.repository_model import RepositoryModel
2
+ from alpha.repositories.refresh.cache_repository import (
3
+ CacheRefreshRepository,
4
+ )
5
+ from alpha.repositories.refresh.database_repository import (
6
+ DatabaseRefreshRepository,
7
+ )
8
+ from alpha.repositories.refresh.file_repository import (
9
+ FileRefreshRepository,
10
+ )
11
+ from alpha.repositories.refresh.memory_repository import (
12
+ MemoryRefreshRepository,
13
+ )
2
14
  from alpha.repositories.rest_api_repository import RestApiRepository
3
15
  from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
4
16
 
@@ -6,4 +18,8 @@ __all__ = [
6
18
  "RepositoryModel",
7
19
  "RestApiRepository",
8
20
  "SqlAlchemyRepository",
21
+ "CacheRefreshRepository",
22
+ "DatabaseRefreshRepository",
23
+ "FileRefreshRepository",
24
+ "MemoryRefreshRepository",
9
25
  ]
@@ -0,0 +1,19 @@
1
+ from alpha.repositories.refresh.cache_repository import (
2
+ CacheRefreshRepository,
3
+ )
4
+ from alpha.repositories.refresh.database_repository import (
5
+ DatabaseRefreshRepository,
6
+ )
7
+ from alpha.repositories.refresh.file_repository import (
8
+ FileRefreshRepository,
9
+ )
10
+ from alpha.repositories.refresh.memory_repository import (
11
+ MemoryRefreshRepository,
12
+ )
13
+
14
+ __all__ = [
15
+ "CacheRefreshRepository",
16
+ "DatabaseRefreshRepository",
17
+ "FileRefreshRepository",
18
+ "MemoryRefreshRepository",
19
+ ]
@@ -0,0 +1,54 @@
1
+ from typing import Any
2
+
3
+ from alpha.providers.models.token import Token
4
+
5
+
6
+ class CacheRefreshRepository:
7
+ def __init__(
8
+ self,
9
+ cache_connector: Any,
10
+ token_model: type[Token] = Token,
11
+ token_max_age_seconds: int = 7 * 24 * 3600,
12
+ token_length: int = 32,
13
+ ):
14
+ """Initialize the CacheRefreshRepository with the given cache connector.
15
+
16
+ Parameters
17
+ ----------
18
+ cache_connector
19
+ The cache connector instance to use for cache operations.
20
+ token_model, optional
21
+ The model class for tokens, by default Token. The model class
22
+ should have a `from_dict` class method that takes a dictionary and
23
+ returns an instance of the model. The dictionary will have the same
24
+ structure as the token data in the JSON file. The model class
25
+ should also have a `to_dict` method that converts an instance of
26
+ the model to a dictionary with the same structure as the token data
27
+ in the JSON file. The model class should also have a
28
+ `create_refresh` class method that creates a new refresh token.
29
+ token_max_age_seconds, optional
30
+ The maximum age of a token in seconds, by default the equivalent of
31
+ 7 days in seconds
32
+ token_length, optional
33
+ The length of the generated token string, by default 32 characters
34
+ """
35
+ self._cache_connector = cache_connector
36
+ self._token_model = token_model
37
+ self._token_max_age_seconds = token_max_age_seconds
38
+ self._token_length = token_length
39
+
40
+ def get(self, token: str) -> Token:
41
+ """Get a token by its value."""
42
+ raise NotImplementedError("Method not implemented yet.")
43
+
44
+ def create(self, subject: str) -> Token:
45
+ """Create a new token for a given subject."""
46
+ raise NotImplementedError("Method not implemented yet.")
47
+
48
+ def delete(self, token: str) -> None:
49
+ """Delete a token by its value."""
50
+ raise NotImplementedError("Method not implemented yet.")
51
+
52
+ def delete_all(self, subject: str) -> None:
53
+ """Delete all tokens for a given subject."""
54
+ raise NotImplementedError("Method not implemented yet.")
@@ -0,0 +1,145 @@
1
+ from alpha import exceptions
2
+ from alpha.infra.connectors.sql_alchemy import SqlAlchemyDatabase
3
+ from alpha.providers.models.token import Token
4
+
5
+
6
+ class DatabaseRefreshRepository:
7
+ """Implementation of the RefreshRepository interface for database
8
+ operations.
9
+
10
+ This repository uses a SQLAlchemy database connector to manage refresh
11
+ tokens in a database. It provides methods to get, create, delete, and
12
+ delete all refresh tokens for a given subject. The tokens are stored in a
13
+ database table which is mapped to the token model.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ database_connector: SqlAlchemyDatabase,
19
+ token_model: type[Token] = Token,
20
+ token_max_age_seconds: int = 7 * 24 * 3600,
21
+ token_length: int = 32,
22
+ ):
23
+ """Initialize the DatabaseRefreshRepository with the given database
24
+ connector and token model.
25
+
26
+ Parameters
27
+ ----------
28
+ database_connector
29
+ The database connector instance to use for database operations.
30
+ token_model, optional
31
+ The model class for tokens, by default Token. The model class
32
+ should have a `from_dict` class method that takes a dictionary and
33
+ returns an instance of the model. The dictionary will have the same
34
+ structure as the token data in the JSON file. The model class
35
+ should also have a `to_dict` method that converts an instance of
36
+ the model to a dictionary with the same structure as the token data
37
+ in the JSON file. The model class should also have a
38
+ `create_refresh` class method that creates a new refresh token.
39
+ token_max_age_seconds, optional
40
+ The maximum age of a token in seconds, by default the equivalent of
41
+ 7 days in seconds
42
+ token_length, optional
43
+ The length of the generated token string, by default 32 characters
44
+ """
45
+ self._database_connector = database_connector
46
+ self._token_model = token_model
47
+ self._token_max_age_seconds = token_max_age_seconds
48
+ self._token_length = token_length
49
+
50
+ def get(self, token: str) -> Token:
51
+ """Get a token by its value.
52
+
53
+ Parameters
54
+ ----------
55
+ token
56
+ The value of the token to retrieve.
57
+
58
+ Returns
59
+ -------
60
+ Token
61
+ The token object corresponding to the given value.
62
+
63
+ Raises
64
+ ------
65
+ NotFoundException
66
+ If the token is not found in the database.
67
+ """
68
+ with self._database_connector.get_session() as session:
69
+ result = (
70
+ session.query(self._token_model)
71
+ .filter_by(value=token)
72
+ .one_or_none()
73
+ )
74
+
75
+ if result is None:
76
+ raise exceptions.NotFoundException("Refresh token not found")
77
+
78
+ return result
79
+
80
+ def create(self, subject: str) -> Token:
81
+ """Create a new token for a given subject.
82
+
83
+ Parameters
84
+ ----------
85
+ subject
86
+ The subject for which to create the token.
87
+
88
+ Returns
89
+ -------
90
+ Token
91
+ The newly created token object.
92
+ """
93
+ token = self._token_model.create_refresh(
94
+ subject=subject,
95
+ max_age_seconds=self._token_max_age_seconds,
96
+ token_length=self._token_length,
97
+ )
98
+
99
+ with self._database_connector.get_session() as session:
100
+ session.add(token)
101
+ session.commit()
102
+ session.refresh(token)
103
+ return token
104
+
105
+ def delete(self, token: str) -> None:
106
+ """Delete a token by its value.
107
+
108
+ Parameters
109
+ ----------
110
+ token
111
+ The value of the token to delete.
112
+
113
+ Raises
114
+ ------
115
+ NotFoundException
116
+ If the token is not found in the database.
117
+ """
118
+ with self._database_connector.get_session() as session:
119
+ token_obj = (
120
+ session.query(self._token_model)
121
+ .filter_by(value=token)
122
+ .one_or_none()
123
+ )
124
+ if token_obj is None:
125
+ raise exceptions.NotFoundException("Refresh token not found")
126
+ session.delete(token_obj)
127
+ session.commit()
128
+
129
+ def delete_all(self, subject: str) -> None:
130
+ """Delete all tokens for a given subject.
131
+
132
+ Parameters
133
+ ----------
134
+ subject
135
+ The subject for which to delete all tokens.
136
+ """
137
+ with self._database_connector.get_session() as session:
138
+ tokens = (
139
+ session.query(self._token_model)
140
+ .filter_by(subject=subject)
141
+ .all()
142
+ )
143
+ for token in tokens:
144
+ session.delete(token)
145
+ session.commit()