python-general-be-lib 0.8.4__py3-none-any.whl → 0.9.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.
Files changed (35) hide show
  1. general/exception/__init__.py +24 -0
  2. general/exception/access_exceptions.py +8 -9
  3. general/exception/crud_exceptions.py +34 -35
  4. general/exception/exception_interface.py +14 -21
  5. general/interface/__init__.py +8 -0
  6. general/interface/base/__init__.py +10 -0
  7. general/interface/base/base_model.py +28 -28
  8. general/interface/base/declarative_base.py +69 -62
  9. general/interface/metadata/__init__.py +12 -0
  10. general/interface/metadata/crud_metadata.py +193 -207
  11. general/interface/metadata/geom_metadata.py +92 -98
  12. general/interface/repository/__init__.py +9 -0
  13. general/interface/repository/crud_repository.py +303 -334
  14. general/interface/repository/geometry_repository.py +68 -92
  15. general/interface/repository/handler/__init__.py +20 -0
  16. general/interface/repository/handler/base_handler.py +29 -70
  17. general/interface/repository/handler/handler.py +72 -0
  18. general/interface/repository/handler/ilike_handler.py +40 -56
  19. general/interface/repository/handler/interval_handler.py +53 -90
  20. general/interface/repository/handler/json_handler.py +63 -51
  21. general/interface/repository/handler/json_ilike_handler.py +28 -53
  22. general/interface/repository/handler/or_handler.py +25 -58
  23. general/interface/repository/handler/statement_manager.py +73 -72
  24. general/interface/repository/many_to_many_repository.py +57 -74
  25. general/interface/repository/view_repository.py +81 -54
  26. general/logger.py +29 -65
  27. general/paginator_dto.py +16 -38
  28. general/py.typed +0 -0
  29. {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.0.dist-info}/METADATA +12 -3
  30. python_general_be_lib-0.9.0.dist-info/RECORD +34 -0
  31. {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.0.dist-info}/WHEEL +1 -1
  32. general/log_config.ini +0 -21
  33. python_general_be_lib-0.8.4.dist-info/RECORD +0 -32
  34. {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.0.dist-info}/licenses/LICENSE +0 -0
  35. {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,24 @@
1
+ """HTTP exceptions of the library.
2
+
3
+ They all extend ``ExceptionInterface``, which in turn extends
4
+ ``fastapi.HTTPException``: raised inside an endpoint they are automatically
5
+ serialized into the response with their own status code. The mapping applied by
6
+ ``CrudRepository._handle_exception`` to the SQLAlchemy errors is documented in
7
+ ``crud_exceptions``.
8
+ """
9
+
10
+ from .access_exceptions import *
11
+ from .crud_exceptions import *
12
+ from .exception_interface import *
13
+
14
+ __all__ = [
15
+ "ExceptionInterface",
16
+ "BadRequestException",
17
+ "EntityNotFoundException",
18
+ "ServiceUnavailableException",
19
+ "UnprocessableEntityException",
20
+ "HasNoAttributeException",
21
+ "InternalServerException",
22
+ "ForbiddenException",
23
+ "UnauthorizedException",
24
+ ]
@@ -1,23 +1,22 @@
1
- """Eccezioni HTTP per autenticazione e autorizzazione.
1
+ """HTTP exceptions for authentication and authorization.
2
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``.
3
+ Unlike ``crud_exceptions``, these exceptions are never raised automatically by the
4
+ library: they are available to the authentication and authorization layers of the
5
+ projects using it.
9
6
  """
10
7
 
8
+ __all__ = ["ForbiddenException", "UnauthorizedException"]
9
+
11
10
  from .exception_interface import ExceptionInterface
12
11
 
13
12
 
14
13
  class ForbiddenException(ExceptionInterface):
15
- """403 — Utente autenticato ma senza i permessi per la risorsa richiesta."""
14
+ """403 - Authenticated user without the permissions for the requested resource."""
16
15
  status_code = 403
17
16
  default_message = "Risorsa non accessibile da questo utente"
18
17
 
19
18
 
20
19
  class UnauthorizedException(ExceptionInterface):
21
- """401 — Richiesta priva di credenziali valide."""
20
+ """401 - Request without valid credentials."""
22
21
  status_code = 401
23
22
  default_message = "Non autorizzato"
@@ -1,77 +1,77 @@
1
- """Eccezioni HTTP per le operazioni di persistenza.
1
+ """HTTP exceptions for the persistence operations.
2
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``.
3
+ These are the exceptions ``CrudRepository._handle_exception`` maps the SQLAlchemy
4
+ errors onto: ``OperationalError`` to ``ServiceUnavailableException`` (503),
5
+ ``NoResultFound`` to ``EntityNotFoundException`` (404), ``DataError`` and
6
+ ``IntegrityError`` to ``BadRequestException`` (400).
12
7
  """
13
8
 
9
+ __all__ = [
10
+ "BadRequestException",
11
+ "EntityNotFoundException",
12
+ "ServiceUnavailableException",
13
+ "UnprocessableEntityException",
14
+ "HasNoAttributeException",
15
+ "InternalServerException",
16
+ ]
17
+
14
18
  from .exception_interface import ExceptionInterface
15
19
 
16
20
 
17
21
  class BadRequestException(ExceptionInterface):
18
- """400 — Dati della richiesta non validi o vincolo di integrità violato.
22
+ """400 - Invalid request data or violated integrity constraint.
19
23
 
20
- Sollevata al posto di ``DataError`` e ``IntegrityError`` (es. violazione di
21
- una foreign key, di un unique o di un NOT NULL).
24
+ Raised in place of ``DataError`` and ``IntegrityError``, such as a foreign key,
25
+ unique or NOT NULL violation.
22
26
  """
23
27
  status_code = 400
24
28
  default_message = "Richiesta non corretta"
25
29
 
26
30
 
27
31
  class EntityNotFoundException(ExceptionInterface):
28
- """404 — Nessun record corrisponde ai criteri richiesti.
32
+ """404 - No record matches the requested criteria.
29
33
 
30
- Sollevata al posto di ``NoResultFound``.
34
+ Raised in place of ``NoResultFound``.
31
35
  """
32
36
  status_code = 404
33
37
  default_message = "Dato non trovato"
34
38
 
35
39
 
36
40
  class ServiceUnavailableException(ExceptionInterface):
37
- """503 — Database non raggiungibile.
41
+ """503 - Database unreachable.
38
42
 
39
- Sollevata al posto di ``OperationalError`` (connessione caduta, pool esaurito,
40
- server non in ascolto).
43
+ Raised in place of ``OperationalError``: dropped connection, exhausted pool, server
44
+ not listening.
41
45
  """
42
46
  status_code = 503
43
47
  default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
44
48
 
45
49
 
46
50
  class UnprocessableEntityException(ExceptionInterface):
47
- """422 — Entità sintatticamente valida ma non processabile.
51
+ """422 - Syntactically valid but unprocessable entity.
48
52
 
49
- Non è sollevata automaticamente dai repository: è a disposizione dei service
50
- per gli errori di validazione applicativa.
53
+ Never raised automatically by the repositories: it is available to the services for
54
+ their application validation errors.
51
55
  """
52
56
  status_code = 422
53
57
  default_message = "Entità non processabile"
54
58
 
55
59
 
56
60
  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
+ """400 - Column or attribute missing on the entity.
61
62
 
62
- Example:
63
- >>> raise HasNoAttributeException("cognome")
64
- # detail: "Richiesta non corretta - Attributo cognome non presente"
63
+ Raised when a column name passed to ``columns``, ``exclude_columns`` or ``order_by``
64
+ does not exist on the entity.
65
65
  """
66
66
  status_code = 400
67
67
  default_message = "Richiesta non corretta"
68
68
 
69
69
  def __init__(self, attr: str = ""):
70
- """Compone il dettaglio in base alla presenza del nome dell'attributo.
70
+ """Compose the detail according to the presence of the attribute name.
71
71
 
72
72
  Args:
73
- attr (str): Nome dell'attributo mancante. Se omesso, viene usato un
74
- messaggio generico che non identifica la colonna.
73
+ attr (str): Name of the missing attribute. When omitted, a generic message not
74
+ identifying the column is used.
75
75
  """
76
76
  if attr:
77
77
  super().__init__(custom_message=f"Attributo {attr} non presente", )
@@ -80,11 +80,10 @@ class HasNoAttributeException(ExceptionInterface):
80
80
 
81
81
 
82
82
  class InternalServerException(ExceptionInterface):
83
- """500 — Errore interno non previsto.
83
+ """500 - Unexpected internal error.
84
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.
85
+ Used as a fallback by the ``@log`` decorator for every exception that is not already
86
+ an ``HTTPException``, and by ``CrudMetadata.get_table`` for a missing table.
88
87
  """
89
88
  status_code = 500
90
89
  default_message = "Impossibile gestire la richiesta al momento"
@@ -1,38 +1,31 @@
1
+ __all__ = ["ExceptionInterface"]
2
+
1
3
  from fastapi import HTTPException
2
4
 
3
5
 
4
6
  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.
7
+ """Base interface for every exception of the library.
10
8
 
11
- Sollevata dentro un endpoint FastAPI, viene serializzata automaticamente nella
12
- risposta di errore con lo status code dichiarato.
9
+ Extends ``fastapi.HTTPException`` moving status code and message to **class
10
+ attributes**: every subclass declares its own ``status_code`` and
11
+ ``default_message`` and can therefore be raised without arguments. Raised inside a
12
+ FastAPI endpoint, it is automatically serialized into the error response with the
13
+ declared status code.
13
14
 
14
15
  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è"
16
+ status_code (int): HTTP status code of the response.
17
+ default_message (str): Default message used as ``detail``.
24
18
  """
25
19
  status_code: int
26
20
  default_message: str
27
21
 
28
22
  def __init__(self, custom_message: str = None):
29
- """Compone il ``detail`` a partire dal messaggio di default e da quello custom.
23
+ """Compose the ``detail`` from the default message and the custom one.
30
24
 
31
25
  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``.
26
+ custom_message (str, optional): Additional detail, appended to the
27
+ ``default_message`` as ``"{default_message} - {custom_message}"``. When
28
+ omitted, the ``detail`` is the ``default_message`` alone.
36
29
  """
37
30
  detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
38
31
  super().__init__(status_code=self.status_code, detail=detail)
@@ -0,0 +1,8 @@
1
+ """ORM repositories, ORM-free metadata layer and shared base classes.
2
+
3
+ Container package: the symbols are imported from the subpackages
4
+ (``general.interface.base``, ``general.interface.repository``,
5
+ ``general.interface.repository.handler``, ``general.interface.metadata``). It is
6
+ declared explicitly, instead of being left as an implicit namespace package, so as
7
+ not to depend on the PEP 420 support of the tools consuming the distribution.
8
+ """
@@ -1,2 +1,12 @@
1
+ """SQLAlchemy declarative base class and preconfigured Pydantic models."""
2
+
1
3
  from .base_model import *
2
4
  from .declarative_base import *
5
+
6
+ __all__ = [
7
+ "Base",
8
+ "retrieve_mapper_entity",
9
+ "retrieve_mapper_from_table",
10
+ "ExtBaseModel",
11
+ "CamelExtBaseModel",
12
+ ]
@@ -8,66 +8,66 @@ from pydantic.alias_generators import to_camel
8
8
 
9
9
  @lru_cache(maxsize=None)
10
10
  def _alias_to_name(model: type[BaseModel]) -> dict[str, str]:
11
- """Mappa ``{alias: nome_campo}`` di un modello, costruita una volta per classe.
11
+ """Build the ``{alias: field_name}`` map of a model, once per class.
12
12
 
13
- Cachata sulla classe (long-lived) per evitare la scansione lineare di
14
- ``model_fields`` a ogni ``get_attr_by_alias``.
13
+ Cached on the class, which is long-lived, to avoid the linear scan of
14
+ ``model_fields`` on every ``get_attr_by_alias`` call.
15
+
16
+ Args:
17
+ model (type[BaseModel]): Pydantic model to inspect.
18
+
19
+ Returns:
20
+ dict[str, str]: Map from alias to Python field name, limited to the fields
21
+ declaring an alias.
15
22
  """
16
23
  return {field.alias: name for name, field in model.model_fields.items() if field.alias}
17
24
 
18
25
 
19
26
  class ConfigModel(BaseModel):
20
- """Modello Pydantic base con configurazione condivisa.
27
+ """Base Pydantic model carrying the shared configuration.
21
28
 
22
- Abilita:
23
- - ``from_attributes=True``: validazione da oggetti ORM SQLAlchemy.
24
- - ``populate_by_name=True``: accetta sia il nome Python che l'alias.
25
- - ``arbitrary_types_allowed=True``: consente tipi non standard (es. ``WKBElement``).
26
- - ``validate_default=True``: valida i valori di default alla creazione.
29
+ Enables ``from_attributes=True`` for the validation from SQLAlchemy ORM objects,
30
+ ``populate_by_name=True`` to accept both the Python name and the alias,
31
+ ``arbitrary_types_allowed=True`` to allow non-standard types such as ``WKBElement``,
32
+ and ``validate_default=True`` to validate the default values on creation.
27
33
  """
28
34
  model_config = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
29
35
 
30
36
 
31
37
  class CamelConfigModel(ConfigModel):
32
- """Estende ``ConfigModel`` aggiungendo la generazione automatica di alias camelCase."""
38
+ """Extend ``ConfigModel`` adding the automatic generation of camelCase aliases."""
33
39
  model_config = ConfigDict(alias_generator=to_camel)
34
40
 
35
41
  @classmethod
36
42
  def get_attr_by_alias(cls, alias: str) -> str:
37
- """Restituisce il nome del campo Python dato un alias camelCase.
43
+ """Return the Python field name given a camelCase alias.
38
44
 
39
- Utile per risalire al nome interno del campo quando si riceve
40
- un payload con chiavi in camelCase.
45
+ Useful to resolve the internal field name when receiving a payload with camelCase
46
+ keys.
41
47
 
42
48
  Args:
43
- alias (str): Alias camelCase del campo (es. ``"myField"``).
49
+ alias (str): camelCase alias of the field, such as ``"myField"``.
44
50
 
45
51
  Returns:
46
- str: Nome Python del campo (es. ``"my_field"``),
47
- oppure l'alias stesso se non trovato.
48
-
49
- Example:
50
- >>> class MyModel(CamelExtBaseModel):
51
- ... my_field: str
52
- >>> MyModel.get_attr_by_alias("myField")
53
- 'my_field'
52
+ str: Python name of the field, such as ``"my_field"``, or the alias itself when
53
+ not found.
54
54
  """
55
55
  return _alias_to_name(cls).get(alias, alias)
56
56
 
57
57
 
58
58
  class ExtBaseModel(ConfigModel):
59
- """Modello Pydantic con configurazione standard per l'integrazione ORM.
59
+ """Pydantic model with the standard configuration for the ORM integration.
60
60
 
61
- Eredita da ``ConfigModel``. Da usare come base per tutti i modelli
62
- che devono essere validati a partire da entità SQLAlchemy.
61
+ Inherits from ``ConfigModel``. Meant as the base of every model that has to be
62
+ validated from a SQLAlchemy entity.
63
63
  """
64
64
  pass
65
65
 
66
66
 
67
67
  class CamelExtBaseModel(CamelConfigModel):
68
- """Modello Pydantic con alias camelCase automatici e integrazione ORM.
68
+ """Pydantic model with automatic camelCase aliases and ORM integration.
69
69
 
70
- Eredita da ``CamelConfigModel``. Da usare quando l'API espone
71
- campi in camelCase ma internamente si lavora con snake_case.
70
+ Inherits from ``CamelConfigModel``. Meant for the cases where the API exposes
71
+ camelCase fields while the code works in snake_case.
72
72
  """
73
73
  pass
@@ -1,46 +1,51 @@
1
1
  __all__ = ["Base", "retrieve_mapper_entity", "retrieve_mapper_from_table"]
2
2
 
3
+ from functools import lru_cache
3
4
  from typing import Optional, Any, Sequence
4
5
 
5
6
  from geoalchemy2 import Geometry, WKBElement
6
- from pydantic import create_model
7
+ from pydantic import create_model, ConfigDict
7
8
  from sqlalchemy.inspection import inspect
8
9
  from sqlalchemy.orm import Mapper, as_declarative
9
10
 
10
11
 
11
12
  @as_declarative()
12
13
  class Base:
13
- """Classe base dichiarativa SQLAlchemy arricchita con metodi di utilità.
14
+ """SQLAlchemy declarative base class enriched with utility methods.
14
15
 
15
- Tutte le entità ORM del progetto devono estendere questa classe.
16
- Fornisce accesso rapido a colonne, chiavi primarie e conversione verso Pydantic.
16
+ Every ORM entity of the project has to extend this class. It gives quick access to
17
+ columns and primary keys, plus the conversion towards Pydantic.
17
18
  """
18
19
 
19
20
  @classmethod
20
21
  def to_pydantic(cls, model_name: str = None, full_optional: bool = False, default_values: dict[str, Any] = None, include_only: Sequence[str] = None, **create_model_kwargs):
21
- """Genera dinamicamente un modello Pydantic a partire dalle colonne dell'entità.
22
+ """Generate a Pydantic model dynamically from the entity columns.
22
23
 
23
- Itera sulle colonne mappate dall'ORM e costruisce i campi del modello
24
- rispettando la nullabilità e i valori di default forniti.
25
- Le colonne di tipo ``Geometry`` vengono mappate su ``WKBElement``.
24
+ Iterates over the ORM-mapped columns and builds the model fields honouring the
25
+ nullability and the given default values; ``Geometry`` columns are mapped onto
26
+ ``WKBElement``. When the caller passes neither ``__config__`` nor ``__base__``, the
27
+ generated model gets the same configuration as ``ExtBaseModel``. The default is all
28
+ or nothing: passing ``__config__`` puts the whole configuration in the hands of the
29
+ caller, and passing ``__base__`` lets the base carry its own.
26
30
 
27
31
  Args:
28
- model_name (str, optional): Nome del modello generato.
29
- Se non fornito, viene usato ``{ClassName}Model``.
30
- full_optional (bool): Se ``True``, tutti i campi diventano ``Optional``
31
- con default ``None``, indipendentemente dalla nullabilità nel DB.
32
+ model_name (str, optional): Name of the generated model. When not given,
33
+ ``{ClassName}Model`` is used.
34
+ full_optional (bool): If ``True``, every field becomes ``Optional`` with a
35
+ ``None`` default, regardless of the nullability in the database.
32
36
  Default: ``False``.
33
- default_values (dict[str, Any], optional): Dizionario
34
- ``{nome_campo: valore_default}`` per sovrascrivere i default inferiti.
35
- **create_model_kwargs: Argomenti aggiuntivi passati a ``pydantic.create_model``.
37
+ default_values (dict[str, Any], optional): ``{field_name: default_value}`` map
38
+ overriding the inferred defaults.
39
+ include_only (Sequence[str], optional): Attribute names to restrict the model to.
40
+ When ``None``, every mapped column is included.
41
+ **create_model_kwargs: Additional arguments passed to ``pydantic.create_model``.
36
42
 
37
43
  Returns:
38
- Type[BaseModel]: Classe Pydantic generata dinamicamente.
39
-
40
- Example:
41
- >>> UserModel = User.to_pydantic(full_optional=True)
42
- >>> PatchModel = User.to_pydantic(model_name="UserPatch", full_optional=True)
44
+ Type[BaseModel]: Dynamically generated Pydantic class.
43
45
  """
46
+ if "__config__" not in create_model_kwargs.keys() and "__base__" not in create_model_kwargs.keys():
47
+ create_model_kwargs["__config__"] = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
48
+
44
49
  if not default_values:
45
50
  default_values = dict()
46
51
  mapper: Mapper = inspect(cls)
@@ -67,70 +72,76 @@ class Base:
67
72
 
68
73
  @classmethod
69
74
  def columns(cls):
70
- """Restituisce la collezione di colonne della tabella associata all'entità.
75
+ """Return the column collection of the table the entity is mapped to.
71
76
 
72
77
  Returns:
73
- ReadOnlyColumnCollection: Collezione delle colonne SQLAlchemy.
74
-
75
- Example:
76
- >>> User.columns()
78
+ ReadOnlyColumnCollection: Collection of the SQLAlchemy columns.
77
79
  """
78
80
  return cls.__table__.c
79
81
 
80
82
  @classmethod
81
83
  def columns_names(cls):
82
- """Restituisce i nomi di tutte le colonne della tabella.
84
+ """Return the names of every column of the table.
83
85
 
84
86
  Returns:
85
- list[str]: Lista dei nomi delle colonne nell'ordine di definizione.
86
-
87
- Example:
88
- >>> User.columns_names()
89
- ['id', 'name', 'email']
87
+ list[str]: Names of the columns, in definition order. They are the **database**
88
+ names, which differ from the Python attribute names on the entities
89
+ declaring an explicit column name.
90
90
  """
91
91
  return [col.name for col in cls.__table__.c]
92
92
 
93
93
  @classmethod
94
94
  def primary_key(cls):
95
- """Restituisce le colonne che compongono la chiave primaria della tabella.
95
+ """Return the columns making up the primary key of the table.
96
96
 
97
97
  Returns:
98
- list[Column]: Lista delle colonne che formano la primary key.
99
-
100
- Example:
101
- >>> User.primary_key()
102
- [Column('id', Integer(), ...)]
98
+ list[Column]: Columns forming the primary key.
103
99
  """
104
100
  return cls.__table__.primary_key.columns.values()
105
101
 
106
102
  def to_dict(self):
107
- """Converte l'istanza in un dizionario ``{colonna: valore}``.
103
+ """Convert the instance into an ``{attribute: value}`` dictionary.
108
104
 
109
- Returns:
110
- dict[str, Any]: Dizionario con i valori correnti dell'istanza.
105
+ The keys are the names of the **Python attributes** of the mapped columns, not the
106
+ names of the columns in the database; the two differ only on the entities declaring
107
+ an explicit column name. Relationships are not included, only columns.
111
108
 
112
- Example:
113
- >>> user.to_dict()
114
- {'id': 1, 'name': 'Mario', 'email': 'mario@example.com'}
109
+ Returns:
110
+ dict[str, Any]: Current values of the instance.
115
111
  """
116
- return {key: getattr(self, key) for key in self.__class__.columns_names()}
112
+ return {key: getattr(self, key) for key in _attributes_names(self.__class__)}
117
113
 
118
114
 
119
- def retrieve_mapper_entity(entity_name: str):
120
- """Cerca e restituisce una sottoclasse di ``Base`` per nome della classe.
115
+ @lru_cache(maxsize=None)
116
+ def _attributes_names(entity: type) -> tuple[str, ...]:
117
+ """Return the Python attribute names of the mapped columns, memoized per class.
121
118
 
122
- Itera su ``Base.__subclasses__()``: trova quindi solo le sottoclassi **dirette**
123
- di ``Base``, non quelle annidate in ulteriori livelli di ereditarietà.
119
+ The memoization is at module level rather than a class attribute: the cache key is
120
+ the class itself, so a subclass in single table inheritance gets its own tuple
121
+ instead of inheriting the one of its parent. The names are read from
122
+ ``__mapper__.columns`` and not from ``column_attrs``, which would force
123
+ ``configure_mappers()``.
124
124
 
125
125
  Args:
126
- entity_name (str): Nome della classe ORM da cercare (es. ``"User"``).
126
+ entity (type): ORM class to inspect.
127
127
 
128
128
  Returns:
129
- Type[Base] | None: La classe ORM corrispondente, o ``None`` se non trovata.
129
+ tuple[str, ...]: Attribute names, in definition order.
130
+ """
131
+ return tuple(entity.__mapper__.columns.keys())
130
132
 
131
- Example:
132
- >>> retrieve_mapper_entity("User").__tablename__
133
- 'users'
133
+
134
+ def retrieve_mapper_entity(entity_name: str):
135
+ """Look up a subclass of ``Base`` by class name.
136
+
137
+ Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
138
+ of ``Base`` and not the ones nested in further inheritance levels.
139
+
140
+ Args:
141
+ entity_name (str): Name of the ORM class to look for, such as ``"User"``.
142
+
143
+ Returns:
144
+ Type[Base] | None: The matching ORM class, or ``None`` when not found.
134
145
  """
135
146
  for mapper in Base.__subclasses__():
136
147
  if mapper.__name__ == entity_name:
@@ -138,20 +149,16 @@ def retrieve_mapper_entity(entity_name: str):
138
149
 
139
150
 
140
151
  def retrieve_mapper_from_table(table_name: str):
141
- """Cerca e restituisce una sottoclasse di ``Base`` per nome della tabella.
152
+ """Look up a subclass of ``Base`` by table name.
142
153
 
143
- Itera su ``Base.__subclasses__()``: trova quindi solo le sottoclassi **dirette**
144
- di ``Base``, non quelle annidate in ulteriori livelli di ereditarietà.
154
+ Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
155
+ of ``Base`` and not the ones nested in further inheritance levels.
145
156
 
146
157
  Args:
147
- table_name (str): Nome della tabella nel database (es. ``"users"``).
158
+ table_name (str): Name of the table in the database, such as ``"users"``.
148
159
 
149
160
  Returns:
150
- Type[Base] | None: La classe ORM corrispondente, o ``None`` se non trovata.
151
-
152
- Example:
153
- >>> retrieve_mapper_from_table("users").__name__
154
- 'User'
161
+ Type[Base] | None: The matching ORM class, or ``None`` when not found.
155
162
  """
156
163
  for mapper in Base.__subclasses__():
157
164
  if mapper.__tablename__ == table_name:
@@ -0,0 +1,12 @@
1
+ """CRUD and spatial layer over reflected ``Table`` objects, without the ORM.
2
+
3
+ Alternative to the repositories to operate on tables having no ORM model: the schema
4
+ is reflected at runtime and the tables are resolved by name. Unlike the
5
+ repositories, this layer does **not** use the handler pipeline and does **not** map
6
+ the SQLAlchemy exceptions onto HTTP exceptions.
7
+ """
8
+
9
+ from .crud_metadata import *
10
+ from .geom_metadata import *
11
+
12
+ __all__ = ["CrudMetadata", "GeomMetadata"]