python-general-be-lib 0.12.0__tar.gz → 0.12.1__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 (88) hide show
  1. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/PKG-INFO +1 -1
  2. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/crud_repository.py +28 -12
  3. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/pyproject.toml +1 -1
  4. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/python_general_be_lib.egg-info/PKG-INFO +1 -1
  5. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/python_general_be_lib.egg-info/SOURCES.txt +1 -0
  6. python_general_be_lib-0.12.1/tests/test_rollback.py +85 -0
  7. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/LICENSE +0 -0
  8. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/README.md +0 -0
  9. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/__init__.py +0 -0
  10. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/auth/__init__.py +0 -0
  11. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/auth/jwt_model.py +0 -0
  12. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/auth/key_provider.py +0 -0
  13. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/auth/keycloak_auth.py +0 -0
  14. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/auth/middleware.py +0 -0
  15. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/context.py +0 -0
  16. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/exception/__init__.py +0 -0
  17. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/exception/access_exceptions.py +0 -0
  18. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/exception/crud_exceptions.py +0 -0
  19. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/exception/exception_interface.py +0 -0
  20. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/__init__.py +0 -0
  21. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/base/__init__.py +0 -0
  22. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/base/base_model.py +0 -0
  23. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/base/declarative_base.py +0 -0
  24. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/metadata/__init__.py +0 -0
  25. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/metadata/crud_metadata.py +0 -0
  26. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/metadata/geom_metadata.py +0 -0
  27. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/mixin/__init__.py +0 -0
  28. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/mixin/audit_mixin.py +0 -0
  29. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/mixin/softdelete_mixin.py +0 -0
  30. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/mixin/timestamp_mixin.py +0 -0
  31. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/__init__.py +0 -0
  32. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/geometry_repository.py +0 -0
  33. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/__init__.py +0 -0
  34. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/base_handler.py +0 -0
  35. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/handler.py +0 -0
  36. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/ilike_handler.py +0 -0
  37. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/interval_handler.py +0 -0
  38. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/json_handler.py +0 -0
  39. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/json_ilike_handler.py +0 -0
  40. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/or_handler.py +0 -0
  41. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/soft_delete_handler.py +0 -0
  42. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/handler/statement_manager.py +0 -0
  43. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/many_to_many_repository.py +0 -0
  44. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/interface/repository/view_repository.py +0 -0
  45. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/logger.py +0 -0
  46. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/paginator_dto.py +0 -0
  47. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/py.typed +0 -0
  48. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/utils/__init__.py +0 -0
  49. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/general/utils/consts.py +0 -0
  50. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/python_general_be_lib.egg-info/dependency_links.txt +0 -0
  51. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/python_general_be_lib.egg-info/requires.txt +0 -0
  52. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/python_general_be_lib.egg-info/top_level.txt +0 -0
  53. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/setup.cfg +0 -0
  54. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_auth_chiavi.py +0 -0
  55. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_auth_fastapi.py +0 -0
  56. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_auth_token.py +0 -0
  57. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_caricamento_parziale.py +0 -0
  58. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_compiled_cache.py +0 -0
  59. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_costo_import.py +0 -0
  60. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_engine_defaults.py +0 -0
  61. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_entity_has_columns.py +0 -0
  62. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_execute_contratto.py +0 -0
  63. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_exists.py +0 -0
  64. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_find_one.py +0 -0
  65. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_get.py +0 -0
  66. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_has_columns.py +0 -0
  67. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_import_pubblici.py +0 -0
  68. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_logger.py +0 -0
  69. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_many_to_many.py +0 -0
  70. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_metadata_eccezioni.py +0 -0
  71. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_metadata_geom.py +0 -0
  72. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_metadata_paginazione.py +0 -0
  73. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_metadata_pipeline.py +0 -0
  74. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_metadata_scorciatoie.py +0 -0
  75. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_mixin.py +0 -0
  76. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_negazione.py +0 -0
  77. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_ordinamento.py +0 -0
  78. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_pipeline_handler.py +0 -0
  79. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_query_count.py +0 -0
  80. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_retrieve_mapper.py +0 -0
  81. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_smoke_crud.py +0 -0
  82. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_softdelete.py +0 -0
  83. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_sql_snapshot.py +0 -0
  84. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_to_dict.py +0 -0
  85. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_to_pydantic.py +0 -0
  86. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_upsert_chiavi.py +0 -0
  87. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/tests/test_valori_falsy.py +0 -0
  88. {python_general_be_lib-0.12.0 → python_general_be_lib-0.12.1}/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.12.0
3
+ Version: 0.12.1
4
4
  Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, Keycloak JWT auth, 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
@@ -169,14 +169,23 @@ class CrudRepository[Entity: Base | Type[Base]]:
169
169
  return _reduced_model(self.entity, frozenset(selected_columns))
170
170
 
171
171
  def _handle_exception(self, session: Session, e: Exception):
172
- """Close the session, log the error and raise an HTTP-friendly exception.
172
+ """Roll back and close the session, log the error and raise an HTTP-friendly exception.
173
173
 
174
174
  An ``OperationalError`` becomes a ``ServiceUnavailableException``, a
175
175
  ``NoResultFound`` an ``EntityNotFoundException``, a ``DataError`` or an
176
176
  ``IntegrityError`` a ``BadRequestException``; any other exception is re-raised as is.
177
177
 
178
+ ``close`` alone already rolls back the database transaction. What the explicit
179
+ ``rollback`` adds is the expiry of the ORM instances: an entity still held by the
180
+ caller raises ``DetachedInstanceError`` on access instead of exposing values that
181
+ were flushed but never committed. It runs on every error, a failed SELECT included,
182
+ since on PostgreSQL that aborts the transaction as well. An external session is
183
+ rolled back and closed too, so the caller's transaction is lost. A failure of the
184
+ rollback itself is logged and does not replace ``e``, and the session is closed
185
+ anyway.
186
+
178
187
  Args:
179
- session (Session): Session to close.
188
+ session (Session): Session to roll back and close.
180
189
  e (Exception): Caught exception to handle.
181
190
 
182
191
  Raises:
@@ -184,6 +193,11 @@ class CrudRepository[Entity: Base | Type[Base]]:
184
193
  EntityNotFoundException: If no record matches.
185
194
  BadRequestException: If the data is malformed or violates a constraint.
186
195
  """
196
+ try:
197
+ session.rollback()
198
+ except Exception:
199
+ # un rollback fallito non deve sostituire l'errore originale, né saltare la close
200
+ logging.exception("Rollback failed")
187
201
  session.close()
188
202
  logging.error(e)
189
203
  raise_http_exception(e)
@@ -195,12 +209,13 @@ class CrudRepository[Entity: Base | Type[Base]]:
195
209
  session (Session): Session to commit.
196
210
 
197
211
  Raises:
198
- ServiceUnavailableException | BadRequestException: On error during the commit.
212
+ ServiceUnavailableException | BadRequestException: On error during the commit,
213
+ after rolling back and closing the session.
199
214
  """
200
215
  try:
201
216
  session.commit()
202
217
  except Exception as e:
203
- self._handle_exception(session, e)
218
+ self._handle_exception(session=session, e=e)
204
219
 
205
220
  def _flush(self, session: Session):
206
221
  """Flush the session, handling the possible exceptions.
@@ -214,12 +229,13 @@ class CrudRepository[Entity: Base | Type[Base]]:
214
229
  session (Session): Session to flush.
215
230
 
216
231
  Raises:
217
- ServiceUnavailableException | BadRequestException: On error during the flush.
232
+ ServiceUnavailableException | BadRequestException: On error during the flush,
233
+ after rolling back and closing the session.
218
234
  """
219
235
  try:
220
236
  session.flush()
221
237
  except Exception as e:
222
- self._handle_exception(session, e)
238
+ self._handle_exception(session=session, e=e)
223
239
 
224
240
  def _entity_has_columns(self, *columns: str, entity: Type[Base] | Base = None) -> bool:
225
241
  """Check that every given name is a column of the entity.
@@ -441,7 +457,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
441
457
  try:
442
458
  entity = session.get(self.entity, values)
443
459
  except Exception as e:
444
- self._handle_exception(session, e)
460
+ self._handle_exception(session=session, e=e)
445
461
  else:
446
462
  return [entity] if entity is not None else []
447
463
 
@@ -607,7 +623,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
607
623
  try:
608
624
  session.add_all(entities)
609
625
  except Exception as e:
610
- self._handle_exception(session, e)
626
+ self._handle_exception(session=session, e=e)
611
627
  return self._return(session=session, entities=entities, keep_open=keep_open, commit=True, parsing_model=parsing_model)
612
628
 
613
629
  def update(self, session: Session = None, where: dict[str, Any] = None, returning: bool = True, parsing_model: Type[BaseModel] | BaseModel = None, **kwargs) -> list[Entity] | list[BaseModel]:
@@ -1049,9 +1065,9 @@ class CrudRepository[Entity: Base | Type[Base]]:
1049
1065
  """Execute a SQLAlchemy statement and return the result as it is.
1050
1066
 
1051
1067
  The method owns the error handling and nothing else: every exception goes to
1052
- ``_handle_exception``, which closes the session and maps the error to an HTTP
1053
- status. Reading the result is left to the caller, which is the only one knowing the
1054
- shape it needs — ``.scalars().all()`` for entities, ``.scalar()`` for an aggregate,
1068
+ ``_handle_exception``, which rolls back and closes the session and maps the error
1069
+ to an HTTP status. Reading the result is left to the caller, which is the only one
1070
+ knowing the shape it needs — ``.scalars().all()`` for entities, ``.scalar()`` for an aggregate,
1055
1071
  ``.all()`` for the multi-column rows of a statement built with ``add_columns``,
1056
1072
  ``.rowcount`` for a DML without ``RETURNING``. The result is bound to the session,
1057
1073
  so it has to be consumed before the session is closed. ``Session.execute`` returns
@@ -1076,6 +1092,6 @@ class CrudRepository[Entity: Base | Type[Base]]:
1076
1092
  try:
1077
1093
  result = session.execute(statement=stmt, params=values)
1078
1094
  except Exception as e:
1079
- self._handle_exception(session, e)
1095
+ self._handle_exception(session=session, e=e)
1080
1096
  else:
1081
1097
  return result
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-general-be-lib"
7
- version = "0.12.0"
7
+ version = "0.12.1"
8
8
  description = "General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, Keycloak JWT auth, 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.12.0
3
+ Version: 0.12.1
4
4
  Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, Keycloak JWT auth, 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
@@ -75,6 +75,7 @@ tests/test_ordinamento.py
75
75
  tests/test_pipeline_handler.py
76
76
  tests/test_query_count.py
77
77
  tests/test_retrieve_mapper.py
78
+ tests/test_rollback.py
78
79
  tests/test_smoke_crud.py
79
80
  tests/test_softdelete.py
80
81
  tests/test_sql_snapshot.py
@@ -0,0 +1,85 @@
1
+ """Rollback di ``CrudRepository._handle_exception``.
2
+
3
+ ``Session.close()`` annulla già la transazione sul database: quello che il
4
+ ``rollback()`` esplicito aggiunge è la scadenza delle istanze ORM. Senza, un'entità
5
+ rimasta in mano al chiamante resta leggibile con i valori del ``flush`` mai
6
+ confermati; con, la lettura alza ``DetachedInstanceError``. Il rollback gira su ogni
7
+ errore, SELECT compresa, e un suo fallimento non deve coprire l'errore originale.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+
14
+ import pytest
15
+ from sqlalchemy import select, text
16
+ from sqlalchemy.engine import Engine
17
+ from sqlalchemy.exc import IntegrityError, ProgrammingError
18
+ from sqlalchemy.orm import Session
19
+ from sqlalchemy.orm.exc import DetachedInstanceError
20
+
21
+ from general.exception.crud_exceptions import BadRequestException, ServiceUnavailableException
22
+ from general.interface.repository.crud_repository import CrudRepository
23
+ from models import Prodotto, ProdottoModel, prodotto_repository
24
+
25
+
26
+ @pytest.fixture
27
+ def repo(engine: Engine) -> CrudRepository:
28
+ repository = prodotto_repository(engine)
29
+ repository.insert(models=[ProdottoModel(id=1, codice="pulito")])
30
+ return repository
31
+
32
+
33
+ def test_errore_di_scrittura_fa_scadere_le_entita_della_sessione_esterna(engine: Engine, repo: CrudRepository) -> None:
34
+ """Dopo un ``IntegrityError`` l'entità modificata non espone più il valore mai confermato."""
35
+ sessione = Session(engine)
36
+ entita = sessione.get(Prodotto, 1)
37
+ entita.codice = "sporco"
38
+
39
+ with pytest.raises(BadRequestException):
40
+ repo.insert(session=sessione, models=[ProdottoModel(id=1, codice="duplicato")])
41
+
42
+ with pytest.raises(DetachedInstanceError):
43
+ _ = entita.codice
44
+ assert repo.get(1).codice == "pulito"
45
+
46
+
47
+ def test_anche_una_select_fallita_fa_scadere_le_entita(engine: Engine, repo: CrudRepository) -> None:
48
+ """Su PostgreSQL una SELECT fallita blocca la transazione: il rollback vale anche lì."""
49
+ sessione = Session(engine)
50
+ entita = sessione.get(Prodotto, 1)
51
+ entita.codice = "sporco"
52
+ sessione.flush()
53
+
54
+ # SQLite la segnala come OperationalError (503), PostgreSQL come ProgrammingError (non mappato)
55
+ with pytest.raises((ServiceUnavailableException, ProgrammingError)):
56
+ repo.execute(sessione, select(Prodotto).where(text("colonna_inesistente = 1")))
57
+
58
+ with pytest.raises(DetachedInstanceError):
59
+ _ = entita.codice
60
+ assert repo.get(1).codice == "pulito"
61
+
62
+
63
+ class _SessioneConRollbackRotto:
64
+ """Sessione finta il cui ``rollback`` fallisce, per verificare cosa succede dopo."""
65
+
66
+ def __init__(self) -> None:
67
+ self.chiusa = False
68
+
69
+ def rollback(self) -> None:
70
+ raise RuntimeError("rollback rotto")
71
+
72
+ def close(self) -> None:
73
+ self.chiusa = True
74
+
75
+
76
+ def test_un_rollback_fallito_non_copre_l_errore_originale(sqlite_engine: Engine, caplog) -> None:
77
+ """L'errore originale resta quello mappato, la sessione viene chiusa e il guasto registrato."""
78
+ repository = prodotto_repository(sqlite_engine)
79
+ sessione = _SessioneConRollbackRotto()
80
+
81
+ with caplog.at_level(logging.ERROR), pytest.raises(BadRequestException):
82
+ repository._handle_exception(session=sessione, e=IntegrityError("s", None, Exception()))
83
+
84
+ assert sessione.chiusa is True
85
+ assert any(record.exc_info and isinstance(record.exc_info[1], RuntimeError) for record in caplog.records)