python-general-be-lib 0.5.5__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/PKG-INFO +1 -1
  2. python_general_be_lib-0.6.0/general/interface/metadata/crud_metadata.py +502 -0
  3. python_general_be_lib-0.6.0/general/interface/metadata/geom_metadata.py +211 -0
  4. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/crud_repository.py +83 -23
  5. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/geometry_repository.py +23 -15
  6. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/base_handler.py +13 -2
  7. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/pyproject.toml +1 -1
  8. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/python_general_be_lib.egg-info/PKG-INFO +1 -1
  9. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/setup.py +1 -1
  10. python_general_be_lib-0.5.5/general/interface/metadata/crud_metadata.py +0 -183
  11. python_general_be_lib-0.5.5/general/interface/metadata/geom_metadata.py +0 -88
  12. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/LICENSE +0 -0
  13. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/README.md +0 -0
  14. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/__init__.py +0 -0
  15. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/exception/__init__.py +0 -0
  16. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/exception/access_exceptions.py +0 -0
  17. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/exception/crud_exceptions.py +0 -0
  18. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/exception/exception_interface.py +0 -0
  19. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/base/__init__.py +0 -0
  20. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/base/base_model.py +0 -0
  21. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/base/declarative_base.py +0 -0
  22. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/metadata/__init__.py +0 -0
  23. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/__init__.py +0 -0
  24. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/__init__.py +0 -0
  25. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/ilike_handler.py +0 -0
  26. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/interval_handler.py +0 -0
  27. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/json_handler.py +0 -0
  28. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/json_ilike_handler.py +0 -0
  29. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/handler/statement_manager.py +0 -0
  30. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/many_to_many_repository.py +0 -0
  31. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/interface/repository/view_repository.py +0 -0
  32. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/logger.py +0 -0
  33. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/general/paginator_dto.py +0 -0
  34. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/python_general_be_lib.egg-info/SOURCES.txt +0 -0
  35. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
  36. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/python_general_be_lib.egg-info/requires.txt +0 -0
  37. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/python_general_be_lib.egg-info/top_level.txt +0 -0
  38. {python_general_be_lib-0.5.5 → python_general_be_lib-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-general-be-lib
3
- Version: 0.5.5
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
@@ -0,0 +1,502 @@
1
+ __all__ = ["CrudMetadata"]
2
+
3
+ import logging
4
+ from typing import Optional, Any, Type, Iterable, Sequence
5
+
6
+ from pydantic import BaseModel
7
+ from sqlalchemy import MetaData, Engine, Column, Table, inspect, Connection, select, RowMapping, CursorResult, not_
8
+ from sqlalchemy.exc import NoSuchTableError
9
+
10
+ from ...exception.crud_exceptions import HasNoAttributeException, InternalServerException
11
+
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
+ """
37
+ __slots__ = "schema", "metadata", "engine", "engine_inspection"
38
+
39
+ @property
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
+ """
46
+ return self.engine.connect()
47
+
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
+ """
60
+ self.schema = schema_name
61
+ self.metadata = MetaData(schema_name) if schema_name else MetaData()
62
+ self.engine = engine
63
+ self.engine_inspection = inspect(self.engine)
64
+ if reflect:
65
+ self.metadata.reflect(bind=self.engine, schema=schema_name)
66
+
67
+ # ---------------------------
68
+
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
+ """
81
+ connection.close()
82
+ logging.error(e)
83
+ if to_raise:
84
+ raise e
85
+
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
+ """
95
+ try:
96
+ connection.commit()
97
+ except Exception as e:
98
+ self._handle_exception(connection, e)
99
+
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
+ """
110
+ return all([column in [c.key for c in table.c] for column in columns])
111
+
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
+ """
126
+ try:
127
+ stmt = table.select() if not columns else select(*[table.c[column] for column in columns])
128
+ except:
129
+ raise HasNoAttributeException()
130
+ return stmt
131
+
132
+ def _return(self, connection: Connection, result: CursorResult | RowMapping | Sequence[RowMapping] | Sequence[dict] = None, keep_open: bool = False, commit: bool = True,
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
+ """
153
+ if keep_open:
154
+ return result
155
+ else:
156
+ if commit:
157
+ self.commit(connection=connection)
158
+ result = result if parsing_model is None else [parsing_model(**r) for r in result]
159
+ connection.close()
160
+ return result
161
+
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
+ """
171
+ table = Table(name=name, metadata=self.metadata, *columns, **kwargs)
172
+ table.create(self.engine)
173
+
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
+ """
180
+ table = self.get_table(name=name)
181
+ table.drop(self.engine)
182
+ self.metadata.remove(table=table)
183
+
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
+ """
205
+ keep_open = False if connection is None else True
206
+ connection = self.open_connection if connection is None else connection
207
+ table = self.get_table(name=from_table)
208
+ result = self._find(table=table, connection=connection, where=where, columns=columns, limit=limit, **kwhere)
209
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=False, parsing_model=parsing_model)
210
+
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
+ """
226
+ if where is None:
227
+ where = kwhere if kwhere else {}
228
+ stmt = self._select(table=table, columns=columns)
229
+ stmt = stmt.where(*[self._eq_where(table.c[column], condition) for column, condition in where.items()])
230
+ stmt = self._add_limit_offset_condition(stmt, limit)
231
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
232
+
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,
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
+ """
259
+ keep_open = False if connection is None else True
260
+ connection = self.open_connection if connection is None else connection
261
+ table = self.get_table(name=from_table)
262
+ values_condition = values if values else []
263
+ values_condition.extend([model.model_dump(exclude_none=True) for model in models]) if models else values_condition
264
+ result = self._insert(table=table, connection=connection, values=values_condition, returning=returning)
265
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=True, parsing_model=parsing_model)
266
+
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
+ """
281
+ if values:
282
+ stmt = table.insert().values(values)
283
+ stmt = stmt if not returning else stmt.returning()
284
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
285
+ else:
286
+ return []
287
+
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
+ """
308
+ keep_open = False if connection is None else True
309
+ connection = self.open_connection if connection is None else connection
310
+ table = self.get_table(name=from_table)
311
+ result = self._update(table=table, connection=connection, where=where, returning=returning, **kwargs)
312
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=True, parsing_model=parsing_model)
313
+
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
+ """
327
+ if where is None:
328
+ where = {}
329
+ stmt = table.update()
330
+ stmt = stmt.where(*[self._eq_where(table.c[column], condition) for column, condition in where.items()])
331
+ stmt = stmt.values(**kwargs)
332
+ stmt = stmt if not returning else stmt.returning()
333
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
334
+
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
+ """
352
+ keep_open = False if connection is None else True
353
+ connection = self.open_connection if connection is None else connection
354
+ table = self.get_table(name=from_table)
355
+ num_rows = self._delete(table=table, connection=connection, where=where, **kwhere)
356
+ if not num_rows or keep_open:
357
+ return num_rows
358
+ else:
359
+ self.commit(connection)
360
+ connection.close()
361
+ return num_rows
362
+
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
+ """
376
+ if where is None:
377
+ where = kwhere if kwhere else {}
378
+ stmt = table.delete()
379
+ stmt = stmt.where(*[self._eq_where(table.c[column], condition) for column, condition in where.items()])
380
+ return self.execute(connection=connection, stmt=stmt).rowcount
381
+
382
+ # --------------
383
+
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
+ """
401
+ try:
402
+ result = connection.execute(stmt)
403
+ except Exception as e:
404
+ self._handle_exception(connection, e)
405
+ else:
406
+ if mapping:
407
+ result = result.mappings().all()
408
+ return result
409
+
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
+ """
421
+ if self.schema is not None:
422
+ table_name = name if name.startswith(f"{self.schema}.") else f"{self.schema}.{name}"
423
+ else:
424
+ table_name = name
425
+ return self.metadata.tables.get(table_name)
426
+
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
+ """
442
+ table = self._get_table(name=name)
443
+ if table is None:
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}")
448
+ return table
449
+
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
+ """
459
+ return self.engine_inspection.has_table(name, self.schema)
460
+
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
+ """
476
+ if limit and limit > 0:
477
+ stmt = stmt.limit(limit)
478
+ if page and page > 0:
479
+ offset = (page - 1) * limit
480
+ stmt = stmt.offset(offset)
481
+ return stmt
482
+
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
+ """
499
+ eq_where = col.in_(condition) if isinstance(condition, list) else col == condition if str(col.type) != "ARRAY" else condition == col.any_()
500
+ if neq:
501
+ eq_where = not_(eq_where)
502
+ return eq_where