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.
@@ -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], optional): Entità su cui verificare. Se ``None``,
183
- usa ``self.entity``.
182
+ entity (Type[Base]): Entità su cui verificare. Obbligatoria nei fatti:
183
+ pur avendo default ``None``, il metodo non applica nessun fallback
184
+ a ``self.entity`` e con ``None`` solleva ``AttributeError``.
184
185
 
185
186
  Returns:
186
187
  bool: ``True`` se tutte le colonne sono presenti, ``False`` altrimenti.
@@ -301,6 +302,13 @@ class CrudRepository[Entity: Base | Type[Base]]:
301
302
  list[Entity]: Lista di modelli serializzati (o entità grezze se
302
303
  sessione esterna).
303
304
 
305
+ Raises:
306
+ HasNoAttributeException: Se una colonna in ``columns``,
307
+ ``exclude_columns`` o ``order_by`` non esiste sull'entità.
308
+
309
+ Note:
310
+ Essendo una lettura pura non esegue nessun commit (``commit=False``).
311
+
304
312
  Example:
305
313
  >>> repo.find(active=True, limit=10)
306
314
  >>> repo.find(where={"role": ["admin", "editor"]})
@@ -606,16 +614,20 @@ class CrudRepository[Entity: Base | Type[Base]]:
606
614
  """Esegue una ricerca paginata con supporto a ordinamento e offset.
607
615
 
608
616
  Calcola il numero totale di pagine in base al count reale dei record
609
- filtrati, e restituisce solo la pagina richiesta.
617
+ filtrati, e restituisce solo la pagina richiesta. Il count riusa la
618
+ ``whereclause`` già costruita sullo statement di selezione
619
+ (``_preconf_count``), così i filtri vengono tradotti una sola volta.
610
620
 
611
621
  Args:
612
622
  session (Session, optional): Sessione esterna.
613
623
  where (dict[str, Any], optional): Filtri come dizionario.
614
624
  columns (list[str], optional): Colonne da includere.
615
625
  exclude_columns (list[str], optional): Colonne da escludere.
616
- limit (int): Numero di elementi per pagina. Default: 0 (nessun limite).
626
+ limit (int): Numero di elementi per pagina. Default: 0 (nessun limite,
627
+ quindi una sola pagina).
617
628
  page (int): Numero di pagina richiesta (1-based). Default: 1.
618
- order_by (str, optional): Colonna per l'ordinamento.
629
+ order_by (str, optional): Colonna per l'ordinamento. Se ``None``, viene
630
+ usata la prima colonna della chiave primaria.
619
631
  asc (bool): Ordinamento crescente se ``True``. Default: ``True``.
620
632
  parsing_model (Type[BaseModel], optional): Modello alternativo per
621
633
  la serializzazione.
@@ -681,8 +693,8 @@ class CrudRepository[Entity: Base | Type[Base]]:
681
693
  """Coppie distinte ``(key, value)`` per i record che soddisfano ``where``.
682
694
 
683
695
  Calcola i valori distinti lato DB con ``SELECT DISTINCT`` invece di caricare
684
- tutte le righe e dedupplicare in Python (usato da ``BaseService.company_filters``
685
- per popolare le opzioni dei filtri). Per le colonne ``ARRAY`` srotola in
696
+ tutte le righe e dedupplicare in Python. Tipicamente usato per popolare le
697
+ opzioni di un filtro lato client. Per le colonne ``ARRAY`` srotola in
686
698
  parallelo chiave ed etichetta con ``unnest`` (lateral implicito).
687
699
 
688
700
  Args:
@@ -694,6 +706,14 @@ class CrudRepository[Entity: Base | Type[Base]]:
694
706
 
695
707
  Returns:
696
708
  list[Row]: Coppie ``(key, value)`` distinte.
709
+
710
+ Note:
711
+ A differenza degli altri metodi restituisce ``Row`` (tuple) e non modelli
712
+ Pydantic: non passa da ``_return`` e non accetta un ``parsing_model``.
713
+
714
+ Example:
715
+ >>> repo.distinct_pairs("id_operatore", "operatore", where={"id_azienda": 3})
716
+ [(1, 'Rossi'), (2, 'Bianchi')]
697
717
  """
698
718
  if where is None:
699
719
  where = kwhere if kwhere else {}
@@ -723,6 +743,11 @@ class CrudRepository[Entity: Base | Type[Base]]:
723
743
 
724
744
  In caso di errore, delega la gestione a ``_handle_exception``.
725
745
 
746
+ Poiché usa ``.scalars()``, di ogni riga viene restituita solo la **prima**
747
+ colonna: statement arricchiti con ``add_columns`` perdono le colonne
748
+ aggiuntive. Per leggerle serve eseguire lo statement direttamente sulla
749
+ sessione (vedi ``distinct_pairs``, che usa ``session.execute(...).all()``).
750
+
726
751
  Args:
727
752
  session (Session): Sessione su cui eseguire lo statement.
728
753
  stmt (Select | Insert | Update | Delete): Statement da eseguire.
@@ -150,50 +150,48 @@ class GeometryRepository[Entity: Base | Type[Base]](CrudRepository[Entity]):
150
150
  return self.execute(session, stmt)
151
151
 
152
152
  def nearest(self, geom: WKBElement, session: Session = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 1, where: dict[str, Any] = None, geom_srid: int = 4326,
153
- distance: bool = True, parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
153
+ parsing_model: Type[BaseModel] | BaseModel = None) -> list[Entity]:
154
154
  """Restituisce le entità più vicine alla geometria fornita, ordinate per distanza.
155
155
 
156
- Aggiunge una colonna ``distance`` ai risultati con la distanza calcolata
157
- tramite ``ST_Distance``. L'ordinamento usa l'operatore KNN ``<->``
158
- (``distance_centroid``) anziché ``ST_Distance``: con ``LIMIT`` impostato,
159
- PostGIS può servire la ricerca dei più vicini tramite l'indice spaziale
160
- GiST, evitando il calcolo della distanza su tutte le righe.
156
+ L'ordinamento usa l'operatore KNN ``<->`` (``distance_centroid``) anziché
157
+ ``ST_Distance``: con ``LIMIT`` impostato, PostGIS può servire la ricerca dei
158
+ più vicini tramite l'indice spaziale GiST, evitando il calcolo della
159
+ distanza su tutte le righe.
161
160
 
162
161
  Args:
163
- geom (WKBElement): Geometria di riferimento per il calcolo della distanza.
162
+ geom (WKBElement): Geometria di riferimento per il calcolo della prossimità.
164
163
  session (Session, optional): Sessione esterna.
165
164
  columns (list[str], optional): Colonne da includere.
166
165
  exclude_columns (list[str], optional): Colonne da escludere.
167
166
  limit (int): Numero di entità più vicine da restituire. Default: 1.
168
167
  where (dict[str, Any], optional): Filtri aggiuntivi.
169
168
  geom_srid (int): SRID della geometria fornita. Default: 4326.
170
- distance (bool): Se ``True``, aggiunge ai risultati la colonna ``distance``
171
- con la distanza calcolata via ``ST_Distance``. Default: ``True``.
172
- L'ordinamento per prossimità (operatore KNN ``<->``) viene applicato
173
- in ogni caso, indipendentemente da questo flag.
174
169
  parsing_model (Type[BaseModel], optional): Modello alternativo.
175
170
 
176
171
  Returns:
177
172
  list[Entity]: Entità ordinate dalla più vicina alla più lontana.
178
173
 
174
+ Note:
175
+ Restituisce solo le entità, senza il valore della distanza: la distanza
176
+ è usata unicamente per l'ordinamento lato database. Per ottenerla come
177
+ colonna serve uno statement custom, dato che ``execute`` restituisce
178
+ ``result.scalars().all()`` e terrebbe solo la prima colonna di ogni riga.
179
+
179
180
  Example:
180
181
  >>> nearest = repo.nearest(geom=my_point, limit=5)
181
182
  """
182
183
  keep_open = False if session is None else True
183
184
  session = self.open_session if session is None else session
184
- entities = self._nearest(session=session, geom=geom, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, geom_srid=geom_srid, distance=distance)
185
+ entities = self._nearest(session=session, geom=geom, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, geom_srid=geom_srid)
185
186
  return self._return(session=session, entities=entities, keep_open=keep_open, commit=False, parsing_model=parsing_model)
186
187
 
187
- def _nearest(self, session: Session, geom: WKBElement, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 1, where: dict[str, Any] = None, geom_srid: int = 4326, distance: bool = True):
188
+ def _nearest(self, session: Session, geom: WKBElement, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 1, where: dict[str, Any] = None, geom_srid: int = 4326):
188
189
  if where is None:
189
190
  where = {}
190
191
  stmt = self._select(entity=self.entity, columns=columns, exclude_columns=exclude_columns)
191
192
 
192
193
  geom_condition = self._geom_condition(geom=geom, geom_srid=geom_srid)
193
194
 
194
- if distance:
195
- stmt = stmt.add_columns(self.geom_column.ST_Distance(geom_condition).label("distance"))
196
-
197
195
  stmt = self.stmt_manager.prepare_statement(stmt=stmt, **where)
198
196
  stmt = stmt.order_by(self.geom_column.distance_centroid(geom_condition))
199
197
  stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit)
@@ -10,7 +10,14 @@ class BaseHandler:
10
10
  """Handler base per la costruzione di filtri WHERE su statement SQLAlchemy.
11
11
 
12
12
  Gestisce i casi standard di uguaglianza, liste (``IN``) e colonne di tipo
13
- ``ARRAY``. Può essere esteso per supportare logiche di filtraggio personalizzate.
13
+ ``ARRAY``. È anche la classe base di tutti gli handler specializzati, che
14
+ possono estenderla per logiche di filtraggio personalizzate.
15
+
16
+ Nella pipeline dello ``StatementManager`` è sempre l'**ultimo** handler
17
+ eseguito: riceve le chiavi del ``where`` non consumate dagli handler
18
+ precedenti e le tratta come filtri su colonne reali. A differenza degli
19
+ handler specializzati non rimuove nulla dal dizionario, perché non ha
20
+ successori.
14
21
 
15
22
  Attributes:
16
23
  columns (ReadOnlyColumnCollection): Colonne della tabella su cui operare.
@@ -43,9 +50,14 @@ class BaseHandler:
43
50
  neq (bool): Se ``True``, applica la negazione (``NOT``) a ogni condizione.
44
51
  Default: ``False``.
45
52
  where (dict[str, Any]): Dizionario ``{nome_colonna: valore}`` con i filtri.
53
+ Tutte le chiavi devono corrispondere a colonne reali della tabella.
46
54
 
47
55
  Returns:
48
56
  Select | Update | Delete: Statement arricchito con le clausole WHERE.
57
+
58
+ Raises:
59
+ KeyError: Se una chiave del ``where`` non corrisponde a nessuna colonna,
60
+ ovvero se nessun handler precedente l'ha consumata.
49
61
  """
50
62
  stmt = stmt.where(*[self._handle(column=self.columns[column], condition=condition, neq=neq) for column, condition in where.items()])
51
63
  return stmt
@@ -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): Chiave nel dizionario ``where`` per il limite inferiore.
20
- to\\_ (Any): Chiave nel dizionario ``where`` per il limite superiore.
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_="start_date",
28
- ... to_="end_date",
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={"start_date": "2024-01-01", "end_date": "2024-12-31"})
32
- # WHERE start_date >= '2024-01-01' AND (end_date <= '2024-12-31' OR end_date IS NULL)
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 chiavi per il limite inferiore e superiore.
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): Chiave nel ``where`` per il limite inferiore
58
+ from\\_ (Any, optional): Nome della colonna per il limite inferiore
47
59
  (condizione ``>=``). Default: ``None`` (disabilitato).
48
- to\\_ (Any, optional): Chiave nel ``where`` per il limite superiore
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 ``from_`` e ``to_`` dal dizionario
66
- ``where``, quindi aggiunge le relative condizioni allo statement.
67
- Se una chiave non è presente in ``where``, quella condizione viene ignorata.
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: ``col >= value``
80
- - Limite superiore: ``col <= value`` oppure ``(col <= value OR col IS NULL)``
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_] <= from_condition)
116
+ stmt = stmt.where(self.columns[self.from_] >= from_condition)
87
117
  if to_condition:
88
- condition = self.columns[self.to_] >= to_condition
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 case-insensitive (ILIKE) su più colonne tramite un alias.
11
+ """Handler per ricerche ILIKE su colonne reali **e** su sotto-chiavi JSON/JSONB.
12
12
 
13
- Permette di esporre un singolo parametro di ricerca testuale (es. ``"search"``)
14
- che viene applicato come ``ILIKE '%value%'`` su un insieme di colonne con ``OR``.
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 ILIKE (es. ``["search", "q"]``).
19
- ilike_attributes (list[str]): Nomi delle colonne su cui applicare ILIKE
20
- (es. ``["name", "surname", "email"]``).
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=User.columns(),
34
+ ... columns=Item.columns(),
25
35
  ... ilike_aliases=["search"],
26
- ... ilike_attributes=["name", "surname"]
36
+ ... ilike_attributes=["name", "codice_interno"] # "codice_interno" è una chiave JSON
27
37
  ... )
28
- >>> repo.find(where={"search": "mario"})
29
- # WHERE name ILIKE '%mario%' OR surname ILIKE '%mario%'
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 validando che le colonne ILIKE esistano sull'entità.
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 su cui applicare ILIKE.
45
- Default: lista vuota.
59
+ ilike_attributes (list[str], optional): Colonne reali e/o chiavi JSON
60
+ su cui applicare ILIKE. Default: nessun attributo.
46
61
 
47
- Raises:
48
- ValueError: Se uno degli ``ilike_attributes`` non è presente tra le colonne.
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 trovato in ``where``,
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 in ``ilike_attributes`` con ``OR``.
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 ILIKE vengono
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 con OR su tutte le colonne configurate.
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 (usato per identificare la ricerca, non la colonna).
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(col1 ILIKE '%val%', col2 ILIKE '%val%', ...)``.
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 ricerche case-insensitive (ILIKE) su più colonne tramite un alias.
12
+ """Handler per filtri di uguaglianza applicati a più colonne in ``OR``.
13
13
 
14
- Permette di esporre un singolo parametro di ricerca testuale (es. ``"search"``)
15
- che viene applicato come ``ILIKE '%value%'`` su un insieme di colonne con ``OR``.
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
- la ricerca ILIKE (es. ``["search", "q"]``).
20
- or_attributes (list[str]): Nomi delle colonne su cui applicare ILIKE
21
- (es. ``["name", "surname", "email"]``).
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=User.columns(),
26
- ... or_aliases=["search"],
27
- ... or_attributes=["name", "surname"]
34
+ ... columns=Doc.columns(),
35
+ ... or_aliases=["id_soggetto"],
36
+ ... or_attributes=["id_mittente", "id_destinatario"]
28
37
  ... )
29
- >>> repo.find(where={"search": "mario"})
30
- # WHERE name ILIKE '%mario%' OR surname ILIKE '%mario%'
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 ILIKE esistano sull'entità.
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 la ricerca.
50
+ or_aliases (list[str], optional): Chiavi alias che attivano il filtro.
42
51
  Default: lista vuota.
43
- or_attributes (list[str], optional): Colonne su cui applicare ILIKE.
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 ILIKE allo statement per ogni alias trovato in ``where``.
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
- Per ogni alias presente in ``or_aliases`` e trovato in ``where``,
61
- estrae il valore, lo rimuove dal dizionario e aggiunge la condizione ILIKE
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 ILIKE vengono
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 ILIKE aggiunte.
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 ILIKE con OR su tutte le colonne configurate.
92
+ """Costruisce la condizione di uguaglianza in OR su tutte le colonne configurate.
81
93
 
82
94
  Args:
83
- alias (str): Alias attivato (usato per identificare la ricerca, non la colonna).
84
- condition (str): Stringa da cercare (viene wrappata con ``%``).
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 ILIKE '%val%', col2 ILIKE '%val%', ...)``.
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,,