alpha-python 0.3.4__py3-none-any.whl → 0.5.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 +22 -8
- alpha/adapters/__init__.py +2 -0
- alpha/adapters/rest_api_unit_of_work.py +105 -0
- alpha/adapters/sqla_unit_of_work.py +9 -9
- alpha/domain/models/group.py +35 -0
- alpha/exceptions.py +12 -0
- alpha/factories/jwt_factory.py +18 -0
- alpha/handlers/templates/python-flask/controller.mustache +1 -0
- alpha/interfaces/__init__.py +5 -1
- alpha/interfaces/api_repository.py +610 -0
- alpha/interfaces/providers.py +1 -1
- alpha/interfaces/sql_repository.py +0 -11
- alpha/mixins/jwt_provider.py +7 -7
- alpha/providers/database_provider.py +2 -2
- alpha/providers/ldap_provider.py +9 -9
- alpha/providers/models/identity.py +55 -21
- alpha/providers/models/token.py +51 -3
- alpha/providers/oidc_provider.py +7 -5
- alpha/repositories/__init__.py +2 -0
- alpha/repositories/models/repository_model.py +24 -6
- alpha/repositories/rest_api_repository.py +1199 -0
- alpha/services/authentication_service.py +669 -45
- alpha/services/models/cookie.py +51 -0
- alpha/utils/response_object.py +151 -11
- alpha/utils/secret_generator.py +19 -0
- {alpha_python-0.3.4.dist-info → alpha_python-0.5.0.dist-info}/METADATA +58 -2
- {alpha_python-0.3.4.dist-info → alpha_python-0.5.0.dist-info}/RECORD +32 -26
- {alpha_python-0.3.4.dist-info → alpha_python-0.5.0.dist-info}/WHEEL +1 -1
- {alpha_python-0.3.4.dist-info → alpha_python-0.5.0.dist-info}/licenses/LICENSE +1 -1
- /alpha/{providers/local_provider.py → services/models/__init__.py} +0 -0
- {alpha_python-0.3.4.dist-info → alpha_python-0.5.0.dist-info}/entry_points.txt +0 -0
- {alpha_python-0.3.4.dist-info → alpha_python-0.5.0.dist-info}/top_level.txt +0 -0
alpha/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from alpha.adapters.rest_api_unit_of_work import RestApiUnitOfWork
|
|
1
2
|
from alpha.adapters.sqla_unit_of_work import SqlAlchemyUnitOfWork
|
|
2
3
|
from alpha.factories.jwt_factory import JWTFactory
|
|
3
4
|
from alpha.factories.logging_handler_factory import LoggingHandlerFactory
|
|
@@ -20,6 +21,7 @@ from alpha.interfaces.pydantic_instance import PydanticInstance
|
|
|
20
21
|
from alpha.interfaces.openapi_model import OpenAPIModel
|
|
21
22
|
from alpha.interfaces.updateable import Updateable
|
|
22
23
|
from alpha.interfaces.patchable import Patchable
|
|
24
|
+
from alpha.interfaces.api_repository import ApiRepository
|
|
23
25
|
from alpha.interfaces.sql_repository import SqlRepository
|
|
24
26
|
from alpha.interfaces.sql_mapper import SqlMapper
|
|
25
27
|
from alpha.interfaces.sql_database import SqlDatabase
|
|
@@ -44,6 +46,7 @@ from alpha.providers.models.credentials import PasswordCredentials
|
|
|
44
46
|
from alpha.providers.models.token import Token
|
|
45
47
|
from alpha.providers.oidc_provider import OIDCProvider, KeyCloakProvider
|
|
46
48
|
from alpha.repositories.models.repository_model import RepositoryModel
|
|
49
|
+
from alpha.repositories.rest_api_repository import RestApiRepository
|
|
47
50
|
from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
|
|
48
51
|
from alpha.services.authentication_service import AuthenticationService
|
|
49
52
|
from alpha.utils.is_attrs import is_attrs
|
|
@@ -60,13 +63,20 @@ from alpha.encoder import JSONEncoder
|
|
|
60
63
|
|
|
61
64
|
# Optional LDAP support - only import if ldap3 is available
|
|
62
65
|
try:
|
|
63
|
-
from alpha.infra.connectors.ldap_connector import
|
|
64
|
-
|
|
66
|
+
from alpha.infra.connectors.ldap_connector import (
|
|
67
|
+
LDAPConnector, # noqa: F401
|
|
68
|
+
)
|
|
69
|
+
from alpha.providers.ldap_provider import (
|
|
70
|
+
LDAPProvider, # noqa: F401
|
|
71
|
+
ADProvider, # noqa: F401
|
|
72
|
+
)
|
|
73
|
+
|
|
65
74
|
_LDAP_AVAILABLE = True
|
|
66
75
|
except ImportError:
|
|
67
|
-
_LDAP_AVAILABLE = False
|
|
76
|
+
_LDAP_AVAILABLE = False # type: ignore
|
|
68
77
|
|
|
69
78
|
__all__ = [
|
|
79
|
+
"RestApiUnitOfWork",
|
|
70
80
|
"SqlAlchemyUnitOfWork",
|
|
71
81
|
"JWTFactory",
|
|
72
82
|
"LoggingHandlerFactory",
|
|
@@ -91,6 +101,7 @@ __all__ = [
|
|
|
91
101
|
"OpenAPIModel",
|
|
92
102
|
"Updateable",
|
|
93
103
|
"Patchable",
|
|
104
|
+
"ApiRepository",
|
|
94
105
|
"SqlRepository",
|
|
95
106
|
"SqlMapper",
|
|
96
107
|
"SqlDatabase",
|
|
@@ -112,6 +123,7 @@ __all__ = [
|
|
|
112
123
|
"OIDCProvider",
|
|
113
124
|
"KeyCloakProvider",
|
|
114
125
|
"RepositoryModel",
|
|
126
|
+
"RestApiRepository",
|
|
115
127
|
"SqlAlchemyRepository",
|
|
116
128
|
"AuthenticationService",
|
|
117
129
|
"is_attrs",
|
|
@@ -128,8 +140,10 @@ __all__ = [
|
|
|
128
140
|
|
|
129
141
|
# Conditionally add LDAP-related exports if available
|
|
130
142
|
if _LDAP_AVAILABLE:
|
|
131
|
-
__all__.extend(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
__all__.extend(
|
|
144
|
+
[
|
|
145
|
+
"LDAPConnector",
|
|
146
|
+
"LDAPProvider",
|
|
147
|
+
"ADProvider",
|
|
148
|
+
]
|
|
149
|
+
)
|
alpha/adapters/__init__.py
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Contains the REST API Unit of Work implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from alpha.repositories.models.repository_model import RepositoryModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
UOW = TypeVar("UOW", bound="RestApiUnitOfWork")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RestApiUnitOfWork:
|
|
14
|
+
"""Unit of Work implementation for REST API interactions."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
repos: list[RepositoryModel[Any]],
|
|
19
|
+
session: requests.sessions.Session | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize the Unit of Work with repositories.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
repos : list[RepositoryModel]
|
|
26
|
+
The list of repository models to use.
|
|
27
|
+
session : requests.sessions.Session | None
|
|
28
|
+
The requests session (or compatible HTTP client, e.g., httpx) to
|
|
29
|
+
use for context management, by default None
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
TypeError
|
|
34
|
+
If any repository does not implement its specified interface.
|
|
35
|
+
"""
|
|
36
|
+
self._repositories = repos
|
|
37
|
+
self._session = session
|
|
38
|
+
|
|
39
|
+
def __enter__(self: UOW) -> UOW:
|
|
40
|
+
"""Enter the REST API Unit of Work context.
|
|
41
|
+
Initializes a :class:`requests.sessions.Session` if one was not
|
|
42
|
+
provided and attaches the configured repositories as attributes on the
|
|
43
|
+
unit of work instance. Each repository is constructed using the shared
|
|
44
|
+
session and its associated configuration, and optionally validated
|
|
45
|
+
against a declared interface.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
UOW
|
|
50
|
+
The configured :class:`RestApiUnitOfWork` instance to be used
|
|
51
|
+
within the context manager.
|
|
52
|
+
"""
|
|
53
|
+
self._session = self._session or requests.sessions.Session()
|
|
54
|
+
|
|
55
|
+
for repo in self._repositories:
|
|
56
|
+
name: str = repo.name
|
|
57
|
+
interface: Any = repo.interface
|
|
58
|
+
additional_config: dict[str, Any] = dict(
|
|
59
|
+
repo.additional_config or {}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.__setattr__(
|
|
63
|
+
name,
|
|
64
|
+
repo.repository(
|
|
65
|
+
session=self._session,
|
|
66
|
+
default_model=repo.default_model,
|
|
67
|
+
**additional_config,
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if interface:
|
|
72
|
+
if not isinstance(getattr(self, name), interface):
|
|
73
|
+
raise TypeError(f"Repository for {name} has no interface")
|
|
74
|
+
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def __exit__(self, *args: Any) -> None:
|
|
78
|
+
"""Finalize the Unit of Work context."""
|
|
79
|
+
if self._session:
|
|
80
|
+
self._session.close()
|
|
81
|
+
|
|
82
|
+
def commit(self) -> None:
|
|
83
|
+
raise NotImplementedError("RestApiUnitOfWork does not support commit")
|
|
84
|
+
|
|
85
|
+
def flush(self) -> None:
|
|
86
|
+
raise NotImplementedError("RestApiUnitOfWork does not support flush")
|
|
87
|
+
|
|
88
|
+
def rollback(self) -> None:
|
|
89
|
+
raise NotImplementedError(
|
|
90
|
+
"RestApiUnitOfWork does not support rollback"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def refresh(self, obj: object) -> None:
|
|
94
|
+
raise NotImplementedError("RestApiUnitOfWork does not support refresh")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def session(self) -> requests.sessions.Session | None:
|
|
98
|
+
"""Get the current session.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
requests.sessions.Session | None
|
|
103
|
+
The current session used for API interactions.
|
|
104
|
+
"""
|
|
105
|
+
return self._session
|
|
@@ -14,7 +14,11 @@ UOW = TypeVar("UOW", bound="SqlAlchemyUnitOfWork")
|
|
|
14
14
|
class SqlAlchemyUnitOfWork:
|
|
15
15
|
"""Unit of Work implementation for SQLAlchemy databases."""
|
|
16
16
|
|
|
17
|
-
def __init__(
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
db: SqlDatabase,
|
|
20
|
+
repos: list[RepositoryModel[Any]],
|
|
21
|
+
) -> None:
|
|
18
22
|
"""Initialize the Unit of Work with a database and repositories.
|
|
19
23
|
|
|
20
24
|
Parameters
|
|
@@ -28,9 +32,6 @@ class SqlAlchemyUnitOfWork:
|
|
|
28
32
|
------
|
|
29
33
|
TypeError
|
|
30
34
|
If the provided database is not a valid SqlDatabase instance.
|
|
31
|
-
TypeError
|
|
32
|
-
If the provided repositories list is empty or contains invalid
|
|
33
|
-
models.
|
|
34
35
|
"""
|
|
35
36
|
if not isinstance(db, SqlDatabase): # type: ignore
|
|
36
37
|
raise TypeError("No valid database provided")
|
|
@@ -55,17 +56,16 @@ class SqlAlchemyUnitOfWork:
|
|
|
55
56
|
self._session = self._db.get_session()
|
|
56
57
|
|
|
57
58
|
for repo in self._repositories:
|
|
58
|
-
session = self._session
|
|
59
|
-
model = repo.default_model
|
|
60
|
-
|
|
61
59
|
name: str = repo.name
|
|
62
|
-
repository = repo.repository
|
|
63
60
|
interface: Any = repo.interface
|
|
64
61
|
|
|
65
62
|
self.__setattr__(
|
|
66
63
|
name,
|
|
67
|
-
repository(
|
|
64
|
+
repo.repository(
|
|
65
|
+
session=self._session, default_model=repo.default_model
|
|
66
|
+
),
|
|
68
67
|
)
|
|
68
|
+
|
|
69
69
|
if interface:
|
|
70
70
|
if not isinstance(getattr(self, name), interface):
|
|
71
71
|
raise TypeError(f"Repository for {name} has no interface")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Sequence, cast
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from alpha.domain.models.base_model import BaseDomainModel, DomainModel
|
|
7
|
+
from alpha.domain.models.life_cycle_base import LifeCycleBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(kw_only=True)
|
|
11
|
+
class Group(LifeCycleBase, BaseDomainModel):
|
|
12
|
+
id: UUID | int | str | None = None
|
|
13
|
+
name: str | None = None
|
|
14
|
+
description: str | None = None
|
|
15
|
+
permissions: Sequence[str] | None = None
|
|
16
|
+
is_active: bool = True
|
|
17
|
+
|
|
18
|
+
def update(self, obj: DomainModel) -> DomainModel:
|
|
19
|
+
"""Update the Group instance with data from another Group instance.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
obj
|
|
24
|
+
Group object to update from.
|
|
25
|
+
"""
|
|
26
|
+
if not isinstance(obj, Group):
|
|
27
|
+
raise TypeError("Group.update expects a Group instance.")
|
|
28
|
+
|
|
29
|
+
self.name = obj.name
|
|
30
|
+
self.description = obj.description
|
|
31
|
+
self.permissions = obj.permissions
|
|
32
|
+
self.modified_at = datetime.now(tz=timezone.utc)
|
|
33
|
+
self.is_active = obj.is_active
|
|
34
|
+
|
|
35
|
+
return cast(DomainModel, self)
|
alpha/exceptions.py
CHANGED
|
@@ -19,6 +19,10 @@ class NotFoundException(ClientErrorException):
|
|
|
19
19
|
"""Equivalent to HTTP code 404"""
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class MethodNotAllowedException(ClientErrorException):
|
|
23
|
+
"""Equivalent to HTTP code 405"""
|
|
24
|
+
|
|
25
|
+
|
|
22
26
|
class NotAcceptableException(ClientErrorException):
|
|
23
27
|
"""Equivalent to HTTP code 406"""
|
|
24
28
|
|
|
@@ -55,11 +59,19 @@ class ServiceUnavailableException(ServerErrorException):
|
|
|
55
59
|
"""Equivalent to HTTP code 503"""
|
|
56
60
|
|
|
57
61
|
|
|
62
|
+
class GatewayTimeoutException(ServerErrorException):
|
|
63
|
+
"""Equivalent to HTTP code 504"""
|
|
64
|
+
|
|
65
|
+
|
|
58
66
|
# General Exceptions
|
|
59
67
|
class MissingConfigurationException(Exception):
|
|
60
68
|
"""Raised when a required configuration is missing."""
|
|
61
69
|
|
|
62
70
|
|
|
71
|
+
class InvalidAttributeError(Exception):
|
|
72
|
+
"""Raised when a required attribute is invalid."""
|
|
73
|
+
|
|
74
|
+
|
|
63
75
|
class MissingDependencyException(Exception):
|
|
64
76
|
"""Raised when a required dependency is missing."""
|
|
65
77
|
|
alpha/factories/jwt_factory.py
CHANGED
|
@@ -19,6 +19,24 @@ class JWTFactory:
|
|
|
19
19
|
issuer: str = "http://localhost",
|
|
20
20
|
jwt_algorithm: str = "HS256",
|
|
21
21
|
) -> None:
|
|
22
|
+
"""Initialize the JWTFactory.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
secret
|
|
27
|
+
The secret key used to sign the JWT.
|
|
28
|
+
lifetime_hours, optional
|
|
29
|
+
The lifetime of the JWT in hours, by default "12"
|
|
30
|
+
issuer, optional
|
|
31
|
+
The issuer of the JWT, by default "http://localhost"
|
|
32
|
+
jwt_algorithm, optional
|
|
33
|
+
The algorithm used to sign the JWT, by default "HS256"
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
ValueError
|
|
38
|
+
If the secret value is empty.
|
|
39
|
+
"""
|
|
22
40
|
if not secret:
|
|
23
41
|
raise ValueError("Secret value cannot be empty")
|
|
24
42
|
if lifetime_hours is None:
|
|
@@ -231,6 +231,7 @@ def {{operationId}}(
|
|
|
231
231
|
status_message='{{message}}',
|
|
232
232
|
data=result,
|
|
233
233
|
{{#vendorExtensions.x-content-type}}data_type='{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}
|
|
234
|
+
{{#vendorExtensions.x-alpha-cookie-support}}response_type='flask'{{/vendorExtensions.x-alpha-cookie-support}}
|
|
234
235
|
)
|
|
235
236
|
return response_object, status_code
|
|
236
237
|
{{/returnType}}
|
alpha/interfaces/__init__.py
CHANGED
|
@@ -6,8 +6,11 @@ from alpha.interfaces.openapi_model import OpenAPIModel
|
|
|
6
6
|
from alpha.interfaces.updateable import Updateable
|
|
7
7
|
from alpha.interfaces.patchable import Patchable
|
|
8
8
|
|
|
9
|
-
# import all
|
|
9
|
+
# import all repository related interfaces
|
|
10
|
+
from alpha.interfaces.api_repository import ApiRepository
|
|
10
11
|
from alpha.interfaces.sql_repository import SqlRepository
|
|
12
|
+
|
|
13
|
+
# import all database related interfaces
|
|
11
14
|
from alpha.interfaces.sql_mapper import SqlMapper
|
|
12
15
|
from alpha.interfaces.sql_database import SqlDatabase
|
|
13
16
|
from alpha.interfaces.unit_of_work import UnitOfWork
|
|
@@ -30,6 +33,7 @@ __all__ = [
|
|
|
30
33
|
"OpenAPIModel",
|
|
31
34
|
"Updateable",
|
|
32
35
|
"Patchable",
|
|
36
|
+
"ApiRepository",
|
|
33
37
|
"SqlRepository",
|
|
34
38
|
"SqlMapper",
|
|
35
39
|
"SqlDatabase",
|