python-general-be-lib 0.8.3__tar.gz → 0.9.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 (78) hide show
  1. {python_general_be_lib-0.8.3/python_general_be_lib.egg-info → python_general_be_lib-0.9.0}/PKG-INFO +12 -3
  2. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/README.md +12 -2
  3. python_general_be_lib-0.9.0/general/exception/__init__.py +24 -0
  4. python_general_be_lib-0.9.0/general/exception/access_exceptions.py +22 -0
  5. python_general_be_lib-0.9.0/general/exception/crud_exceptions.py +89 -0
  6. python_general_be_lib-0.9.0/general/exception/exception_interface.py +31 -0
  7. python_general_be_lib-0.9.0/general/interface/__init__.py +8 -0
  8. python_general_be_lib-0.9.0/general/interface/base/__init__.py +12 -0
  9. python_general_be_lib-0.9.0/general/interface/base/base_model.py +73 -0
  10. python_general_be_lib-0.9.0/general/interface/base/declarative_base.py +165 -0
  11. python_general_be_lib-0.9.0/general/interface/metadata/__init__.py +12 -0
  12. python_general_be_lib-0.9.0/general/interface/metadata/crud_metadata.py +505 -0
  13. python_general_be_lib-0.9.0/general/interface/metadata/geom_metadata.py +208 -0
  14. python_general_be_lib-0.9.0/general/interface/repository/__init__.py +13 -0
  15. python_general_be_lib-0.9.0/general/interface/repository/crud_repository.py +790 -0
  16. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/general/interface/repository/geometry_repository.py +68 -92
  17. python_general_be_lib-0.9.0/general/interface/repository/handler/__init__.py +26 -0
  18. python_general_be_lib-0.9.0/general/interface/repository/handler/base_handler.py +81 -0
  19. python_general_be_lib-0.9.0/general/interface/repository/handler/handler.py +72 -0
  20. python_general_be_lib-0.9.0/general/interface/repository/handler/ilike_handler.py +89 -0
  21. python_general_be_lib-0.9.0/general/interface/repository/handler/interval_handler.py +106 -0
  22. python_general_be_lib-0.9.0/general/interface/repository/handler/json_handler.py +105 -0
  23. python_general_be_lib-0.9.0/general/interface/repository/handler/json_ilike_handler.py +75 -0
  24. python_general_be_lib-0.9.0/general/interface/repository/handler/or_handler.py +70 -0
  25. python_general_be_lib-0.9.0/general/interface/repository/handler/statement_manager.py +156 -0
  26. python_general_be_lib-0.9.0/general/interface/repository/many_to_many_repository.py +177 -0
  27. python_general_be_lib-0.9.0/general/interface/repository/view_repository.py +161 -0
  28. python_general_be_lib-0.9.0/general/logger.py +132 -0
  29. python_general_be_lib-0.9.0/general/paginator_dto.py +52 -0
  30. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/pyproject.toml +9 -2
  31. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0/python_general_be_lib.egg-info}/PKG-INFO +12 -3
  32. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/python_general_be_lib.egg-info/SOURCES.txt +10 -1
  33. python_general_be_lib-0.9.0/tests/test_entity_has_columns.py +161 -0
  34. python_general_be_lib-0.9.0/tests/test_has_columns.py +133 -0
  35. python_general_be_lib-0.9.0/tests/test_import_pubblici.py +134 -0
  36. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_logger.py +10 -5
  37. python_general_be_lib-0.9.0/tests/test_negazione.py +229 -0
  38. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_pipeline_handler.py +216 -35
  39. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_smoke_crud.py +112 -19
  40. python_general_be_lib-0.9.0/tests/test_to_dict.py +185 -0
  41. python_general_be_lib-0.9.0/tests/test_to_pydantic.py +160 -0
  42. python_general_be_lib-0.9.0/tests/test_valori_falsy.py +237 -0
  43. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_view_repository.py +216 -10
  44. python_general_be_lib-0.8.3/general/exception/access_exceptions.py +0 -23
  45. python_general_be_lib-0.8.3/general/exception/crud_exceptions.py +0 -90
  46. python_general_be_lib-0.8.3/general/exception/exception_interface.py +0 -38
  47. python_general_be_lib-0.8.3/general/interface/base/__init__.py +0 -2
  48. python_general_be_lib-0.8.3/general/interface/base/base_model.py +0 -73
  49. python_general_be_lib-0.8.3/general/interface/base/declarative_base.py +0 -158
  50. python_general_be_lib-0.8.3/general/interface/metadata/__init__.py +0 -0
  51. python_general_be_lib-0.8.3/general/interface/metadata/crud_metadata.py +0 -519
  52. python_general_be_lib-0.8.3/general/interface/metadata/geom_metadata.py +0 -214
  53. python_general_be_lib-0.8.3/general/interface/repository/__init__.py +0 -4
  54. python_general_be_lib-0.8.3/general/interface/repository/crud_repository.py +0 -825
  55. python_general_be_lib-0.8.3/general/interface/repository/handler/__init__.py +0 -6
  56. python_general_be_lib-0.8.3/general/interface/repository/handler/base_handler.py +0 -122
  57. python_general_be_lib-0.8.3/general/interface/repository/handler/ilike_handler.py +0 -105
  58. python_general_be_lib-0.8.3/general/interface/repository/handler/interval_handler.py +0 -143
  59. python_general_be_lib-0.8.3/general/interface/repository/handler/json_handler.py +0 -93
  60. python_general_be_lib-0.8.3/general/interface/repository/handler/json_ilike_handler.py +0 -100
  61. python_general_be_lib-0.8.3/general/interface/repository/handler/or_handler.py +0 -103
  62. python_general_be_lib-0.8.3/general/interface/repository/handler/statement_manager.py +0 -155
  63. python_general_be_lib-0.8.3/general/interface/repository/many_to_many_repository.py +0 -194
  64. python_general_be_lib-0.8.3/general/interface/repository/view_repository.py +0 -134
  65. python_general_be_lib-0.8.3/general/logger.py +0 -168
  66. python_general_be_lib-0.8.3/general/paginator_dto.py +0 -74
  67. python_general_be_lib-0.8.3/setup.py +0 -14
  68. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/LICENSE +0 -0
  69. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/general/__init__.py +0 -0
  70. /python_general_be_lib-0.8.3/general/exception/__init__.py → /python_general_be_lib-0.9.0/general/py.typed +0 -0
  71. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
  72. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/python_general_be_lib.egg-info/requires.txt +0 -0
  73. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/python_general_be_lib.egg-info/top_level.txt +0 -0
  74. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/setup.cfg +0 -0
  75. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_caricamento_parziale.py +0 -0
  76. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_compiled_cache.py +0 -0
  77. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_query_count.py +0 -0
  78. {python_general_be_lib-0.8.3 → python_general_be_lib-0.9.0}/tests/test_sql_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-general-be-lib
3
- Version: 0.8.3
3
+ Version: 0.9.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
@@ -31,7 +31,6 @@ Requires-Dist: psycopg[binary]>=3; extra == "dev"
31
31
  Requires-Dist: build; extra == "dev"
32
32
  Requires-Dist: twine; extra == "dev"
33
33
  Dynamic: license-file
34
- Dynamic: requires-python
35
34
 
36
35
  # python-general-be-lib
37
36
 
@@ -50,7 +49,7 @@ pip install python-general-be-lib
50
49
  ```python
51
50
  from sqlalchemy import create_engine
52
51
 
53
- from general.interface.repository.crud_repository import CrudRepository
52
+ from general.interface.repository import CrudRepository
54
53
 
55
54
  engine = create_engine("postgresql://user:pass@localhost/db")
56
55
  repo = CrudRepository(entity=User, model=UserModel, engine=engine)
@@ -72,3 +71,13 @@ repo.find(active=True, order_by="name", asc=True)
72
71
  | Paginazione | `PaginatorRequestModel`, `PaginatorResponseModel` |
73
72
  | Eccezioni | `ExceptionInterface` e sottoclassi HTTP (400/401/403/404/422/500/503) |
74
73
  | Logging | `init_logger`, decorator `@log` |
74
+
75
+ ---
76
+
77
+ ## Documentazione
78
+
79
+ | Documento | Contenuto |
80
+ |---|---|
81
+ | [`CHANGELOG.md`](https://github.com/Arpes-IS/python-general-be-lib/blob/main/CHANGELOG.md) | Modifiche per versione, con le note di migrazione |
82
+ | `general/README.md` | Panoramica della libreria: struttura, contratto sulle sessioni, handler, operazioni spaziali |
83
+ | `general/DOCS.md` | Riferimento completo, metodo per metodo |
@@ -15,7 +15,7 @@ pip install python-general-be-lib
15
15
  ```python
16
16
  from sqlalchemy import create_engine
17
17
 
18
- from general.interface.repository.crud_repository import CrudRepository
18
+ from general.interface.repository import CrudRepository
19
19
 
20
20
  engine = create_engine("postgresql://user:pass@localhost/db")
21
21
  repo = CrudRepository(entity=User, model=UserModel, engine=engine)
@@ -36,4 +36,14 @@ repo.find(active=True, order_by="name", asc=True)
36
36
  | Basi | `Base` (declarative base estesa), `ExtBaseModel`, `CamelExtBaseModel` |
37
37
  | Paginazione | `PaginatorRequestModel`, `PaginatorResponseModel` |
38
38
  | Eccezioni | `ExceptionInterface` e sottoclassi HTTP (400/401/403/404/422/500/503) |
39
- | Logging | `init_logger`, decorator `@log` |
39
+ | Logging | `init_logger`, decorator `@log` |
40
+
41
+ ---
42
+
43
+ ## Documentazione
44
+
45
+ | Documento | Contenuto |
46
+ |---|---|
47
+ | [`CHANGELOG.md`](https://github.com/Arpes-IS/python-general-be-lib/blob/main/CHANGELOG.md) | Modifiche per versione, con le note di migrazione |
48
+ | `general/README.md` | Panoramica della libreria: struttura, contratto sulle sessioni, handler, operazioni spaziali |
49
+ | `general/DOCS.md` | Riferimento completo, metodo per metodo |
@@ -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
+ ]
@@ -0,0 +1,22 @@
1
+ """HTTP exceptions for authentication and authorization.
2
+
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.
6
+ """
7
+
8
+ __all__ = ["ForbiddenException", "UnauthorizedException"]
9
+
10
+ from .exception_interface import ExceptionInterface
11
+
12
+
13
+ class ForbiddenException(ExceptionInterface):
14
+ """403 - Authenticated user without the permissions for the requested resource."""
15
+ status_code = 403
16
+ default_message = "Risorsa non accessibile da questo utente"
17
+
18
+
19
+ class UnauthorizedException(ExceptionInterface):
20
+ """401 - Request without valid credentials."""
21
+ status_code = 401
22
+ default_message = "Non autorizzato"
@@ -0,0 +1,89 @@
1
+ """HTTP exceptions for the persistence operations.
2
+
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).
7
+ """
8
+
9
+ __all__ = [
10
+ "BadRequestException",
11
+ "EntityNotFoundException",
12
+ "ServiceUnavailableException",
13
+ "UnprocessableEntityException",
14
+ "HasNoAttributeException",
15
+ "InternalServerException",
16
+ ]
17
+
18
+ from .exception_interface import ExceptionInterface
19
+
20
+
21
+ class BadRequestException(ExceptionInterface):
22
+ """400 - Invalid request data or violated integrity constraint.
23
+
24
+ Raised in place of ``DataError`` and ``IntegrityError``, such as a foreign key,
25
+ unique or NOT NULL violation.
26
+ """
27
+ status_code = 400
28
+ default_message = "Richiesta non corretta"
29
+
30
+
31
+ class EntityNotFoundException(ExceptionInterface):
32
+ """404 - No record matches the requested criteria.
33
+
34
+ Raised in place of ``NoResultFound``.
35
+ """
36
+ status_code = 404
37
+ default_message = "Dato non trovato"
38
+
39
+
40
+ class ServiceUnavailableException(ExceptionInterface):
41
+ """503 - Database unreachable.
42
+
43
+ Raised in place of ``OperationalError``: dropped connection, exhausted pool, server
44
+ not listening.
45
+ """
46
+ status_code = 503
47
+ default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
48
+
49
+
50
+ class UnprocessableEntityException(ExceptionInterface):
51
+ """422 - Syntactically valid but unprocessable entity.
52
+
53
+ Never raised automatically by the repositories: it is available to the services for
54
+ their application validation errors.
55
+ """
56
+ status_code = 422
57
+ default_message = "Entità non processabile"
58
+
59
+
60
+ class HasNoAttributeException(ExceptionInterface):
61
+ """400 - Column or attribute missing on the entity.
62
+
63
+ Raised when a column name passed to ``columns``, ``exclude_columns`` or ``order_by``
64
+ does not exist on the entity.
65
+ """
66
+ status_code = 400
67
+ default_message = "Richiesta non corretta"
68
+
69
+ def __init__(self, attr: str = ""):
70
+ """Compose the detail according to the presence of the attribute name.
71
+
72
+ Args:
73
+ attr (str): Name of the missing attribute. When omitted, a generic message not
74
+ identifying the column is used.
75
+ """
76
+ if attr:
77
+ super().__init__(custom_message=f"Attributo {attr} non presente", )
78
+ else:
79
+ super().__init__(custom_message="Alcuni attributi non sono presenti per questa entità", )
80
+
81
+
82
+ class InternalServerException(ExceptionInterface):
83
+ """500 - Unexpected internal error.
84
+
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.
87
+ """
88
+ status_code = 500
89
+ default_message = "Impossibile gestire la richiesta al momento"
@@ -0,0 +1,31 @@
1
+ __all__ = ["ExceptionInterface"]
2
+
3
+ from fastapi import HTTPException
4
+
5
+
6
+ class ExceptionInterface(HTTPException):
7
+ """Base interface for every exception of the library.
8
+
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.
14
+
15
+ Attributes:
16
+ status_code (int): HTTP status code of the response.
17
+ default_message (str): Default message used as ``detail``.
18
+ """
19
+ status_code: int
20
+ default_message: str
21
+
22
+ def __init__(self, custom_message: str = None):
23
+ """Compose the ``detail`` from the default message and the custom one.
24
+
25
+ Args:
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.
29
+ """
30
+ detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
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
+ """
@@ -0,0 +1,12 @@
1
+ """SQLAlchemy declarative base class and preconfigured Pydantic models."""
2
+
3
+ from .base_model import *
4
+ from .declarative_base import *
5
+
6
+ __all__ = [
7
+ "Base",
8
+ "retrieve_mapper_entity",
9
+ "retrieve_mapper_from_table",
10
+ "ExtBaseModel",
11
+ "CamelExtBaseModel",
12
+ ]
@@ -0,0 +1,73 @@
1
+ __all__ = ["ExtBaseModel", "CamelExtBaseModel"]
2
+
3
+ from functools import lru_cache
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+ from pydantic.alias_generators import to_camel
7
+
8
+
9
+ @lru_cache(maxsize=None)
10
+ def _alias_to_name(model: type[BaseModel]) -> dict[str, str]:
11
+ """Build the ``{alias: field_name}`` map of a model, once per class.
12
+
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.
22
+ """
23
+ return {field.alias: name for name, field in model.model_fields.items() if field.alias}
24
+
25
+
26
+ class ConfigModel(BaseModel):
27
+ """Base Pydantic model carrying the shared configuration.
28
+
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.
33
+ """
34
+ model_config = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
35
+
36
+
37
+ class CamelConfigModel(ConfigModel):
38
+ """Extend ``ConfigModel`` adding the automatic generation of camelCase aliases."""
39
+ model_config = ConfigDict(alias_generator=to_camel)
40
+
41
+ @classmethod
42
+ def get_attr_by_alias(cls, alias: str) -> str:
43
+ """Return the Python field name given a camelCase alias.
44
+
45
+ Useful to resolve the internal field name when receiving a payload with camelCase
46
+ keys.
47
+
48
+ Args:
49
+ alias (str): camelCase alias of the field, such as ``"myField"``.
50
+
51
+ Returns:
52
+ str: Python name of the field, such as ``"my_field"``, or the alias itself when
53
+ not found.
54
+ """
55
+ return _alias_to_name(cls).get(alias, alias)
56
+
57
+
58
+ class ExtBaseModel(ConfigModel):
59
+ """Pydantic model with the standard configuration for the ORM integration.
60
+
61
+ Inherits from ``ConfigModel``. Meant as the base of every model that has to be
62
+ validated from a SQLAlchemy entity.
63
+ """
64
+ pass
65
+
66
+
67
+ class CamelExtBaseModel(CamelConfigModel):
68
+ """Pydantic model with automatic camelCase aliases and ORM integration.
69
+
70
+ Inherits from ``CamelConfigModel``. Meant for the cases where the API exposes
71
+ camelCase fields while the code works in snake_case.
72
+ """
73
+ pass
@@ -0,0 +1,165 @@
1
+ __all__ = ["Base", "retrieve_mapper_entity", "retrieve_mapper_from_table"]
2
+
3
+ from functools import lru_cache
4
+ from typing import Optional, Any, Sequence
5
+
6
+ from geoalchemy2 import Geometry, WKBElement
7
+ from pydantic import create_model, ConfigDict
8
+ from sqlalchemy.inspection import inspect
9
+ from sqlalchemy.orm import Mapper, as_declarative
10
+
11
+
12
+ @as_declarative()
13
+ class Base:
14
+ """SQLAlchemy declarative base class enriched with utility methods.
15
+
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.
18
+ """
19
+
20
+ @classmethod
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):
22
+ """Generate a Pydantic model dynamically from the entity columns.
23
+
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.
30
+
31
+ Args:
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.
36
+ Default: ``False``.
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``.
42
+
43
+ Returns:
44
+ Type[BaseModel]: Dynamically generated Pydantic class.
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
+
49
+ if not default_values:
50
+ default_values = dict()
51
+ mapper: Mapper = inspect(cls)
52
+
53
+ # ``mapper.columns`` è indicizzato per **nome di attributo** e non forza la
54
+ # configurazione dei mapper: ``column_attrs``/``attrs``/``relationships`` la
55
+ # forzerebbero, e chiamare ``to_pydantic`` a import-time — il pattern
56
+ # documentato — romperebbe le relationship con forward-ref non ancora
57
+ # risolvibili (stessa ragione per cui ``relationship_attributes`` è lazy).
58
+ selected = [(name, column) for name, column in mapper.columns.items()
59
+ if include_only is None or name in include_only]
60
+
61
+ model_name = f"{cls.__name__}Model" if not model_name else model_name
62
+ fields = dict()
63
+ field_values = {name: None if full_optional else ... for name, _ in selected}
64
+ field_values.update(default_values)
65
+ for field_name, col in selected:
66
+ if isinstance(col.type, Geometry):
67
+ field_type = Optional[WKBElement] if col.nullable or full_optional else WKBElement
68
+ else:
69
+ field_type = Optional[col.type.python_type] if col.nullable or full_optional else col.type.python_type
70
+ fields[field_name] = (field_type, field_values[field_name])
71
+ return create_model(model_name, **create_model_kwargs, **fields)
72
+
73
+ @classmethod
74
+ def columns(cls):
75
+ """Return the column collection of the table the entity is mapped to.
76
+
77
+ Returns:
78
+ ReadOnlyColumnCollection: Collection of the SQLAlchemy columns.
79
+ """
80
+ return cls.__table__.c
81
+
82
+ @classmethod
83
+ def columns_names(cls):
84
+ """Return the names of every column of the table.
85
+
86
+ Returns:
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
+ """
91
+ return [col.name for col in cls.__table__.c]
92
+
93
+ @classmethod
94
+ def primary_key(cls):
95
+ """Return the columns making up the primary key of the table.
96
+
97
+ Returns:
98
+ list[Column]: Columns forming the primary key.
99
+ """
100
+ return cls.__table__.primary_key.columns.values()
101
+
102
+ def to_dict(self):
103
+ """Convert the instance into an ``{attribute: value}`` dictionary.
104
+
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.
108
+
109
+ Returns:
110
+ dict[str, Any]: Current values of the instance.
111
+ """
112
+ return {key: getattr(self, key) for key in _attributes_names(self.__class__)}
113
+
114
+
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.
118
+
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
+
125
+ Args:
126
+ entity (type): ORM class to inspect.
127
+
128
+ Returns:
129
+ tuple[str, ...]: Attribute names, in definition order.
130
+ """
131
+ return tuple(entity.__mapper__.columns.keys())
132
+
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.
145
+ """
146
+ for mapper in Base.__subclasses__():
147
+ if mapper.__name__ == entity_name:
148
+ return mapper
149
+
150
+
151
+ def retrieve_mapper_from_table(table_name: str):
152
+ """Look up a subclass of ``Base`` by table name.
153
+
154
+ Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
155
+ of ``Base`` and not the ones nested in further inheritance levels.
156
+
157
+ Args:
158
+ table_name (str): Name of the table in the database, such as ``"users"``.
159
+
160
+ Returns:
161
+ Type[Base] | None: The matching ORM class, or ``None`` when not found.
162
+ """
163
+ for mapper in Base.__subclasses__():
164
+ if mapper.__tablename__ == table_name:
165
+ return mapper
@@ -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"]