alpha-python 0.6.3__py3-none-any.whl → 0.7.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
@@ -30,6 +30,7 @@ from alpha.interfaces.pydantic_instance import PydanticInstance
30
30
  from alpha.interfaces.openapi_model import OpenAPIModel
31
31
  from alpha.interfaces.updatable import Updatable
32
32
  from alpha.interfaces.patchable import Patchable
33
+ from alpha.interfaces.http_client import HTTPClient, HTTPResponse
33
34
  from alpha.interfaces.api_repository import ApiRepository
34
35
  from alpha.interfaces.sql_repository import SqlRepository
35
36
  from alpha.interfaces.sql_mapper import SqlMapper
@@ -64,6 +65,18 @@ from alpha.providers.oidc_provider import (
64
65
  from alpha.repositories.models.repository_model import RepositoryModel
65
66
  from alpha.repositories.rest_api_repository import RestApiRepository
66
67
  from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
68
+ from alpha.repositories.refresh.cache_repository import (
69
+ CacheRefreshRepository,
70
+ )
71
+ from alpha.repositories.refresh.database_repository import (
72
+ DatabaseRefreshRepository,
73
+ )
74
+ from alpha.repositories.refresh.file_repository import (
75
+ FileRefreshRepository,
76
+ )
77
+ from alpha.repositories.refresh.memory_repository import (
78
+ MemoryRefreshRepository,
79
+ )
67
80
  from alpha.services.authentication_service import AuthenticationService
68
81
  from alpha.services.user_lifecycle_management import UserLifecycleManagement
69
82
  from alpha.utils.is_attrs import is_attrs
@@ -128,6 +141,8 @@ __all__ = [
128
141
  "OpenAPIModel",
129
142
  "Updatable",
130
143
  "Patchable",
144
+ "HTTPClient",
145
+ "HTTPResponse",
131
146
  "ApiRepository",
132
147
  "SqlRepository",
133
148
  "SqlMapper",
@@ -156,6 +171,10 @@ __all__ = [
156
171
  "RepositoryModel",
157
172
  "RestApiRepository",
158
173
  "SqlAlchemyRepository",
174
+ "CacheRefreshRepository",
175
+ "DatabaseRefreshRepository",
176
+ "FileRefreshRepository",
177
+ "MemoryRefreshRepository",
159
178
  "AuthenticationService",
160
179
  "UserLifecycleManagement",
161
180
  "is_attrs",
@@ -6,9 +6,13 @@ from alpha.interfaces.openapi_model import OpenAPIModel
6
6
  from alpha.interfaces.updatable import Updatable
7
7
  from alpha.interfaces.patchable import Patchable
8
8
 
9
+ # import all http client related interfaces
10
+ from alpha.interfaces.http_client import HTTPClient, HTTPResponse
11
+
9
12
  # import all repository related interfaces
10
13
  from alpha.interfaces.api_repository import ApiRepository
11
14
  from alpha.interfaces.sql_repository import SqlRepository
15
+ from alpha.interfaces.refresh_repository import RefreshRepository
12
16
 
13
17
  # import all database related interfaces
14
18
  from alpha.interfaces.sql_mapper import SqlMapper
@@ -33,8 +37,11 @@ __all__ = [
33
37
  "OpenAPIModel",
34
38
  "Updatable",
35
39
  "Patchable",
40
+ "HTTPClient",
41
+ "HTTPResponse",
36
42
  "ApiRepository",
37
43
  "SqlRepository",
44
+ "RefreshRepository",
38
45
  "SqlMapper",
39
46
  "SqlDatabase",
40
47
  "UnitOfWork",
@@ -263,7 +263,7 @@ class ApiRepository(Protocol[DomainModel]):
263
263
  Whether to use the model factory method for creating models from
264
264
  response data.
265
265
  endpoint
266
- The API endpoint to which the object should be added.
266
+ The API endpoint from which the object should be retrieved.
267
267
  parent_endpoint
268
268
  The parent API endpoint, if the resource is nested under a parent
269
269
  resource.
@@ -340,7 +340,7 @@ class ApiRepository(Protocol[DomainModel]):
340
340
  Whether to use the model factory method for creating models from
341
341
  response data.
342
342
  endpoint
343
- The API endpoint to which the object should be added.
343
+ The API endpoint from which the objects should be retrieved.
344
344
  parent_endpoint
345
345
  The parent API endpoint, if the resource is nested under a parent
346
346
  resource.
@@ -441,7 +441,7 @@ class ApiRepository(Protocol[DomainModel]):
441
441
  Whether to use the model factory method for creating models from
442
442
  response data.
443
443
  endpoint
444
- The API endpoint to which the object should be added.
444
+ The API endpoint to which the patch should be applied.
445
445
  parent_endpoint
446
446
  The parent API endpoint, if the resource is nested under a parent
447
447
  resource.
@@ -485,7 +485,7 @@ class ApiRepository(Protocol[DomainModel]):
485
485
  Parameters
486
486
  ----------
487
487
  endpoint
488
- The API endpoint to which the object should be added.
488
+ The API endpoint from which the object should be removed.
489
489
  parent_endpoint
490
490
  The parent API endpoint, if the resource is nested under a parent
491
491
  resource.
@@ -585,7 +585,7 @@ class ApiRepository(Protocol[DomainModel]):
585
585
  Whether to use the model factory method for creating models from
586
586
  response data.
587
587
  endpoint
588
- The API endpoint to which the object should be added.
588
+ The API endpoint to which the object should be updated.
589
589
  parent_endpoint
590
590
  The parent API endpoint, if the resource is nested under a parent
591
591
  resource.
@@ -0,0 +1,61 @@
1
+ from typing import MutableMapping, Protocol, Any, runtime_checkable
2
+
3
+
4
+ @runtime_checkable
5
+ class HTTPResponse(Protocol):
6
+ """Interface for HTTP response objects returned by HTTP clients.
7
+
8
+ This interface is compatible with the response objects returned by popular
9
+ synchronous HTTP client libraries, for example, the `requests` library,
10
+ `httpx` library or any custom implementation that follows the same
11
+ attributes and method signatures.
12
+
13
+ This interface defines the attributes and methods that an HTTP response
14
+ object should have to be compatible with the REST API repository. It
15
+ includes attributes for accessing the response status code, headers, and
16
+ content, as well as a method for parsing the response content as JSON.
17
+ """
18
+
19
+ status_code: int
20
+ headers: dict[str, str]
21
+ content: bytes
22
+ text: str
23
+
24
+ def json(self) -> Any: ...
25
+ def raise_for_status(self) -> None: ...
26
+
27
+
28
+ @runtime_checkable
29
+ class HTTPClient(Protocol):
30
+ """Interface for HTTP clients like requests, httpx or a custom
31
+ implementation.
32
+
33
+ This interface is compatible with the popular synchronous HTTP client
34
+ libraries, for example, the `requests` library, `httpx` library or any
35
+ custom implementation that follows the same method signatures.
36
+
37
+ This interface defines the methods that an HTTP client should implement to
38
+ be compatible with the REST API repository. It includes methods for making
39
+ HTTP requests (POST, GET, DELETE, PUT, PATCH) and allows for additional
40
+ parameters to be passed as needed.
41
+ """
42
+
43
+ cookies: MutableMapping[str, str]
44
+ headers: MutableMapping[str, str]
45
+
46
+ def request(
47
+ self, method: str, url: str, **kwargs: Any
48
+ ) -> HTTPResponse: ...
49
+ def close(self) -> None: ...
50
+
51
+ def post(
52
+ self, url: str, json: Any = None, **kwargs: Any
53
+ ) -> HTTPResponse: ...
54
+ def get(self, url: str, **kwargs: Any) -> HTTPResponse: ...
55
+ def delete(self, url: str, **kwargs: Any) -> HTTPResponse: ...
56
+ def put(
57
+ self, url: str, json: Any = None, **kwargs: Any
58
+ ) -> HTTPResponse: ...
59
+ def patch(
60
+ self, url: str, json: Any = None, **kwargs: Any
61
+ ) -> HTTPResponse: ...
@@ -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
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
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,57 @@
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
+ *** This class is not fully implemented yet. The methods are defined
17
+ but not implemented. ***
18
+
19
+ Parameters
20
+ ----------
21
+ cache_connector
22
+ The cache connector instance to use for cache operations.
23
+ token_model
24
+ The model class for tokens, by default Token. The model class
25
+ should have a `from_dict` class method that takes a dictionary and
26
+ returns an instance of the model. The dictionary will have the same
27
+ structure as the token data in the JSON file. The model class
28
+ should also have a `to_dict` method that converts an instance of
29
+ the model to a dictionary with the same structure as the token data
30
+ in the JSON file. The model class should also have a
31
+ `create_refresh` class method that creates a new refresh token.
32
+ token_max_age_seconds
33
+ The maximum age of a token in seconds, by default the equivalent of
34
+ 7 days in seconds
35
+ token_length
36
+ The length of the generated token string, by default 32 characters
37
+ """
38
+ self._cache_connector = cache_connector
39
+ self._token_model = token_model
40
+ self._token_max_age_seconds = token_max_age_seconds
41
+ self._token_length = token_length
42
+
43
+ def get(self, token: str) -> Token:
44
+ """Get a token by its value."""
45
+ raise NotImplementedError("Method not implemented yet.")
46
+
47
+ def create(self, subject: str) -> Token:
48
+ """Create a new token for a given subject."""
49
+ raise NotImplementedError("Method not implemented yet.")
50
+
51
+ def delete(self, token: str) -> None:
52
+ """Delete a token by its value."""
53
+ raise NotImplementedError("Method not implemented yet.")
54
+
55
+ def delete_all(self, subject: str) -> None:
56
+ """Delete all tokens for a given subject."""
57
+ 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
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
40
+ The maximum age of a token in seconds, by default the equivalent of
41
+ 7 days in seconds
42
+ token_length
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()