python-general-be-lib 0.8.4__tar.gz → 0.9.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.
- {python_general_be_lib-0.8.4/python_general_be_lib.egg-info → python_general_be_lib-0.9.1}/PKG-INFO +13 -3
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/README.md +12 -2
- python_general_be_lib-0.9.1/general/exception/__init__.py +24 -0
- python_general_be_lib-0.9.1/general/exception/access_exceptions.py +22 -0
- python_general_be_lib-0.9.1/general/exception/crud_exceptions.py +89 -0
- python_general_be_lib-0.9.1/general/exception/exception_interface.py +31 -0
- python_general_be_lib-0.9.1/general/interface/__init__.py +8 -0
- python_general_be_lib-0.9.1/general/interface/base/__init__.py +12 -0
- python_general_be_lib-0.9.1/general/interface/base/base_model.py +73 -0
- python_general_be_lib-0.9.1/general/interface/base/declarative_base.py +165 -0
- python_general_be_lib-0.9.1/general/interface/metadata/__init__.py +12 -0
- python_general_be_lib-0.9.1/general/interface/metadata/crud_metadata.py +505 -0
- python_general_be_lib-0.9.1/general/interface/metadata/geom_metadata.py +208 -0
- python_general_be_lib-0.9.1/general/interface/repository/__init__.py +13 -0
- python_general_be_lib-0.9.1/general/interface/repository/crud_repository.py +812 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/general/interface/repository/geometry_repository.py +68 -92
- python_general_be_lib-0.9.1/general/interface/repository/handler/__init__.py +26 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/base_handler.py +81 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/handler.py +72 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/ilike_handler.py +89 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/interval_handler.py +106 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/json_handler.py +105 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/json_ilike_handler.py +75 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/or_handler.py +70 -0
- python_general_be_lib-0.9.1/general/interface/repository/handler/statement_manager.py +163 -0
- python_general_be_lib-0.9.1/general/interface/repository/many_to_many_repository.py +177 -0
- python_general_be_lib-0.9.1/general/interface/repository/view_repository.py +161 -0
- python_general_be_lib-0.9.1/general/logger.py +132 -0
- python_general_be_lib-0.9.1/general/paginator_dto.py +56 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/pyproject.toml +10 -2
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1/python_general_be_lib.egg-info}/PKG-INFO +13 -3
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/python_general_be_lib.egg-info/SOURCES.txt +11 -1
- python_general_be_lib-0.9.1/tests/test_entity_has_columns.py +161 -0
- python_general_be_lib-0.9.1/tests/test_has_columns.py +133 -0
- python_general_be_lib-0.9.1/tests/test_import_pubblici.py +134 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_logger.py +10 -5
- python_general_be_lib-0.9.1/tests/test_negazione.py +229 -0
- python_general_be_lib-0.9.1/tests/test_ordinamento.py +285 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_pipeline_handler.py +216 -35
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_smoke_crud.py +112 -19
- python_general_be_lib-0.9.1/tests/test_to_dict.py +185 -0
- python_general_be_lib-0.9.1/tests/test_to_pydantic.py +160 -0
- python_general_be_lib-0.9.1/tests/test_valori_falsy.py +237 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_view_repository.py +216 -10
- python_general_be_lib-0.8.4/general/exception/access_exceptions.py +0 -23
- python_general_be_lib-0.8.4/general/exception/crud_exceptions.py +0 -90
- python_general_be_lib-0.8.4/general/exception/exception_interface.py +0 -38
- python_general_be_lib-0.8.4/general/interface/base/__init__.py +0 -2
- python_general_be_lib-0.8.4/general/interface/base/base_model.py +0 -73
- python_general_be_lib-0.8.4/general/interface/base/declarative_base.py +0 -158
- python_general_be_lib-0.8.4/general/interface/metadata/__init__.py +0 -0
- python_general_be_lib-0.8.4/general/interface/metadata/crud_metadata.py +0 -519
- python_general_be_lib-0.8.4/general/interface/metadata/geom_metadata.py +0 -214
- python_general_be_lib-0.8.4/general/interface/repository/__init__.py +0 -4
- python_general_be_lib-0.8.4/general/interface/repository/crud_repository.py +0 -821
- python_general_be_lib-0.8.4/general/interface/repository/handler/__init__.py +0 -6
- python_general_be_lib-0.8.4/general/interface/repository/handler/base_handler.py +0 -122
- python_general_be_lib-0.8.4/general/interface/repository/handler/ilike_handler.py +0 -105
- python_general_be_lib-0.8.4/general/interface/repository/handler/interval_handler.py +0 -143
- python_general_be_lib-0.8.4/general/interface/repository/handler/json_handler.py +0 -93
- python_general_be_lib-0.8.4/general/interface/repository/handler/json_ilike_handler.py +0 -100
- python_general_be_lib-0.8.4/general/interface/repository/handler/or_handler.py +0 -103
- python_general_be_lib-0.8.4/general/interface/repository/handler/statement_manager.py +0 -155
- python_general_be_lib-0.8.4/general/interface/repository/many_to_many_repository.py +0 -194
- python_general_be_lib-0.8.4/general/interface/repository/view_repository.py +0 -134
- python_general_be_lib-0.8.4/general/logger.py +0 -168
- python_general_be_lib-0.8.4/general/paginator_dto.py +0 -74
- python_general_be_lib-0.8.4/setup.py +0 -14
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/LICENSE +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/general/__init__.py +0 -0
- /python_general_be_lib-0.8.4/general/exception/__init__.py → /python_general_be_lib-0.9.1/general/py.typed +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/python_general_be_lib.egg-info/requires.txt +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/python_general_be_lib.egg-info/top_level.txt +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/setup.cfg +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_caricamento_parziale.py +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_compiled_cache.py +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_query_count.py +0 -0
- {python_general_be_lib-0.8.4 → python_general_be_lib-0.9.1}/tests/test_sql_snapshot.py +0 -0
{python_general_be_lib-0.8.4/python_general_be_lib.egg-info → python_general_be_lib-0.9.1}/PKG-INFO
RENAMED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-general-be-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities.
|
|
5
5
|
Author-email: Andrea Di Placido <a.diplacido@arpes.it>, "Arpes S.r.l." <it.admin@arpes.it>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Arpes-IS/python-general-be-lib
|
|
8
8
|
Project-URL: Repository, https://github.com/Arpes-IS/python-general-be-lib
|
|
9
|
+
Project-URL: Changelog, https://github.com/Arpes-IS/python-general-be-lib/blob/main/CHANGELOG.md
|
|
9
10
|
Keywords: backend,sqlalchemy,fastapi,pydantic,repository,crud,geoalchemy2
|
|
10
11
|
Classifier: Development Status :: 3 - Alpha
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -31,7 +32,6 @@ Requires-Dist: psycopg[binary]>=3; extra == "dev"
|
|
|
31
32
|
Requires-Dist: build; extra == "dev"
|
|
32
33
|
Requires-Dist: twine; extra == "dev"
|
|
33
34
|
Dynamic: license-file
|
|
34
|
-
Dynamic: requires-python
|
|
35
35
|
|
|
36
36
|
# python-general-be-lib
|
|
37
37
|
|
|
@@ -50,7 +50,7 @@ pip install python-general-be-lib
|
|
|
50
50
|
```python
|
|
51
51
|
from sqlalchemy import create_engine
|
|
52
52
|
|
|
53
|
-
from general.interface.repository
|
|
53
|
+
from general.interface.repository import CrudRepository
|
|
54
54
|
|
|
55
55
|
engine = create_engine("postgresql://user:pass@localhost/db")
|
|
56
56
|
repo = CrudRepository(entity=User, model=UserModel, engine=engine)
|
|
@@ -72,3 +72,13 @@ repo.find(active=True, order_by="name", asc=True)
|
|
|
72
72
|
| Paginazione | `PaginatorRequestModel`, `PaginatorResponseModel` |
|
|
73
73
|
| Eccezioni | `ExceptionInterface` e sottoclassi HTTP (400/401/403/404/422/500/503) |
|
|
74
74
|
| Logging | `init_logger`, decorator `@log` |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Documentazione
|
|
79
|
+
|
|
80
|
+
| Documento | Contenuto |
|
|
81
|
+
|---|---|
|
|
82
|
+
| [`CHANGELOG.md`](https://github.com/Arpes-IS/python-general-be-lib/blob/main/CHANGELOG.md) | Modifiche per versione, con le note di migrazione |
|
|
83
|
+
| `general/README.md` | Panoramica della libreria: struttura, contratto sulle sessioni, handler, operazioni spaziali |
|
|
84
|
+
| `general/DOCS.md` | Riferimento completo, metodo per metodo |
|
|
@@ -15,7 +15,7 @@ pip install python-general-be-lib
|
|
|
15
15
|
```python
|
|
16
16
|
from sqlalchemy import create_engine
|
|
17
17
|
|
|
18
|
-
from general.interface.repository
|
|
18
|
+
from general.interface.repository import CrudRepository
|
|
19
19
|
|
|
20
20
|
engine = create_engine("postgresql://user:pass@localhost/db")
|
|
21
21
|
repo = CrudRepository(entity=User, model=UserModel, engine=engine)
|
|
@@ -36,4 +36,14 @@ repo.find(active=True, order_by="name", asc=True)
|
|
|
36
36
|
| Basi | `Base` (declarative base estesa), `ExtBaseModel`, `CamelExtBaseModel` |
|
|
37
37
|
| Paginazione | `PaginatorRequestModel`, `PaginatorResponseModel` |
|
|
38
38
|
| Eccezioni | `ExceptionInterface` e sottoclassi HTTP (400/401/403/404/422/500/503) |
|
|
39
|
-
| Logging | `init_logger`, decorator `@log` |
|
|
39
|
+
| Logging | `init_logger`, decorator `@log` |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Documentazione
|
|
44
|
+
|
|
45
|
+
| Documento | Contenuto |
|
|
46
|
+
|---|---|
|
|
47
|
+
| [`CHANGELOG.md`](https://github.com/Arpes-IS/python-general-be-lib/blob/main/CHANGELOG.md) | Modifiche per versione, con le note di migrazione |
|
|
48
|
+
| `general/README.md` | Panoramica della libreria: struttura, contratto sulle sessioni, handler, operazioni spaziali |
|
|
49
|
+
| `general/DOCS.md` | Riferimento completo, metodo per metodo |
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""HTTP exceptions of the library.
|
|
2
|
+
|
|
3
|
+
They all extend ``ExceptionInterface``, which in turn extends
|
|
4
|
+
``fastapi.HTTPException``: raised inside an endpoint they are automatically
|
|
5
|
+
serialized into the response with their own status code. The mapping applied by
|
|
6
|
+
``CrudRepository._handle_exception`` to the SQLAlchemy errors is documented in
|
|
7
|
+
``crud_exceptions``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .access_exceptions import *
|
|
11
|
+
from .crud_exceptions import *
|
|
12
|
+
from .exception_interface import *
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ExceptionInterface",
|
|
16
|
+
"BadRequestException",
|
|
17
|
+
"EntityNotFoundException",
|
|
18
|
+
"ServiceUnavailableException",
|
|
19
|
+
"UnprocessableEntityException",
|
|
20
|
+
"HasNoAttributeException",
|
|
21
|
+
"InternalServerException",
|
|
22
|
+
"ForbiddenException",
|
|
23
|
+
"UnauthorizedException",
|
|
24
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""HTTP exceptions for authentication and authorization.
|
|
2
|
+
|
|
3
|
+
Unlike ``crud_exceptions``, these exceptions are never raised automatically by the
|
|
4
|
+
library: they are available to the authentication and authorization layers of the
|
|
5
|
+
projects using it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ["ForbiddenException", "UnauthorizedException"]
|
|
9
|
+
|
|
10
|
+
from .exception_interface import ExceptionInterface
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ForbiddenException(ExceptionInterface):
|
|
14
|
+
"""403 - Authenticated user without the permissions for the requested resource."""
|
|
15
|
+
status_code = 403
|
|
16
|
+
default_message = "Risorsa non accessibile da questo utente"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnauthorizedException(ExceptionInterface):
|
|
20
|
+
"""401 - Request without valid credentials."""
|
|
21
|
+
status_code = 401
|
|
22
|
+
default_message = "Non autorizzato"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""HTTP exceptions for the persistence operations.
|
|
2
|
+
|
|
3
|
+
These are the exceptions ``CrudRepository._handle_exception`` maps the SQLAlchemy
|
|
4
|
+
errors onto: ``OperationalError`` to ``ServiceUnavailableException`` (503),
|
|
5
|
+
``NoResultFound`` to ``EntityNotFoundException`` (404), ``DataError`` and
|
|
6
|
+
``IntegrityError`` to ``BadRequestException`` (400).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BadRequestException",
|
|
11
|
+
"EntityNotFoundException",
|
|
12
|
+
"ServiceUnavailableException",
|
|
13
|
+
"UnprocessableEntityException",
|
|
14
|
+
"HasNoAttributeException",
|
|
15
|
+
"InternalServerException",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
from .exception_interface import ExceptionInterface
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BadRequestException(ExceptionInterface):
|
|
22
|
+
"""400 - Invalid request data or violated integrity constraint.
|
|
23
|
+
|
|
24
|
+
Raised in place of ``DataError`` and ``IntegrityError``, such as a foreign key,
|
|
25
|
+
unique or NOT NULL violation.
|
|
26
|
+
"""
|
|
27
|
+
status_code = 400
|
|
28
|
+
default_message = "Richiesta non corretta"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EntityNotFoundException(ExceptionInterface):
|
|
32
|
+
"""404 - No record matches the requested criteria.
|
|
33
|
+
|
|
34
|
+
Raised in place of ``NoResultFound``.
|
|
35
|
+
"""
|
|
36
|
+
status_code = 404
|
|
37
|
+
default_message = "Dato non trovato"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ServiceUnavailableException(ExceptionInterface):
|
|
41
|
+
"""503 - Database unreachable.
|
|
42
|
+
|
|
43
|
+
Raised in place of ``OperationalError``: dropped connection, exhausted pool, server
|
|
44
|
+
not listening.
|
|
45
|
+
"""
|
|
46
|
+
status_code = 503
|
|
47
|
+
default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UnprocessableEntityException(ExceptionInterface):
|
|
51
|
+
"""422 - Syntactically valid but unprocessable entity.
|
|
52
|
+
|
|
53
|
+
Never raised automatically by the repositories: it is available to the services for
|
|
54
|
+
their application validation errors.
|
|
55
|
+
"""
|
|
56
|
+
status_code = 422
|
|
57
|
+
default_message = "Entità non processabile"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HasNoAttributeException(ExceptionInterface):
|
|
61
|
+
"""400 - Column or attribute missing on the entity.
|
|
62
|
+
|
|
63
|
+
Raised when a column name passed to ``columns``, ``exclude_columns`` or ``order_by``
|
|
64
|
+
does not exist on the entity.
|
|
65
|
+
"""
|
|
66
|
+
status_code = 400
|
|
67
|
+
default_message = "Richiesta non corretta"
|
|
68
|
+
|
|
69
|
+
def __init__(self, attr: str = ""):
|
|
70
|
+
"""Compose the detail according to the presence of the attribute name.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
attr (str): Name of the missing attribute. When omitted, a generic message not
|
|
74
|
+
identifying the column is used.
|
|
75
|
+
"""
|
|
76
|
+
if attr:
|
|
77
|
+
super().__init__(custom_message=f"Attributo {attr} non presente", )
|
|
78
|
+
else:
|
|
79
|
+
super().__init__(custom_message="Alcuni attributi non sono presenti per questa entità", )
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InternalServerException(ExceptionInterface):
|
|
83
|
+
"""500 - Unexpected internal error.
|
|
84
|
+
|
|
85
|
+
Used as a fallback by the ``@log`` decorator for every exception that is not already
|
|
86
|
+
an ``HTTPException``, and by ``CrudMetadata.get_table`` for a missing table.
|
|
87
|
+
"""
|
|
88
|
+
status_code = 500
|
|
89
|
+
default_message = "Impossibile gestire la richiesta al momento"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__all__ = ["ExceptionInterface"]
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExceptionInterface(HTTPException):
|
|
7
|
+
"""Base interface for every exception of the library.
|
|
8
|
+
|
|
9
|
+
Extends ``fastapi.HTTPException`` moving status code and message to **class
|
|
10
|
+
attributes**: every subclass declares its own ``status_code`` and
|
|
11
|
+
``default_message`` and can therefore be raised without arguments. Raised inside a
|
|
12
|
+
FastAPI endpoint, it is automatically serialized into the error response with the
|
|
13
|
+
declared status code.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
status_code (int): HTTP status code of the response.
|
|
17
|
+
default_message (str): Default message used as ``detail``.
|
|
18
|
+
"""
|
|
19
|
+
status_code: int
|
|
20
|
+
default_message: str
|
|
21
|
+
|
|
22
|
+
def __init__(self, custom_message: str = None):
|
|
23
|
+
"""Compose the ``detail`` from the default message and the custom one.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
custom_message (str, optional): Additional detail, appended to the
|
|
27
|
+
``default_message`` as ``"{default_message} - {custom_message}"``. When
|
|
28
|
+
omitted, the ``detail`` is the ``default_message`` alone.
|
|
29
|
+
"""
|
|
30
|
+
detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
|
|
31
|
+
super().__init__(status_code=self.status_code, detail=detail)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""ORM repositories, ORM-free metadata layer and shared base classes.
|
|
2
|
+
|
|
3
|
+
Container package: the symbols are imported from the subpackages
|
|
4
|
+
(``general.interface.base``, ``general.interface.repository``,
|
|
5
|
+
``general.interface.repository.handler``, ``general.interface.metadata``). It is
|
|
6
|
+
declared explicitly, instead of being left as an implicit namespace package, so as
|
|
7
|
+
not to depend on the PEP 420 support of the tools consuming the distribution.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""SQLAlchemy declarative base class and preconfigured Pydantic models."""
|
|
2
|
+
|
|
3
|
+
from .base_model import *
|
|
4
|
+
from .declarative_base import *
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Base",
|
|
8
|
+
"retrieve_mapper_entity",
|
|
9
|
+
"retrieve_mapper_from_table",
|
|
10
|
+
"ExtBaseModel",
|
|
11
|
+
"CamelExtBaseModel",
|
|
12
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
__all__ = ["ExtBaseModel", "CamelExtBaseModel"]
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
from pydantic.alias_generators import to_camel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@lru_cache(maxsize=None)
|
|
10
|
+
def _alias_to_name(model: type[BaseModel]) -> dict[str, str]:
|
|
11
|
+
"""Build the ``{alias: field_name}`` map of a model, once per class.
|
|
12
|
+
|
|
13
|
+
Cached on the class, which is long-lived, to avoid the linear scan of
|
|
14
|
+
``model_fields`` on every ``get_attr_by_alias`` call.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
model (type[BaseModel]): Pydantic model to inspect.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict[str, str]: Map from alias to Python field name, limited to the fields
|
|
21
|
+
declaring an alias.
|
|
22
|
+
"""
|
|
23
|
+
return {field.alias: name for name, field in model.model_fields.items() if field.alias}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConfigModel(BaseModel):
|
|
27
|
+
"""Base Pydantic model carrying the shared configuration.
|
|
28
|
+
|
|
29
|
+
Enables ``from_attributes=True`` for the validation from SQLAlchemy ORM objects,
|
|
30
|
+
``populate_by_name=True`` to accept both the Python name and the alias,
|
|
31
|
+
``arbitrary_types_allowed=True`` to allow non-standard types such as ``WKBElement``,
|
|
32
|
+
and ``validate_default=True`` to validate the default values on creation.
|
|
33
|
+
"""
|
|
34
|
+
model_config = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CamelConfigModel(ConfigModel):
|
|
38
|
+
"""Extend ``ConfigModel`` adding the automatic generation of camelCase aliases."""
|
|
39
|
+
model_config = ConfigDict(alias_generator=to_camel)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_attr_by_alias(cls, alias: str) -> str:
|
|
43
|
+
"""Return the Python field name given a camelCase alias.
|
|
44
|
+
|
|
45
|
+
Useful to resolve the internal field name when receiving a payload with camelCase
|
|
46
|
+
keys.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
alias (str): camelCase alias of the field, such as ``"myField"``.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: Python name of the field, such as ``"my_field"``, or the alias itself when
|
|
53
|
+
not found.
|
|
54
|
+
"""
|
|
55
|
+
return _alias_to_name(cls).get(alias, alias)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ExtBaseModel(ConfigModel):
|
|
59
|
+
"""Pydantic model with the standard configuration for the ORM integration.
|
|
60
|
+
|
|
61
|
+
Inherits from ``ConfigModel``. Meant as the base of every model that has to be
|
|
62
|
+
validated from a SQLAlchemy entity.
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CamelExtBaseModel(CamelConfigModel):
|
|
68
|
+
"""Pydantic model with automatic camelCase aliases and ORM integration.
|
|
69
|
+
|
|
70
|
+
Inherits from ``CamelConfigModel``. Meant for the cases where the API exposes
|
|
71
|
+
camelCase fields while the code works in snake_case.
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
__all__ = ["Base", "retrieve_mapper_entity", "retrieve_mapper_from_table"]
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Optional, Any, Sequence
|
|
5
|
+
|
|
6
|
+
from geoalchemy2 import Geometry, WKBElement
|
|
7
|
+
from pydantic import create_model, ConfigDict
|
|
8
|
+
from sqlalchemy.inspection import inspect
|
|
9
|
+
from sqlalchemy.orm import Mapper, as_declarative
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@as_declarative()
|
|
13
|
+
class Base:
|
|
14
|
+
"""SQLAlchemy declarative base class enriched with utility methods.
|
|
15
|
+
|
|
16
|
+
Every ORM entity of the project has to extend this class. It gives quick access to
|
|
17
|
+
columns and primary keys, plus the conversion towards Pydantic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def to_pydantic(cls, model_name: str = None, full_optional: bool = False, default_values: dict[str, Any] = None, include_only: Sequence[str] = None, **create_model_kwargs):
|
|
22
|
+
"""Generate a Pydantic model dynamically from the entity columns.
|
|
23
|
+
|
|
24
|
+
Iterates over the ORM-mapped columns and builds the model fields honouring the
|
|
25
|
+
nullability and the given default values; ``Geometry`` columns are mapped onto
|
|
26
|
+
``WKBElement``. When the caller passes neither ``__config__`` nor ``__base__``, the
|
|
27
|
+
generated model gets the same configuration as ``ExtBaseModel``. The default is all
|
|
28
|
+
or nothing: passing ``__config__`` puts the whole configuration in the hands of the
|
|
29
|
+
caller, and passing ``__base__`` lets the base carry its own.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
model_name (str, optional): Name of the generated model. When not given,
|
|
33
|
+
``{ClassName}Model`` is used.
|
|
34
|
+
full_optional (bool): If ``True``, every field becomes ``Optional`` with a
|
|
35
|
+
``None`` default, regardless of the nullability in the database.
|
|
36
|
+
Default: ``False``.
|
|
37
|
+
default_values (dict[str, Any], optional): ``{field_name: default_value}`` map
|
|
38
|
+
overriding the inferred defaults.
|
|
39
|
+
include_only (Sequence[str], optional): Attribute names to restrict the model to.
|
|
40
|
+
When ``None``, every mapped column is included.
|
|
41
|
+
**create_model_kwargs: Additional arguments passed to ``pydantic.create_model``.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Type[BaseModel]: Dynamically generated Pydantic class.
|
|
45
|
+
"""
|
|
46
|
+
if "__config__" not in create_model_kwargs.keys() and "__base__" not in create_model_kwargs.keys():
|
|
47
|
+
create_model_kwargs["__config__"] = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
|
|
48
|
+
|
|
49
|
+
if not default_values:
|
|
50
|
+
default_values = dict()
|
|
51
|
+
mapper: Mapper = inspect(cls)
|
|
52
|
+
|
|
53
|
+
# ``mapper.columns`` è indicizzato per **nome di attributo** e non forza la
|
|
54
|
+
# configurazione dei mapper: ``column_attrs``/``attrs``/``relationships`` la
|
|
55
|
+
# forzerebbero, e chiamare ``to_pydantic`` a import-time — il pattern
|
|
56
|
+
# documentato — romperebbe le relationship con forward-ref non ancora
|
|
57
|
+
# risolvibili (stessa ragione per cui ``relationship_attributes`` è lazy).
|
|
58
|
+
selected = [(name, column) for name, column in mapper.columns.items()
|
|
59
|
+
if include_only is None or name in include_only]
|
|
60
|
+
|
|
61
|
+
model_name = f"{cls.__name__}Model" if not model_name else model_name
|
|
62
|
+
fields = dict()
|
|
63
|
+
field_values = {name: None if full_optional else ... for name, _ in selected}
|
|
64
|
+
field_values.update(default_values)
|
|
65
|
+
for field_name, col in selected:
|
|
66
|
+
if isinstance(col.type, Geometry):
|
|
67
|
+
field_type = Optional[WKBElement] if col.nullable or full_optional else WKBElement
|
|
68
|
+
else:
|
|
69
|
+
field_type = Optional[col.type.python_type] if col.nullable or full_optional else col.type.python_type
|
|
70
|
+
fields[field_name] = (field_type, field_values[field_name])
|
|
71
|
+
return create_model(model_name, **create_model_kwargs, **fields)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def columns(cls):
|
|
75
|
+
"""Return the column collection of the table the entity is mapped to.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ReadOnlyColumnCollection: Collection of the SQLAlchemy columns.
|
|
79
|
+
"""
|
|
80
|
+
return cls.__table__.c
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def columns_names(cls):
|
|
84
|
+
"""Return the names of every column of the table.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
list[str]: Names of the columns, in definition order. They are the **database**
|
|
88
|
+
names, which differ from the Python attribute names on the entities
|
|
89
|
+
declaring an explicit column name.
|
|
90
|
+
"""
|
|
91
|
+
return [col.name for col in cls.__table__.c]
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def primary_key(cls):
|
|
95
|
+
"""Return the columns making up the primary key of the table.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
list[Column]: Columns forming the primary key.
|
|
99
|
+
"""
|
|
100
|
+
return cls.__table__.primary_key.columns.values()
|
|
101
|
+
|
|
102
|
+
def to_dict(self):
|
|
103
|
+
"""Convert the instance into an ``{attribute: value}`` dictionary.
|
|
104
|
+
|
|
105
|
+
The keys are the names of the **Python attributes** of the mapped columns, not the
|
|
106
|
+
names of the columns in the database; the two differ only on the entities declaring
|
|
107
|
+
an explicit column name. Relationships are not included, only columns.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict[str, Any]: Current values of the instance.
|
|
111
|
+
"""
|
|
112
|
+
return {key: getattr(self, key) for key in _attributes_names(self.__class__)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@lru_cache(maxsize=None)
|
|
116
|
+
def _attributes_names(entity: type) -> tuple[str, ...]:
|
|
117
|
+
"""Return the Python attribute names of the mapped columns, memoized per class.
|
|
118
|
+
|
|
119
|
+
The memoization is at module level rather than a class attribute: the cache key is
|
|
120
|
+
the class itself, so a subclass in single table inheritance gets its own tuple
|
|
121
|
+
instead of inheriting the one of its parent. The names are read from
|
|
122
|
+
``__mapper__.columns`` and not from ``column_attrs``, which would force
|
|
123
|
+
``configure_mappers()``.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
entity (type): ORM class to inspect.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
tuple[str, ...]: Attribute names, in definition order.
|
|
130
|
+
"""
|
|
131
|
+
return tuple(entity.__mapper__.columns.keys())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def retrieve_mapper_entity(entity_name: str):
|
|
135
|
+
"""Look up a subclass of ``Base`` by class name.
|
|
136
|
+
|
|
137
|
+
Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
|
|
138
|
+
of ``Base`` and not the ones nested in further inheritance levels.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
entity_name (str): Name of the ORM class to look for, such as ``"User"``.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Type[Base] | None: The matching ORM class, or ``None`` when not found.
|
|
145
|
+
"""
|
|
146
|
+
for mapper in Base.__subclasses__():
|
|
147
|
+
if mapper.__name__ == entity_name:
|
|
148
|
+
return mapper
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def retrieve_mapper_from_table(table_name: str):
|
|
152
|
+
"""Look up a subclass of ``Base`` by table name.
|
|
153
|
+
|
|
154
|
+
Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
|
|
155
|
+
of ``Base`` and not the ones nested in further inheritance levels.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
table_name (str): Name of the table in the database, such as ``"users"``.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Type[Base] | None: The matching ORM class, or ``None`` when not found.
|
|
162
|
+
"""
|
|
163
|
+
for mapper in Base.__subclasses__():
|
|
164
|
+
if mapper.__tablename__ == table_name:
|
|
165
|
+
return mapper
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CRUD and spatial layer over reflected ``Table`` objects, without the ORM.
|
|
2
|
+
|
|
3
|
+
Alternative to the repositories to operate on tables having no ORM model: the schema
|
|
4
|
+
is reflected at runtime and the tables are resolved by name. Unlike the
|
|
5
|
+
repositories, this layer does **not** use the handler pipeline and does **not** map
|
|
6
|
+
the SQLAlchemy exceptions onto HTTP exceptions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .crud_metadata import *
|
|
10
|
+
from .geom_metadata import *
|
|
11
|
+
|
|
12
|
+
__all__ = ["CrudMetadata", "GeomMetadata"]
|