python-general-be-lib 0.7.1__tar.gz → 0.8.0__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.0/PKG-INFO +108 -0
- python_general_be_lib-0.8.0/README.md +76 -0
- python_general_be_lib-0.8.0/general/exception/access_exceptions.py +23 -0
- python_general_be_lib-0.8.0/general/exception/crud_exceptions.py +90 -0
- python_general_be_lib-0.8.0/general/exception/exception_interface.py +38 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/base/declarative_base.py +6 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/metadata/crud_metadata.py +11 -2
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/metadata/geom_metadata.py +4 -1
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/crud_repository.py +32 -7
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/geometry_repository.py +14 -16
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/base_handler.py +13 -1
- python_general_be_lib-0.8.0/general/interface/repository/handler/interval_handler.py +122 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/json_handler.py +8 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/json_ilike_handler.py +45 -21
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/or_handler.py +37 -24
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/statement_manager.py +13 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/many_to_many_repository.py +10 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/logger.py +7 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/pyproject.toml +1 -1
- python_general_be_lib-0.8.0/python_general_be_lib.egg-info/PKG-INFO +108 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/setup.py +1 -1
- python_general_be_lib-0.7.1/PKG-INFO +0 -33
- python_general_be_lib-0.7.1/README.md +0 -1
- python_general_be_lib-0.7.1/general/exception/access_exceptions.py +0 -11
- python_general_be_lib-0.7.1/general/exception/crud_exceptions.py +0 -37
- python_general_be_lib-0.7.1/general/exception/exception_interface.py +0 -10
- python_general_be_lib-0.7.1/general/interface/repository/handler/interval_handler.py +0 -92
- python_general_be_lib-0.7.1/python_general_be_lib.egg-info/PKG-INFO +0 -33
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/LICENSE +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/__init__.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/exception/__init__.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/base/__init__.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/base/base_model.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/metadata/__init__.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/__init__.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/__init__.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/ilike_handler.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/view_repository.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/paginator_dto.py +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/python_general_be_lib.egg-info/SOURCES.txt +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/python_general_be_lib.egg-info/requires.txt +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/python_general_be_lib.egg-info/top_level.txt +0 -0
- {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/setup.cfg +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-general-be-lib
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities.
|
|
5
|
+
Author-email: Andrea Di Placido <a.diplacido@arpes.it>, "Arpes S.r.l." <it.admin@arpes.it>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Arpes-IS/python-general-be-lib
|
|
8
|
+
Project-URL: Repository, https://github.com/Arpes-IS/python-general-be-lib
|
|
9
|
+
Keywords: backend,sqlalchemy,fastapi,pydantic,repository,crud,geoalchemy2
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fastapi>=0.100.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
24
|
+
Requires-Dist: geoalchemy2>=0.14.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
|
|
33
|
+
# python-general-be-lib
|
|
34
|
+
|
|
35
|
+
Libreria backend general purpose per progetti Python: repository CRUD e spaziali
|
|
36
|
+
su **SQLAlchemy** / **GeoAlchemy2**, eccezioni **FastAPI**, modelli base
|
|
37
|
+
**Pydantic** e utility di logging.
|
|
38
|
+
|
|
39
|
+
- Distribuzione PyPI: `python-general-be-lib`
|
|
40
|
+
- Pacchetto importabile: `general`
|
|
41
|
+
- Python richiesto: **3.12+**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install python-general-be-lib
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from sqlalchemy import create_engine
|
|
49
|
+
|
|
50
|
+
from general.interface.repository.crud_repository import CrudRepository
|
|
51
|
+
|
|
52
|
+
engine = create_engine("postgresql://user:pass@localhost/db")
|
|
53
|
+
repo = CrudRepository(entity=User, model=UserModel, engine=engine)
|
|
54
|
+
|
|
55
|
+
repo.insert(models=[UserModel(name="Mario", email="mario@example.com")])
|
|
56
|
+
repo.find(active=True, order_by="name", asc=True)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Cosa contiene
|
|
62
|
+
|
|
63
|
+
| Area | Componenti |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Repository ORM | `CrudRepository`, `GeometryRepository`, `ManyToManyRepository`, `ViewRepository` |
|
|
66
|
+
| Layer Core (senza ORM) | `CrudMetadata`, `GeomMetadata` — CRUD e query spaziali su `Table` riflesse |
|
|
67
|
+
| Motore di filtraggio | `StatementManager` + `BaseHandler`, `JsonHandler`, `ILikeHandler`, `JsonILikeHandler`, `OrHandler`, `IntervalHandler` |
|
|
68
|
+
| Basi | `Base` (declarative base estesa), `ExtBaseModel`, `CamelExtBaseModel` |
|
|
69
|
+
| Paginazione | `PaginatorRequestModel`, `PaginatorResponseModel` |
|
|
70
|
+
| Eccezioni | `ExceptionInterface` e sottoclassi HTTP (400/401/403/404/422/500/503) |
|
|
71
|
+
| Logging | `init_logger`, decorator `@log` |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Documentazione
|
|
76
|
+
|
|
77
|
+
Il codice della libreria vive nella directory `general/`, che è un **git
|
|
78
|
+
submodule** ([novatellus/shared-be-libs](https://github.com/novatellus/shared-be-libs)).
|
|
79
|
+
Questo repository esterno è il wrapper di packaging e pubblicazione.
|
|
80
|
+
|
|
81
|
+
- **[general/README.md](general/README.md)** — panoramica, quickstart, contratto
|
|
82
|
+
di sessione, handler, operazioni spaziali
|
|
83
|
+
- **[general/DOCS.md](general/DOCS.md)** — riferimento completo di ogni metodo
|
|
84
|
+
|
|
85
|
+
Dopo il clone, inizializza il submodule:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git submodule update --init --recursive
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Build e pubblicazione
|
|
94
|
+
|
|
95
|
+
La versione è definita in **entrambi** `setup.py` e `pyproject.toml`: vanno
|
|
96
|
+
aggiornati insieme. I messaggi di commit sono prefissati con la nuova versione
|
|
97
|
+
(es. `0.7.2 OrHandler e distinct_pairs`).
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 setup.py sdist bdist_wheel # build di sdist + wheel in dist/
|
|
101
|
+
python3 -m twine upload dist/* # pubblicazione su PyPI
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Licenza
|
|
107
|
+
|
|
108
|
+
MIT — vedi [LICENSE](LICENSE).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# python-general-be-lib
|
|
2
|
+
|
|
3
|
+
Libreria backend general purpose per progetti Python: repository CRUD e spaziali
|
|
4
|
+
su **SQLAlchemy** / **GeoAlchemy2**, eccezioni **FastAPI**, modelli base
|
|
5
|
+
**Pydantic** e utility di logging.
|
|
6
|
+
|
|
7
|
+
- Distribuzione PyPI: `python-general-be-lib`
|
|
8
|
+
- Pacchetto importabile: `general`
|
|
9
|
+
- Python richiesto: **3.12+**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install python-general-be-lib
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from sqlalchemy import create_engine
|
|
17
|
+
|
|
18
|
+
from general.interface.repository.crud_repository import CrudRepository
|
|
19
|
+
|
|
20
|
+
engine = create_engine("postgresql://user:pass@localhost/db")
|
|
21
|
+
repo = CrudRepository(entity=User, model=UserModel, engine=engine)
|
|
22
|
+
|
|
23
|
+
repo.insert(models=[UserModel(name="Mario", email="mario@example.com")])
|
|
24
|
+
repo.find(active=True, order_by="name", asc=True)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Cosa contiene
|
|
30
|
+
|
|
31
|
+
| Area | Componenti |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Repository ORM | `CrudRepository`, `GeometryRepository`, `ManyToManyRepository`, `ViewRepository` |
|
|
34
|
+
| Layer Core (senza ORM) | `CrudMetadata`, `GeomMetadata` — CRUD e query spaziali su `Table` riflesse |
|
|
35
|
+
| Motore di filtraggio | `StatementManager` + `BaseHandler`, `JsonHandler`, `ILikeHandler`, `JsonILikeHandler`, `OrHandler`, `IntervalHandler` |
|
|
36
|
+
| Basi | `Base` (declarative base estesa), `ExtBaseModel`, `CamelExtBaseModel` |
|
|
37
|
+
| Paginazione | `PaginatorRequestModel`, `PaginatorResponseModel` |
|
|
38
|
+
| Eccezioni | `ExceptionInterface` e sottoclassi HTTP (400/401/403/404/422/500/503) |
|
|
39
|
+
| Logging | `init_logger`, decorator `@log` |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Documentazione
|
|
44
|
+
|
|
45
|
+
Il codice della libreria vive nella directory `general/`, che è un **git
|
|
46
|
+
submodule** ([novatellus/shared-be-libs](https://github.com/novatellus/shared-be-libs)).
|
|
47
|
+
Questo repository esterno è il wrapper di packaging e pubblicazione.
|
|
48
|
+
|
|
49
|
+
- **[general/README.md](general/README.md)** — panoramica, quickstart, contratto
|
|
50
|
+
di sessione, handler, operazioni spaziali
|
|
51
|
+
- **[general/DOCS.md](general/DOCS.md)** — riferimento completo di ogni metodo
|
|
52
|
+
|
|
53
|
+
Dopo il clone, inizializza il submodule:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git submodule update --init --recursive
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Build e pubblicazione
|
|
62
|
+
|
|
63
|
+
La versione è definita in **entrambi** `setup.py` e `pyproject.toml`: vanno
|
|
64
|
+
aggiornati insieme. I messaggi di commit sono prefissati con la nuova versione
|
|
65
|
+
(es. `0.7.2 OrHandler e distinct_pairs`).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python3 setup.py sdist bdist_wheel # build di sdist + wheel in dist/
|
|
69
|
+
python3 -m twine upload dist/* # pubblicazione su PyPI
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Licenza
|
|
75
|
+
|
|
76
|
+
MIT — vedi [LICENSE](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Eccezioni HTTP per autenticazione e autorizzazione.
|
|
2
|
+
|
|
3
|
+
A differenza di ``crud_exceptions``, queste eccezioni non sono sollevate
|
|
4
|
+
automaticamente dalla libreria: sono a disposizione dei layer di autenticazione
|
|
5
|
+
e autorizzazione dei progetti che la usano.
|
|
6
|
+
|
|
7
|
+
Il package ``general.exception`` non re-esporta questi simboli: vanno importati
|
|
8
|
+
dal modulo, ``from general.exception.access_exceptions import ForbiddenException``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .exception_interface import ExceptionInterface
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ForbiddenException(ExceptionInterface):
|
|
15
|
+
"""403 — Utente autenticato ma senza i permessi per la risorsa richiesta."""
|
|
16
|
+
status_code = 403
|
|
17
|
+
default_message = "Risorsa non accessibile da questo utente"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UnauthorizedException(ExceptionInterface):
|
|
21
|
+
"""401 — Richiesta priva di credenziali valide."""
|
|
22
|
+
status_code = 401
|
|
23
|
+
default_message = "Non autorizzato"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Eccezioni HTTP per le operazioni di persistenza.
|
|
2
|
+
|
|
3
|
+
Sono le eccezioni su cui ``CrudRepository._handle_exception`` mappa gli errori
|
|
4
|
+
SQLAlchemy:
|
|
5
|
+
|
|
6
|
+
- ``OperationalError`` → ``ServiceUnavailableException`` (503)
|
|
7
|
+
- ``NoResultFound`` → ``EntityNotFoundException`` (404)
|
|
8
|
+
- ``DataError`` | ``IntegrityError`` → ``BadRequestException`` (400)
|
|
9
|
+
|
|
10
|
+
Il package ``general.exception`` non re-esporta questi simboli: vanno importati
|
|
11
|
+
dal modulo, ``from general.exception.crud_exceptions import BadRequestException``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .exception_interface import ExceptionInterface
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BadRequestException(ExceptionInterface):
|
|
18
|
+
"""400 — Dati della richiesta non validi o vincolo di integrità violato.
|
|
19
|
+
|
|
20
|
+
Sollevata al posto di ``DataError`` e ``IntegrityError`` (es. violazione di
|
|
21
|
+
una foreign key, di un unique o di un NOT NULL).
|
|
22
|
+
"""
|
|
23
|
+
status_code = 400
|
|
24
|
+
default_message = "Richiesta non corretta"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EntityNotFoundException(ExceptionInterface):
|
|
28
|
+
"""404 — Nessun record corrisponde ai criteri richiesti.
|
|
29
|
+
|
|
30
|
+
Sollevata al posto di ``NoResultFound``.
|
|
31
|
+
"""
|
|
32
|
+
status_code = 404
|
|
33
|
+
default_message = "Dato non trovato"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ServiceUnavailableException(ExceptionInterface):
|
|
37
|
+
"""503 — Database non raggiungibile.
|
|
38
|
+
|
|
39
|
+
Sollevata al posto di ``OperationalError`` (connessione caduta, pool esaurito,
|
|
40
|
+
server non in ascolto).
|
|
41
|
+
"""
|
|
42
|
+
status_code = 503
|
|
43
|
+
default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UnprocessableEntityException(ExceptionInterface):
|
|
47
|
+
"""422 — Entità sintatticamente valida ma non processabile.
|
|
48
|
+
|
|
49
|
+
Non è sollevata automaticamente dai repository: è a disposizione dei service
|
|
50
|
+
per gli errori di validazione applicativa.
|
|
51
|
+
"""
|
|
52
|
+
status_code = 422
|
|
53
|
+
default_message = "Entità non processabile"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HasNoAttributeException(ExceptionInterface):
|
|
57
|
+
"""400 — Colonna o attributo non presente sull'entità.
|
|
58
|
+
|
|
59
|
+
Sollevata quando un nome di colonna passato a ``columns``, ``exclude_columns``
|
|
60
|
+
o ``order_by`` non esiste sull'entità.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> raise HasNoAttributeException("cognome")
|
|
64
|
+
# detail: "Richiesta non corretta - Attributo cognome non presente"
|
|
65
|
+
"""
|
|
66
|
+
status_code = 400
|
|
67
|
+
default_message = "Richiesta non corretta"
|
|
68
|
+
|
|
69
|
+
def __init__(self, attr: str = ""):
|
|
70
|
+
"""Compone il dettaglio in base alla presenza del nome dell'attributo.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
attr (str): Nome dell'attributo mancante. Se omesso, viene usato un
|
|
74
|
+
messaggio generico che non identifica la colonna.
|
|
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 — Errore interno non previsto.
|
|
84
|
+
|
|
85
|
+
Usata come fallback dal decorator ``@log`` per ogni eccezione che non sia già
|
|
86
|
+
una ``HTTPException``, e da ``CrudMetadata.get_table`` per una tabella
|
|
87
|
+
inesistente.
|
|
88
|
+
"""
|
|
89
|
+
status_code = 500
|
|
90
|
+
default_message = "Impossibile gestire la richiesta al momento"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExceptionInterface(HTTPException):
|
|
5
|
+
"""Interfaccia base per tutte le eccezioni della libreria.
|
|
6
|
+
|
|
7
|
+
Estende ``fastapi.HTTPException`` spostando status code e messaggio su
|
|
8
|
+
**attributi di classe**: ogni sottoclasse dichiara il proprio ``status_code``
|
|
9
|
+
e ``default_message`` e può quindi essere sollevata senza argomenti.
|
|
10
|
+
|
|
11
|
+
Sollevata dentro un endpoint FastAPI, viene serializzata automaticamente nella
|
|
12
|
+
risposta di errore con lo status code dichiarato.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
status_code (int): Status code HTTP della risposta.
|
|
16
|
+
default_message (str): Messaggio di default usato come ``detail``.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> class TeapotException(ExceptionInterface):
|
|
20
|
+
... status_code = 418
|
|
21
|
+
... default_message = "Sono una teiera"
|
|
22
|
+
>>> raise TeapotException("niente caffè")
|
|
23
|
+
# detail: "Sono una teiera - niente caffè"
|
|
24
|
+
"""
|
|
25
|
+
status_code: int
|
|
26
|
+
default_message: str
|
|
27
|
+
|
|
28
|
+
def __init__(self, custom_message: str = None):
|
|
29
|
+
"""Compone il ``detail`` a partire dal messaggio di default e da quello custom.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
custom_message (str, optional): Dettaglio aggiuntivo, accodato al
|
|
33
|
+
``default_message`` nella forma
|
|
34
|
+
``"{default_message} - {custom_message}"``. Se omesso, il ``detail``
|
|
35
|
+
è il solo ``default_message``.
|
|
36
|
+
"""
|
|
37
|
+
detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
|
|
38
|
+
super().__init__(status_code=self.status_code, detail=detail)
|
|
@@ -111,6 +111,9 @@ class Base:
|
|
|
111
111
|
def retrieve_mapper_entity(entity_name: str):
|
|
112
112
|
"""Cerca e restituisce una sottoclasse di ``Base`` per nome della classe.
|
|
113
113
|
|
|
114
|
+
Itera su ``Base.__subclasses__()``: trova quindi solo le sottoclassi **dirette**
|
|
115
|
+
di ``Base``, non quelle annidate in ulteriori livelli di ereditarietà.
|
|
116
|
+
|
|
114
117
|
Args:
|
|
115
118
|
entity_name (str): Nome della classe ORM da cercare (es. ``"User"``).
|
|
116
119
|
|
|
@@ -129,6 +132,9 @@ def retrieve_mapper_entity(entity_name: str):
|
|
|
129
132
|
def retrieve_mapper_from_table(table_name: str):
|
|
130
133
|
"""Cerca e restituisce una sottoclasse di ``Base`` per nome della tabella.
|
|
131
134
|
|
|
135
|
+
Itera su ``Base.__subclasses__()``: trova quindi solo le sottoclassi **dirette**
|
|
136
|
+
di ``Base``, non quelle annidate in ulteriori livelli di ereditarietà.
|
|
137
|
+
|
|
132
138
|
Args:
|
|
133
139
|
table_name (str): Nome della tabella nel database (es. ``"users"``).
|
|
134
140
|
|
|
@@ -23,6 +23,14 @@ class CrudMetadata:
|
|
|
23
23
|
chiusura; altrimenti la connessione viene aperta, committata e chiusa
|
|
24
24
|
internamente.
|
|
25
25
|
|
|
26
|
+
Differenze rispetto a ``CrudRepository``:
|
|
27
|
+
- **non** usa la pipeline degli handler: i filtri passano direttamente da
|
|
28
|
+
``_eq_where``, quindi non c'è supporto per JSON, ILIKE, intervalli o alias;
|
|
29
|
+
- le eccezioni SQLAlchemy **non** vengono mappate su eccezioni HTTP: vengono
|
|
30
|
+
loggate e rilanciate tal quali (vedi ``_handle_exception``);
|
|
31
|
+
- la serializzazione avviene con ``parsing_model(**row)`` sui mapping delle
|
|
32
|
+
righe, non con ``model_validate`` su entità ORM.
|
|
33
|
+
|
|
26
34
|
Attributes:
|
|
27
35
|
schema (str | None): Nome dello schema di default per le tabelle.
|
|
28
36
|
metadata (MetaData): Contenitore SQLAlchemy delle tabelle riflesse/definite.
|
|
@@ -483,9 +491,10 @@ class CrudMetadata:
|
|
|
483
491
|
def _eq_where(self, col: Column, condition, neq: bool = False):
|
|
484
492
|
"""Costruisce la singola condizione WHERE per una colonna.
|
|
485
493
|
|
|
486
|
-
Logica applicata:
|
|
494
|
+
Logica applicata (stessa di ``BaseHandler._handle``):
|
|
495
|
+
- Colonna ``ARRAY`` + ``list`` → ``col && ARRAY[values]`` (overlap)
|
|
496
|
+
- Colonna ``ARRAY`` + scalare → ``value = ANY(col)``
|
|
487
497
|
- ``list`` → ``col IN (values)``
|
|
488
|
-
- Tipo ``ARRAY`` → ``value = ANY(col)``
|
|
489
498
|
- Scalare → ``col == value``
|
|
490
499
|
|
|
491
500
|
Args:
|
|
@@ -19,7 +19,10 @@ class GeomMetadata(CrudMetadata):
|
|
|
19
19
|
della tabella.
|
|
20
20
|
|
|
21
21
|
Gestisce la riproiezione automatica tra SRID diversi e il buffering opzionale
|
|
22
|
-
della geometria di input.
|
|
22
|
+
della geometria di input. Il parametro ``tolerance`` è sempre un **buffer**
|
|
23
|
+
(``ST_Buffer``) applicato alla geometria di input: a differenza di
|
|
24
|
+
``GeometryRepository.intersects``, che con ``tolerance > 0`` passa a
|
|
25
|
+
``ST_DWithin``, qui il predicato resta ``ST_Intersects``.
|
|
23
26
|
|
|
24
27
|
Example:
|
|
25
28
|
>>> meta = GeomMetadata(engine=engine, schema_name="public")
|
|
@@ -179,8 +179,9 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
179
179
|
|
|
180
180
|
Args:
|
|
181
181
|
*columns (str): Nomi delle colonne da verificare.
|
|
182
|
-
entity (Type[Base]
|
|
183
|
-
|
|
182
|
+
entity (Type[Base]): Entità su cui verificare. Obbligatoria nei fatti:
|
|
183
|
+
pur avendo default ``None``, il metodo non applica nessun fallback
|
|
184
|
+
a ``self.entity`` e con ``None`` solleva ``AttributeError``.
|
|
184
185
|
|
|
185
186
|
Returns:
|
|
186
187
|
bool: ``True`` se tutte le colonne sono presenti, ``False`` altrimenti.
|
|
@@ -301,6 +302,13 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
301
302
|
list[Entity]: Lista di modelli serializzati (o entità grezze se
|
|
302
303
|
sessione esterna).
|
|
303
304
|
|
|
305
|
+
Raises:
|
|
306
|
+
HasNoAttributeException: Se una colonna in ``columns``,
|
|
307
|
+
``exclude_columns`` o ``order_by`` non esiste sull'entità.
|
|
308
|
+
|
|
309
|
+
Note:
|
|
310
|
+
Essendo una lettura pura non esegue nessun commit (``commit=False``).
|
|
311
|
+
|
|
304
312
|
Example:
|
|
305
313
|
>>> repo.find(active=True, limit=10)
|
|
306
314
|
>>> repo.find(where={"role": ["admin", "editor"]})
|
|
@@ -606,16 +614,20 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
606
614
|
"""Esegue una ricerca paginata con supporto a ordinamento e offset.
|
|
607
615
|
|
|
608
616
|
Calcola il numero totale di pagine in base al count reale dei record
|
|
609
|
-
filtrati, e restituisce solo la pagina richiesta.
|
|
617
|
+
filtrati, e restituisce solo la pagina richiesta. Il count riusa la
|
|
618
|
+
``whereclause`` già costruita sullo statement di selezione
|
|
619
|
+
(``_preconf_count``), così i filtri vengono tradotti una sola volta.
|
|
610
620
|
|
|
611
621
|
Args:
|
|
612
622
|
session (Session, optional): Sessione esterna.
|
|
613
623
|
where (dict[str, Any], optional): Filtri come dizionario.
|
|
614
624
|
columns (list[str], optional): Colonne da includere.
|
|
615
625
|
exclude_columns (list[str], optional): Colonne da escludere.
|
|
616
|
-
limit (int): Numero di elementi per pagina. Default: 0 (nessun limite
|
|
626
|
+
limit (int): Numero di elementi per pagina. Default: 0 (nessun limite,
|
|
627
|
+
quindi una sola pagina).
|
|
617
628
|
page (int): Numero di pagina richiesta (1-based). Default: 1.
|
|
618
|
-
order_by (str, optional): Colonna per l'ordinamento.
|
|
629
|
+
order_by (str, optional): Colonna per l'ordinamento. Se ``None``, viene
|
|
630
|
+
usata la prima colonna della chiave primaria.
|
|
619
631
|
asc (bool): Ordinamento crescente se ``True``. Default: ``True``.
|
|
620
632
|
parsing_model (Type[BaseModel], optional): Modello alternativo per
|
|
621
633
|
la serializzazione.
|
|
@@ -681,8 +693,8 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
681
693
|
"""Coppie distinte ``(key, value)`` per i record che soddisfano ``where``.
|
|
682
694
|
|
|
683
695
|
Calcola i valori distinti lato DB con ``SELECT DISTINCT`` invece di caricare
|
|
684
|
-
tutte le righe e dedupplicare in Python
|
|
685
|
-
|
|
696
|
+
tutte le righe e dedupplicare in Python. Tipicamente usato per popolare le
|
|
697
|
+
opzioni di un filtro lato client. Per le colonne ``ARRAY`` srotola in
|
|
686
698
|
parallelo chiave ed etichetta con ``unnest`` (lateral implicito).
|
|
687
699
|
|
|
688
700
|
Args:
|
|
@@ -694,6 +706,14 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
694
706
|
|
|
695
707
|
Returns:
|
|
696
708
|
list[Row]: Coppie ``(key, value)`` distinte.
|
|
709
|
+
|
|
710
|
+
Note:
|
|
711
|
+
A differenza degli altri metodi restituisce ``Row`` (tuple) e non modelli
|
|
712
|
+
Pydantic: non passa da ``_return`` e non accetta un ``parsing_model``.
|
|
713
|
+
|
|
714
|
+
Example:
|
|
715
|
+
>>> repo.distinct_pairs("id_operatore", "operatore", where={"id_azienda": 3})
|
|
716
|
+
[(1, 'Rossi'), (2, 'Bianchi')]
|
|
697
717
|
"""
|
|
698
718
|
if where is None:
|
|
699
719
|
where = kwhere if kwhere else {}
|
|
@@ -723,6 +743,11 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
723
743
|
|
|
724
744
|
In caso di errore, delega la gestione a ``_handle_exception``.
|
|
725
745
|
|
|
746
|
+
Poiché usa ``.scalars()``, di ogni riga viene restituita solo la **prima**
|
|
747
|
+
colonna: statement arricchiti con ``add_columns`` perdono le colonne
|
|
748
|
+
aggiuntive. Per leggerle serve eseguire lo statement direttamente sulla
|
|
749
|
+
sessione (vedi ``distinct_pairs``, che usa ``session.execute(...).all()``).
|
|
750
|
+
|
|
726
751
|
Args:
|
|
727
752
|
session (Session): Sessione su cui eseguire lo statement.
|
|
728
753
|
stmt (Select | Insert | Update | Delete): Statement da eseguire.
|
|
@@ -150,50 +150,48 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
150
150
|
return self.execute(session, stmt)
|
|
151
151
|
|
|
152
152
|
def nearest(self, geom: WKBElement, session: Session = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 1, where: dict[str, Any] = None, geom_srid: int = 4326,
|
|
153
|
-
|
|
153
|
+
parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
|
|
154
154
|
"""Restituisce le entità più vicine alla geometria fornita, ordinate per distanza.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
GiST, evitando il calcolo della distanza su tutte le righe.
|
|
156
|
+
L'ordinamento usa l'operatore KNN ``<->`` (``distance_centroid``) anziché
|
|
157
|
+
``ST_Distance``: con ``LIMIT`` impostato, PostGIS può servire la ricerca dei
|
|
158
|
+
più vicini tramite l'indice spaziale GiST, evitando il calcolo della
|
|
159
|
+
distanza su tutte le righe.
|
|
161
160
|
|
|
162
161
|
Args:
|
|
163
|
-
geom (WKBElement): Geometria di riferimento per il calcolo della
|
|
162
|
+
geom (WKBElement): Geometria di riferimento per il calcolo della prossimità.
|
|
164
163
|
session (Session, optional): Sessione esterna.
|
|
165
164
|
columns (list[str], optional): Colonne da includere.
|
|
166
165
|
exclude_columns (list[str], optional): Colonne da escludere.
|
|
167
166
|
limit (int): Numero di entità più vicine da restituire. Default: 1.
|
|
168
167
|
where (dict[str, Any], optional): Filtri aggiuntivi.
|
|
169
168
|
geom_srid (int): SRID della geometria fornita. Default: 4326.
|
|
170
|
-
distance (bool): Se ``True``, aggiunge ai risultati la colonna ``distance``
|
|
171
|
-
con la distanza calcolata via ``ST_Distance``. Default: ``True``.
|
|
172
|
-
L'ordinamento per prossimità (operatore KNN ``<->``) viene applicato
|
|
173
|
-
in ogni caso, indipendentemente da questo flag.
|
|
174
169
|
parsing_model (Type[BaseModel], optional): Modello alternativo.
|
|
175
170
|
|
|
176
171
|
Returns:
|
|
177
172
|
list[Entity]: Entità ordinate dalla più vicina alla più lontana.
|
|
178
173
|
|
|
174
|
+
Note:
|
|
175
|
+
Restituisce solo le entità, senza il valore della distanza: la distanza
|
|
176
|
+
è usata unicamente per l'ordinamento lato database. Per ottenerla come
|
|
177
|
+
colonna serve uno statement custom, dato che ``execute`` restituisce
|
|
178
|
+
``result.scalars().all()`` e terrebbe solo la prima colonna di ogni riga.
|
|
179
|
+
|
|
179
180
|
Example:
|
|
180
181
|
>>> nearest = repo.nearest(geom=my_point, limit=5)
|
|
181
182
|
"""
|
|
182
183
|
keep_open = False if session is None else True
|
|
183
184
|
session = self.open_session if session is None else session
|
|
184
|
-
entities = self._nearest(session=session, geom=geom, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, geom_srid=geom_srid
|
|
185
|
+
entities = self._nearest(session=session, geom=geom, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, geom_srid=geom_srid)
|
|
185
186
|
return self._return(session=session, entities=entities, keep_open=keep_open, commit=False, parsing_model=parsing_model)
|
|
186
187
|
|
|
187
|
-
def _nearest(self, session: Session, geom: WKBElement, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 1, where: dict[str, Any] = None, geom_srid: int = 4326
|
|
188
|
+
def _nearest(self, session: Session, geom: WKBElement, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 1, where: dict[str, Any] = None, geom_srid: int = 4326):
|
|
188
189
|
if where is None:
|
|
189
190
|
where = {}
|
|
190
191
|
stmt = self._select(entity=self.entity, columns=columns, exclude_columns=exclude_columns)
|
|
191
192
|
|
|
192
193
|
geom_condition = self._geom_condition(geom=geom, geom_srid=geom_srid)
|
|
193
194
|
|
|
194
|
-
if distance:
|
|
195
|
-
stmt = stmt.add_columns(self.geom_column.ST_Distance(geom_condition).label("distance"))
|
|
196
|
-
|
|
197
195
|
stmt = self.stmt_manager.prepare_statement(stmt=stmt, **where)
|
|
198
196
|
stmt = stmt.order_by(self.geom_column.distance_centroid(geom_condition))
|
|
199
197
|
stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit)
|
|
@@ -10,7 +10,14 @@ class BaseHandler:
|
|
|
10
10
|
"""Handler base per la costruzione di filtri WHERE su statement SQLAlchemy.
|
|
11
11
|
|
|
12
12
|
Gestisce i casi standard di uguaglianza, liste (``IN``) e colonne di tipo
|
|
13
|
-
``ARRAY``.
|
|
13
|
+
``ARRAY``. È anche la classe base di tutti gli handler specializzati, che
|
|
14
|
+
possono estenderla per logiche di filtraggio personalizzate.
|
|
15
|
+
|
|
16
|
+
Nella pipeline dello ``StatementManager`` è sempre l'**ultimo** handler
|
|
17
|
+
eseguito: riceve le chiavi del ``where`` non consumate dagli handler
|
|
18
|
+
precedenti e le tratta come filtri su colonne reali. A differenza degli
|
|
19
|
+
handler specializzati non rimuove nulla dal dizionario, perché non ha
|
|
20
|
+
successori.
|
|
14
21
|
|
|
15
22
|
Attributes:
|
|
16
23
|
columns (ReadOnlyColumnCollection): Colonne della tabella su cui operare.
|
|
@@ -43,9 +50,14 @@ class BaseHandler:
|
|
|
43
50
|
neq (bool): Se ``True``, applica la negazione (``NOT``) a ogni condizione.
|
|
44
51
|
Default: ``False``.
|
|
45
52
|
where (dict[str, Any]): Dizionario ``{nome_colonna: valore}`` con i filtri.
|
|
53
|
+
Tutte le chiavi devono corrispondere a colonne reali della tabella.
|
|
46
54
|
|
|
47
55
|
Returns:
|
|
48
56
|
Select | Update | Delete: Statement arricchito con le clausole WHERE.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
KeyError: Se una chiave del ``where`` non corrisponde a nessuna colonna,
|
|
60
|
+
ovvero se nessun handler precedente l'ha consumata.
|
|
49
61
|
"""
|
|
50
62
|
stmt = stmt.where(*[self._handle(column=self.columns[column], condition=condition, neq=neq) for column, condition in where.items()])
|
|
51
63
|
return stmt
|