alpha-python 0.4.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alpha_python-0.4.0 → alpha_python-0.5.1}/LICENSE +1 -1
- {alpha_python-0.4.0/src/alpha_python.egg-info → alpha_python-0.5.1}/PKG-INFO +58 -2
- alpha_python-0.5.1/README.md +58 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/pyproject.toml +10 -2
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/__init__.py +26 -8
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/adapters/__init__.py +2 -0
- alpha_python-0.5.1/src/alpha/adapters/rest_api_unit_of_work.py +105 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/adapters/sqla_unit_of_work.py +9 -9
- alpha_python-0.5.1/src/alpha/domain/models/group.py +35 -0
- alpha_python-0.5.1/src/alpha/domain/models/user.py +130 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/exceptions.py +12 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/jwt_factory.py +88 -25
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/password_factory.py +1 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/controller.mustache +37 -28
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/__init__.py +5 -1
- alpha_python-0.5.1/src/alpha/interfaces/api_repository.py +610 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/providers.py +1 -1
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/sql_repository.py +0 -11
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/token_factory.py +6 -1
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/mixins/jwt_provider.py +7 -7
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/database_provider.py +2 -2
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/ldap_provider.py +9 -9
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/models/identity.py +56 -22
- alpha_python-0.5.1/src/alpha/providers/models/token.py +55 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/oidc_provider.py +7 -5
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/repositories/__init__.py +2 -0
- alpha_python-0.5.1/src/alpha/repositories/models/repository_model.py +34 -0
- alpha_python-0.5.1/src/alpha/repositories/rest_api_repository.py +1199 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/services/__init__.py +2 -0
- alpha_python-0.5.1/src/alpha/services/authentication_service.py +835 -0
- alpha_python-0.5.1/src/alpha/services/models/cookie.py +51 -0
- alpha_python-0.5.1/src/alpha/services/user_lifecycle_management.py +331 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/__init__.py +2 -0
- alpha_python-0.5.1/src/alpha/utils/request_headers.py +108 -0
- alpha_python-0.5.1/src/alpha/utils/response_object.py +166 -0
- alpha_python-0.5.1/src/alpha/utils/secret_generator.py +19 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1/src/alpha_python.egg-info}/PKG-INFO +58 -2
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/SOURCES.txt +9 -1
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/requires.txt +1 -0
- alpha_python-0.4.0/README.md +0 -3
- alpha_python-0.4.0/src/alpha/domain/models/user.py +0 -66
- alpha_python-0.4.0/src/alpha/providers/models/token.py +0 -7
- alpha_python-0.4.0/src/alpha/repositories/models/repository_model.py +0 -16
- alpha_python-0.4.0/src/alpha/services/authentication_service.py +0 -200
- alpha_python-0.4.0/src/alpha/utils/response_object.py +0 -26
- {alpha_python-0.4.0 → alpha_python-0.5.1}/setup.cfg +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/cli.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/containers/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/containers/container.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/domain/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/domain/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/domain/models/base_model.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/domain/models/life_cycle_base.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/encoder.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/_type_conversion_matrix.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/_type_mapping.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/class_factories.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/default_field_factory.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/field_iterator.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/logging_handler_factory.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/model_class_factory.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/models/factory_classes.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/request_factory.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/response_factory.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/factories/type_factories.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/api_generate_handler.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/api_run_handler.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/base_handler.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/gen-code.sh +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/models/argument.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/models/command.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/models/section.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/models/subparser.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/run-api.sh +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/Dockerfile.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/README.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/__init__model.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/__init__test.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/__main__.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/base_model.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/controller_test.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/dockerignore.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/encoder.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/git_push.sh.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/gitignore.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/model.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/openapi.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/param_type.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/requirements.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/security_controller_.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/setup.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/test-requirements.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/tox.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/travis.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/typing_utils.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/util.mustache +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/connectors/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/connectors/ldap_connector.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/connectors/oidc_connector.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/databases/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/databases/sql_alchemy.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/models/filter_operators.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/models/json_patch.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/models/order_by.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/models/query_clause.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/infra/models/search_filter.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/attrs_instance.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/dataclass_instance.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/factories.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/handler.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/openapi_model.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/patchable.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/pydantic_instance.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/sql_database.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/sql_mapper.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/unit_of_work.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/interfaces/updateable.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/mixins/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/api_key_provider.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/providers/models/credentials.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/py.typed +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/repositories/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/repositories/sql_alchemy_repository.py +0 -0
- /alpha_python-0.4.0/src/alpha/providers/local_provider.py → /alpha_python-0.5.1/src/alpha/services/models/__init__.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/_http_codes.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/is_attrs.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/is_pydantic.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/logging_configurator.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/logging_level_checker.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/verify_identity.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha/utils/version_checker.py +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/dependency_links.txt +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/entry_points.txt +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/top_level.txt +0 -0
- {alpha_python-0.4.0 → alpha_python-0.5.1}/tests/test_encoder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-python
|
|
3
|
-
Version: 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
|
|
@@ -29,6 +29,7 @@ Requires-Dist: itsdangerous==1.1.0; extra == "flask"
|
|
|
29
29
|
Requires-Dist: MarkupSafe<2.0.2; extra == "flask"
|
|
30
30
|
Requires-Dist: Jinja2==2.11.2; extra == "flask"
|
|
31
31
|
Requires-Dist: Flask<2,>=1.1.2; extra == "flask"
|
|
32
|
+
Requires-Dist: Werkzeug<2.1.0,>=1.0.1; extra == "flask"
|
|
32
33
|
Requires-Dist: flask-cors>=3.0.10; extra == "flask"
|
|
33
34
|
Requires-Dist: Flask-Compress>=1.13; extra == "flask"
|
|
34
35
|
Provides-Extra: mysql
|
|
@@ -41,4 +42,59 @@ Dynamic: license-file
|
|
|
41
42
|
|
|
42
43
|
# alpha
|
|
43
44
|
|
|
44
|
-
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.
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
## Badges
|
|
48
|
+
|
|
49
|
+
[](https://badge.fury.io/py/alpha-python)
|
|
50
|
+
[](https://travis-ci.com/breijling/alpha)
|
|
51
|
+
[](https://coveralls.io/github/breijling/alpha?branch=main)
|
|
52
|
+
[](https://choosealicense.com/licenses/mit/)
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
TODO: Add documentation link when available.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
The library is still in development, but you can already install it using pip:
|
|
61
|
+
|
|
62
|
+
```shell
|
|
63
|
+
pip install alpha-python
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If you want to use the alpha cli for generating API code, you can install it using pip as well:
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
pip install alpha-python[api-generator]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you want to add the library to your API project, you can add it to your pyproject.toml file:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
# Poetry example
|
|
76
|
+
poetry add alpha-python --extras "flask, postgresql"
|
|
77
|
+
poetry add --dev alpha-python --extras "api-generator"
|
|
78
|
+
|
|
79
|
+
# UV example
|
|
80
|
+
uv add alpha-python --extra flask --extra postgresql
|
|
81
|
+
uv add --dev alpha-python --extra api-generator
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
The library contains a lot of different components, but the most important ones are:
|
|
87
|
+
|
|
88
|
+
- `alpha.encoder.JSONEncoder`: A JSON encoder that can be used to serialize complex objects to JSON.
|
|
89
|
+
|
|
90
|
+
## Features
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
If you want to contribute to the development of this library, you can fork the repository and create a pull request with your changes.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
This library is licensed under the MIT License. See the LICENSE file for more information.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# alpha
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Badges
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/py/alpha-python)
|
|
8
|
+
[](https://travis-ci.com/breijling/alpha)
|
|
9
|
+
[](https://coveralls.io/github/breijling/alpha?branch=main)
|
|
10
|
+
[](https://choosealicense.com/licenses/mit/)
|
|
11
|
+
|
|
12
|
+
## Documentation
|
|
13
|
+
|
|
14
|
+
TODO: Add documentation link when available.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
The library is still in development, but you can already install it using pip:
|
|
19
|
+
|
|
20
|
+
```shell
|
|
21
|
+
pip install alpha-python
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If you want to use the alpha cli for generating API code, you can install it using pip as well:
|
|
25
|
+
|
|
26
|
+
```shell
|
|
27
|
+
pip install alpha-python[api-generator]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If you want to add the library to your API project, you can add it to your pyproject.toml file:
|
|
31
|
+
|
|
32
|
+
```shell
|
|
33
|
+
# Poetry example
|
|
34
|
+
poetry add alpha-python --extras "flask, postgresql"
|
|
35
|
+
poetry add --dev alpha-python --extras "api-generator"
|
|
36
|
+
|
|
37
|
+
# UV example
|
|
38
|
+
uv add alpha-python --extra flask --extra postgresql
|
|
39
|
+
uv add --dev alpha-python --extra api-generator
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
The library contains a lot of different components, but the most important ones are:
|
|
45
|
+
|
|
46
|
+
- `alpha.encoder.JSONEncoder`: A JSON encoder that can be used to serialize complex objects to JSON.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Contributing
|
|
53
|
+
|
|
54
|
+
If you want to contribute to the development of this library, you can fork the repository and create a pull request with your changes.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
This library is licensed under the MIT License. See the LICENSE file for more information.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "alpha-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -56,7 +56,10 @@ dev = [
|
|
|
56
56
|
"psycopg2-binary>=2.9.11",
|
|
57
57
|
"pymysql>=1.1.2",
|
|
58
58
|
"ldap3>=2.9.1",
|
|
59
|
-
"cryptography>=46.0.3"
|
|
59
|
+
"cryptography>=46.0.3",
|
|
60
|
+
"ipykernel>=7.2.0",
|
|
61
|
+
"flask<2",
|
|
62
|
+
"httpx>=0.28.1",
|
|
60
63
|
]
|
|
61
64
|
|
|
62
65
|
[project.optional-dependencies]
|
|
@@ -72,6 +75,7 @@ flask = [
|
|
|
72
75
|
"MarkupSafe<2.0.2",
|
|
73
76
|
"Jinja2==2.11.2",
|
|
74
77
|
"Flask>=1.1.2,<2",
|
|
78
|
+
"Werkzeug>=1.0.1,<2.1.0",
|
|
75
79
|
"flask-cors>=3.0.10",
|
|
76
80
|
"Flask-Compress>=1.13",
|
|
77
81
|
]
|
|
@@ -116,5 +120,9 @@ force_grid_wrap = 0
|
|
|
116
120
|
python_version = "3.11"
|
|
117
121
|
exclude = ['^tests/']
|
|
118
122
|
|
|
123
|
+
[[tool.mypy.overrides]]
|
|
124
|
+
module = ["flask", "flask.*"]
|
|
125
|
+
ignore_missing_imports = true
|
|
126
|
+
|
|
119
127
|
[tool.ruff]
|
|
120
128
|
line-length = 79
|
|
@@ -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,8 +46,10 @@ 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
|
|
52
|
+
from alpha.services.user_lifecycle_management import UserLifecycleManagement
|
|
49
53
|
from alpha.utils.is_attrs import is_attrs
|
|
50
54
|
from alpha.utils.is_pydantic import is_pydantic
|
|
51
55
|
from alpha.utils.logging_configurator import (
|
|
@@ -53,6 +57,7 @@ from alpha.utils.logging_configurator import (
|
|
|
53
57
|
GunicornLogger,
|
|
54
58
|
)
|
|
55
59
|
from alpha.utils.logging_level_checker import logging_level_checker
|
|
60
|
+
from alpha.utils.request_headers import Headers
|
|
56
61
|
from alpha.utils.response_object import create_response_object
|
|
57
62
|
from alpha.utils.verify_identity import verify_identity
|
|
58
63
|
from alpha.utils.version_checker import minor_version_gte
|
|
@@ -60,13 +65,20 @@ from alpha.encoder import JSONEncoder
|
|
|
60
65
|
|
|
61
66
|
# Optional LDAP support - only import if ldap3 is available
|
|
62
67
|
try:
|
|
63
|
-
from alpha.infra.connectors.ldap_connector import
|
|
64
|
-
|
|
68
|
+
from alpha.infra.connectors.ldap_connector import (
|
|
69
|
+
LDAPConnector, # noqa: F401
|
|
70
|
+
)
|
|
71
|
+
from alpha.providers.ldap_provider import (
|
|
72
|
+
LDAPProvider, # noqa: F401
|
|
73
|
+
ADProvider, # noqa: F401
|
|
74
|
+
)
|
|
75
|
+
|
|
65
76
|
_LDAP_AVAILABLE = True
|
|
66
77
|
except ImportError:
|
|
67
|
-
_LDAP_AVAILABLE = False
|
|
78
|
+
_LDAP_AVAILABLE = False # type: ignore
|
|
68
79
|
|
|
69
80
|
__all__ = [
|
|
81
|
+
"RestApiUnitOfWork",
|
|
70
82
|
"SqlAlchemyUnitOfWork",
|
|
71
83
|
"JWTFactory",
|
|
72
84
|
"LoggingHandlerFactory",
|
|
@@ -91,6 +103,7 @@ __all__ = [
|
|
|
91
103
|
"OpenAPIModel",
|
|
92
104
|
"Updateable",
|
|
93
105
|
"Patchable",
|
|
106
|
+
"ApiRepository",
|
|
94
107
|
"SqlRepository",
|
|
95
108
|
"SqlMapper",
|
|
96
109
|
"SqlDatabase",
|
|
@@ -112,13 +125,16 @@ __all__ = [
|
|
|
112
125
|
"OIDCProvider",
|
|
113
126
|
"KeyCloakProvider",
|
|
114
127
|
"RepositoryModel",
|
|
128
|
+
"RestApiRepository",
|
|
115
129
|
"SqlAlchemyRepository",
|
|
116
130
|
"AuthenticationService",
|
|
131
|
+
"UserLifecycleManagement",
|
|
117
132
|
"is_attrs",
|
|
118
133
|
"is_pydantic",
|
|
119
134
|
"LoggingConfigurator",
|
|
120
135
|
"GunicornLogger",
|
|
121
136
|
"logging_level_checker",
|
|
137
|
+
"Headers",
|
|
122
138
|
"create_response_object",
|
|
123
139
|
"verify_identity",
|
|
124
140
|
"minor_version_gte",
|
|
@@ -128,8 +144,10 @@ __all__ = [
|
|
|
128
144
|
|
|
129
145
|
# Conditionally add LDAP-related exports if available
|
|
130
146
|
if _LDAP_AVAILABLE:
|
|
131
|
-
__all__.extend(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
__all__.extend(
|
|
148
|
+
[
|
|
149
|
+
"LDAPConnector",
|
|
150
|
+
"LDAPProvider",
|
|
151
|
+
"ADProvider",
|
|
152
|
+
]
|
|
153
|
+
)
|
|
@@ -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)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
from typing import Self, Sequence, cast
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from alpha.domain.models.base_model import BaseDomainModel, DomainModel
|
|
8
|
+
from alpha.domain.models.group import Group
|
|
9
|
+
from alpha.domain.models.life_cycle_base import LifeCycleBase
|
|
10
|
+
|
|
11
|
+
from alpha.providers.models.identity import Identity
|
|
12
|
+
|
|
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
|
+
|
|
76
|
+
@dataclass(kw_only=True)
|
|
77
|
+
class User(LifeCycleBase, BaseDomainModel):
|
|
78
|
+
id: UUID | int | str | None = None
|
|
79
|
+
username: str | None = None
|
|
80
|
+
password: str | None = None
|
|
81
|
+
role: str | Role | None = None
|
|
82
|
+
email: str | None = None
|
|
83
|
+
phone: str | None = None
|
|
84
|
+
display_name: str | None = None
|
|
85
|
+
permissions: Sequence[str] | None = None
|
|
86
|
+
groups: Sequence[str | Group] | None = None
|
|
87
|
+
is_active: bool = True
|
|
88
|
+
admin: bool = False
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_identity(cls, identity: Identity) -> Self:
|
|
92
|
+
"""Create a User instance from an Identity instance.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
identity
|
|
97
|
+
Identity object to convert.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
User instance created from the Identity.
|
|
102
|
+
"""
|
|
103
|
+
return cls(
|
|
104
|
+
id=identity.subject,
|
|
105
|
+
username=identity.username,
|
|
106
|
+
email=identity.email,
|
|
107
|
+
display_name=identity.display_name,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def update(self, obj: DomainModel) -> DomainModel:
|
|
111
|
+
"""Update the User instance with data from another User instance.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
obj
|
|
116
|
+
User object to update from.
|
|
117
|
+
"""
|
|
118
|
+
if not isinstance(obj, User):
|
|
119
|
+
raise TypeError("User.update expects a User instance.")
|
|
120
|
+
|
|
121
|
+
self.username = obj.username
|
|
122
|
+
self.email = obj.email
|
|
123
|
+
self.phone = obj.phone
|
|
124
|
+
self.display_name = obj.display_name
|
|
125
|
+
self.permissions = obj.permissions
|
|
126
|
+
self.groups = obj.groups
|
|
127
|
+
self.modified_at = datetime.now(tz=timezone.utc)
|
|
128
|
+
self.is_active = obj.is_active
|
|
129
|
+
self.admin = obj.admin
|
|
130
|
+
return cast(DomainModel, self)
|
|
@@ -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
|
|