timber-common 1.2.0__tar.gz → 1.2.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 (182) hide show
  1. {timber_common-1.2.0 → timber_common-1.2.2}/CHANGELOG.md +29 -0
  2. {timber_common-1.2.0 → timber_common-1.2.2}/PKG-INFO +1 -1
  3. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/base.py +2 -1
  4. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/db_service.py +18 -23
  5. timber_common-1.2.2/common/utils/db_url.py +41 -0
  6. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/db_utils.py +2 -1
  7. {timber_common-1.2.0 → timber_common-1.2.2}/pyproject.toml +1 -1
  8. {timber_common-1.2.0 → timber_common-1.2.2}/LICENSE +0 -0
  9. {timber_common-1.2.0 → timber_common-1.2.2}/README.md +0 -0
  10. {timber_common-1.2.0 → timber_common-1.2.2}/common/__init__.py +0 -0
  11. {timber_common-1.2.0 → timber_common-1.2.2}/common/cli/__init__.py +0 -0
  12. {timber_common-1.2.0 → timber_common-1.2.2}/common/cli/commands/__init__.py +0 -0
  13. {timber_common-1.2.0 → timber_common-1.2.2}/common/cli/commands/validate.py +0 -0
  14. {timber_common-1.2.0 → timber_common-1.2.2}/common/cli/main.py +0 -0
  15. {timber_common-1.2.0 → timber_common-1.2.2}/common/config/__init__.py +0 -0
  16. {timber_common-1.2.0 → timber_common-1.2.2}/common/config/model_loader.py +0 -0
  17. {timber_common-1.2.0 → timber_common-1.2.2}/common/engine/__init__.py +0 -0
  18. {timber_common-1.2.0 → timber_common-1.2.2}/common/engine/config_executor.py +0 -0
  19. {timber_common-1.2.0 → timber_common-1.2.2}/common/engine/operation_registry.py +0 -0
  20. {timber_common-1.2.0 → timber_common-1.2.2}/common/init.py +0 -0
  21. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/__init__.py +0 -0
  22. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/a2ui.py +0 -0
  23. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/configs/__init__.py +0 -0
  24. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/core/__init.__.py +0 -0
  25. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/core/enterprise.py +0 -0
  26. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/core/tag.py +0 -0
  27. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/core/user.py +0 -0
  28. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/factory.py +0 -0
  29. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/mixins.py +0 -0
  30. {timber_common-1.2.0 → timber_common-1.2.2}/common/models/registry.py +0 -0
  31. {timber_common-1.2.0 → timber_common-1.2.2}/common/plugins/__init__.py +0 -0
  32. {timber_common-1.2.0 → timber_common-1.2.2}/common/plugins/contract.py +0 -0
  33. {timber_common-1.2.0 → timber_common-1.2.2}/common/plugins/discovery.py +0 -0
  34. {timber_common-1.2.0 → timber_common-1.2.2}/common/render/__init__.py +0 -0
  35. {timber_common-1.2.0 → timber_common-1.2.2}/common/render/pdf.py +0 -0
  36. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/README.md +0 -0
  37. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/__init__.py +0 -0
  38. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/base.py +0 -0
  39. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/workflow/__init__.py +0 -0
  40. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/workflow/definition.py +0 -0
  41. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/workflow/enums.py +0 -0
  42. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/workflow/patterns.py +0 -0
  43. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/workflow/state.py +0 -0
  44. {timber_common-1.2.0 → timber_common-1.2.2}/common/schemas/workflow/transition.py +0 -0
  45. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/__init__.py +0 -0
  46. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/a2a/__init__.py +0 -0
  47. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/a2a/client.py +0 -0
  48. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/communication/__init__.py +0 -0
  49. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/communication/email_service.py +0 -0
  50. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/communication/messaging_service.py +0 -0
  51. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/communication/sms_service.py +0 -0
  52. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/decisioning/__init__.py +0 -0
  53. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/decisioning/decision_engine.py +0 -0
  54. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/decisioning/expression_engine.py +0 -0
  55. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/decisioning/graph_evaluator.py +0 -0
  56. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/decisioning/models.py +0 -0
  57. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/decisioning/table_evaluator.py +0 -0
  58. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/encryption/__init__.py +0 -0
  59. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/encryption/field_encryption.py +0 -0
  60. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/gdpr/__init__.py +0 -0
  61. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/gdpr/deletion.py +0 -0
  62. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/integrations/__init__.py +0 -0
  63. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/integrations/auth_service.py +0 -0
  64. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/integrations/integration_service.py +0 -0
  65. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/integrations/mapping_service.py +0 -0
  66. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/integrations/models.py +0 -0
  67. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/integrations/registry.py +0 -0
  68. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/inventory/__init__.py +0 -0
  69. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/inventory/available_capabilities.py +0 -0
  70. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/inventory/cached_capabilities.py +0 -0
  71. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/inventory/loader.py +0 -0
  72. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/__init__.py +0 -0
  73. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/batch.py +0 -0
  74. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/calibration.py +0 -0
  75. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/confidence.py +0 -0
  76. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/contract.py +0 -0
  77. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/guards.py +0 -0
  78. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/models.py +0 -0
  79. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/providers/__init__.py +0 -0
  80. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/providers/fixture.py +0 -0
  81. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/judgment/providers/typesafe.py +0 -0
  82. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/__init__.py +0 -0
  83. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/base.py +0 -0
  84. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/claude_provider.py +0 -0
  85. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/gemini_provider.py +0 -0
  86. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/groq_provider.py +0 -0
  87. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/model_choice.py +0 -0
  88. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm/perplexity_provider.py +0 -0
  89. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/llm_service.py +0 -0
  90. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/DIRECTORY_STRUCTURE.md +0 -0
  91. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/MIGRATION_GUIDE.md +0 -0
  92. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/QUICKSTART.md +0 -0
  93. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/README.md +0 -0
  94. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/VIDEO_README.md +0 -0
  95. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/__init__.py +0 -0
  96. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/config_helpers.py +0 -0
  97. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/examples.py +0 -0
  98. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/image_generation.py +0 -0
  99. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/model_resolver.py +0 -0
  100. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/video_examples.py +0 -0
  101. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/media/video_generation.py +0 -0
  102. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/__init__.py +0 -0
  103. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/base.py +0 -0
  104. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/cache.py +0 -0
  105. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/instances.py +0 -0
  106. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/manager.py +0 -0
  107. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/notification.py +0 -0
  108. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/research.py +0 -0
  109. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/session.py +0 -0
  110. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/persistence/tracker.py +0 -0
  111. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/registry.py +0 -0
  112. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/runtime_cache/__init__.py +0 -0
  113. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/runtime_cache/invalidator.py +0 -0
  114. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/runtime_cache/user_context_cache.py +0 -0
  115. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/runtime_cache/valkey_client.py +0 -0
  116. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/runtime_cache/workflow_catalog_cache.py +0 -0
  117. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/security/__init__.py +0 -0
  118. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/security/oauth_service.py +0 -0
  119. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/vector/__init__.py +0 -0
  120. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/vector/auto_ingestion.py +0 -0
  121. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/vector/search.py +0 -0
  122. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/vector/tag_embedding.py +0 -0
  123. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/vendors/__init__.py +0 -0
  124. {timber_common-1.2.0 → timber_common-1.2.2}/common/services/vendors/stripe_service.py +0 -0
  125. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/__init__.py +0 -0
  126. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/config.py +0 -0
  127. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/decisioning/__init__.py +0 -0
  128. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/decisioning/csv_parser.py +0 -0
  129. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/decisioning/dmn_parser.py +0 -0
  130. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/decisioning/yaml_exporter.py +0 -0
  131. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/decisioning/yaml_parser.py +0 -0
  132. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/helpers.py +0 -0
  133. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/llm/chunk_aggregator.py +0 -0
  134. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/llm/content_condenser.py +0 -0
  135. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/llm/content_handler.py +0 -0
  136. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/llm/hierarchical_summarizer.py +0 -0
  137. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/serialization_helpers.py +0 -0
  138. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/time_helpers.py +0 -0
  139. {timber_common-1.2.0 → timber_common-1.2.2}/common/utils/validators.py +0 -0
  140. {timber_common-1.2.0 → timber_common-1.2.2}/data/models/00_association_tables.yaml +0 -0
  141. {timber_common-1.2.0 → timber_common-1.2.2}/data/models/messaging_models.yaml +0 -0
  142. {timber_common-1.2.0 → timber_common-1.2.2}/data/models/notification_models.yaml +0 -0
  143. {timber_common-1.2.0 → timber_common-1.2.2}/data/models/oauth_models.yaml +0 -0
  144. {timber_common-1.2.0 → timber_common-1.2.2}/data/models/user_preferences_models.yaml +0 -0
  145. {timber_common-1.2.0 → timber_common-1.2.2}/data/models/vector_db_models.yaml +0 -0
  146. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/DOCUMENTATION_INDEX.md +0 -0
  147. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/DOCUMENTATION_SUMMARY.md +0 -0
  148. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/PROGRESS_UPDATE.md +0 -0
  149. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/TIMBER_SESSION_API_REFERENCE.md +0 -0
  150. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/01_model_design_patterns.md +0 -0
  151. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/02_service_architecture.md +0 -0
  152. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/03_data_fetching_strategies.md +0 -0
  153. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/04_caching_strategies.md +0 -0
  154. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/05_error_handling.md +0 -0
  155. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/06_performance_optimization.md +0 -0
  156. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/best_practices/07_security_best_practices.md +0 -0
  157. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/design_guides/01_system_architecture.md +0 -0
  158. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/design_guides/02_config_driven_models.md +0 -0
  159. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/design_guides/03_persistence_layer.md +0 -0
  160. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/design_guides/04_vector_integration.md +0 -0
  161. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/design_guides/05_multi_app_support.md +0 -0
  162. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/01_getting_started.md +0 -0
  163. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/02_creating_models.md +0 -0
  164. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/03_using_services.md +0 -0
  165. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/04_financial_data_fetching.md +0 -0
  166. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/05_encryption_and_security.md +0 -0
  167. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/06_vector_search.md +0 -0
  168. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/07_gdpr_compliance.md +0 -0
  169. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/08_testing_guide.md +0 -0
  170. {timber_common-1.2.0 → timber_common-1.2.2}/documentation/how_to/09_typed_judgment.md +0 -0
  171. {timber_common-1.2.0 → timber_common-1.2.2}/modules/__init__.py +0 -0
  172. {timber_common-1.2.0 → timber_common-1.2.2}/modules/config/custom_analysis.yaml +0 -0
  173. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/README.md +0 -0
  174. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/__init__.py +0 -0
  175. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/bootstrap.py +0 -0
  176. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/cache.py +0 -0
  177. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/canopy_auth.py +0 -0
  178. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/client.py +0 -0
  179. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/env.py +0 -0
  180. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/models.py +0 -0
  181. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/specs.py +0 -0
  182. {timber_common-1.2.0 → timber_common-1.2.2}/oak_config/subscriber.py +0 -0
@@ -5,6 +5,35 @@ All notable changes to Timber will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.2] - 2026-10-24
9
+
10
+ ### Fixed
11
+
12
+ - Reject malformed and unknown database filters instead of dropping their predicates.
13
+ - Require non-empty filter dictionaries for updates and deletes, and preserve null
14
+ filters as SQL `IS NULL` predicates so a scoped mutation cannot become table-wide.
15
+
16
+ ## [1.2.1] - 2026-10-06
17
+
18
+ ### Fixed
19
+
20
+ - **`import common` failed on a fresh install with `No module named 'psycopg'`.**
21
+ SQLAlchemy 2.1 changed the driver a bare `postgresql://` URL selects from
22
+ psycopg2 to psycopg (v3). timber allows `sqlalchemy = "^2.0.36"`, which admits
23
+ 2.1, and depends on `psycopg2-binary` only, and `db_utils` builds an engine at
24
+ import time. So any install without a lock file (a fresh venv, a domain
25
+ package's CI, a host-side script) broke on import. Services were unaffected only
26
+ because grove's lock holds SQLAlchemy 2.0.51; the first `poetry update` that
27
+ moved it would have broken grove at boot.
28
+
29
+ Every `create_engine` call now goes through `common.utils.db_url.with_pinned_driver`,
30
+ which turns `postgresql://` and `postgres://` into `postgresql+psycopg2://`.
31
+ `config.DATABASE_URL` is NOT rewritten, because `Config.validate` and callers in
32
+ other repos compare it against `postgresql://`. An AST test fails if a new
33
+ `create_engine` call skips the pin. Verified in a clean SQLAlchemy 2.1.3 venv
34
+ with no psycopg v3 installed: `import common` succeeds and the engine's driver is
35
+ psycopg2.
36
+
8
37
  ## [1.2.0] - 2026-10-06
9
38
 
10
39
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: timber-common
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Configuration-driven persistence library with ml tools (finance related) config driven db model registration, llm model choice, automatic encryption, caching, vector search, and GDPR compliance for Python applications
5
5
  License: Apache-2.0
6
6
  Keywords: orm,sqlalchemy,persistence,vector-search,semantic-search,encryption,gdpr,yaml-config,postgres,redis,configuration-driven,data-modeling,machine-learning,embeddings
@@ -13,6 +13,7 @@ from sqlalchemy import create_engine, event, text
13
13
  from sqlalchemy.orm import declarative_base
14
14
  from sqlalchemy.orm import sessionmaker, Session, scoped_session
15
15
  from sqlalchemy.pool import QueuePool
16
+ from common.utils.db_url import with_pinned_driver
16
17
  from sqlalchemy.exc import OperationalError, DBAPIError
17
18
  from contextlib import contextmanager
18
19
  from typing import Optional, Generator
@@ -98,7 +99,7 @@ class DatabaseManager:
98
99
 
99
100
  # Create engine with connection pooling
100
101
  self._engine = create_engine(
101
- database_url,
102
+ with_pinned_driver(database_url),
102
103
  echo=echo,
103
104
  poolclass=QueuePool,
104
105
  pool_size=pool_size,
@@ -22,6 +22,7 @@ from sqlalchemy.exc import OperationalError, DBAPIError, IntegrityError
22
22
  from sqlalchemy.pool import QueuePool
23
23
 
24
24
  from common.utils.config import config
25
+ from common.utils.db_url import with_pinned_driver
25
26
 
26
27
  logger = logging.getLogger(__name__)
27
28
 
@@ -81,7 +82,7 @@ class DBService:
81
82
  try:
82
83
  # Create engine with connection pooling
83
84
  self._engine = create_engine(
84
- db_url,
85
+ with_pinned_driver(db_url),
85
86
  echo=config.DATABASE_ECHO,
86
87
  poolclass=QueuePool,
87
88
  pool_size=pool_config['pool_size'],
@@ -322,29 +323,18 @@ class DBService:
322
323
 
323
324
  def _apply_filters(self, query, model_class: Type, filters: Dict[str, Any]):
324
325
  """Apply filters to query."""
325
- # Handle None or empty filters
326
- if not filters:
327
- return query
328
-
329
- # Handle string filters (invalid input - return unfiltered)
330
- if isinstance(filters, str):
331
- logger.warning(f"Received string filters instead of dict: {filters}")
332
- return query
333
-
334
- # Handle non-dict filters
326
+ if filters is None:
327
+ filters = {}
335
328
  if not isinstance(filters, dict):
336
- logger.warning(f"Received non-dict filters: {type(filters)}")
337
- return query
338
-
339
- # Apply filters
329
+ raise ValueError("Filters must be a dict")
330
+ unknown = [field for field in filters if not hasattr(model_class, field)]
331
+ if unknown:
332
+ raise ValueError(f"Unknown filter fields: {', '.join(map(str, unknown))}")
340
333
  for field, value in filters.items():
341
- if value is not None: # Skip None values
342
- if hasattr(model_class, field):
343
- query = query.filter(getattr(model_class, field) == value)
344
- else:
345
- logger.warning(f"Model {model_class.__name__} has no field '{field}'")
334
+ # SQLAlchemy translates None to IS NULL; dropping it broadens scope.
335
+ query = query.filter(getattr(model_class, field) == value)
346
336
  return query
347
-
337
+
348
338
  def _apply_ordering(self, query, model_class: Type, order_by):
349
339
  """Apply ordering to a query.
350
340
 
@@ -461,8 +451,7 @@ class DBService:
461
451
  logger.warning(f"Filters passed as string, using empty filters. Got: {filters}")
462
452
  filters = {}
463
453
  elif not isinstance(filters, dict):
464
- logger.warning(f"Filters not a dict (type: {type(filters)}), using empty filters")
465
- filters = {}
454
+ raise ValueError("Filters must be a dict")
466
455
 
467
456
  if include_relationships is None:
468
457
  include_relationships = []
@@ -593,6 +582,9 @@ class DBService:
593
582
  data={'status': 'completed'}
594
583
  )
595
584
  """
585
+ if not isinstance(filters, dict) or not filters:
586
+ raise ValueError("Mutations require a non-empty filter dict")
587
+
596
588
  model_class = self._get_model_class(model)
597
589
 
598
590
  with self.session_scope() as session:
@@ -626,6 +618,9 @@ class DBService:
626
618
  filters={'id': 'goal-123'}
627
619
  )
628
620
  """
621
+ if not isinstance(filters, dict) or not filters:
622
+ raise ValueError("Mutations require a non-empty filter dict")
623
+
629
624
  model_class = self._get_model_class(model)
630
625
 
631
626
  with self.session_scope() as session:
@@ -0,0 +1,41 @@
1
+ # timber/common/utils/db_url.py
2
+ """Name the Postgres driver in every URL handed to ``create_engine``.
3
+
4
+ ⚠️⚠️ SQLALCHEMY 2.1 CHANGED WHAT ``postgresql://`` MEANS. Under 2.0 a bare
5
+ ``postgresql://`` URL selects psycopg2; under 2.1 it selects psycopg (v3).
6
+ timber declares ``sqlalchemy = "^2.0.36"``, which admits 2.1, and depends on
7
+ ``psycopg2-binary`` only. So any install without a lock file (a fresh venv, a
8
+ domain package's CI, a host-side script) got SQLAlchemy 2.1.3 and failed on
9
+ ``import common`` with ``No module named 'psycopg'``, because ``db_utils``
10
+ builds an engine at import time. Measured 2026-10-06 in a clean venv. The
11
+ services were unaffected only because grove's lock holds 2.0.51, so the first
12
+ ``poetry update`` that moved SQLAlchemy would have broken grove at boot.
13
+
14
+ The fix is to say which driver we mean, at the boundary where the URL becomes
15
+ an engine, so the URL and the declared dependency can never disagree again.
16
+ ``config.DATABASE_URL`` itself is NOT rewritten: ``Config.validate`` and callers
17
+ in other repos compare it against ``postgresql://``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ __all__ = ["PG_DRIVER", "with_pinned_driver"]
23
+
24
+ #: The driver timber declares in pyproject. Change both together or neither.
25
+ PG_DRIVER = "psycopg2"
26
+
27
+ _BARE_SCHEMES = ("postgresql://", "postgres://")
28
+
29
+
30
+ def with_pinned_driver(url: str) -> str:
31
+ """``postgresql://…`` to ``postgresql+psycopg2://…``; anything else unchanged.
32
+
33
+ A URL that already names a driver (``postgresql+psycopg://``) is the
34
+ caller's explicit choice and is left alone. So is SQLite.
35
+ """
36
+ if not url:
37
+ return url
38
+ for scheme in _BARE_SCHEMES:
39
+ if url.startswith(scheme):
40
+ return f"postgresql+{PG_DRIVER}://" + url[len(scheme):]
41
+ return url
@@ -8,6 +8,7 @@ from sqlalchemy.orm import sessionmaker, Session, DeclarativeBase
8
8
  from sqlalchemy.pool import StaticPool
9
9
 
10
10
  from common.utils.config import config
11
+ from common.utils.db_url import with_pinned_driver
11
12
 
12
13
 
13
14
  class Base(DeclarativeBase):
@@ -41,7 +42,7 @@ class DatabaseManager:
41
42
  if ":memory:" in self.database_url:
42
43
  engine_kwargs["poolclass"] = StaticPool
43
44
 
44
- self.engine = create_engine(self.database_url, **engine_kwargs)
45
+ self.engine = create_engine(with_pinned_driver(self.database_url), **engine_kwargs)
45
46
  self.SessionLocal = sessionmaker(
46
47
  autocommit=False,
47
48
  autoflush=False,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "timber-common"
3
- version = "1.2.0"
3
+ version = "1.2.2"
4
4
  description = "Configuration-driven persistence library with ml tools (finance related) config driven db model registration, llm model choice, automatic encryption, caching, vector search, and GDPR compliance for Python applications"
5
5
  authors = ["Pumulo Sikaneta <pumulo@gmail.com>"]
6
6
  maintainers = ["Pumulo Sikaneta <pumulo@gmail.com>"]
File without changes
File without changes