python-general-be-lib 0.7.1__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general/exception/access_exceptions.py +12 -0
- general/exception/crud_exceptions.py +53 -0
- general/exception/exception_interface.py +28 -0
- general/interface/base/declarative_base.py +6 -0
- general/interface/metadata/crud_metadata.py +11 -2
- general/interface/metadata/geom_metadata.py +4 -1
- general/interface/repository/crud_repository.py +32 -7
- general/interface/repository/geometry_repository.py +14 -16
- general/interface/repository/handler/base_handler.py +13 -1
- general/interface/repository/handler/interval_handler.py +46 -16
- general/interface/repository/handler/json_handler.py +8 -0
- general/interface/repository/handler/json_ilike_handler.py +45 -21
- general/interface/repository/handler/or_handler.py +37 -24
- general/interface/repository/handler/statement_manager.py +13 -0
- general/interface/repository/many_to_many_repository.py +10 -0
- general/logger.py +7 -0
- python_general_be_lib-0.8.0.dist-info/METADATA +108 -0
- python_general_be_lib-0.8.0.dist-info/RECORD +32 -0
- python_general_be_lib-0.7.1.dist-info/METADATA +0 -33
- python_general_be_lib-0.7.1.dist-info/RECORD +0 -32
- {python_general_be_lib-0.7.1.dist-info → python_general_be_lib-0.8.0.dist-info}/WHEEL +0 -0
- {python_general_be_lib-0.7.1.dist-info → python_general_be_lib-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {python_general_be_lib-0.7.1.dist-info → python_general_be_lib-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +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
|
+
|
|
1
11
|
from .exception_interface import ExceptionInterface
|
|
2
12
|
|
|
3
13
|
|
|
4
14
|
class ForbiddenException(ExceptionInterface):
|
|
15
|
+
"""403 — Utente autenticato ma senza i permessi per la risorsa richiesta."""
|
|
5
16
|
status_code = 403
|
|
6
17
|
default_message = "Risorsa non accessibile da questo utente"
|
|
7
18
|
|
|
8
19
|
|
|
9
20
|
class UnauthorizedException(ExceptionInterface):
|
|
21
|
+
"""401 — Richiesta priva di credenziali valide."""
|
|
10
22
|
status_code = 401
|
|
11
23
|
default_message = "Non autorizzato"
|
|
@@ -1,31 +1,78 @@
|
|
|
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
|
+
|
|
1
14
|
from .exception_interface import ExceptionInterface
|
|
2
15
|
|
|
3
16
|
|
|
4
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
|
+
"""
|
|
5
23
|
status_code = 400
|
|
6
24
|
default_message = "Richiesta non corretta"
|
|
7
25
|
|
|
8
26
|
|
|
9
27
|
class EntityNotFoundException(ExceptionInterface):
|
|
28
|
+
"""404 — Nessun record corrisponde ai criteri richiesti.
|
|
29
|
+
|
|
30
|
+
Sollevata al posto di ``NoResultFound``.
|
|
31
|
+
"""
|
|
10
32
|
status_code = 404
|
|
11
33
|
default_message = "Dato non trovato"
|
|
12
34
|
|
|
13
35
|
|
|
14
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
|
+
"""
|
|
15
42
|
status_code = 503
|
|
16
43
|
default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
|
|
17
44
|
|
|
18
45
|
|
|
19
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
|
+
"""
|
|
20
52
|
status_code = 422
|
|
21
53
|
default_message = "Entità non processabile"
|
|
22
54
|
|
|
23
55
|
|
|
24
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
|
+
"""
|
|
25
66
|
status_code = 400
|
|
26
67
|
default_message = "Richiesta non corretta"
|
|
27
68
|
|
|
28
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
|
+
"""
|
|
29
76
|
if attr:
|
|
30
77
|
super().__init__(custom_message=f"Attributo {attr} non presente", )
|
|
31
78
|
else:
|
|
@@ -33,5 +80,11 @@ class HasNoAttributeException(ExceptionInterface):
|
|
|
33
80
|
|
|
34
81
|
|
|
35
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
|
+
"""
|
|
36
89
|
status_code = 500
|
|
37
90
|
default_message = "Impossibile gestire la richiesta al momento"
|
|
@@ -2,9 +2,37 @@ from fastapi import HTTPException
|
|
|
2
2
|
|
|
3
3
|
|
|
4
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
|
+
"""
|
|
5
25
|
status_code: int
|
|
6
26
|
default_message: str
|
|
7
27
|
|
|
8
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
|
+
"""
|
|
9
37
|
detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
|
|
10
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
|
|
@@ -15,21 +15,32 @@ class IntervalHandler(BaseHandler):
|
|
|
15
15
|
condizioni ``>=`` e ``<=``. Supporta la semantica di "infinito come NULL"
|
|
16
16
|
per il limite superiore.
|
|
17
17
|
|
|
18
|
+
Gli alias separano il nome del parametro esposto dall'API dal nome fisico
|
|
19
|
+
della colonna: ``from_``/``to_`` indicano le **colonne**, ``from_alias``/
|
|
20
|
+
``to_alias`` le **chiavi del dizionario** ``where`` da cui leggere i valori.
|
|
21
|
+
Se non forniti, gli alias coincidono col nome della colonna.
|
|
22
|
+
|
|
23
|
+
Quando i due alias coincidono l'handler entra in modalità ``between``: una
|
|
24
|
+
singola chiave nel ``where`` valorizza entrambi i limiti (vedi ``handle``).
|
|
25
|
+
|
|
18
26
|
Attributes:
|
|
19
|
-
from\\_ (Any):
|
|
20
|
-
to\\_ (Any):
|
|
27
|
+
from\\_ (Any): Nome della colonna su cui applicare il limite inferiore (``>=``).
|
|
28
|
+
to\\_ (Any): Nome della colonna su cui applicare il limite superiore (``<=``).
|
|
29
|
+
from_alias (str): Chiave del ``where`` che valorizza il limite inferiore.
|
|
30
|
+
to_alias (str): Chiave del ``where`` che valorizza il limite superiore.
|
|
21
31
|
inf_is_none (bool): Se ``True``, un valore ``NULL`` nella colonna ``to_``
|
|
22
32
|
viene trattato come "senza limite superiore" (infinito).
|
|
33
|
+
between (bool): ``True`` se ``from_alias`` e ``to_alias`` coincidono.
|
|
23
34
|
|
|
24
35
|
Example:
|
|
25
36
|
>>> handler = IntervalHandler(
|
|
26
37
|
... columns=Event.columns(),
|
|
27
|
-
... from_="
|
|
28
|
-
... to_="
|
|
38
|
+
... from_="data_inizio", from_alias="da",
|
|
39
|
+
... to_="data_fine", to_alias="a",
|
|
29
40
|
... inf_is_none=True
|
|
30
41
|
... )
|
|
31
|
-
>>> repo.find(where={"
|
|
32
|
-
# WHERE
|
|
42
|
+
>>> repo.find(where={"da": "2024-01-01", "a": "2024-12-31"})
|
|
43
|
+
# WHERE data_inizio >= '2024-01-01' AND (data_fine <= '2024-12-31' OR data_fine IS NULL)
|
|
33
44
|
"""
|
|
34
45
|
__slots__ = "from_", "to_", "inf_is_none", "from_alias", "to_alias", "between"
|
|
35
46
|
from_: Any
|
|
@@ -37,19 +48,28 @@ class IntervalHandler(BaseHandler):
|
|
|
37
48
|
from_alias: str
|
|
38
49
|
to_alias: str
|
|
39
50
|
inf_is_none: bool
|
|
51
|
+
between: bool
|
|
40
52
|
|
|
41
53
|
def __init__(self, columns: ReadOnlyColumnCollection[str, Column[Any]], from_: Any = None, from_alias: str = None, to_: Any = None, to_alias: str = None, inf_is_none: bool = False):
|
|
42
|
-
"""Inizializza l'handler con le
|
|
54
|
+
"""Inizializza l'handler con le colonne dei limiti e i relativi alias.
|
|
43
55
|
|
|
44
56
|
Args:
|
|
45
57
|
columns (ReadOnlyColumnCollection): Colonne della tabella.
|
|
46
|
-
from\\_ (Any, optional):
|
|
58
|
+
from\\_ (Any, optional): Nome della colonna per il limite inferiore
|
|
47
59
|
(condizione ``>=``). Default: ``None`` (disabilitato).
|
|
48
|
-
|
|
60
|
+
from_alias (str, optional): Chiave del ``where`` da cui leggere il limite
|
|
61
|
+
inferiore. Se omessa, vale ``str(from_)``.
|
|
62
|
+
to\\_ (Any, optional): Nome della colonna per il limite superiore
|
|
49
63
|
(condizione ``<=``). Default: ``None`` (disabilitato).
|
|
64
|
+
to_alias (str, optional): Chiave del ``where`` da cui leggere il limite
|
|
65
|
+
superiore. Se omessa, vale ``str(to_)``.
|
|
50
66
|
inf_is_none (bool): Se ``True``, aggiunge ``OR col IS NULL`` alla
|
|
51
67
|
condizione sul limite superiore per trattare NULL come infinito.
|
|
52
68
|
Default: ``False``.
|
|
69
|
+
|
|
70
|
+
Note:
|
|
71
|
+
Se gli alias risultanti sono uguali viene attivata la modalità
|
|
72
|
+
``between`` (attributo ``between``).
|
|
53
73
|
"""
|
|
54
74
|
self.columns = columns
|
|
55
75
|
self.from_ = from_
|
|
@@ -62,9 +82,14 @@ class IntervalHandler(BaseHandler):
|
|
|
62
82
|
def handle(self, stmt: Select | Update | Delete, where: dict[str, Any] = None, **kwargs):
|
|
63
83
|
"""Applica le condizioni di range allo statement estraendo i valori da ``where``.
|
|
64
84
|
|
|
65
|
-
Estrae e rimuove in-place le chiavi ``
|
|
66
|
-
``where``, quindi aggiunge le relative condizioni allo statement.
|
|
67
|
-
|
|
85
|
+
Estrae e rimuove in-place le chiavi ``from_alias`` e ``to_alias`` dal
|
|
86
|
+
dizionario ``where``, quindi aggiunge le relative condizioni allo statement.
|
|
87
|
+
In modalità ``between`` viene letta solo ``from_alias`` e il suo valore è
|
|
88
|
+
usato per entrambi i limiti.
|
|
89
|
+
|
|
90
|
+
I valori falsy (``None``, ``0``, ``""``) vengono ignorati: la condizione non
|
|
91
|
+
viene aggiunta, ma la chiave è comunque rimossa dal ``where``. Un limite
|
|
92
|
+
inferiore pari a ``0`` non produce quindi nessun filtro.
|
|
68
93
|
|
|
69
94
|
Args:
|
|
70
95
|
stmt (Select | Update | Delete): Statement su cui applicare le condizioni.
|
|
@@ -76,16 +101,21 @@ class IntervalHandler(BaseHandler):
|
|
|
76
101
|
Select | Update | Delete: Statement con le condizioni di range aggiunte.
|
|
77
102
|
|
|
78
103
|
Note:
|
|
79
|
-
- Limite inferiore: ``
|
|
80
|
-
- Limite superiore: ``
|
|
104
|
+
- Limite inferiore: ``from_ >= value``
|
|
105
|
+
- Limite superiore: ``to_ <= value`` oppure ``(to_ <= value OR to_ IS NULL)``
|
|
81
106
|
se ``inf_is_none=True``.
|
|
107
|
+
- In modalità ``between`` le condizioni generate restano ``from_ >= value``
|
|
108
|
+
e ``to_ <= value``. Per ottenere la semantica "la riga è valida al valore
|
|
109
|
+
indicato" le colonne vanno mappate invertite, ad es. ``from_="data_fine"``
|
|
110
|
+
e ``to_="data_inizio"`` con lo stesso alias ``"data"``, che produce
|
|
111
|
+
``data_fine >= value AND data_inizio <= value``.
|
|
82
112
|
"""
|
|
83
113
|
from_condition = where.pop(self.from_alias, None)
|
|
84
114
|
to_condition = where.pop(self.to_alias, None) if not self.between else from_condition
|
|
85
115
|
if from_condition:
|
|
86
|
-
stmt = stmt.where(self.columns[self.from_]
|
|
116
|
+
stmt = stmt.where(self.columns[self.from_] >= from_condition)
|
|
87
117
|
if to_condition:
|
|
88
|
-
condition = self.columns[self.to_]
|
|
118
|
+
condition = self.columns[self.to_] <= to_condition
|
|
89
119
|
if self.inf_is_none:
|
|
90
120
|
condition = or_(condition, self.columns[self.to_] == None)
|
|
91
121
|
stmt = stmt.where(condition)
|
|
@@ -16,6 +16,9 @@ class JsonHandler(BaseHandler):
|
|
|
16
16
|
interne al campo JSON. Trova la prima colonna di tipo ``dict`` (JSON/JSONB)
|
|
17
17
|
e applica filtri con ``col['key'].as_string() == str(value)``.
|
|
18
18
|
|
|
19
|
+
Il confronto è sempre stringa-contro-stringa (``as_string()`` sulla chiave JSON
|
|
20
|
+
e ``str(value)`` sul valore): numeri e booleani vengono confrontati come testo.
|
|
21
|
+
|
|
19
22
|
Attributes:
|
|
20
23
|
json_column (str): Nome della colonna JSON/JSONB rilevata automaticamente.
|
|
21
24
|
|
|
@@ -38,6 +41,11 @@ class JsonHandler(BaseHandler):
|
|
|
38
41
|
|
|
39
42
|
Args:
|
|
40
43
|
columns (ReadOnlyColumnCollection): Colonne della tabella.
|
|
44
|
+
|
|
45
|
+
Note:
|
|
46
|
+
Se la tabella non ha colonne JSON, ``json_column`` non viene impostato:
|
|
47
|
+
una chiave sconosciuta nel ``where`` provoca in tal caso un
|
|
48
|
+
``AttributeError`` in ``handle``.
|
|
41
49
|
"""
|
|
42
50
|
self.columns = columns
|
|
43
51
|
for key, column in columns.items():
|
|
@@ -8,25 +8,35 @@ from sqlalchemy.sql.base import ReadOnlyColumnCollection
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class JsonILikeHandler(BaseHandler):
|
|
11
|
-
"""Handler per ricerche
|
|
11
|
+
"""Handler per ricerche ILIKE su colonne reali **e** su sotto-chiavi JSON/JSONB.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Variante di ``ILikeHandler`` per entità che tengono parte dei dati in una
|
|
14
|
+
colonna JSON: un singolo parametro di ricerca testuale (es. ``"search"``)
|
|
15
|
+
viene applicato come ``ILIKE '%value%'`` sia sulle colonne della tabella sia
|
|
16
|
+
sulle chiavi interne al campo JSON, tutto unito da ``OR``.
|
|
17
|
+
|
|
18
|
+
Gli ``ilike_attributes`` passati al costruttore vengono ripartiti
|
|
19
|
+
automaticamente: i nomi che corrispondono a colonne reali finiscono in
|
|
20
|
+
``ilike_attributes``, gli altri in ``json_ilike_keys`` e sono trattati come
|
|
21
|
+
chiavi dentro ``json_column``. A differenza di ``ILikeHandler``, quindi, un
|
|
22
|
+
nome sconosciuto non solleva ``ValueError``: è proprio il caso d'uso previsto.
|
|
15
23
|
|
|
16
24
|
Attributes:
|
|
17
25
|
ilike_aliases (list[str]): Chiavi nel dizionario ``where`` che attivano
|
|
18
|
-
la ricerca
|
|
19
|
-
ilike_attributes (list[str]):
|
|
20
|
-
|
|
26
|
+
la ricerca (es. ``["search", "q"]``).
|
|
27
|
+
ilike_attributes (list[str]): Colonne reali su cui applicare ILIKE.
|
|
28
|
+
json_ilike_keys (list[str]): Chiavi interne al campo JSON su cui applicare ILIKE.
|
|
29
|
+
json_column (str): Nome della colonna JSON/JSONB rilevata automaticamente
|
|
30
|
+
(la prima con ``python_type == dict``).
|
|
21
31
|
|
|
22
32
|
Example:
|
|
23
33
|
>>> handler = JsonILikeHandler(
|
|
24
|
-
... columns=
|
|
34
|
+
... columns=Item.columns(),
|
|
25
35
|
... ilike_aliases=["search"],
|
|
26
|
-
... ilike_attributes=["name", "
|
|
36
|
+
... ilike_attributes=["name", "codice_interno"] # "codice_interno" è una chiave JSON
|
|
27
37
|
... )
|
|
28
|
-
>>> repo.find(where={"search": "
|
|
29
|
-
# WHERE name ILIKE '%
|
|
38
|
+
>>> repo.find(where={"search": "abc"})
|
|
39
|
+
# WHERE name ILIKE '%abc%' OR attributes ->> 'codice_interno' ILIKE '%abc%'
|
|
30
40
|
"""
|
|
31
41
|
__slots__ = "ilike_aliases", "ilike_attributes", "json_column", "json_ilike_keys"
|
|
32
42
|
ilike_aliases: list[str]
|
|
@@ -35,17 +45,24 @@ class JsonILikeHandler(BaseHandler):
|
|
|
35
45
|
json_column: str
|
|
36
46
|
|
|
37
47
|
def __init__(self, columns: ReadOnlyColumnCollection[str, Column[Any]], ilike_aliases: list[str] = None, ilike_attributes: list[str] = None):
|
|
38
|
-
"""Inizializza l'handler
|
|
48
|
+
"""Inizializza l'handler ripartendo gli attributi tra colonne reali e chiavi JSON.
|
|
49
|
+
|
|
50
|
+
Itera sugli ``ilike_attributes`` e li smista in base alla presenza tra le
|
|
51
|
+
colonne della tabella. Individua poi la colonna JSON come la prima con
|
|
52
|
+
``type.python_type == dict``; le colonne che non implementano
|
|
53
|
+
``python_type`` (es. geometrie) vengono ignorate.
|
|
39
54
|
|
|
40
55
|
Args:
|
|
41
56
|
columns (ReadOnlyColumnCollection): Colonne della tabella.
|
|
42
57
|
ilike_aliases (list[str], optional): Chiavi alias che attivano la ricerca.
|
|
43
58
|
Default: lista vuota.
|
|
44
|
-
ilike_attributes (list[str], optional): Colonne
|
|
45
|
-
Default:
|
|
59
|
+
ilike_attributes (list[str], optional): Colonne reali e/o chiavi JSON
|
|
60
|
+
su cui applicare ILIKE. Default: nessun attributo.
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
Note:
|
|
63
|
+
Se la tabella non ha colonne JSON, ``json_column`` non viene impostato:
|
|
64
|
+
usare l'handler con delle ``json_ilike_keys`` provoca in tal caso un
|
|
65
|
+
``AttributeError`` alla prima ricerca.
|
|
49
66
|
"""
|
|
50
67
|
self.columns = columns
|
|
51
68
|
self.ilike_aliases = ilike_aliases if ilike_aliases is not None else []
|
|
@@ -70,13 +87,16 @@ class JsonILikeHandler(BaseHandler):
|
|
|
70
87
|
def handle(self, stmt: Select | Update | Delete, where: dict[str, Any] = None, **kwargs):
|
|
71
88
|
"""Applica i filtri ILIKE allo statement per ogni alias trovato in ``where``.
|
|
72
89
|
|
|
73
|
-
Per ogni alias presente in ``ilike_aliases`` e
|
|
90
|
+
Per ogni alias presente in ``ilike_aliases`` e valorizzato in ``where``,
|
|
74
91
|
estrae il valore, lo rimuove dal dizionario e aggiunge la condizione ILIKE
|
|
75
|
-
su tutte le colonne
|
|
92
|
+
su tutte le colonne reali e le chiavi JSON configurate, unite da ``OR``.
|
|
93
|
+
|
|
94
|
+
I valori falsy (``None``, ``""``) vengono ignorati: la condizione non viene
|
|
95
|
+
aggiunta, ma la chiave è comunque rimossa dal ``where``.
|
|
76
96
|
|
|
77
97
|
Args:
|
|
78
98
|
stmt (Select | Update | Delete): Statement su cui applicare i filtri.
|
|
79
|
-
where (dict[str, Any]): Dizionario dei filtri. Gli alias
|
|
99
|
+
where (dict[str, Any]): Dizionario dei filtri. Gli alias gestiti vengono
|
|
80
100
|
estratti e rimossi in-place.
|
|
81
101
|
**kwargs: Ignorati (compatibilità con l'interfaccia base).
|
|
82
102
|
|
|
@@ -91,14 +111,18 @@ class JsonILikeHandler(BaseHandler):
|
|
|
91
111
|
return stmt
|
|
92
112
|
|
|
93
113
|
def _handle(self, alias: str, condition: str, neq: bool = False):
|
|
94
|
-
"""Costruisce la condizione ILIKE
|
|
114
|
+
"""Costruisce la condizione ILIKE in OR su colonne reali e chiavi JSON.
|
|
115
|
+
|
|
116
|
+
Le chiavi JSON vengono estratte con ``json_column[key].as_string()`` prima
|
|
117
|
+
di applicare l'``ILIKE``, così il confronto avviene sul testo.
|
|
95
118
|
|
|
96
119
|
Args:
|
|
97
|
-
alias (str): Alias attivato
|
|
120
|
+
alias (str): Alias attivato. Serve solo a identificare la ricerca,
|
|
121
|
+
non è il nome di una colonna e non viene usato nella condizione.
|
|
98
122
|
condition (str): Stringa da cercare (viene wrappata con ``%``).
|
|
99
123
|
neq (bool): Non utilizzato in questa implementazione.
|
|
100
124
|
|
|
101
125
|
Returns:
|
|
102
|
-
ColumnElement: Espressione ``OR(
|
|
126
|
+
ColumnElement: Espressione ``OR(col ILIKE '%val%', ..., json_col ->> 'key' ILIKE '%val%', ...)``.
|
|
103
127
|
"""
|
|
104
128
|
return or_(*[self.columns[col].ilike(f'%{condition}%') for col in self.ilike_attributes], *[self.columns[self.json_column][key].as_string().ilike(f'%{condition}%') for key in self.json_ilike_keys])
|
|
@@ -9,38 +9,47 @@ from .base_handler import BaseHandler
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class OrHandler(BaseHandler):
|
|
12
|
-
"""Handler per
|
|
12
|
+
"""Handler per filtri di uguaglianza applicati a più colonne in ``OR``.
|
|
13
13
|
|
|
14
|
-
Permette di esporre un singolo parametro
|
|
15
|
-
|
|
14
|
+
Permette di esporre un singolo parametro (es. ``"id_soggetto"``) il cui valore
|
|
15
|
+
viene confrontato per uguaglianza con un insieme di colonne alternative, unite
|
|
16
|
+
da ``OR``. Utile quando lo stesso dato può comparire indifferentemente su una
|
|
17
|
+
di più colonne (es. mittente/destinatario, operatore di partenza/arrivo).
|
|
18
|
+
|
|
19
|
+
A differenza di ``ILikeHandler`` il confronto è **esatto** (``==``), non
|
|
20
|
+
``ILIKE``: non c'è wrapping con ``%`` né case-insensitivity.
|
|
16
21
|
|
|
17
22
|
Attributes:
|
|
18
23
|
or_aliases (list[str]): Chiavi nel dizionario ``where`` che attivano
|
|
19
|
-
|
|
20
|
-
or_attributes (list[str]): Nomi delle colonne
|
|
21
|
-
(es. ``["
|
|
24
|
+
il filtro (es. ``["id_soggetto"]``).
|
|
25
|
+
or_attributes (list[str]): Nomi delle colonne messe in ``OR``
|
|
26
|
+
(es. ``["id_mittente", "id_destinatario"]``).
|
|
27
|
+
|
|
28
|
+
Note:
|
|
29
|
+
Non è re-esportato da ``handler/__init__.py``: va importato dal modulo,
|
|
30
|
+
``from general.interface.repository.handler.or_handler import OrHandler``.
|
|
22
31
|
|
|
23
32
|
Example:
|
|
24
33
|
>>> handler = OrHandler(
|
|
25
|
-
... columns=
|
|
26
|
-
... or_aliases=["
|
|
27
|
-
... or_attributes=["
|
|
34
|
+
... columns=Doc.columns(),
|
|
35
|
+
... or_aliases=["id_soggetto"],
|
|
36
|
+
... or_attributes=["id_mittente", "id_destinatario"]
|
|
28
37
|
... )
|
|
29
|
-
>>> repo.find(where={"
|
|
30
|
-
# WHERE
|
|
38
|
+
>>> repo.find(where={"id_soggetto": 42})
|
|
39
|
+
# WHERE id_mittente = 42 OR id_destinatario = 42
|
|
31
40
|
"""
|
|
32
41
|
__slots__ = "or_aliases", "or_attributes"
|
|
33
42
|
or_aliases: list[str]
|
|
34
43
|
or_attributes: list[str]
|
|
35
44
|
|
|
36
45
|
def __init__(self, columns: ReadOnlyColumnCollection[str, Column[Any]], or_aliases: list[str] = None, or_attributes: list[str] = None):
|
|
37
|
-
"""Inizializza l'handler validando che le colonne
|
|
46
|
+
"""Inizializza l'handler validando che le colonne in OR esistano sull'entità.
|
|
38
47
|
|
|
39
48
|
Args:
|
|
40
49
|
columns (ReadOnlyColumnCollection): Colonne della tabella.
|
|
41
|
-
or_aliases (list[str], optional): Chiavi alias che attivano
|
|
50
|
+
or_aliases (list[str], optional): Chiavi alias che attivano il filtro.
|
|
42
51
|
Default: lista vuota.
|
|
43
|
-
or_attributes (list[str], optional): Colonne
|
|
52
|
+
or_attributes (list[str], optional): Colonne da confrontare in ``OR``.
|
|
44
53
|
Default: lista vuota.
|
|
45
54
|
|
|
46
55
|
Raises:
|
|
@@ -55,20 +64,23 @@ class OrHandler(BaseHandler):
|
|
|
55
64
|
raise ValueError(f"{or_attribute} not in columns")
|
|
56
65
|
|
|
57
66
|
def handle(self, stmt: Select | Update | Delete, where: dict[str, Any] = None, **kwargs):
|
|
58
|
-
"""Applica i filtri
|
|
67
|
+
"""Applica i filtri in OR allo statement per ogni alias trovato in ``where``.
|
|
68
|
+
|
|
69
|
+
Per ogni alias presente in ``or_aliases`` e valorizzato in ``where``,
|
|
70
|
+
estrae il valore, lo rimuove dal dizionario e aggiunge la condizione di
|
|
71
|
+
uguaglianza su tutte le colonne in ``or_attributes``, unite da ``OR``.
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
su tutte le colonne in ``or_attributes`` con ``OR``.
|
|
73
|
+
I valori falsy (``None``, ``0``, ``""``) vengono ignorati: la condizione
|
|
74
|
+
non viene aggiunta, ma la chiave è comunque rimossa dal ``where``.
|
|
63
75
|
|
|
64
76
|
Args:
|
|
65
77
|
stmt (Select | Update | Delete): Statement su cui applicare i filtri.
|
|
66
|
-
where (dict[str, Any]): Dizionario dei filtri. Gli alias
|
|
78
|
+
where (dict[str, Any]): Dizionario dei filtri. Gli alias gestiti vengono
|
|
67
79
|
estratti e rimossi in-place.
|
|
68
80
|
**kwargs: Ignorati (compatibilità con l'interfaccia base).
|
|
69
81
|
|
|
70
82
|
Returns:
|
|
71
|
-
Select | Update | Delete: Statement con le condizioni
|
|
83
|
+
Select | Update | Delete: Statement con le condizioni in OR aggiunte.
|
|
72
84
|
"""
|
|
73
85
|
for alias in self.or_aliases:
|
|
74
86
|
condition = where.pop(alias, None)
|
|
@@ -77,14 +89,15 @@ class OrHandler(BaseHandler):
|
|
|
77
89
|
return stmt
|
|
78
90
|
|
|
79
91
|
def _handle(self, alias: str, condition: str, neq: bool = False):
|
|
80
|
-
"""Costruisce la condizione
|
|
92
|
+
"""Costruisce la condizione di uguaglianza in OR su tutte le colonne configurate.
|
|
81
93
|
|
|
82
94
|
Args:
|
|
83
|
-
alias (str): Alias attivato
|
|
84
|
-
|
|
95
|
+
alias (str): Alias attivato. Serve solo a identificare la ricerca,
|
|
96
|
+
non è il nome di una colonna e non viene usato nella condizione.
|
|
97
|
+
condition (str): Valore da confrontare per uguaglianza.
|
|
85
98
|
neq (bool): Non utilizzato in questa implementazione.
|
|
86
99
|
|
|
87
100
|
Returns:
|
|
88
|
-
ColumnElement: Espressione ``OR(col1
|
|
101
|
+
ColumnElement: Espressione ``OR(col1 = value, col2 = value, ...)``.
|
|
89
102
|
"""
|
|
90
103
|
return or_(*[self.columns[col] == condition for col in self.or_attributes])
|
|
@@ -17,6 +17,13 @@ class StatementManager:
|
|
|
17
17
|
|
|
18
18
|
L'ordine di esecuzione è: handler aggiuntivi (nell'ordine fornito) → handler base.
|
|
19
19
|
|
|
20
|
+
Il contratto della pipeline è che ogni handler specializzato **consuma
|
|
21
|
+
in-place** le chiavi che riconosce dal dizionario ``where`` (via
|
|
22
|
+
``where.pop(...)``), lasciando ai successivi solo le chiavi non gestite.
|
|
23
|
+
L'handler base, eseguito per ultimo, tratta le chiavi rimanenti come filtri
|
|
24
|
+
su colonne reali: una chiave che nessun handler consuma e che non è una
|
|
25
|
+
colonna provoca un ``KeyError``. L'ordine degli handler è quindi significativo.
|
|
26
|
+
|
|
20
27
|
Attributes:
|
|
21
28
|
_handlers (list[BaseHandler]): Handler aggiuntivi registrati.
|
|
22
29
|
_base_handler (BaseHandler): Handler principale, sempre eseguito per ultimo.
|
|
@@ -50,10 +57,16 @@ class StatementManager:
|
|
|
50
57
|
Args:
|
|
51
58
|
stmt (Select | Update | Delete): Statement su cui applicare i filtri.
|
|
52
59
|
neq (bool): Se ``True``, applica la negazione a tutti i filtri. Default: ``False``.
|
|
60
|
+
Nota: solo ``BaseHandler`` implementa la negazione; gli handler
|
|
61
|
+
specializzati ignorano questo flag.
|
|
53
62
|
**where: Filtri come keyword arguments ``{colonna: valore}``.
|
|
54
63
|
|
|
55
64
|
Returns:
|
|
56
65
|
Select | Update | Delete: Statement con tutte le clausole WHERE applicate.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
KeyError: Se una chiave non consumata da nessun handler non corrisponde
|
|
69
|
+
a una colonna reale (sollevato da ``BaseHandler``).
|
|
57
70
|
"""
|
|
58
71
|
for handler in self._handlers:
|
|
59
72
|
stmt = handler.handle(stmt=stmt, neq=neq, where=where)
|
|
@@ -15,6 +15,16 @@ class ManyToManyRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
15
15
|
Estende ``CrudRepository`` con metodi per sincronizzare collezioni di entità
|
|
16
16
|
figlie rispetto a un parent, gestendo automaticamente insert, update e delete
|
|
17
17
|
in base alle differenze tra la lista corrente e quella desiderata.
|
|
18
|
+
|
|
19
|
+
A differenza degli altri repository, tutti i metodi di questa classe
|
|
20
|
+
**richiedono una ``session`` esplicita**: non ne aprono una propria e non
|
|
21
|
+
eseguono commit, perché una sincronizzazione differenziale è per definizione
|
|
22
|
+
un'operazione da svolgere nella stessa transazione del parent. È il chiamante
|
|
23
|
+
a decidere quando committare. Per lo stesso motivo restituiscono entità ORM
|
|
24
|
+
grezze e non modelli Pydantic.
|
|
25
|
+
|
|
26
|
+
Le ``session.delete(...)`` accodate diventano effettive solo al flush/commit
|
|
27
|
+
del chiamante.
|
|
18
28
|
"""
|
|
19
29
|
|
|
20
30
|
def diff_upsert_from_parent(self, session: Session, parent: Type[Base], children: list[Type[BaseModel]], relationship_attribute: str, child_fks: dict[str, Any], child_pk: str = "id") -> list[Entity]:
|
general/logger.py
CHANGED
|
@@ -57,6 +57,8 @@ class EndpointFilter(logging.Filter):
|
|
|
57
57
|
|
|
58
58
|
Args:
|
|
59
59
|
endpoint (str): Stringa dell'endpoint da filtrare (es. ``"/health"``).
|
|
60
|
+
*args: Argomenti posizionali passati a ``logging.Filter``.
|
|
61
|
+
**kwargs: Argomenti keyword passati a ``logging.Filter`` (es. ``name``).
|
|
60
62
|
"""
|
|
61
63
|
super().__init__(*args, **kwargs)
|
|
62
64
|
self.endpoint = endpoint
|
|
@@ -108,6 +110,11 @@ def log(func):
|
|
|
108
110
|
Returns:
|
|
109
111
|
Callable: Funzione wrapper con logging integrato.
|
|
110
112
|
|
|
113
|
+
Note:
|
|
114
|
+
Il wrapper è **sincrono**: applicato a una coroutine function misura il tempo
|
|
115
|
+
di creazione della coroutine, non della sua esecuzione, e non intercetta le
|
|
116
|
+
eccezioni sollevate durante l'await.
|
|
117
|
+
|
|
111
118
|
Example:
|
|
112
119
|
>>> @log
|
|
113
120
|
... def get_users():
|
|
@@ -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,32 @@
|
|
|
1
|
+
general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
general/log_config.ini,sha256=-uFliGt3D0PYauup7XGJ-B87JF1AvkiS8IakwubNYs8,321
|
|
3
|
+
general/logger.py,sha256=IVWWUHT5HJcCWW1-2QW8x_Ylyc24-Jf_N4eLJHth_fY,4584
|
|
4
|
+
general/paginator_dto.py,sha256=hpR_R0dLOLN_R5IOtSm2slxt4rYl5sxza5id1c-YdnQ,2480
|
|
5
|
+
general/exception/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
general/exception/access_exceptions.py,sha256=pbzNTj3P8-jL7md84px4zZtrdTHUe7Wn8-xVwwetxVg,863
|
|
7
|
+
general/exception/crud_exceptions.py,sha256=C2XPcCDctTAt20-we3II4WbSEb4yCQK4MjgVowlsdLQ,3138
|
|
8
|
+
general/exception/exception_interface.py,sha256=TJ2Yd_RgwP22DWCzNXQSEAMXdRJsAoOH07RLBjY5IwU,1577
|
|
9
|
+
general/interface/base/__init__.py,sha256=SDlTi28M8rzJ9yiKefo5l7V3K109LnnBHW-SuZ-5JtM,58
|
|
10
|
+
general/interface/base/base_model.py,sha256=Tsy70uC6HHe194ZrQaX7pXROqobOkZIOB63VdagmFHE,2561
|
|
11
|
+
general/interface/base/declarative_base.py,sha256=uaY24rt_bfDYRCMZ66QSIjDL5Yf7x5T_Bo26zXv5VLM,5560
|
|
12
|
+
general/interface/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
general/interface/metadata/crud_metadata.py,sha256=BFohkkpUd7w211NYqAD20X4ZpovrXH7BERA1_Rs4Dv0,23291
|
|
14
|
+
general/interface/metadata/geom_metadata.py,sha256=J5SMXMiDUXnzt7bNH12LIQO6S7jcLNf7Cm4CdLXv8Lg,12255
|
|
15
|
+
general/interface/repository/__init__.py,sha256=EcRukwf61Ua-9-JfM_ingVv1wXNRV8HBrrWwjj8m23w,136
|
|
16
|
+
general/interface/repository/crud_repository.py,sha256=ej8J_ybYVAfJUXKLdFPSq4w9iGtoM8z5kqJl34dxRZg,36669
|
|
17
|
+
general/interface/repository/geometry_repository.py,sha256=FZNYWbfiwYSeiJXdZjnmgSm8hbnNXoW_skOw9D5nwNk,11730
|
|
18
|
+
general/interface/repository/many_to_many_repository.py,sha256=ZyuK2fk5ZI9xE8xhZwk5Vb1QItnOxwEF5CGsemnMdn8,9197
|
|
19
|
+
general/interface/repository/view_repository.py,sha256=Rr9toRVop0sLpPwo9QvBDJgLQnB0ca--mp-hiwjGrzE,5835
|
|
20
|
+
general/interface/repository/handler/__init__.py,sha256=J-fc-5Dl_C821oy_EVRdyU-L2iXiNFriYJDbK4ODIMM,184
|
|
21
|
+
general/interface/repository/handler/base_handler.py,sha256=L55CbpMNXPwYNKqPQW12ES08w3KvNC7J8bA_YfXRhSY,4187
|
|
22
|
+
general/interface/repository/handler/ilike_handler.py,sha256=_HHTvj6Tm1L-WvUy1S9faRChoRZni11i9mv76I4Xak0,3919
|
|
23
|
+
general/interface/repository/handler/interval_handler.py,sha256=TFG9xtp92fVp-YfyKfFnD57d89CLRn1n6WrFUIh1KS8,6005
|
|
24
|
+
general/interface/repository/handler/json_handler.py,sha256=1F5t-TFWmzsGNhmazFX9mppZBPBY4WDz-FRpyutACWU,3343
|
|
25
|
+
general/interface/repository/handler/json_ilike_handler.py,sha256=4Rizn9xEUH6VLHSdz8eQhxaUwfo6xAjxdaRGRG4ODNw,6095
|
|
26
|
+
general/interface/repository/handler/or_handler.py,sha256=KG6T0DV3gsAqxgTaXqe6v9rIIyS7VNIqNWajjB3P-qI,4527
|
|
27
|
+
general/interface/repository/handler/statement_manager.py,sha256=PenSFIJ3BK6HJp8M8wemr5DbdrbhJ9Cu3aGcBND3NZ0,5195
|
|
28
|
+
python_general_be_lib-0.8.0.dist-info/licenses/LICENSE,sha256=iUaO1XZyB9P3Tmog0OILuTisP6vXGe3QKz-4yRTxOFk,1069
|
|
29
|
+
python_general_be_lib-0.8.0.dist-info/METADATA,sha256=mCNCdLBpcsDBfzIS890bqp2g-lmgT4bejKv938m18W4,3790
|
|
30
|
+
python_general_be_lib-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
python_general_be_lib-0.8.0.dist-info/top_level.txt,sha256=tTZePW8_CNUqSgKFd2SEH72ZbnhS0OYjRsgcv0ikSFY,8
|
|
32
|
+
python_general_be_lib-0.8.0.dist-info/RECORD,,
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-general-be-lib
|
|
3
|
-
Version: 0.7.1
|
|
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
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
general/log_config.ini,sha256=-uFliGt3D0PYauup7XGJ-B87JF1AvkiS8IakwubNYs8,321
|
|
3
|
-
general/logger.py,sha256=JrU50FOE2CDSJ61NthFMuVrGhUavfdksbEI5aUrakjI,4201
|
|
4
|
-
general/paginator_dto.py,sha256=hpR_R0dLOLN_R5IOtSm2slxt4rYl5sxza5id1c-YdnQ,2480
|
|
5
|
-
general/exception/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
general/exception/access_exceptions.py,sha256=8AV4_gybcdA7mTpA2XiZdl9rRGx08QCjHpBGv60EL4I,300
|
|
7
|
-
general/exception/crud_exceptions.py,sha256=c-VlatTPjEp7FWzA7rDByqfMqsEOworJqqfUJ3s0Z2w,1101
|
|
8
|
-
general/exception/exception_interface.py,sha256=T4bq8jwZ1YFpGlP60TxbvhkcyIrfsjcw5I6Xlxvzjig,352
|
|
9
|
-
general/interface/base/__init__.py,sha256=SDlTi28M8rzJ9yiKefo5l7V3K109LnnBHW-SuZ-5JtM,58
|
|
10
|
-
general/interface/base/base_model.py,sha256=Tsy70uC6HHe194ZrQaX7pXROqobOkZIOB63VdagmFHE,2561
|
|
11
|
-
general/interface/base/declarative_base.py,sha256=5hw2VpJHHebjixuI3d4wpkyAentGBGN54QXcniaoJ-A,5236
|
|
12
|
-
general/interface/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
general/interface/metadata/crud_metadata.py,sha256=wBBFfwq7xUu1sDlAg14qTAToYtSf6omcEOu2oLn-j5w,22672
|
|
14
|
-
general/interface/metadata/geom_metadata.py,sha256=kc6jkWis-WPdWAP-hN6U97ENVjNjny5NE2j-llwZYOQ,11998
|
|
15
|
-
general/interface/repository/__init__.py,sha256=EcRukwf61Ua-9-JfM_ingVv1wXNRV8HBrrWwjj8m23w,136
|
|
16
|
-
general/interface/repository/crud_repository.py,sha256=-68XWLGeUG-knvJ6wAaf8uOwTs5ZK9h4H4N2mN1HeGY,35351
|
|
17
|
-
general/interface/repository/geometry_repository.py,sha256=ULU-FnDATojHLDrObLPyAA3KErgX_tyYEeTU8h2O4Hs,11989
|
|
18
|
-
general/interface/repository/many_to_many_repository.py,sha256=iEhWyD8nPZSAPL8Z2wXrK-aRBnoh3evaSN-voaf0EP0,8662
|
|
19
|
-
general/interface/repository/view_repository.py,sha256=Rr9toRVop0sLpPwo9QvBDJgLQnB0ca--mp-hiwjGrzE,5835
|
|
20
|
-
general/interface/repository/handler/__init__.py,sha256=J-fc-5Dl_C821oy_EVRdyU-L2iXiNFriYJDbK4ODIMM,184
|
|
21
|
-
general/interface/repository/handler/base_handler.py,sha256=jedf7ACFPlYKHC9U9ZpVN4vCC0EXO3s4Rtyn15kKi_w,3558
|
|
22
|
-
general/interface/repository/handler/ilike_handler.py,sha256=_HHTvj6Tm1L-WvUy1S9faRChoRZni11i9mv76I4Xak0,3919
|
|
23
|
-
general/interface/repository/handler/interval_handler.py,sha256=LGg9MQyalTmlxthdzlIJXv5WT58NEz1QMUB-06pNR7M,4135
|
|
24
|
-
general/interface/repository/handler/json_handler.py,sha256=7bS59seB-It5KHsWXTbHGsEvbQZ6lYyvX-d29BcRLVE,2956
|
|
25
|
-
general/interface/repository/handler/json_ilike_handler.py,sha256=yDuKER3yqtnHpVWndF8PVEaqDThcq1EhZzLaqxbQ0bU,4491
|
|
26
|
-
general/interface/repository/handler/or_handler.py,sha256=v97QMQwqrhTpfpZQWfvL6HC4lM9kjMEmY0-Sps968oI,3818
|
|
27
|
-
general/interface/repository/handler/statement_manager.py,sha256=sGDMU4fPkF0_9-SbDmAw8Vpel1Z7eImBs8fHO-5Lr_0,4429
|
|
28
|
-
python_general_be_lib-0.7.1.dist-info/licenses/LICENSE,sha256=iUaO1XZyB9P3Tmog0OILuTisP6vXGe3QKz-4yRTxOFk,1069
|
|
29
|
-
python_general_be_lib-0.7.1.dist-info/METADATA,sha256=WEDCiuFY7P2ToCxtUhOOxMaYhtYuyDF9GnaQHLO_eBY,1384
|
|
30
|
-
python_general_be_lib-0.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
-
python_general_be_lib-0.7.1.dist-info/top_level.txt,sha256=tTZePW8_CNUqSgKFd2SEH72ZbnhS0OYjRsgcv0ikSFY,8
|
|
32
|
-
python_general_be_lib-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
{python_general_be_lib-0.7.1.dist-info → python_general_be_lib-0.8.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{python_general_be_lib-0.7.1.dist-info → python_general_be_lib-0.8.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|