python-general-be-lib 0.10.0__tar.gz → 0.10.0.post2__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 (69) hide show
  1. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/PKG-INFO +1 -1
  2. python_general_be_lib-0.10.0.post2/general/interface/mixin/timestamp_mixin.py +42 -0
  3. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/pyproject.toml +1 -1
  4. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/python_general_be_lib.egg-info/PKG-INFO +1 -1
  5. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_mixin.py +35 -0
  6. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_retrieve_mapper.py +25 -5
  7. python_general_be_lib-0.10.0/general/interface/mixin/timestamp_mixin.py +0 -35
  8. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/LICENSE +0 -0
  9. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/README.md +0 -0
  10. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/__init__.py +0 -0
  11. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/context.py +0 -0
  12. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/exception/__init__.py +0 -0
  13. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/exception/access_exceptions.py +0 -0
  14. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/exception/crud_exceptions.py +0 -0
  15. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/exception/exception_interface.py +0 -0
  16. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/__init__.py +0 -0
  17. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/base/__init__.py +0 -0
  18. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/base/base_model.py +0 -0
  19. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/base/declarative_base.py +0 -0
  20. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/metadata/__init__.py +0 -0
  21. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/metadata/crud_metadata.py +0 -0
  22. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/metadata/geom_metadata.py +0 -0
  23. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/mixin/__init__.py +0 -0
  24. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/mixin/audit_mixin.py +0 -0
  25. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/mixin/softdelete_mixin.py +0 -0
  26. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/__init__.py +0 -0
  27. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/crud_repository.py +0 -0
  28. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/geometry_repository.py +0 -0
  29. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/__init__.py +0 -0
  30. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/base_handler.py +0 -0
  31. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/handler.py +0 -0
  32. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/ilike_handler.py +0 -0
  33. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/interval_handler.py +0 -0
  34. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/json_handler.py +0 -0
  35. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/json_ilike_handler.py +0 -0
  36. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/or_handler.py +0 -0
  37. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/soft_delete_handler.py +0 -0
  38. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/handler/statement_manager.py +0 -0
  39. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/many_to_many_repository.py +0 -0
  40. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/interface/repository/view_repository.py +0 -0
  41. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/logger.py +0 -0
  42. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/paginator_dto.py +0 -0
  43. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/general/py.typed +0 -0
  44. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/python_general_be_lib.egg-info/SOURCES.txt +0 -0
  45. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
  46. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/python_general_be_lib.egg-info/requires.txt +0 -0
  47. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/python_general_be_lib.egg-info/top_level.txt +0 -0
  48. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/setup.cfg +0 -0
  49. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_caricamento_parziale.py +0 -0
  50. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_compiled_cache.py +0 -0
  51. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_entity_has_columns.py +0 -0
  52. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_has_columns.py +0 -0
  53. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_import_pubblici.py +0 -0
  54. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_logger.py +0 -0
  55. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_metadata_eccezioni.py +0 -0
  56. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_metadata_paginazione.py +0 -0
  57. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_metadata_pipeline.py +0 -0
  58. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_negazione.py +0 -0
  59. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_ordinamento.py +0 -0
  60. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_pipeline_handler.py +0 -0
  61. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_query_count.py +0 -0
  62. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_smoke_crud.py +0 -0
  63. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_softdelete.py +0 -0
  64. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_sql_snapshot.py +0 -0
  65. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_to_dict.py +0 -0
  66. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_to_pydantic.py +0 -0
  67. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_upsert_chiavi.py +0 -0
  68. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/tests/test_valori_falsy.py +0 -0
  69. {python_general_be_lib-0.10.0 → python_general_be_lib-0.10.0.post2}/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.10.0
3
+ Version: 0.10.0.post2
4
4
  Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities.
5
5
  Author-email: Andrea Di Placido <a.diplacido@arpes.it>, "Arpes S.r.l." <it.admin@arpes.it>
6
6
  License: MIT
@@ -0,0 +1,42 @@
1
+ __all__ = ["TimestampMixin"]
2
+
3
+ from sqlalchemy import Column, DateTime, func
4
+
5
+
6
+ class TimestampMixin:
7
+ """Mixin adding the creation and last-update timestamps to an entity.
8
+
9
+ Meant to be mixed into a ``Base`` subclass, before or after it, since the columns are
10
+ plain ``Column`` objects and need no ``declared_attr``::
11
+
12
+ class Article(Base, TimestampMixin):
13
+ __tablename__ = "article"
14
+ id = Column(Integer, primary_key=True)
15
+
16
+ Both timestamps are timezone aware: on PostgreSQL they become
17
+ ``TIMESTAMP WITH TIME ZONE``, on the backends that ignore the flag the column is a
18
+ plain timestamp and the value is the one the database produces.
19
+
20
+ Both timestamps carry **two** defaults, and both are needed. ``server_default`` is a
21
+ DDL clause: it ends up in the ``CREATE TABLE`` and covers the rows inserted outside the
22
+ library, by a migration or by hand — but only on the tables the library creates
23
+ afterwards. On a table that already exists the column has no such clause, and since
24
+ SQLAlchemy omits a ``server_default`` column from the INSERT expecting the database to
25
+ fill it, the write would fail on the ``NOT NULL``. The ``default`` is what covers that
26
+ case: being a SQL expression it is rendered inline in the INSERT, so the value comes
27
+ from the database clock either way, and no DDL is required.
28
+
29
+ ``write_date`` is refreshed by ``onupdate``, which SQLAlchemy adds to the UPDATE
30
+ statement it builds: it therefore covers ``CrudRepository.update`` and ``upsert``, and
31
+ does **not** cover a raw SQL UPDATE run outside the library — that would need a
32
+ database trigger, which a mixin cannot install.
33
+
34
+ Attributes:
35
+ create_date (Column): Insert instant, from the ``now()`` of the database. Not null.
36
+ write_date (Column): Instant of the last update through the library. Not null: on
37
+ insert it holds the creation instant, so the two are equal until the first
38
+ update.
39
+ """
40
+
41
+ create_date = Column(DateTime, default=func.now(), server_default=func.now(), nullable=False)
42
+ write_date = Column(DateTime, default=func.now(), server_default=func.now(), onupdate=func.now(), nullable=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.10.0"
7
+ version = "0.10.0-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.10.0
3
+ Version: 0.10.0.post2
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
@@ -144,6 +144,41 @@ def test_una_riga_inserita_fuori_dalla_libreria_ha_i_timestamp(repo, sqlite_engi
144
144
  # ------------------------------------------------------------------------ update
145
145
 
146
146
 
147
+ def test_i_timestamp_funzionano_su_una_tabella_gia_esistente(sqlite_engine: Engine) -> None:
148
+ """Regressione: ``server_default`` da solo non basta su una tabella preesistente.
149
+
150
+ ``server_default`` è una clausola **DDL**: finisce nel ``CREATE TABLE`` e riguarda solo
151
+ le tabelle che la libreria crea dopo l'aggiunta del mixin. Su una tabella che esiste
152
+ già la colonna non ha quel ``DEFAULT``, e siccome SQLAlchemy **omette** dall'INSERT le
153
+ colonne con un ``server_default`` — aspettandosi che le riempia il database — la
154
+ scrittura falliva sul ``NOT NULL``, cioè con un 400.
155
+
156
+ Il rimedio è il secondo default, quello Python-side: essendo un'espressione SQL viene
157
+ reso **inline nell'INSERT**, quindi il valore arriva dall'orologio del database senza
158
+ dipendere dal DDL. I due default coesistono e coprono casi diversi.
159
+ """
160
+ with sqlite_engine.begin() as connessione:
161
+ connessione.exec_driver_sql("drop table if exists articolo_preesistente")
162
+ connessione.exec_driver_sql("""create table articolo_preesistente (
163
+ id integer primary key, titolo varchar(100),
164
+ create_date datetime not null, write_date datetime not null,
165
+ create_uid varchar, write_uid varchar)""")
166
+
167
+ class ArticoloPreesistente(Base, TimestampMixin, AuditMixin):
168
+ __tablename__ = "articolo_preesistente"
169
+
170
+ id = Column(Integer, primary_key=True, autoincrement=False)
171
+ titolo = Column(String(100))
172
+
173
+ repository = CrudRepository(entity=ArticoloPreesistente, model=ArticoloModel, engine=sqlite_engine)
174
+ repository.insert(models=[ArticoloModel(id=1, titolo="su tabella vecchia")])
175
+
176
+ with sqlite_engine.connect() as connessione:
177
+ valori = dict(connessione.execute(select(ArticoloPreesistente.__table__)).mappings().first())
178
+ assert valori["create_date"] is not None
179
+ assert valori["create_date"] == valori["write_date"]
180
+
181
+
147
182
  def test_l_update_aggiorna_solo_i_campi_di_ultima_modifica(repo, sqlite_engine: Engine) -> None:
148
183
  """``onupdate`` tocca ``updated_*`` e lascia ``created_*`` come sono.
149
184
 
@@ -59,7 +59,7 @@ class EntitaDietroAstratta(BaseAstratta):
59
59
  id = Column(Integer, primary_key=True)
60
60
 
61
61
 
62
- class Veicolo(Base):
62
+ class VeicoloRetrieve(Base):
63
63
  """Radice di una gerarchia a tabella singola."""
64
64
 
65
65
  __tablename__ = "veicolo_retrieve"
@@ -70,7 +70,7 @@ class Veicolo(Base):
70
70
  __mapper_args__ = {"polymorphic_on": tipo, "polymorphic_identity": "veicolo"}
71
71
 
72
72
 
73
- class AutocarroRetrieve(Veicolo):
73
+ class AutocarroRetrieve(VeicoloRetrieve):
74
74
  """Sottoclasse sulla **stessa** tabella del padre."""
75
75
 
76
76
  portata = Column(Integer)
@@ -81,13 +81,33 @@ class AutocarroRetrieve(Veicolo):
81
81
  # --------------------------------------------------- ricerca per nome di classe
82
82
 
83
83
 
84
+ @pytest.fixture(autouse=True)
85
+ def _nomi_non_ambigui():
86
+ """Fallisce subito se un altro modulo registra un'entità con gli stessi nomi.
87
+
88
+ Questo modulo cerca **per nome di classe**, che non è unico in un registry, e
89
+ ``Base.registry.mappers`` è un ``frozenset``: l'ordine di iterazione dipende
90
+ dall'identità degli oggetti e quindi cambia fra esecuzioni. Un omonimo renderebbe
91
+ questi test **intermittenti**, che è il modo peggiore di fallire. È già successo due
92
+ volte, con ``Autocarro`` e con ``Veicolo`` di ``test_to_dict.py``.
93
+ """
94
+ nostri = {"EntitaConMixin", "EntitaDietroAstratta", "AutocarroRetrieve", "VeicoloRetrieve"}
95
+ conteggio = {}
96
+ for mapper in Base.registry.mappers:
97
+ nome = mapper.class_.__name__
98
+ if nome in nostri:
99
+ conteggio[nome] = conteggio.get(nome, 0) + 1
100
+ doppioni = [nome for nome, quante in conteggio.items() if quante > 1]
101
+ assert not doppioni, f"nomi ambigui nel registry, rinominare le classi di prova: {doppioni}"
102
+
103
+
84
104
  @pytest.mark.parametrize(
85
105
  "nome, attesa",
86
106
  [
87
107
  ("EntitaConMixin", EntitaConMixin),
88
108
  ("EntitaDietroAstratta", EntitaDietroAstratta),
89
109
  ("AutocarroRetrieve", AutocarroRetrieve),
90
- ("Veicolo", Veicolo),
110
+ ("VeicoloRetrieve", VeicoloRetrieve),
91
111
  ],
92
112
  ids=["con_mixin", "dietro_base_astratta", "tabella_singola", "radice"],
93
113
  )
@@ -148,12 +168,12 @@ def test_una_base_astratta_nel_progetto_non_rompe_piu_la_ricerca() -> None:
148
168
  def test_su_tabella_singola_restituisce_la_radice() -> None:
149
169
  """Più classi condividono la tabella: la risposta deve essere deterministica.
150
170
 
151
- ``Veicolo`` e ``AutocarroRetrieve`` corrispondono entrambe a ``veicolo_retrieve``, e
171
+ ``VeicoloRetrieve`` e ``AutocarroRetrieve`` corrispondono entrambe a ``veicolo_retrieve``, e
152
172
  ``Base.registry.mappers`` è un ``frozenset``: restituire la prima trovata dipenderebbe
153
173
  dall'ordine di iterazione, stabile entro un processo ma non garantito fra processi.
154
174
  La radice della gerarchia è la risposta stabile, e quasi sempre quella cercata.
155
175
  """
156
- assert retrieve_mapper_from_table("veicolo_retrieve") is Veicolo
176
+ assert retrieve_mapper_from_table("veicolo_retrieve") is VeicoloRetrieve
157
177
 
158
178
 
159
179
  # ---------------------------------------------------------- nessun effetto collaterale
@@ -1,35 +0,0 @@
1
- __all__ = ["TimestampMixin"]
2
-
3
- from sqlalchemy import Column, DateTime, func
4
-
5
-
6
- class TimestampMixin:
7
- """Mixin adding the creation and last-update timestamps to an entity.
8
-
9
- Meant to be mixed into a ``Base`` subclass, before or after it, since the columns are
10
- plain ``Column`` objects and need no ``declared_attr``::
11
-
12
- class Article(Base, TimestampMixin):
13
- __tablename__ = "article"
14
- id = Column(Integer, primary_key=True)
15
-
16
- Both timestamps are timezone aware: on PostgreSQL they become
17
- ``TIMESTAMP WITH TIME ZONE``, on the backends that ignore the flag the column is a
18
- plain timestamp and the value is the one the database produces.
19
-
20
- ``create_date`` is filled by a **server-side** default, so it is correct even for rows
21
- inserted outside the library, by a migration or by hand. ``write_date`` is refreshed by
22
- ``onupdate``, which SQLAlchemy adds to the UPDATE statement it builds: it therefore
23
- covers ``CrudRepository.update`` and ``upsert``, and does **not** cover a raw SQL
24
- UPDATE run outside the library — that would need a database trigger, which a mixin
25
- cannot install.
26
-
27
- Attributes:
28
- create_date (Column): Insert instant, from the ``now()`` of the database. Not null.
29
- write_date (Column): Instant of the last update through the library. Not null: on
30
- insert it holds the creation instant, so the two are equal until the first
31
- update.
32
- """
33
-
34
- create_date = Column(DateTime, server_default=func.now(), nullable=False)
35
- write_date = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)