python-general-be-lib 0.5.4__py3-none-any.whl → 0.6.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/interface/metadata/crud_metadata.py +326 -3
- general/interface/metadata/geom_metadata.py +125 -2
- general/interface/repository/crud_repository.py +85 -24
- general/interface/repository/geometry_repository.py +23 -15
- general/interface/repository/handler/base_handler.py +13 -2
- {python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/METADATA +1 -1
- {python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/RECORD +10 -10
- {python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/WHEEL +0 -0
- {python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -5,18 +5,58 @@ from typing import Optional, Any, Type, Iterable, Sequence
|
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
from sqlalchemy import MetaData, Engine, Column, Table, inspect, Connection, select, RowMapping, CursorResult, not_
|
|
8
|
+
from sqlalchemy.exc import NoSuchTableError
|
|
8
9
|
|
|
9
|
-
from ...exception.crud_exceptions import HasNoAttributeException
|
|
10
|
+
from ...exception.crud_exceptions import HasNoAttributeException, InternalServerException
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class CrudMetadata:
|
|
14
|
+
"""Repository CRUD basato su SQLAlchemy Core e ``MetaData``, senza ORM.
|
|
15
|
+
|
|
16
|
+
Alternativa a ``CrudRepository`` che opera su tabelle riflesse (``Table``)
|
|
17
|
+
e ``Connection`` invece che su entità mappate e ``Session``. Utile per
|
|
18
|
+
lavorare con tabelle prive di un modello ORM: lo schema viene riflesso a
|
|
19
|
+
runtime e le tabelle sono risolte per nome.
|
|
20
|
+
|
|
21
|
+
Le operazioni accettano una connessione esterna opzionale: se fornita
|
|
22
|
+
(``keep_open=True``) i risultati grezzi vengono restituiti senza commit né
|
|
23
|
+
chiusura; altrimenti la connessione viene aperta, committata e chiusa
|
|
24
|
+
internamente.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
schema (str | None): Nome dello schema di default per le tabelle.
|
|
28
|
+
metadata (MetaData): Contenitore SQLAlchemy delle tabelle riflesse/definite.
|
|
29
|
+
engine (Engine): Istanza SQLAlchemy Engine connessa al database.
|
|
30
|
+
engine_inspection (Inspector): Inspector usato per le introspezioni
|
|
31
|
+
(es. presenza tabelle).
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> meta = CrudMetadata(engine=engine, schema_name="public")
|
|
35
|
+
>>> rows = meta.find(from_table="users", where={"active": True}, limit=10)
|
|
36
|
+
"""
|
|
13
37
|
__slots__ = "schema", "metadata", "engine", "engine_inspection"
|
|
14
38
|
|
|
15
39
|
@property
|
|
16
40
|
def open_connection(self):
|
|
41
|
+
"""Apre e restituisce una nuova connessione all'engine.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Connection: Nuova connessione associata all'engine configurato.
|
|
45
|
+
"""
|
|
17
46
|
return self.engine.connect()
|
|
18
47
|
|
|
19
48
|
def __init__(self, engine: Engine, schema_name: str = None, reflect: bool = True):
|
|
49
|
+
"""Inizializza il repository con l'engine e (opzionalmente) riflette lo schema.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
engine (Engine): Istanza SQLAlchemy Engine connessa al database.
|
|
53
|
+
schema_name (str, optional): Nome dello schema di default a cui
|
|
54
|
+
appartengono le tabelle. Se ``None``, viene usato lo schema di
|
|
55
|
+
default della connessione.
|
|
56
|
+
reflect (bool): Se ``True``, riflette tutte le tabelle dello schema
|
|
57
|
+
indicato alla costruzione (operazione potenzialmente costosa su
|
|
58
|
+
schemi grandi). Default: ``True``.
|
|
59
|
+
"""
|
|
20
60
|
self.schema = schema_name
|
|
21
61
|
self.metadata = MetaData(schema_name) if schema_name else MetaData()
|
|
22
62
|
self.engine = engine
|
|
@@ -27,21 +67,62 @@ class CrudMetadata:
|
|
|
27
67
|
# ---------------------------
|
|
28
68
|
|
|
29
69
|
def _handle_exception(self, connection: Connection, e: Exception, to_raise: bool = True):
|
|
70
|
+
"""Chiude la connessione, logga l'errore ed eventualmente lo rilancia.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
connection (Connection): Connessione da chiudere.
|
|
74
|
+
e (Exception): Eccezione catturata.
|
|
75
|
+
to_raise (bool): Se ``True``, rilancia l'eccezione dopo il logging.
|
|
76
|
+
Default: ``True``.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
Exception: L'eccezione originale, se ``to_raise=True``.
|
|
80
|
+
"""
|
|
30
81
|
connection.close()
|
|
31
82
|
logging.error(e)
|
|
32
83
|
if to_raise:
|
|
33
84
|
raise e
|
|
34
85
|
|
|
35
86
|
def commit(self, connection: Connection):
|
|
87
|
+
"""Esegue il commit sulla connessione, gestendo eventuali eccezioni.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
connection (Connection): Connessione su cui eseguire il commit.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
Exception: In caso di errore durante il commit (via ``_handle_exception``).
|
|
94
|
+
"""
|
|
36
95
|
try:
|
|
37
96
|
connection.commit()
|
|
38
97
|
except Exception as e:
|
|
39
98
|
self._handle_exception(connection, e)
|
|
40
99
|
|
|
41
100
|
def has_columns(self, *columns: Iterable[str], table: Table):
|
|
101
|
+
"""Verifica che tutte le colonne specificate esistano sulla tabella.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
*columns (str): Nomi delle colonne da verificare.
|
|
105
|
+
table (Table): Tabella su cui effettuare la verifica.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
bool: ``True`` se tutte le colonne sono presenti, ``False`` altrimenti.
|
|
109
|
+
"""
|
|
42
110
|
return all([column in [c.key for c in table.c] for column in columns])
|
|
43
111
|
|
|
44
112
|
def _select(self, table: Table, columns: list[str] = None):
|
|
113
|
+
"""Costruisce lo statement SELECT sulla tabella.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
table (Table): Tabella su cui costruire il SELECT.
|
|
117
|
+
columns (list[str], optional): Colonne da selezionare. Se ``None``,
|
|
118
|
+
seleziona tutte le colonne della tabella.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Select: Statement SELECT configurato.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
HasNoAttributeException: Se una colonna specificata non esiste sulla tabella.
|
|
125
|
+
"""
|
|
45
126
|
try:
|
|
46
127
|
stmt = table.select() if not columns else select(*[table.c[column] for column in columns])
|
|
47
128
|
except:
|
|
@@ -50,6 +131,25 @@ class CrudMetadata:
|
|
|
50
131
|
|
|
51
132
|
def _return(self, connection: Connection, result: CursorResult | RowMapping | Sequence[RowMapping] | Sequence[dict] = None, keep_open: bool = False, commit: bool = True,
|
|
52
133
|
parsing_model: Type[BaseModel] | BaseModel = None):
|
|
134
|
+
"""Serializza il risultato e chiude la connessione se necessario.
|
|
135
|
+
|
|
136
|
+
Se ``keep_open=True`` (connessione esterna), restituisce il risultato
|
|
137
|
+
grezzo senza chiudere la connessione. Altrimenti esegue il commit (se
|
|
138
|
+
richiesto), serializza con il ``parsing_model`` e chiude la connessione.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
connection (Connection): Connessione corrente.
|
|
142
|
+
result (CursorResult | RowMapping | Sequence): Righe risultanti
|
|
143
|
+
(tipicamente mapping ``{colonna: valore}``).
|
|
144
|
+
keep_open (bool): Se ``True``, non chiude la connessione e non serializza.
|
|
145
|
+
commit (bool): Se ``True`` e ``keep_open=False``, esegue il commit.
|
|
146
|
+
parsing_model (Type[BaseModel], optional): Modello Pydantic con cui
|
|
147
|
+
istanziare ogni riga (``parsing_model(**row)``). Se ``None``,
|
|
148
|
+
restituisce le righe grezze.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
list[BaseModel] | Sequence: Modelli serializzati o righe grezze.
|
|
152
|
+
"""
|
|
53
153
|
if keep_open:
|
|
54
154
|
return result
|
|
55
155
|
else:
|
|
@@ -60,15 +160,48 @@ class CrudMetadata:
|
|
|
60
160
|
return result
|
|
61
161
|
|
|
62
162
|
def create_table(self, name: str, columns: Iterable[Column], **kwargs):
|
|
163
|
+
"""Crea una nuova tabella sul database.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name (str): Nome della tabella da creare.
|
|
167
|
+
columns (Iterable[Column]): Colonne che compongono la tabella.
|
|
168
|
+
**kwargs: Argomenti aggiuntivi passati al costruttore ``Table``
|
|
169
|
+
(es. ``schema``, vincoli, ``Index``).
|
|
170
|
+
"""
|
|
63
171
|
table = Table(name=name, metadata=self.metadata, *columns, **kwargs)
|
|
64
172
|
table.create(self.engine)
|
|
65
173
|
|
|
66
174
|
def drop_table(self, name: str):
|
|
175
|
+
"""Elimina una tabella dal database e dal ``MetaData`` locale.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
name (str): Nome della tabella da eliminare.
|
|
179
|
+
"""
|
|
67
180
|
table = self.get_table(name=name)
|
|
68
181
|
table.drop(self.engine)
|
|
69
182
|
self.metadata.remove(table=table)
|
|
70
183
|
|
|
71
184
|
def find(self, from_table: str, connection: Connection = None, where: dict[str, Any] = None, columns: list[str] = None, limit: int = 0, parsing_model: Type[BaseModel] = None, **kwhere):
|
|
185
|
+
"""Cerca le righe della tabella che corrispondono ai criteri specificati.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
from_table (str): Nome della tabella su cui eseguire la ricerca.
|
|
189
|
+
connection (Connection, optional): Connessione esterna. Se ``None``,
|
|
190
|
+
ne viene aperta una nuova che viene chiusa al termine.
|
|
191
|
+
where (dict[str, Any], optional): Filtri come dizionario
|
|
192
|
+
``{colonna: valore}``. Ha precedenza su ``**kwhere``.
|
|
193
|
+
columns (list[str], optional): Colonne da includere nel risultato.
|
|
194
|
+
limit (int): Numero massimo di righe. 0 = nessun limite. Default: 0.
|
|
195
|
+
parsing_model (Type[BaseModel], optional): Modello Pydantic per la
|
|
196
|
+
serializzazione delle righe.
|
|
197
|
+
**kwhere: Filtri aggiuntivi come keyword arguments.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
list[BaseModel] | Sequence[RowMapping]: Righe serializzate o mapping grezzi.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> meta.find(from_table="users", name="Mario", limit=10)
|
|
204
|
+
"""
|
|
72
205
|
keep_open = False if connection is None else True
|
|
73
206
|
connection = self.open_connection if connection is None else connection
|
|
74
207
|
table = self.get_table(name=from_table)
|
|
@@ -76,6 +209,20 @@ class CrudMetadata:
|
|
|
76
209
|
return self._return(connection=connection, result=result, keep_open=keep_open, commit=False, parsing_model=parsing_model)
|
|
77
210
|
|
|
78
211
|
def _find(self, table: Table, connection: Connection, where: dict[str, Any] = None, columns: list[str] = None, limit: int = 0, **kwhere):
|
|
212
|
+
"""Costruisce ed esegue lo statement SELECT con filtri e limit.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
table (Table): Tabella su cui eseguire la SELECT.
|
|
216
|
+
connection (Connection): Connessione corrente.
|
|
217
|
+
where (dict[str, Any], optional): Filtri ``{colonna: valore}``.
|
|
218
|
+
Se ``None``, usa ``**kwhere``.
|
|
219
|
+
columns (list[str], optional): Colonne da selezionare.
|
|
220
|
+
limit (int): Numero massimo di righe. Default: 0 (nessun limite).
|
|
221
|
+
**kwhere: Filtri aggiuntivi come keyword arguments.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Sequence[RowMapping]: Righe risultanti come mapping.
|
|
225
|
+
"""
|
|
79
226
|
if where is None:
|
|
80
227
|
where = kwhere if kwhere else {}
|
|
81
228
|
stmt = self._select(table=table, columns=columns)
|
|
@@ -85,6 +232,30 @@ class CrudMetadata:
|
|
|
85
232
|
|
|
86
233
|
def insert(self, from_table: str, connection: Connection = None, models: list[Type[BaseModel] | BaseModel] = None, values: list[dict[str, Any]] = None, returning: bool = True,
|
|
87
234
|
parsing_model: Type[BaseModel] | BaseModel = None):
|
|
235
|
+
"""Inserisce righe nella tabella a partire da modelli e/o dizionari di valori.
|
|
236
|
+
|
|
237
|
+
Le righe possono essere fornite come modelli Pydantic (``models``, da cui
|
|
238
|
+
si estraggono i valori con ``model_dump(exclude_none=True)``) e/o come
|
|
239
|
+
dizionari grezzi (``values``). Le due fonti vengono unite.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
from_table (str): Nome della tabella su cui inserire.
|
|
243
|
+
connection (Connection, optional): Connessione esterna. Se ``None``,
|
|
244
|
+
ne viene aperta una nuova con commit automatico.
|
|
245
|
+
models (list[BaseModel], optional): Modelli Pydantic da inserire.
|
|
246
|
+
values (list[dict[str, Any]], optional): Righe come dizionari grezzi.
|
|
247
|
+
returning (bool): Se ``True``, usa ``RETURNING`` per recuperare le righe
|
|
248
|
+
inserite. Default: ``True``.
|
|
249
|
+
parsing_model (Type[BaseModel], optional): Modello per la serializzazione
|
|
250
|
+
del risultato.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
list[BaseModel] | Sequence[RowMapping]: Righe inserite (serializzate o grezze).
|
|
254
|
+
|
|
255
|
+
Note:
|
|
256
|
+
Se ``values`` è fornito, viene esteso in-place con i valori derivati
|
|
257
|
+
da ``models``.
|
|
258
|
+
"""
|
|
88
259
|
keep_open = False if connection is None else True
|
|
89
260
|
connection = self.open_connection if connection is None else connection
|
|
90
261
|
table = self.get_table(name=from_table)
|
|
@@ -94,6 +265,19 @@ class CrudMetadata:
|
|
|
94
265
|
return self._return(connection=connection, result=result, keep_open=keep_open, commit=True, parsing_model=parsing_model)
|
|
95
266
|
|
|
96
267
|
def _insert(self, table: Table, connection: Connection, values: list[dict[str, Any]] = None, returning: bool = True):
|
|
268
|
+
"""Costruisce ed esegue lo statement INSERT con i valori forniti.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
table (Table): Tabella su cui inserire.
|
|
272
|
+
connection (Connection): Connessione corrente.
|
|
273
|
+
values (list[dict[str, Any]], optional): Righe da inserire. Se vuoto,
|
|
274
|
+
non esegue nessuna operazione.
|
|
275
|
+
returning (bool): Se ``True``, usa ``RETURNING``. Default: ``True``.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Sequence[RowMapping] | list: Righe inserite come mapping, o lista vuota
|
|
279
|
+
se non ci sono valori.
|
|
280
|
+
"""
|
|
97
281
|
if values:
|
|
98
282
|
stmt = table.insert().values(values)
|
|
99
283
|
stmt = stmt if not returning else stmt.returning()
|
|
@@ -102,6 +286,25 @@ class CrudMetadata:
|
|
|
102
286
|
return []
|
|
103
287
|
|
|
104
288
|
def update(self, from_table: str, connection: Connection = None, where: dict[str, Any] = None, returning: bool = True, parsing_model: Type[BaseModel] | BaseModel = None, **kwargs):
|
|
289
|
+
"""Aggiorna le righe che soddisfano i criteri ``where`` con i valori in ``**kwargs``.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
from_table (str): Nome della tabella da aggiornare.
|
|
293
|
+
connection (Connection, optional): Connessione esterna. Se ``None``,
|
|
294
|
+
ne viene aperta una nuova con commit automatico.
|
|
295
|
+
where (dict[str, Any], optional): Filtri per selezionare le righe da
|
|
296
|
+
aggiornare. Se ``None``, aggiorna tutte le righe (usare con cautela).
|
|
297
|
+
returning (bool): Se ``True``, usa ``RETURNING``. Default: ``True``.
|
|
298
|
+
parsing_model (Type[BaseModel], optional): Modello per la serializzazione
|
|
299
|
+
del risultato.
|
|
300
|
+
**kwargs: Valori da impostare sulle righe trovate.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
list[BaseModel] | Sequence[RowMapping]: Righe aggiornate (serializzate o grezze).
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
>>> meta.update(from_table="users", where={"id": 1}, name="Luigi")
|
|
307
|
+
"""
|
|
105
308
|
keep_open = False if connection is None else True
|
|
106
309
|
connection = self.open_connection if connection is None else connection
|
|
107
310
|
table = self.get_table(name=from_table)
|
|
@@ -109,6 +312,18 @@ class CrudMetadata:
|
|
|
109
312
|
return self._return(connection=connection, result=result, keep_open=keep_open, commit=True, parsing_model=parsing_model)
|
|
110
313
|
|
|
111
314
|
def _update(self, table: Table, connection: Connection, where: dict[str, Any] = None, returning: bool = True, **kwargs):
|
|
315
|
+
"""Costruisce ed esegue lo statement UPDATE con filtri e valori.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
table (Table): Tabella da aggiornare.
|
|
319
|
+
connection (Connection): Connessione corrente.
|
|
320
|
+
where (dict[str, Any], optional): Filtri per le righe da aggiornare.
|
|
321
|
+
returning (bool): Se ``True``, usa ``RETURNING``. Default: ``True``.
|
|
322
|
+
**kwargs: Valori da impostare sulle righe.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Sequence[RowMapping]: Righe aggiornate come mapping.
|
|
326
|
+
"""
|
|
112
327
|
if where is None:
|
|
113
328
|
where = {}
|
|
114
329
|
stmt = table.update()
|
|
@@ -118,6 +333,22 @@ class CrudMetadata:
|
|
|
118
333
|
return self.execute(connection=connection, stmt=stmt, mapping=True)
|
|
119
334
|
|
|
120
335
|
def delete(self, from_table: str, connection: Connection = None, where: dict[str, Any] = None, **kwhere):
|
|
336
|
+
"""Elimina le righe che soddisfano i criteri specificati.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
from_table (str): Nome della tabella da cui eliminare.
|
|
340
|
+
connection (Connection, optional): Connessione esterna. Se fornita, il
|
|
341
|
+
commit non viene eseguito automaticamente.
|
|
342
|
+
where (dict[str, Any], optional): Filtri come dizionario.
|
|
343
|
+
Ha precedenza su ``**kwhere``.
|
|
344
|
+
**kwhere: Filtri come keyword arguments.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
int: Numero di righe eliminate.
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> meta.delete(from_table="users", id=5)
|
|
351
|
+
"""
|
|
121
352
|
keep_open = False if connection is None else True
|
|
122
353
|
connection = self.open_connection if connection is None else connection
|
|
123
354
|
table = self.get_table(name=from_table)
|
|
@@ -130,6 +361,18 @@ class CrudMetadata:
|
|
|
130
361
|
return num_rows
|
|
131
362
|
|
|
132
363
|
def _delete(self, table: Table, connection: Connection, where: dict[str, Any] = None, **kwhere):
|
|
364
|
+
"""Costruisce ed esegue lo statement DELETE con i filtri forniti.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
table (Table): Tabella da cui eliminare.
|
|
368
|
+
connection (Connection): Connessione corrente.
|
|
369
|
+
where (dict[str, Any], optional): Filtri ``{colonna: valore}``.
|
|
370
|
+
Se ``None``, usa ``**kwhere``.
|
|
371
|
+
**kwhere: Filtri come keyword arguments.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
int: Numero di righe eliminate (``rowcount``).
|
|
375
|
+
"""
|
|
133
376
|
if where is None:
|
|
134
377
|
where = kwhere if kwhere else {}
|
|
135
378
|
stmt = table.delete()
|
|
@@ -139,6 +382,22 @@ class CrudMetadata:
|
|
|
139
382
|
# --------------
|
|
140
383
|
|
|
141
384
|
def execute(self, connection: Connection, stmt, mapping: bool = False):
|
|
385
|
+
"""Esegue uno statement sulla connessione, gestendo le eccezioni.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
connection (Connection): Connessione su cui eseguire lo statement.
|
|
389
|
+
stmt: Statement SQLAlchemy Core da eseguire.
|
|
390
|
+
mapping (bool): Se ``True``, restituisce le righe come mapping
|
|
391
|
+
(``result.mappings().all()``); altrimenti restituisce il
|
|
392
|
+
``CursorResult`` grezzo. Default: ``False``.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Sequence[RowMapping] | CursorResult: Righe come mapping se ``mapping=True``,
|
|
396
|
+
altrimenti il result grezzo.
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
Exception: In caso di errore durante l'esecuzione (via ``_handle_exception``).
|
|
400
|
+
"""
|
|
142
401
|
try:
|
|
143
402
|
result = connection.execute(stmt)
|
|
144
403
|
except Exception as e:
|
|
@@ -149,6 +408,16 @@ class CrudMetadata:
|
|
|
149
408
|
return result
|
|
150
409
|
|
|
151
410
|
def _get_table(self, name: str):
|
|
411
|
+
"""Recupera una tabella dal ``MetaData`` locale per nome.
|
|
412
|
+
|
|
413
|
+
Normalizza il nome anteponendo lo schema configurato, se necessario.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
name (str): Nome della tabella (con o senza prefisso di schema).
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Table | None: La tabella se già presente nel ``MetaData``, altrimenti ``None``.
|
|
420
|
+
"""
|
|
152
421
|
if self.schema is not None:
|
|
153
422
|
table_name = name if name.startswith(f"{self.schema}.") else f"{self.schema}.{name}"
|
|
154
423
|
else:
|
|
@@ -156,15 +425,54 @@ class CrudMetadata:
|
|
|
156
425
|
return self.metadata.tables.get(table_name)
|
|
157
426
|
|
|
158
427
|
def get_table(self, name: str) -> Optional[Table]:
|
|
428
|
+
"""Restituisce una tabella, riflettendola dal database se non già nota.
|
|
429
|
+
|
|
430
|
+
Cerca prima nel ``MetaData`` locale; se assente, tenta il caricamento
|
|
431
|
+
automatico (``autoload_with``) dal database.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
name (str): Nome della tabella da recuperare.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Optional[Table]: La tabella richiesta.
|
|
438
|
+
|
|
439
|
+
Raises:
|
|
440
|
+
InternalServerException: Se la tabella non esiste nel database.
|
|
441
|
+
"""
|
|
159
442
|
table = self._get_table(name=name)
|
|
160
443
|
if table is None:
|
|
161
|
-
|
|
444
|
+
try:
|
|
445
|
+
table = Table(name, self.metadata, schema=self.schema, autoload_with=self.engine)
|
|
446
|
+
except NoSuchTableError:
|
|
447
|
+
raise InternalServerException(f"No such table {name}")
|
|
162
448
|
return table
|
|
163
449
|
|
|
164
450
|
def has_table(self, name: str):
|
|
451
|
+
"""Verifica se una tabella esiste nel database.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
name (str): Nome della tabella da verificare.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
bool: ``True`` se la tabella esiste nello schema configurato.
|
|
458
|
+
"""
|
|
165
459
|
return self.engine_inspection.has_table(name, self.schema)
|
|
166
460
|
|
|
167
461
|
def _add_limit_offset_condition(self, stmt, limit: int, page: int = None):
|
|
462
|
+
"""Aggiunge le clausole LIMIT e OFFSET allo statement per la paginazione.
|
|
463
|
+
|
|
464
|
+
Non applica nessuna modifica se ``limit`` è 0 o negativo.
|
|
465
|
+
L'offset viene calcolato come ``(page - 1) * limit``.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
stmt: Statement su cui applicare LIMIT/OFFSET.
|
|
469
|
+
limit (int): Numero massimo di righe. 0 = nessun limite.
|
|
470
|
+
page (int, optional): Numero di pagina (1-based). Se ``None`` o <= 0,
|
|
471
|
+
non viene applicato nessun offset.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Statement con LIMIT e/o OFFSET applicati.
|
|
475
|
+
"""
|
|
168
476
|
if limit and limit > 0:
|
|
169
477
|
stmt = stmt.limit(limit)
|
|
170
478
|
if page and page > 0:
|
|
@@ -173,7 +481,22 @@ class CrudMetadata:
|
|
|
173
481
|
return stmt
|
|
174
482
|
|
|
175
483
|
def _eq_where(self, col: Column, condition, neq: bool = False):
|
|
484
|
+
"""Costruisce la singola condizione WHERE per una colonna.
|
|
485
|
+
|
|
486
|
+
Logica applicata:
|
|
487
|
+
- ``list`` → ``col IN (values)``
|
|
488
|
+
- Tipo ``ARRAY`` → ``value = ANY(col)``
|
|
489
|
+
- Scalare → ``col == value``
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
col (Column): Colonna su cui applicare la condizione.
|
|
493
|
+
condition: Valore o lista di valori da confrontare.
|
|
494
|
+
neq (bool): Se ``True``, nega la condizione (``NOT``). Default: ``False``.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
ColumnElement: Espressione booleana SQLAlchemy.
|
|
498
|
+
"""
|
|
176
499
|
eq_where = col.in_(condition) if isinstance(condition, list) else col == condition if str(col.type) != "ARRAY" else condition == col.any_()
|
|
177
500
|
if neq:
|
|
178
|
-
not_(eq_where)
|
|
501
|
+
eq_where = not_(eq_where)
|
|
179
502
|
return eq_where
|
|
@@ -10,11 +10,57 @@ from .crud_metadata import CrudMetadata
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class GeomMetadata(CrudMetadata):
|
|
13
|
+
"""Repository Core con operazioni spaziali PostGIS su tabelle riflesse.
|
|
14
|
+
|
|
15
|
+
Estende ``CrudMetadata`` (SQLAlchemy Core, senza ORM) aggiungendo query
|
|
16
|
+
geografiche di intersezione e contenimento via GeoAlchemy2. A differenza di
|
|
17
|
+
``GeometryRepository``, la colonna geometrica e il suo SRID non sono fissati
|
|
18
|
+
sulla classe ma passati come argomenti a ciascun metodo, insieme al nome
|
|
19
|
+
della tabella.
|
|
20
|
+
|
|
21
|
+
Gestisce la riproiezione automatica tra SRID diversi e il buffering opzionale
|
|
22
|
+
della geometria di input.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> meta = GeomMetadata(engine=engine, schema_name="public")
|
|
26
|
+
>>> rows = meta.intersects(
|
|
27
|
+
... from_table="zones", geom=point,
|
|
28
|
+
... polygon_column_name="geom", polygon_column_srid=32632,
|
|
29
|
+
... )
|
|
30
|
+
"""
|
|
13
31
|
|
|
14
32
|
def intersects(self, from_table: str, geom: WKBElement, polygon_column_name: str, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None,
|
|
15
33
|
where: dict[str, Any] = None, not_where: dict[str, Any] = None, limit: int = 0, geom_srid: int = 4326, parsing_model: Type[BaseModel] | BaseModel = None, intersection: bool = False,
|
|
16
34
|
intersection_area: bool = False):
|
|
17
|
-
|
|
35
|
+
"""Restituisce le righe la cui geometria interseca quella fornita.
|
|
36
|
+
|
|
37
|
+
Supporta buffer opzionale, filtri positivi e negativi, riproiezione
|
|
38
|
+
automatica e l'aggiunta facoltativa della geometria di intersezione e
|
|
39
|
+
della sua area.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
from_table (str): Nome della tabella su cui eseguire la query.
|
|
43
|
+
geom (WKBElement): Geometria di riferimento per l'intersezione.
|
|
44
|
+
polygon_column_name (str): Nome della colonna geometrica della tabella.
|
|
45
|
+
polygon_column_srid (int): SRID della colonna geometrica.
|
|
46
|
+
connection (Connection, optional): Connessione esterna.
|
|
47
|
+
tolerance (float): Buffer applicato alla geometria prima dell'intersezione
|
|
48
|
+
(nelle unità del SRID della colonna). Default: 0.0 (nessun buffer).
|
|
49
|
+
columns (list[str], optional): Colonne da includere nel risultato.
|
|
50
|
+
where (dict[str, Any], optional): Filtri aggiuntivi positivi.
|
|
51
|
+
not_where (dict[str, Any], optional): Filtri aggiuntivi negativi.
|
|
52
|
+
limit (int): Numero massimo di righe. Default: 0 (nessun limite).
|
|
53
|
+
geom_srid (int): SRID della geometria fornita. Default: 4326.
|
|
54
|
+
parsing_model (Type[BaseModel], optional): Modello per la serializzazione.
|
|
55
|
+
intersection (bool): Se ``True``, aggiunge la colonna ``intersection``
|
|
56
|
+
con la geometria di intersezione. Default: ``False``.
|
|
57
|
+
intersection_area (bool): Se ``True``, aggiunge la colonna
|
|
58
|
+
``intersection_area`` con l'area dell'intersezione (riproiettata in
|
|
59
|
+
EPSG:3857). Default: ``False``.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
list[BaseModel] | Sequence[RowMapping]: Righe che intersecano la geometria.
|
|
63
|
+
"""
|
|
18
64
|
keep_open = False if connection is None else True
|
|
19
65
|
connection = self.open_connection if connection is None else connection
|
|
20
66
|
table = self.get_table(name=from_table)
|
|
@@ -25,6 +71,26 @@ class GeomMetadata(CrudMetadata):
|
|
|
25
71
|
|
|
26
72
|
def _intersects(self, table: Table, geom: WKBElement, polygon_column: Column, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None, where: dict[str, Any] = None,
|
|
27
73
|
not_where: dict[str, Any] = None, limit: int = 0, geom_srid: int = 4326, intersection: bool = False, intersection_area: bool = False):
|
|
74
|
+
"""Costruisce ed esegue lo statement di intersezione spaziale.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
table (Table): Tabella su cui eseguire la query.
|
|
78
|
+
geom (WKBElement): Geometria di riferimento.
|
|
79
|
+
polygon_column (Column): Colonna geometrica della tabella.
|
|
80
|
+
polygon_column_srid (int): SRID della colonna geometrica.
|
|
81
|
+
connection (Connection, optional): Connessione corrente.
|
|
82
|
+
tolerance (float): Buffer applicato alla geometria. Default: 0.0.
|
|
83
|
+
columns (list[str], optional): Colonne da selezionare.
|
|
84
|
+
where (dict[str, Any], optional): Filtri positivi.
|
|
85
|
+
not_where (dict[str, Any], optional): Filtri negativi.
|
|
86
|
+
limit (int): Numero massimo di righe. Default: 0.
|
|
87
|
+
geom_srid (int): SRID della geometria fornita. Default: 4326.
|
|
88
|
+
intersection (bool): Se ``True``, aggiunge la geometria di intersezione.
|
|
89
|
+
intersection_area (bool): Se ``True``, aggiunge l'area dell'intersezione.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Sequence[RowMapping]: Righe risultanti come mapping.
|
|
93
|
+
"""
|
|
28
94
|
if where is None:
|
|
29
95
|
where = {}
|
|
30
96
|
if not_where is None:
|
|
@@ -53,7 +119,30 @@ class GeomMetadata(CrudMetadata):
|
|
|
53
119
|
|
|
54
120
|
def contains(self, from_table: str, geom: WKBElement, polygon_column_name: str, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None,
|
|
55
121
|
where: dict[str, Any] = None, limit: int = 0, contained: bool = False, geom_srid: int = 4326, parsing_model: Type[BaseModel] | BaseModel = None):
|
|
56
|
-
|
|
122
|
+
"""Restituisce le righe che contengono (o sono contenute in) una geometria.
|
|
123
|
+
|
|
124
|
+
Il parametro ``contained`` inverte la direzione del contenimento:
|
|
125
|
+
- ``False`` (default): righe la cui geometria **contiene** ``geom``.
|
|
126
|
+
- ``True``: righe la cui geometria è **contenuta in** ``geom``.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
from_table (str): Nome della tabella su cui eseguire la query.
|
|
130
|
+
geom (WKBElement): Geometria di riferimento.
|
|
131
|
+
polygon_column_name (str): Nome della colonna geometrica della tabella.
|
|
132
|
+
polygon_column_srid (int): SRID della colonna geometrica.
|
|
133
|
+
connection (Connection, optional): Connessione esterna.
|
|
134
|
+
tolerance (float): Buffer applicato alla geometria. Default: 0.0.
|
|
135
|
+
columns (list[str], optional): Colonne da includere nel risultato.
|
|
136
|
+
where (dict[str, Any], optional): Filtri aggiuntivi.
|
|
137
|
+
limit (int): Numero massimo di righe. Default: 0.
|
|
138
|
+
contained (bool): Se ``True``, inverte la direzione del contenimento.
|
|
139
|
+
Default: ``False``.
|
|
140
|
+
geom_srid (int): SRID della geometria fornita. Default: 4326.
|
|
141
|
+
parsing_model (Type[BaseModel], optional): Modello per la serializzazione.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
list[BaseModel] | Sequence[RowMapping]: Righe che soddisfano il contenimento.
|
|
145
|
+
"""
|
|
57
146
|
keep_open = False if connection is None else True
|
|
58
147
|
connection = self.open_connection if connection is None else connection
|
|
59
148
|
table = self.get_table(name=from_table)
|
|
@@ -64,6 +153,24 @@ class GeomMetadata(CrudMetadata):
|
|
|
64
153
|
|
|
65
154
|
def _contains(self, table: Table, geom: WKBElement, polygon_column: Column, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None, where: dict[str, Any] = None,
|
|
66
155
|
limit: int = 0, contained: bool = False, geom_srid: int = 4326):
|
|
156
|
+
"""Costruisce ed esegue lo statement di contenimento spaziale.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
table (Table): Tabella su cui eseguire la query.
|
|
160
|
+
geom (WKBElement): Geometria di riferimento.
|
|
161
|
+
polygon_column (Column): Colonna geometrica della tabella.
|
|
162
|
+
polygon_column_srid (int): SRID della colonna geometrica.
|
|
163
|
+
connection (Connection, optional): Connessione corrente.
|
|
164
|
+
tolerance (float): Buffer applicato alla geometria. Default: 0.0.
|
|
165
|
+
columns (list[str], optional): Colonne da selezionare.
|
|
166
|
+
where (dict[str, Any], optional): Filtri aggiuntivi.
|
|
167
|
+
limit (int): Numero massimo di righe. Default: 0.
|
|
168
|
+
contained (bool): Se ``True``, inverte la direzione del contenimento.
|
|
169
|
+
geom_srid (int): SRID della geometria fornita. Default: 4326.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Sequence[RowMapping]: Righe risultanti come mapping.
|
|
173
|
+
"""
|
|
67
174
|
if columns is None:
|
|
68
175
|
columns = []
|
|
69
176
|
if where is None:
|
|
@@ -82,6 +189,22 @@ class GeomMetadata(CrudMetadata):
|
|
|
82
189
|
return self.execute(connection=connection, stmt=stmt, mapping=True)
|
|
83
190
|
|
|
84
191
|
def _geom_condition(self, geom: WKBElement, polygon_column_srid: int = 4326, geom_srid: int = 4326, buffer: float = 0):
|
|
192
|
+
"""Prepara la geometria per le query applicando riproiezione e buffer.
|
|
193
|
+
|
|
194
|
+
Se l'SRID della geometria fornita differisce da quello della colonna,
|
|
195
|
+
applica ``ST_Transform``. Se è specificato un buffer > 0, applica
|
|
196
|
+
``ST_Buffer`` dopo la riproiezione.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
geom (WKBElement): Geometria di input.
|
|
200
|
+
polygon_column_srid (int): SRID della colonna geometrica. Default: 4326.
|
|
201
|
+
geom_srid (int): SRID della geometria di input. Default: 4326.
|
|
202
|
+
buffer (float): Distanza del buffer (nelle unità del SRID della colonna).
|
|
203
|
+
0 = nessun buffer. Default: 0.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
WKBElement: Geometria pronta per l'uso nelle condizioni PostGIS.
|
|
207
|
+
"""
|
|
85
208
|
geom_condition = geom.ST_Transform(polygon_column_srid) if geom_srid != polygon_column_srid else geom
|
|
86
209
|
geom_condition = geom_condition.ST_Buffer(buffer) if buffer else geom_condition
|
|
87
210
|
|
|
@@ -5,7 +5,7 @@ import math
|
|
|
5
5
|
from typing import Any, Type, Sequence, Iterable, Optional, Mapping
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
|
-
from sqlalchemy import select, delete, update, insert, Row, RowMapping, ColumnElement, Select, Insert, Update, Delete
|
|
8
|
+
from sqlalchemy import select, delete, update, insert, tuple_, Row, RowMapping, ColumnElement, Select, Insert, Update, Delete
|
|
9
9
|
from sqlalchemy.engine.base import Engine
|
|
10
10
|
from sqlalchemy.exc import DataError, IntegrityError, NoResultFound, OperationalError
|
|
11
11
|
from sqlalchemy.inspection import inspect
|
|
@@ -126,6 +126,26 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
126
126
|
except Exception as e:
|
|
127
127
|
self._handle_exception(session, e)
|
|
128
128
|
|
|
129
|
+
def _flush(self, session: Session):
|
|
130
|
+
"""Esegue il flush sulla sessione, gestendo eventuali eccezioni.
|
|
131
|
+
|
|
132
|
+
Invia al database le modifiche pendenti (INSERT/UPDATE) senza chiudere
|
|
133
|
+
la transazione, popolando i valori generati lato server (es. chiavi
|
|
134
|
+
primarie autoincrement e default). A differenza del commit, non scade
|
|
135
|
+
(expire) le istanze ORM: gli attributi restano leggibili in memoria.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
session (Session): Sessione su cui eseguire il flush.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ServiceUnavailableException | BadRequestException: In caso di errore
|
|
142
|
+
durante il flush.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
session.flush()
|
|
146
|
+
except Exception as e:
|
|
147
|
+
self._handle_exception(session, e)
|
|
148
|
+
|
|
129
149
|
def _entity_has_columns(self, *columns: Iterable[str], entity: Type[Base] | Base = None):
|
|
130
150
|
"""Verifica che tutte le colonne specificate esistano sull'entità.
|
|
131
151
|
|
|
@@ -175,14 +195,21 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
175
195
|
"""Serializza le entità e chiude la sessione se necessario.
|
|
176
196
|
|
|
177
197
|
Se ``keep_open=True`` (sessione esterna), restituisce le entità grezze
|
|
178
|
-
senza chiudere la sessione. Altrimenti
|
|
179
|
-
|
|
198
|
+
senza chiudere la sessione. Altrimenti, quando ``commit=True``, esegue
|
|
199
|
+
prima un ``flush`` (per popolare i valori generati lato server),
|
|
200
|
+
serializza con ``model_validate`` e solo dopo esegue il ``commit``.
|
|
201
|
+
|
|
202
|
+
L'ordine flush → serializzazione → commit evita le query N+1 di reload:
|
|
203
|
+
il commit con ``expire_on_commit`` di default scadrebbe le istanze ORM,
|
|
204
|
+
forzando un SELECT per ogni entità al successivo accesso degli attributi.
|
|
205
|
+
Serializzando prima del commit gli attributi sono letti dallo stato già
|
|
206
|
+
popolato in memoria.
|
|
180
207
|
|
|
181
208
|
Args:
|
|
182
209
|
session (Session): Sessione corrente.
|
|
183
210
|
entities (Sequence): Entità SQLAlchemy da serializzare.
|
|
184
211
|
keep_open (bool): Se ``True``, non chiude la sessione e non serializza.
|
|
185
|
-
commit (bool): Se ``True`` e ``keep_open=False``, esegue
|
|
212
|
+
commit (bool): Se ``True`` e ``keep_open=False``, esegue flush + commit.
|
|
186
213
|
parsing_model (Type[BaseModel], optional): Modello alternativo per la
|
|
187
214
|
serializzazione. Se ``None``, usa ``self.model``.
|
|
188
215
|
|
|
@@ -192,10 +219,12 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
192
219
|
if keep_open:
|
|
193
220
|
return entities
|
|
194
221
|
else:
|
|
195
|
-
if commit:
|
|
196
|
-
self.commit(session=session)
|
|
197
222
|
return_model = parsing_model or self.model
|
|
223
|
+
if commit:
|
|
224
|
+
self._flush(session=session)
|
|
198
225
|
models = [return_model.model_validate(entity) for entity in entities]
|
|
226
|
+
if commit:
|
|
227
|
+
self.commit(session=session)
|
|
199
228
|
session.close()
|
|
200
229
|
return models
|
|
201
230
|
|
|
@@ -441,25 +470,58 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
441
470
|
def _upsert(self, session: Session, models: list[Type[BaseModel] | BaseModel] = None):
|
|
442
471
|
if models is None:
|
|
443
472
|
models = []
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
473
|
+
|
|
474
|
+
pk_columns = self.entity.primary_key()
|
|
475
|
+
|
|
476
|
+
# Chiave primaria (eventualmente composta) di ogni modello, oppure None
|
|
477
|
+
# se non tutte le colonne PK sono valorizzate (→ candidato all'insert).
|
|
478
|
+
model_keys = [self._pk_key(model, pk_columns) for model in models]
|
|
479
|
+
|
|
480
|
+
# Un'unica query per recuperare tutte le entità esistenti che corrispondono
|
|
481
|
+
# alle PK candidate, indicizzate per chiave PK in una mappa in memoria.
|
|
482
|
+
candidate_keys = [key for key in model_keys if key is not None]
|
|
483
|
+
existing: dict[tuple, Base] = {}
|
|
484
|
+
if candidate_keys:
|
|
485
|
+
stmt = select(self.entity).where(tuple_(*pk_columns).in_(candidate_keys))
|
|
486
|
+
for entity in self.execute(session=session, stmt=stmt):
|
|
487
|
+
existing[self._pk_key(entity, pk_columns)] = entity
|
|
488
|
+
|
|
489
|
+
# Ripartizione insert/update in base alla presenza nella mappa.
|
|
490
|
+
inserts = []
|
|
491
|
+
to_update = []
|
|
492
|
+
for model, key in zip(models, model_keys):
|
|
493
|
+
entity = existing.get(key) if key is not None else None
|
|
494
|
+
if entity is not None:
|
|
495
|
+
to_update.append((entity, model))
|
|
449
496
|
else:
|
|
450
|
-
|
|
451
|
-
inserts = [models[i] for i in range(len(models)) if i not in updates.keys()]
|
|
497
|
+
inserts.append(model)
|
|
452
498
|
|
|
453
499
|
result = self._insert(session=session, models=inserts)
|
|
454
|
-
for
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
result.extend(updates.values())
|
|
500
|
+
for entity, model in to_update:
|
|
501
|
+
for attr, value in model.model_dump(exclude_none=True).items():
|
|
502
|
+
setattr(entity, attr, value)
|
|
503
|
+
result.append(entity)
|
|
460
504
|
|
|
461
505
|
return result
|
|
462
506
|
|
|
507
|
+
def _pk_key(self, obj: Type[BaseModel] | BaseModel | Base, pk_columns) -> tuple | None:
|
|
508
|
+
"""Estrae la chiave primaria (eventualmente composta) da un modello o entità.
|
|
509
|
+
|
|
510
|
+
Legge i valori delle colonne PK in ordine e li restituisce come tupla,
|
|
511
|
+
usabile come chiave di mappa e per il filtro ``tuple_(...).in_(...)``.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
obj: Modello Pydantic o entità ORM da cui leggere la PK.
|
|
515
|
+
pk_columns: Colonne che compongono la chiave primaria
|
|
516
|
+
(tipicamente ``self.entity.primary_key()``).
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
tuple | None: Tupla dei valori PK in ordine di colonna, oppure ``None``
|
|
520
|
+
se almeno un valore non è valorizzato (PK incompleta).
|
|
521
|
+
"""
|
|
522
|
+
values = tuple(getattr(obj, column.name, None) for column in pk_columns)
|
|
523
|
+
return values if all(values) else None
|
|
524
|
+
|
|
463
525
|
def count(self, session: Session = None, where: dict[str, Any] = None, **kwhere):
|
|
464
526
|
"""Conta i record che soddisfano i criteri specificati.
|
|
465
527
|
|
|
@@ -491,8 +553,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
491
553
|
where = kwhere if kwhere else {}
|
|
492
554
|
stmt = select(count(pk))
|
|
493
555
|
stmt = self.stmt_manager.prepare_statement(stmt=stmt, **where)
|
|
494
|
-
|
|
495
|
-
return result[0]
|
|
556
|
+
return session.execute(stmt).scalar()
|
|
496
557
|
|
|
497
558
|
def _preconf_count(self, session: Session, where: Optional[ColumnElement[Any]]):
|
|
498
559
|
"""Conta i record usando una clausola WHERE già costruita (ColumnElement).
|
|
@@ -509,9 +570,9 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
509
570
|
"""
|
|
510
571
|
pk = self.entity.primary_key()[0]
|
|
511
572
|
stmt = select(count(pk))
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return
|
|
573
|
+
if where is not None:
|
|
574
|
+
stmt = stmt.where(where)
|
|
575
|
+
return session.execute(stmt).scalar()
|
|
515
576
|
|
|
516
577
|
def paging_find(self, session: Session = None, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, page: int = 1, order_by: str = None, asc: bool = True,
|
|
517
578
|
parsing_model: Type[BaseModel] = None, **kwhere) -> PaginatorResponseModel[Entity]:
|
|
@@ -4,7 +4,7 @@ from typing import Any, Type
|
|
|
4
4
|
|
|
5
5
|
from geoalchemy2 import WKBElement, Geometry
|
|
6
6
|
from pydantic import BaseModel
|
|
7
|
-
from sqlalchemy import
|
|
7
|
+
from sqlalchemy import Column
|
|
8
8
|
from sqlalchemy.orm import Session
|
|
9
9
|
|
|
10
10
|
from .crud_repository import CrudRepository
|
|
@@ -47,15 +47,17 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
47
47
|
limit: int = 0, geom_srid: int = 4326, parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
|
|
48
48
|
"""Restituisce le entità la cui geometria interseca quella fornita.
|
|
49
49
|
|
|
50
|
-
Supporta
|
|
51
|
-
e riproiezione automatica se l'SRID della geometria differisce
|
|
52
|
-
quello della colonna.
|
|
50
|
+
Supporta una tolleranza di distanza opzionale, filtri aggiuntivi positivi
|
|
51
|
+
e negativi, e riproiezione automatica se l'SRID della geometria differisce
|
|
52
|
+
da quello della colonna.
|
|
53
53
|
|
|
54
54
|
Args:
|
|
55
55
|
geom (WKBElement): Geometria di riferimento per l'intersezione.
|
|
56
56
|
session (Session, optional): Sessione esterna.
|
|
57
|
-
tolerance (float):
|
|
58
|
-
(nelle unità del SRID della colonna).
|
|
57
|
+
tolerance (float): Distanza massima entro cui considerare le geometrie
|
|
58
|
+
"intersecanti" (nelle unità del SRID della colonna). Se > 0 usa
|
|
59
|
+
``ST_DWithin`` (servibile dall'indice spaziale GiST); se 0.0 usa
|
|
60
|
+
``ST_Intersects``. Default: 0.0.
|
|
59
61
|
columns (list[str], optional): Colonne da includere nel risultato.
|
|
60
62
|
exclude_columns (list[str], optional): Colonne da escludere.
|
|
61
63
|
where (dict[str, Any], optional): Filtri aggiuntivi positivi.
|
|
@@ -85,12 +87,13 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
85
87
|
|
|
86
88
|
stmt = self._select(entity=self.entity, columns=columns, exclude_columns=exclude_columns)
|
|
87
89
|
|
|
88
|
-
intersecting_geom = self._geom_condition(geom=geom, geom_srid=geom_srid
|
|
90
|
+
intersecting_geom = self._geom_condition(geom=geom, geom_srid=geom_srid)
|
|
89
91
|
|
|
90
92
|
stmt = self.stmt_manager.prepare_statement(stmt=stmt, **where)
|
|
91
93
|
stmt = self.stmt_manager.prepare_statement(stmt=stmt, neq=True, **not_where)
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
geom_condition = self.geom_column.ST_DWithin(intersecting_geom, tolerance) if tolerance else self.geom_column.ST_Intersects(intersecting_geom)
|
|
96
|
+
stmt = stmt.where(geom_condition)
|
|
94
97
|
stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit)
|
|
95
98
|
|
|
96
99
|
return self.execute(session, stmt)
|
|
@@ -147,11 +150,14 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
147
150
|
return self.execute(session, stmt)
|
|
148
151
|
|
|
149
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,
|
|
150
|
-
parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
|
|
153
|
+
distance: bool = True, parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
|
|
151
154
|
"""Restituisce le entità più vicine alla geometria fornita, ordinate per distanza.
|
|
152
155
|
|
|
153
156
|
Aggiunge una colonna ``distance`` ai risultati con la distanza calcolata
|
|
154
|
-
tramite ``ST_Distance
|
|
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.
|
|
155
161
|
|
|
156
162
|
Args:
|
|
157
163
|
geom (WKBElement): Geometria di riferimento per il calcolo della distanza.
|
|
@@ -161,6 +167,10 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
161
167
|
limit (int): Numero di entità più vicine da restituire. Default: 1.
|
|
162
168
|
where (dict[str, Any], optional): Filtri aggiuntivi.
|
|
163
169
|
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.
|
|
164
174
|
parsing_model (Type[BaseModel], optional): Modello alternativo.
|
|
165
175
|
|
|
166
176
|
Returns:
|
|
@@ -171,7 +181,7 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
171
181
|
"""
|
|
172
182
|
keep_open = False if session is None else True
|
|
173
183
|
session = self.open_session if session is None else session
|
|
174
|
-
entities = self._nearest(session=session, geom=geom, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, geom_srid=geom_srid)
|
|
184
|
+
entities = self._nearest(session=session, geom=geom, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, geom_srid=geom_srid, distance=distance)
|
|
175
185
|
return self._return(session=session, entities=entities, keep_open=keep_open, commit=False, parsing_model=parsing_model)
|
|
176
186
|
|
|
177
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):
|
|
@@ -181,13 +191,11 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
|
|
|
181
191
|
|
|
182
192
|
geom_condition = self._geom_condition(geom=geom, geom_srid=geom_srid)
|
|
183
193
|
|
|
184
|
-
distance_col = self.geom_column.ST_Distance(geom_condition).label("distance")
|
|
185
|
-
|
|
186
194
|
if distance:
|
|
187
|
-
stmt = stmt.add_columns(
|
|
195
|
+
stmt = stmt.add_columns(self.geom_column.ST_Distance(geom_condition).label("distance"))
|
|
188
196
|
|
|
189
197
|
stmt = self.stmt_manager.prepare_statement(stmt=stmt, **where)
|
|
190
|
-
stmt = stmt.order_by(
|
|
198
|
+
stmt = stmt.order_by(self.geom_column.distance_centroid(geom_condition))
|
|
191
199
|
stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit)
|
|
192
200
|
|
|
193
201
|
rows = self.execute(session, stmt)
|
|
@@ -14,17 +14,23 @@ class BaseHandler:
|
|
|
14
14
|
|
|
15
15
|
Attributes:
|
|
16
16
|
columns (ReadOnlyColumnCollection): Colonne della tabella su cui operare.
|
|
17
|
+
array_columns (set[str]): Nomi delle colonne di tipo ``ARRAY``, pre-calcolati
|
|
18
|
+
in fase di inizializzazione.
|
|
17
19
|
"""
|
|
18
|
-
__slots__ = "columns"
|
|
20
|
+
__slots__ = "columns", "array_columns"
|
|
19
21
|
|
|
20
22
|
def __init__(self, columns: ReadOnlyColumnCollection[str, Column[Any]]):
|
|
21
23
|
"""Inizializza l'handler con la collezione di colonne dell'entità.
|
|
22
24
|
|
|
25
|
+
Pre-calcola una volta l'insieme dei nomi delle colonne di tipo ``ARRAY``,
|
|
26
|
+
evitando il confronto ``str(column.type)`` ad ogni filtro in ``_handle``.
|
|
27
|
+
|
|
23
28
|
Args:
|
|
24
29
|
columns (ReadOnlyColumnCollection): Colonne della tabella, tipicamente
|
|
25
30
|
ottenute da ``Entity.__table__.c``.
|
|
26
31
|
"""
|
|
27
32
|
self.columns = columns
|
|
33
|
+
self.array_columns = {column.name for column in columns if str(column.type) == "ARRAY"}
|
|
28
34
|
|
|
29
35
|
def handle(self, stmt: Select | Update | Delete, neq: bool = False, where: dict[str, Any] = None):
|
|
30
36
|
"""Applica i filtri WHERE allo statement per tutte le coppie chiave-valore in ``where``.
|
|
@@ -62,7 +68,12 @@ class BaseHandler:
|
|
|
62
68
|
Returns:
|
|
63
69
|
ColumnElement: Espressione booleana SQLAlchemy.
|
|
64
70
|
"""
|
|
65
|
-
|
|
71
|
+
if isinstance(condition, list):
|
|
72
|
+
eq_where = column.in_(condition)
|
|
73
|
+
elif column.name in self.array_columns:
|
|
74
|
+
eq_where = condition == column.any_()
|
|
75
|
+
else:
|
|
76
|
+
eq_where = column == condition
|
|
66
77
|
if neq:
|
|
67
78
|
eq_where = not_(eq_where)
|
|
68
79
|
return eq_where
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-general-be-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities.
|
|
5
5
|
Author-email: Andrea Di Placido <a.diplacido@arpes.it>, "Arpes S.r.l." <it.admin@arpes.it>
|
|
6
6
|
License: MIT
|
|
@@ -10,22 +10,22 @@ general/interface/base/__init__.py,sha256=SDlTi28M8rzJ9yiKefo5l7V3K109LnnBHW-SuZ
|
|
|
10
10
|
general/interface/base/base_model.py,sha256=SVrYl1kSAj37yCScfO3Qe6ELyLxou65njpvbYmjTG00,2215
|
|
11
11
|
general/interface/base/declarative_base.py,sha256=5hw2VpJHHebjixuI3d4wpkyAentGBGN54QXcniaoJ-A,5236
|
|
12
12
|
general/interface/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
general/interface/metadata/crud_metadata.py,sha256=
|
|
14
|
-
general/interface/metadata/geom_metadata.py,sha256=
|
|
13
|
+
general/interface/metadata/crud_metadata.py,sha256=HcnJkr33buIg8n8aWaH1Tw8QcmlyP_2Gyd1RcWCMtiE,22422
|
|
14
|
+
general/interface/metadata/geom_metadata.py,sha256=kc6jkWis-WPdWAP-hN6U97ENVjNjny5NE2j-llwZYOQ,11998
|
|
15
15
|
general/interface/repository/__init__.py,sha256=EcRukwf61Ua-9-JfM_ingVv1wXNRV8HBrrWwjj8m23w,136
|
|
16
|
-
general/interface/repository/crud_repository.py,sha256=
|
|
17
|
-
general/interface/repository/geometry_repository.py,sha256=
|
|
16
|
+
general/interface/repository/crud_repository.py,sha256=kPuP_Jrguw9C_5dtn80NAVZ26gDcJnAZrOhwygWoD6Y,32095
|
|
17
|
+
general/interface/repository/geometry_repository.py,sha256=ULU-FnDATojHLDrObLPyAA3KErgX_tyYEeTU8h2O4Hs,11989
|
|
18
18
|
general/interface/repository/many_to_many_repository.py,sha256=iEhWyD8nPZSAPL8Z2wXrK-aRBnoh3evaSN-voaf0EP0,8662
|
|
19
19
|
general/interface/repository/view_repository.py,sha256=Sm591FUP0iBYlI-ULNWszD5fOvTGwXX0iYoeDbUqo34,5397
|
|
20
20
|
general/interface/repository/handler/__init__.py,sha256=J-fc-5Dl_C821oy_EVRdyU-L2iXiNFriYJDbK4ODIMM,184
|
|
21
|
-
general/interface/repository/handler/base_handler.py,sha256=
|
|
21
|
+
general/interface/repository/handler/base_handler.py,sha256=hJkvXyVqnE2USmVWAko0Lnu02-537t4PERmBETJhXB0,3285
|
|
22
22
|
general/interface/repository/handler/ilike_handler.py,sha256=_HHTvj6Tm1L-WvUy1S9faRChoRZni11i9mv76I4Xak0,3919
|
|
23
23
|
general/interface/repository/handler/interval_handler.py,sha256=XxMQr6UuWnoCtbOOO7l80_Yja_aPKYtHhDfhFCRMaKg,4135
|
|
24
24
|
general/interface/repository/handler/json_handler.py,sha256=7bS59seB-It5KHsWXTbHGsEvbQZ6lYyvX-d29BcRLVE,2956
|
|
25
25
|
general/interface/repository/handler/json_ilike_handler.py,sha256=yDuKER3yqtnHpVWndF8PVEaqDThcq1EhZzLaqxbQ0bU,4491
|
|
26
26
|
general/interface/repository/handler/statement_manager.py,sha256=sGDMU4fPkF0_9-SbDmAw8Vpel1Z7eImBs8fHO-5Lr_0,4429
|
|
27
|
-
python_general_be_lib-0.
|
|
28
|
-
python_general_be_lib-0.
|
|
29
|
-
python_general_be_lib-0.
|
|
30
|
-
python_general_be_lib-0.
|
|
31
|
-
python_general_be_lib-0.
|
|
27
|
+
python_general_be_lib-0.6.0.dist-info/licenses/LICENSE,sha256=iUaO1XZyB9P3Tmog0OILuTisP6vXGe3QKz-4yRTxOFk,1069
|
|
28
|
+
python_general_be_lib-0.6.0.dist-info/METADATA,sha256=QKlhIJ_5M8jBwSxHTM7uorYD5q2VUTdAs43l6BSUQaM,1384
|
|
29
|
+
python_general_be_lib-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
+
python_general_be_lib-0.6.0.dist-info/top_level.txt,sha256=tTZePW8_CNUqSgKFd2SEH72ZbnhS0OYjRsgcv0ikSFY,8
|
|
31
|
+
python_general_be_lib-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
{python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{python_general_be_lib-0.5.4.dist-info → python_general_be_lib-0.6.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|