boti-data 1.3.1__tar.gz → 1.3.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 (118) hide show
  1. {boti_data-1.3.1 → boti_data-1.3.2}/PKG-INFO +1 -1
  2. {boti_data-1.3.1 → boti_data-1.3.2}/pyproject.toml +1 -1
  3. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_config.py +16 -2
  4. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_model_registry.py +63 -48
  5. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data.egg-info/PKG-INFO +1 -1
  6. {boti_data-1.3.1 → boti_data-1.3.2}/README.md +0 -0
  7. {boti_data-1.3.1 → boti_data-1.3.2}/setup.cfg +0 -0
  8. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/__init__.py +0 -0
  9. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/arrow_schema.py +0 -0
  10. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/connection_catalog.py +0 -0
  11. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/datacube/__init__.py +0 -0
  12. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/datacube/artifact.py +0 -0
  13. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/datacube/base.py +0 -0
  14. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/datacube/contract.py +0 -0
  15. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/datacube/resource.py +0 -0
  16. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/dataset/__init__.py +0 -0
  17. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/dataset/hybrid.py +0 -0
  18. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/dataset/hybrid_support.py +0 -0
  19. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/__init__.py +0 -0
  20. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/arrow_schema_mapper.py +0 -0
  21. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/engine_registry.py +0 -0
  22. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_execution.py +0 -0
  23. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_loader.py +0 -0
  24. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_planner.py +0 -0
  25. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_planner_async.py +0 -0
  26. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_planning_math.py +0 -0
  27. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_row_materialization.py +0 -0
  28. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_statements.py +0 -0
  29. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/partitioned_types.py +0 -0
  30. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_engine.py +0 -0
  31. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_manager.py +0 -0
  32. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_model_builder.py +0 -0
  33. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_model_module_injection.py +0 -0
  34. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_model_registry_types.py +0 -0
  35. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_readonly.py +0 -0
  36. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_repository.py +0 -0
  37. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sql_resource.py +0 -0
  38. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/sqlalchemy_async.py +0 -0
  39. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/db/vector_search.py +0 -0
  40. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/enrichment/__init__.py +0 -0
  41. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/enrichment/async_enricher.py +0 -0
  42. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/enrichment/composite.py +0 -0
  43. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/enrichment/protocol.py +0 -0
  44. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/enrichment/specs.py +0 -0
  45. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/enrichment/transformers.py +0 -0
  46. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/field_map.py +0 -0
  47. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/__init__.py +0 -0
  48. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/arrow_kernels.py +0 -0
  49. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/arrow_kernels_comparison.py +0 -0
  50. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/arrow_kernels_compile.py +0 -0
  51. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/arrow_kernels_string.py +0 -0
  52. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/backend_ops.py +0 -0
  53. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/expressions.py +0 -0
  54. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/handler.py +0 -0
  55. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/parquet_compiler.py +0 -0
  56. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/utils.py +0 -0
  57. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/filters/value_parsing.py +0 -0
  58. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/__init__.py +0 -0
  59. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/_backend_strategies.py +0 -0
  60. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/_factory.py +0 -0
  61. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/_gateway_init.py +0 -0
  62. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/_payloads.py +0 -0
  63. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/_series_filters.py +0 -0
  64. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/arrow_adapters.py +0 -0
  65. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/build_context.py +0 -0
  66. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/chunking.py +0 -0
  67. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/configured_load.py +0 -0
  68. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/core.py +0 -0
  69. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/core_load.py +0 -0
  70. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/datacube_strategy.py +0 -0
  71. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/frame_convert.py +0 -0
  72. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/frame_strategies.py +0 -0
  73. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/incremental.py +0 -0
  74. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/load_execution.py +0 -0
  75. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/load_request.py +0 -0
  76. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/loaders.py +0 -0
  77. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/normalization.py +0 -0
  78. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/parquet_strategy.py +0 -0
  79. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/planning.py +0 -0
  80. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/policies.py +0 -0
  81. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/post_process.py +0 -0
  82. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/requests.py +0 -0
  83. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/return_type.py +0 -0
  84. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/select_cache.py +0 -0
  85. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/semi_join.py +0 -0
  86. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/sql_guard.py +0 -0
  87. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/sql_partitioned_exec.py +0 -0
  88. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/sql_size_estimation.py +0 -0
  89. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/sql_strategy.py +0 -0
  90. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/gateway/sql_strategy_configured_async.py +0 -0
  91. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/helper.py +0 -0
  92. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/joins.py +0 -0
  93. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/__init__.py +0 -0
  94. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/config.py +0 -0
  95. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/discovery.py +0 -0
  96. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/file_info.py +0 -0
  97. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/filesystem.py +0 -0
  98. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/reader.py +0 -0
  99. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/reader_config.py +0 -0
  100. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/resource.py +0 -0
  101. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/parquet/schema_filters.py +0 -0
  102. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/__init__.py +0 -0
  103. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/base.py +0 -0
  104. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/parquet_pipeline.py +0 -0
  105. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/registry.py +0 -0
  106. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/sinks.py +0 -0
  107. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/sinks_common.py +0 -0
  108. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/sinks_parquet.py +0 -0
  109. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/pipelines/sinks_text.py +0 -0
  110. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/py.typed +0 -0
  111. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/schema.py +0 -0
  112. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/watermark/__init__.py +0 -0
  113. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/watermark/incremental.py +0 -0
  114. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data/watermark/store.py +0 -0
  115. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data.egg-info/SOURCES.txt +0 -0
  116. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data.egg-info/dependency_links.txt +0 -0
  117. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data.egg-info/requires.txt +0 -0
  118. {boti_data-1.3.1 → boti_data-1.3.2}/src/boti_data.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boti-data
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: Data infrastructure for the Boti ecosystem
5
5
  Author: Luis Valverde
6
6
  Maintainer-email: Luis Valverde <lvalverdeb@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boti-data"
3
- version = "1.3.1"
3
+ version = "1.3.2"
4
4
  description = "Data infrastructure for the Boti ecosystem"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -18,7 +18,19 @@ _ALLOWED_POOLCLASS_IMPORTS = {
18
18
 
19
19
 
20
20
  class SqlDatabaseConfig(ResourceConfig):
21
- """Immutable configuration for SQLAlchemy engine initialization."""
21
+ """Immutable configuration for SQLAlchemy engine initialization.
22
+
23
+ ``query_only=True`` enforces read-only access at the database level (via
24
+ a ``SET ... READ ONLY``-style statement issued on the SQLAlchemy
25
+ ``"connect"`` event, not just by blocking ORM methods in Python) — this
26
+ assumes ``connection_url`` points directly at Postgres/MySQL, or at a
27
+ *session*-pooling-mode connection pooler (one physical backend connection
28
+ dedicated to one logical client for its whole lifetime). It does **not**
29
+ cover a *transaction*-pooling-mode pooler (e.g. PgBouncer in ``transaction``
30
+ mode): there, a single physical connection can be handed to a different,
31
+ unrelated logical client between transactions, and session-level state set
32
+ once at physical-connect time is not guaranteed to survive that handoff.
33
+ """
22
34
 
23
35
  model_config = ConfigDict(arbitrary_types_allowed=True)
24
36
 
@@ -27,7 +39,9 @@ class SqlDatabaseConfig(ResourceConfig):
27
39
  default=True,
28
40
  description=(
29
41
  "When true, native database-enforced read-only access is required. "
30
- "Set explicitly to false to allow writes."
42
+ "Set explicitly to false to allow writes. Assumes a direct connection "
43
+ "or a session-pooling-mode proxy — transaction-pooling-mode poolers "
44
+ "(e.g. PgBouncer 'transaction' mode) are out of scope; see class docstring."
31
45
  ),
32
46
  )
33
47
  worker_connection_env_var: str | None = Field(
@@ -238,22 +238,34 @@ class SqlModelRegistry:
238
238
  md = self._get_or_create_metadata(ekey, schema)
239
239
  md_lock = self._get_or_create_md_lock(md_key)
240
240
  qname = self._qualified_key(schema, tname)
241
- tbl = self._reflect_table(engine, md, md_lock, qname=qname, tname=tname, schema=schema)
242
-
243
- context = ModelBuildContext(
244
- qname=qname,
245
- tname=tname,
246
- base_class=base_class,
247
- module_label=module_label,
248
- prefer_stable_names=prefer_stable_names,
249
- ekey=ekey,
250
- schema=schema,
251
- missing_pk_warning_suffix="Ensure ORM mutation sequences validate bounds manually.",
252
- )
253
- model_cls = self._resolve_or_build_model(tbl, qname, context, target_label="DB target")
254
- with self._lock:
255
- self._model_cache[model_key] = model_cls
256
- return model_cls
241
+
242
+ # The whole reflect -> resolve-or-build -> cache-write sequence must
243
+ # be one atomic unit per md_key: reflection alone was already
244
+ # serialized via md_lock, but resolve-or-build and the cache write
245
+ # previously ran outside it, so two concurrent first-accesses for the
246
+ # same table could both pass "does a mapped class already exist?"
247
+ # before either finished building — each got a distinct Python class
248
+ # mapped to the same Table.
249
+ with md_lock:
250
+ cached = self._check_cache(model_key, refresh=False)
251
+ if cached is not None:
252
+ return cached
253
+
254
+ tbl = self._reflect_table(engine, md, qname=qname, tname=tname, schema=schema)
255
+ context = ModelBuildContext(
256
+ qname=qname,
257
+ tname=tname,
258
+ base_class=base_class,
259
+ module_label=module_label,
260
+ prefer_stable_names=prefer_stable_names,
261
+ ekey=ekey,
262
+ schema=schema,
263
+ missing_pk_warning_suffix="Ensure ORM mutation sequences validate bounds manually.",
264
+ )
265
+ model_cls = self._resolve_or_build_model(tbl, qname, context, target_label="DB target")
266
+ with self._lock:
267
+ self._model_cache[model_key] = model_cls
268
+ return model_cls
257
269
 
258
270
  async def get_model_async(
259
271
  self,
@@ -278,26 +290,33 @@ class SqlModelRegistry:
278
290
  md = self._get_or_create_metadata(ekey, schema)
279
291
  md_lock = await self._get_or_create_md_lock_async(md_key)
280
292
  qname = self._qualified_key(schema, tname)
281
- tbl = await self._reflect_table_async(
282
- engine, md, md_lock, qname=qname, tname=tname, schema=schema
283
- )
284
293
 
285
- context = ModelBuildContext(
286
- qname=qname,
287
- tname=tname,
288
- base_class=base_class,
289
- module_label=module_label,
290
- prefer_stable_names=prefer_stable_names,
291
- ekey=ekey,
292
- schema=schema,
293
- missing_pk_warning_suffix="Ensure Async ORM validation sequences capture bounds correctly.",
294
- )
295
- model_cls = self._resolve_or_build_model(
296
- tbl, qname, context, target_label="Async DB target"
297
- )
298
- async with self._async_lock:
299
- self._model_cache[model_key] = model_cls
300
- return model_cls
294
+ # See get_model()'s comment: the whole sequence must be one atomic
295
+ # unit per md_key, not just the reflection step.
296
+ async with md_lock:
297
+ cached = await self._check_cache_async(model_key, refresh=False)
298
+ if cached is not None:
299
+ return cached
300
+
301
+ tbl = await self._reflect_table_async(
302
+ engine, md, qname=qname, tname=tname, schema=schema
303
+ )
304
+ context = ModelBuildContext(
305
+ qname=qname,
306
+ tname=tname,
307
+ base_class=base_class,
308
+ module_label=module_label,
309
+ prefer_stable_names=prefer_stable_names,
310
+ ekey=ekey,
311
+ schema=schema,
312
+ missing_pk_warning_suffix="Ensure Async ORM validation sequences capture bounds correctly.",
313
+ )
314
+ model_cls = self._resolve_or_build_model(
315
+ tbl, qname, context, target_label="Async DB target"
316
+ )
317
+ async with self._async_lock:
318
+ self._model_cache[model_key] = model_cls
319
+ return model_cls
301
320
 
302
321
  def _resolve_or_build_model(
303
322
  self,
@@ -335,12 +354,15 @@ class SqlModelRegistry:
335
354
 
336
355
  # Not a copy-pasted twin: sync md.reflect() vs conn.run_sync(md.reflect) is
337
356
  # a genuine concurrency-primitive difference (see get_model()'s comment above).
357
+ # Both callers (get_model/get_model_async) already hold this md_key's
358
+ # md_lock/md_async_lock for the whole reflect -> resolve-or-build -> cache
359
+ # sequence, so no locking happens here — acquiring the same (non-reentrant)
360
+ # lock again inside would deadlock.
338
361
  @staticmethod
339
362
  # spaghetti-ignore[sync-async-duplication]: see above
340
363
  def _reflect_table(
341
364
  engine: Engine,
342
365
  md: MetaData,
343
- md_lock: threading.Lock,
344
366
  *,
345
367
  qname: str,
346
368
  tname: str,
@@ -348,18 +370,14 @@ class SqlModelRegistry:
348
370
  ) -> Table | None:
349
371
  tbl = md.tables.get(qname)
350
372
  if tbl is None:
351
- with md_lock:
352
- tbl = md.tables.get(qname)
353
- if tbl is None:
354
- md.reflect(bind=engine, only=[tname], schema=schema)
355
- tbl = md.tables.get(qname)
373
+ md.reflect(bind=engine, only=[tname], schema=schema)
374
+ tbl = md.tables.get(qname)
356
375
  return tbl
357
376
 
358
377
  @staticmethod
359
378
  async def _reflect_table_async(
360
379
  engine: AsyncEngine,
361
380
  md: MetaData,
362
- md_lock: asyncio.Lock,
363
381
  *,
364
382
  qname: str,
365
383
  tname: str,
@@ -367,12 +385,9 @@ class SqlModelRegistry:
367
385
  ) -> Table | None:
368
386
  tbl = md.tables.get(qname)
369
387
  if tbl is None:
370
- async with md_lock:
371
- tbl = md.tables.get(qname)
372
- if tbl is None:
373
- async with engine.begin() as conn:
374
- await conn.run_sync(md.reflect, only=[tname], schema=schema)
375
- tbl = md.tables.get(qname)
388
+ async with engine.begin() as conn:
389
+ await conn.run_sync(md.reflect, only=[tname], schema=schema)
390
+ tbl = md.tables.get(qname)
376
391
  return tbl
377
392
 
378
393
  def clear(self) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boti-data
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: Data infrastructure for the Boti ecosystem
5
5
  Author: Luis Valverde
6
6
  Maintainer-email: Luis Valverde <lvalverdeb@gmail.com>
File without changes
File without changes