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.
Files changed (44) hide show
  1. python_general_be_lib-0.8.0/PKG-INFO +108 -0
  2. python_general_be_lib-0.8.0/README.md +76 -0
  3. python_general_be_lib-0.8.0/general/exception/access_exceptions.py +23 -0
  4. python_general_be_lib-0.8.0/general/exception/crud_exceptions.py +90 -0
  5. python_general_be_lib-0.8.0/general/exception/exception_interface.py +38 -0
  6. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/base/declarative_base.py +6 -0
  7. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/metadata/crud_metadata.py +11 -2
  8. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/metadata/geom_metadata.py +4 -1
  9. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/crud_repository.py +32 -7
  10. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/geometry_repository.py +14 -16
  11. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/base_handler.py +13 -1
  12. python_general_be_lib-0.8.0/general/interface/repository/handler/interval_handler.py +122 -0
  13. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/json_handler.py +8 -0
  14. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/json_ilike_handler.py +45 -21
  15. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/or_handler.py +37 -24
  16. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/statement_manager.py +13 -0
  17. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/many_to_many_repository.py +10 -0
  18. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/logger.py +7 -0
  19. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/pyproject.toml +1 -1
  20. python_general_be_lib-0.8.0/python_general_be_lib.egg-info/PKG-INFO +108 -0
  21. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/setup.py +1 -1
  22. python_general_be_lib-0.7.1/PKG-INFO +0 -33
  23. python_general_be_lib-0.7.1/README.md +0 -1
  24. python_general_be_lib-0.7.1/general/exception/access_exceptions.py +0 -11
  25. python_general_be_lib-0.7.1/general/exception/crud_exceptions.py +0 -37
  26. python_general_be_lib-0.7.1/general/exception/exception_interface.py +0 -10
  27. python_general_be_lib-0.7.1/general/interface/repository/handler/interval_handler.py +0 -92
  28. python_general_be_lib-0.7.1/python_general_be_lib.egg-info/PKG-INFO +0 -33
  29. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/LICENSE +0 -0
  30. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/__init__.py +0 -0
  31. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/exception/__init__.py +0 -0
  32. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/base/__init__.py +0 -0
  33. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/base/base_model.py +0 -0
  34. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/metadata/__init__.py +0 -0
  35. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/__init__.py +0 -0
  36. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/__init__.py +0 -0
  37. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/handler/ilike_handler.py +0 -0
  38. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/interface/repository/view_repository.py +0 -0
  39. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/general/paginator_dto.py +0 -0
  40. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/python_general_be_lib.egg-info/SOURCES.txt +0 -0
  41. {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
  42. {python_general_be_lib-0.7.1 → python_general_be_lib-0.8.0}/python_general_be_lib.egg-info/requires.txt +0 -0
  43. {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
  44. {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], optional): Entità su cui verificare. Se ``None``,
183
- usa ``self.entity``.
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 (usato da ``BaseService.company_filters``
685
- per popolare le opzioni dei filtri). Per le colonne ``ARRAY`` srotola in
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
- distance: bool = True, parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
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
- Aggiunge una colonna ``distance`` ai risultati con la distanza calcolata
157
- tramite ``ST_Distance``. L'ordinamento usa l'operatore KNN ``<->``
158
- (``distance_centroid``) anziché ``ST_Distance``: con ``LIMIT`` impostato,
159
- PostGIS può servire la ricerca dei più vicini tramite l'indice spaziale
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 distanza.
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, distance=distance)
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, distance: bool = True):
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``. Può essere esteso per supportare logiche di filtraggio personalizzate.
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