python-general-be-lib 0.11.0__tar.gz → 0.11.2__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.11.0 → python_general_be_lib-0.11.2}/PKG-INFO +1 -1
  2. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/base/declarative_base.py +7 -0
  3. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/metadata/crud_metadata.py +2 -2
  4. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/crud_repository.py +2 -2
  5. python_general_be_lib-0.11.2/general/utils/__init__.py +3 -0
  6. python_general_be_lib-0.11.2/general/utils/consts.py +6 -0
  7. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/pyproject.toml +1 -1
  8. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/python_general_be_lib.egg-info/PKG-INFO +1 -1
  9. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/python_general_be_lib.egg-info/SOURCES.txt +3 -0
  10. python_general_be_lib-0.11.2/tests/test_engine_defaults.py +103 -0
  11. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_find_one.py +8 -0
  12. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_get.py +8 -0
  13. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_import_pubblici.py +5 -0
  14. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_metadata_scorciatoie.py +6 -2
  15. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_to_pydantic.py +25 -0
  16. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/LICENSE +0 -0
  17. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/README.md +0 -0
  18. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/__init__.py +0 -0
  19. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/context.py +0 -0
  20. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/exception/__init__.py +0 -0
  21. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/exception/access_exceptions.py +0 -0
  22. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/exception/crud_exceptions.py +0 -0
  23. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/exception/exception_interface.py +0 -0
  24. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/__init__.py +0 -0
  25. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/base/__init__.py +0 -0
  26. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/base/base_model.py +0 -0
  27. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/metadata/__init__.py +0 -0
  28. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/metadata/geom_metadata.py +0 -0
  29. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/mixin/__init__.py +0 -0
  30. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/mixin/audit_mixin.py +0 -0
  31. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/mixin/softdelete_mixin.py +0 -0
  32. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/mixin/timestamp_mixin.py +0 -0
  33. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/__init__.py +0 -0
  34. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/geometry_repository.py +0 -0
  35. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/__init__.py +0 -0
  36. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/base_handler.py +0 -0
  37. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/handler.py +0 -0
  38. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/ilike_handler.py +0 -0
  39. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/interval_handler.py +0 -0
  40. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/json_handler.py +0 -0
  41. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/json_ilike_handler.py +0 -0
  42. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/or_handler.py +0 -0
  43. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/soft_delete_handler.py +0 -0
  44. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/handler/statement_manager.py +0 -0
  45. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/many_to_many_repository.py +0 -0
  46. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/interface/repository/view_repository.py +0 -0
  47. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/logger.py +0 -0
  48. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/paginator_dto.py +0 -0
  49. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/general/py.typed +0 -0
  50. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
  51. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/python_general_be_lib.egg-info/requires.txt +0 -0
  52. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/python_general_be_lib.egg-info/top_level.txt +0 -0
  53. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/setup.cfg +0 -0
  54. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_caricamento_parziale.py +0 -0
  55. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_compiled_cache.py +0 -0
  56. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_entity_has_columns.py +0 -0
  57. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_execute_contratto.py +0 -0
  58. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_exists.py +0 -0
  59. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_has_columns.py +0 -0
  60. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_logger.py +0 -0
  61. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_many_to_many.py +0 -0
  62. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_metadata_eccezioni.py +0 -0
  63. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_metadata_geom.py +0 -0
  64. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_metadata_paginazione.py +0 -0
  65. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_metadata_pipeline.py +0 -0
  66. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_mixin.py +0 -0
  67. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_negazione.py +0 -0
  68. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_ordinamento.py +0 -0
  69. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_pipeline_handler.py +0 -0
  70. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_query_count.py +0 -0
  71. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_retrieve_mapper.py +0 -0
  72. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_smoke_crud.py +0 -0
  73. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_softdelete.py +0 -0
  74. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_sql_snapshot.py +0 -0
  75. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_to_dict.py +0 -0
  76. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_upsert_chiavi.py +0 -0
  77. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_valori_falsy.py +0 -0
  78. {python_general_be_lib-0.11.0 → python_general_be_lib-0.11.2}/tests/test_view_repository.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-general-be-lib
3
- Version: 0.11.0
3
+ Version: 0.11.2
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,6 +31,11 @@ class Base:
31
31
  or nothing: passing ``__config__`` puts the whole configuration in the hands of the
32
32
  caller, and passing ``__base__`` lets the base carry its own.
33
33
 
34
+ The generated class is stamped with the module of the entity, unless the caller
35
+ passes ``__module__``: ``create_model`` reads the calling frame otherwise, so every
36
+ generated model would claim to live in this module and a ``repr`` would point at
37
+ the library instead of the entity.
38
+
34
39
  Args:
35
40
  model_name (str, optional): Name of the generated model. When not given,
36
41
  ``{ClassName}Model`` is used.
@@ -49,6 +54,8 @@ class Base:
49
54
  Returns:
50
55
  Type[BaseModel]: Dynamically generated Pydantic class.
51
56
  """
57
+ create_model_kwargs.setdefault("__module__", cls.__module__)
58
+
52
59
  if "__config__" not in create_model_kwargs.keys() and "__base__" not in create_model_kwargs.keys():
53
60
  create_model_kwargs["__config__"] = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
54
61
 
@@ -342,7 +342,7 @@ class CrudMetadata:
342
342
  if rows:
343
343
  return rows[0]
344
344
  if strict:
345
- raise EntityNotFoundException()
345
+ raise EntityNotFoundException(custom_message=table.name)
346
346
  return None
347
347
 
348
348
  def _get(self, table: Table, connection: Connection, pk: Any, columns: list[str] = None, **kwhere):
@@ -429,7 +429,7 @@ class CrudMetadata:
429
429
  if rows:
430
430
  return rows[0]
431
431
  if strict:
432
- raise EntityNotFoundException()
432
+ raise EntityNotFoundException(custom_message=table.name)
433
433
  return None
434
434
 
435
435
  def insert(self, from_table: str, connection: Connection = None, models: list[Type[BaseModel] | BaseModel] = None, values: list[dict[str, Any]] = None, returning: bool = True,
@@ -423,7 +423,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
423
423
  if records:
424
424
  return records[0]
425
425
  if strict:
426
- raise EntityNotFoundException()
426
+ raise EntityNotFoundException(custom_message=self.entity.__name__)
427
427
  return None
428
428
 
429
429
  def _get(self, session: Session, pk: Any, columns: list[str] = None, exclude_columns: list[str] = None, **kwhere):
@@ -523,7 +523,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
523
523
  if records:
524
524
  return records[0]
525
525
  if strict:
526
- raise EntityNotFoundException()
526
+ raise EntityNotFoundException(custom_message=self.entity.__name__)
527
527
  return None
528
528
 
529
529
  def _find(self, session: Session, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, orders_by: dict[str, bool] = None, **kwhere):
@@ -0,0 +1,3 @@
1
+ from .consts import *
2
+
3
+ __all__ = ["ENGINE_DEFAULTS", "ENGINE_DEFAULTS_QUEUE_POOL", "SESSION_DEFAULTS"]
@@ -0,0 +1,6 @@
1
+ __all__ = ["ENGINE_DEFAULTS", "ENGINE_DEFAULTS_QUEUE_POOL", "SESSION_DEFAULTS"]
2
+
3
+ ENGINE_DEFAULTS = {"pool_pre_ping": True,
4
+ "pool_recycle": 1800}
5
+ ENGINE_DEFAULTS_QUEUE_POOL = {"pool_use_lifo": True}
6
+ SESSION_DEFAULTS = {"expire_on_commit": False}
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-general-be-lib"
7
- version = "0.11.0"
7
+ version = "0.11.2"
8
8
  description = "General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-general-be-lib
3
- Version: 0.11.0
3
+ Version: 0.11.2
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
@@ -36,6 +36,8 @@ general/interface/repository/handler/json_ilike_handler.py
36
36
  general/interface/repository/handler/or_handler.py
37
37
  general/interface/repository/handler/soft_delete_handler.py
38
38
  general/interface/repository/handler/statement_manager.py
39
+ general/utils/__init__.py
40
+ general/utils/consts.py
39
41
  python_general_be_lib.egg-info/PKG-INFO
40
42
  python_general_be_lib.egg-info/SOURCES.txt
41
43
  python_general_be_lib.egg-info/dependency_links.txt
@@ -43,6 +45,7 @@ python_general_be_lib.egg-info/requires.txt
43
45
  python_general_be_lib.egg-info/top_level.txt
44
46
  tests/test_caricamento_parziale.py
45
47
  tests/test_compiled_cache.py
48
+ tests/test_engine_defaults.py
46
49
  tests/test_entity_has_columns.py
47
50
  tests/test_execute_contratto.py
48
51
  tests/test_exists.py
@@ -0,0 +1,103 @@
1
+ """`ENGINE_DEFAULTS` e `SESSION_DEFAULTS` (3.1 di `.claude/piano-ottimizzazioni.md`).
2
+
3
+ La voce 3.1 proponeva un `build_engine(...)` ed è stata **scartata**: la configurazione
4
+ dell'engine dipende dal profilo di carico del singolo BE, e una libreria di accesso ai dati
5
+ che decide al posto dell'applicazione sposta la responsabilità dove ci sono meno
6
+ informazioni per deciderla.
7
+
8
+ Le costanti chiudono la stessa esigenza senza prendere possesso di niente: sono **dati** che
9
+ l'applicativo espande nella propria `create_engine`/`sessionmaker`, e che può ispezionare o
10
+ scartare. La regola su cosa può entrarci è ciò che questi test difendono: **solo chiavi
11
+ portabili fra le classi di pool**. Le altre stanno in una costante separata.
12
+
13
+ Nessun test qui apre una connessione: `create_engine` non si connette, quindi anche l'URL
14
+ PostgreSQL è verificabile senza database.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import pytest
20
+ from sqlalchemy import create_engine
21
+ from sqlalchemy.orm import sessionmaker
22
+ from sqlalchemy.pool import QueuePool, SingletonThreadPool
23
+
24
+ from general.utils import ENGINE_DEFAULTS, ENGINE_DEFAULTS_QUEUE_POOL, SESSION_DEFAULTS
25
+
26
+ PG_URL = "postgresql+psycopg://utente:password@localhost:1/inesistente"
27
+
28
+
29
+ @pytest.fixture
30
+ def url_su_file(tmp_path) -> str:
31
+ return f"sqlite:///{tmp_path / 'prova.sqlite'}"
32
+
33
+
34
+ # ------------------------------------------------------------------ portabilità
35
+
36
+
37
+ def test_la_classe_di_pool_dipende_dall_url_non_dal_dialetto(url_su_file: str) -> None:
38
+ """È la premessa di tutto il resto, e non è ovvia.
39
+
40
+ SQLite **in memoria** usa `SingletonThreadPool`, SQLite **su file** usa `QueuePool`
41
+ come PostgreSQL: lo stesso dialetto accetta o rifiuta le stesse opzioni a seconda
42
+ dell'URL.
43
+ """
44
+ assert isinstance(create_engine("sqlite://").pool, SingletonThreadPool)
45
+ assert isinstance(create_engine(url_su_file).pool, QueuePool)
46
+ assert isinstance(create_engine(PG_URL).pool, QueuePool)
47
+
48
+
49
+ @pytest.mark.parametrize("url", ["sqlite://", "file", PG_URL], ids=["sqlite memoria", "sqlite file", "postgresql"])
50
+ def test_engine_defaults_si_applica_su_ogni_pool(url: str, url_su_file: str) -> None:
51
+ """Il contratto della costante portabile: nessuna chiave che un pool rifiuti."""
52
+ engine = create_engine(url_su_file if url == "file" else url, **ENGINE_DEFAULTS)
53
+
54
+ assert engine.pool._pre_ping is True
55
+ engine.dispose()
56
+
57
+
58
+ def test_le_opzioni_di_queue_pool_stanno_in_una_costante_separata(url_su_file: str) -> None:
59
+ """E il motivo per cui è separata: su `SingletonThreadPool` sollevano.
60
+
61
+ È il difetto che la separazione previene — un consumatore che espande tutto sulla
62
+ propria suite di test, che gira su SQLite in memoria come questa, non arriverebbe alla
63
+ prima query.
64
+ """
65
+ with pytest.raises(TypeError, match="pool_use_lifo"):
66
+ create_engine("sqlite://", **ENGINE_DEFAULTS, **ENGINE_DEFAULTS_QUEUE_POOL)
67
+
68
+ for url in (url_su_file, PG_URL):
69
+ engine = create_engine(url, **ENGINE_DEFAULTS, **ENGINE_DEFAULTS_QUEUE_POOL)
70
+ assert engine.pool._pre_ping is True
71
+ engine.dispose()
72
+
73
+
74
+ # ------------------------------------------------------------------ i valori
75
+
76
+
77
+ def test_i_valori_arrivano_davvero_sul_pool(url_su_file: str) -> None:
78
+ """Una costante che non ha effetto è peggio di nessuna costante."""
79
+ engine = create_engine(url_su_file, **ENGINE_DEFAULTS, **ENGINE_DEFAULTS_QUEUE_POOL)
80
+
81
+ assert engine.pool._pre_ping is True
82
+ assert engine.pool._recycle == 1800
83
+ assert engine.pool._pool.use_lifo is True, "il flag vive sulla coda del pool, non sul pool"
84
+
85
+
86
+
87
+ def test_session_defaults_arriva_sulla_sessione() -> None:
88
+ """``expire_on_commit`` è di ``sessionmaker``, non di ``create_engine``: costante a parte."""
89
+ sessione = sessionmaker(bind=create_engine("sqlite://"), **SESSION_DEFAULTS)()
90
+
91
+ assert sessione.expire_on_commit is False
92
+
93
+
94
+ def test_le_costanti_non_sono_condivise_per_riferimento() -> None:
95
+ """Chi le espande con ``**`` ottiene una copia; chi le muta rompe tutti gli altri.
96
+
97
+ Il test non impedisce la mutazione — un ``dict`` è mutabile — ma fissa che l'uso
98
+ documentato (`create_engine(url, **ENGINE_DEFAULTS)`) non la propaga.
99
+ """
100
+ kwargs = {**ENGINE_DEFAULTS}
101
+ kwargs["pool_pre_ping"] = False
102
+
103
+ assert ENGINE_DEFAULTS["pool_pre_ping"] is True
@@ -56,6 +56,14 @@ def test_con_strict_alza_il_404(repo: CrudRepository) -> None:
56
56
  assert errore.value.status_code == 404
57
57
 
58
58
 
59
+ def test_il_404_nomina_l_entita(repo: CrudRepository) -> None:
60
+ """Con più letture in fila il 404 deve dire **quale** è andata a vuoto."""
61
+ with pytest.raises(EntityNotFoundException) as errore:
62
+ repo.find_one(nome="nessuno", strict=True)
63
+
64
+ assert errore.value.detail == "Dato non trovato - Utente"
65
+
66
+
59
67
  def test_con_strict_la_sessione_viene_chiusa_prima_del_raise(repo: CrudRepository, engine: Engine) -> None:
60
68
  """Il ``raise`` sta **dopo** ``_return``, che è quello che chiude la sessione.
61
69
 
@@ -62,6 +62,14 @@ def test_con_strict_alza_il_404(repo: CrudRepository) -> None:
62
62
  assert errore.value.status_code == 404
63
63
 
64
64
 
65
+ def test_il_404_nomina_l_entita(repo: CrudRepository) -> None:
66
+ """Con più letture in fila il 404 deve dire **quale** è andata a vuoto."""
67
+ with pytest.raises(EntityNotFoundException) as errore:
68
+ repo.get(999, strict=True)
69
+
70
+ assert errore.value.detail == "Dato non trovato - Utente"
71
+
72
+
65
73
  def test_costa_una_sola_query(repo: CrudRepository, engine: Engine) -> None:
66
74
  with assert_query_count(engine, 1, verbs=["SELECT"]):
67
75
  repo.get(1)
@@ -19,6 +19,11 @@ import pytest
19
19
 
20
20
  # (package, simboli che devono essere importabili da lì)
21
21
  SUPERFICIE = {
22
+ "general.utils": [
23
+ "ENGINE_DEFAULTS",
24
+ "ENGINE_DEFAULTS_QUEUE_POOL",
25
+ "SESSION_DEFAULTS",
26
+ ],
22
27
  "general.exception": [
23
28
  "raise_http_exception",
24
29
  "ExceptionInterface",
@@ -72,9 +72,11 @@ def test_get_per_chiave_composta_come_tupla_e_come_dizionario(meta: CrudMetadata
72
72
 
73
73
  def test_get_senza_corrispondenza_e_con_strict(meta: CrudMetadata) -> None:
74
74
  assert meta.get(from_table="m21_semplice", pk=999) is None
75
- with pytest.raises(EntityNotFoundException):
75
+ with pytest.raises(EntityNotFoundException) as errore:
76
76
  meta.get(from_table="m21_semplice", pk=999, strict=True)
77
77
 
78
+ assert errore.value.detail == "Dato non trovato - m21_semplice"
79
+
78
80
 
79
81
  def test_get_su_una_tabella_senza_chiave_primaria_lo_dice(meta: CrudMetadata) -> None:
80
82
  """Caso reale di questo layer: tabelle legacy e viste senza chiave dichiarata."""
@@ -116,9 +118,11 @@ def test_find_one_restituisce_una_riga(meta: CrudMetadata) -> None:
116
118
 
117
119
  def test_find_one_senza_corrispondenza_e_con_strict(meta: CrudMetadata) -> None:
118
120
  assert meta.find_one(from_table="m21_semplice", reparto="inesistente") is None
119
- with pytest.raises(EntityNotFoundException):
121
+ with pytest.raises(EntityNotFoundException) as errore:
120
122
  meta.find_one(from_table="m21_semplice", reparto="inesistente", strict=True)
121
123
 
124
+ assert errore.value.detail == "Dato non trovato - m21_semplice"
125
+
122
126
 
123
127
  def test_find_one_orders_by_decide_quale_e_la_prima(meta: CrudMetadata) -> None:
124
128
  assert meta.find_one(from_table="m21_semplice", reparto="legale", orders_by={"id": True})["id"] == 1
@@ -189,3 +189,28 @@ def test_include_only_restringe_i_campi() -> None:
189
189
  """Comportamento preesistente, fissato perché è la base dei modelli ridotti."""
190
190
  modello = Utente.to_pydantic(full_optional=True, include_only=["id", "nome"])
191
191
  assert set(modello.model_fields) == {"id", "nome"}
192
+
193
+
194
+ # ------------------------------------------------------- modulo della classe generata
195
+
196
+
197
+ def test_il_modello_generato_porta_il_modulo_dell_entita() -> None:
198
+ """``create_model`` legge il frame del chiamante, che è ``declarative_base``.
199
+
200
+ Senza lo stamp esplicito il ``repr`` del tipo — quello che si legge in un ``print``,
201
+ in un log o in un errore di validazione — indicava la libreria invece dell'entità:
202
+ `general.interface.base.declarative_base.Utente_1a2b3c4d`, che si scambia facilmente
203
+ per un'entità ORM. È il caso dei modelli ridotti di ``find(columns=[...])``, generati
204
+ con un nome che l'entità non dichiara da nessuna parte.
205
+ """
206
+ modello = Utente.to_pydantic(full_optional=True)
207
+
208
+ assert modello.__module__ == Utente.__module__
209
+ assert "declarative_base" not in repr(modello)
210
+
211
+
212
+ def test_il_modulo_passato_dal_chiamante_vince() -> None:
213
+ """Lo stamp è un default, non un'imposizione: ``__module__`` resta un parametro."""
214
+ modello = Utente.to_pydantic(full_optional=True, __module__="mio.modulo")
215
+
216
+ assert modello.__module__ == "mio.modulo"