boti-data 1.0.3__tar.gz → 1.2.0__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 (131) hide show
  1. {boti_data-1.0.3/src/boti_data.egg-info → boti_data-1.2.0}/PKG-INFO +94 -11
  2. boti_data-1.0.3/PKG-INFO → boti_data-1.2.0/README.md +78 -30
  3. boti_data-1.2.0/pyproject.toml +141 -0
  4. boti_data-1.2.0/src/boti_data/__init__.py +177 -0
  5. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/arrow_schema.py +12 -6
  6. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/connection_catalog.py +5 -4
  7. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/__init__.py +0 -1
  8. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/artifact.py +19 -21
  9. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/base.py +21 -27
  10. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/contract.py +19 -1
  11. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/resource.py +1 -4
  12. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/dataset/__init__.py +0 -1
  13. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/dataset/hybrid.py +64 -152
  14. boti_data-1.2.0/src/boti_data/dataset/hybrid_support.py +133 -0
  15. boti_data-1.2.0/src/boti_data/db/__init__.py +65 -0
  16. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/arrow_schema_mapper.py +42 -35
  17. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/engine_registry.py +42 -17
  18. boti_data-1.2.0/src/boti_data/db/partitioned_execution.py +310 -0
  19. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/partitioned_loader.py +61 -36
  20. boti_data-1.2.0/src/boti_data/db/partitioned_planner.py +383 -0
  21. boti_data-1.2.0/src/boti_data/db/partitioned_planner_async.py +110 -0
  22. boti_data-1.2.0/src/boti_data/db/partitioned_planning_math.py +100 -0
  23. boti_data-1.2.0/src/boti_data/db/partitioned_row_materialization.py +168 -0
  24. boti_data-1.2.0/src/boti_data/db/partitioned_statements.py +82 -0
  25. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/partitioned_types.py +50 -3
  26. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_config.py +1 -3
  27. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_engine.py +16 -14
  28. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_model_builder.py +21 -12
  29. boti_data-1.2.0/src/boti_data/db/sql_model_module_injection.py +51 -0
  30. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_model_registry.py +139 -155
  31. boti_data-1.2.0/src/boti_data/db/sql_model_registry_types.py +51 -0
  32. boti_data-1.2.0/src/boti_data/db/sql_repository.py +186 -0
  33. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_resource.py +3 -1
  34. boti_data-1.2.0/src/boti_data/db/vector_search.py +71 -0
  35. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/async_enricher.py +37 -21
  36. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/protocol.py +1 -3
  37. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/specs.py +3 -1
  38. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/transformers.py +3 -1
  39. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/field_map.py +2 -2
  40. boti_data-1.2.0/src/boti_data/filters/arrow_kernels.py +90 -0
  41. boti_data-1.2.0/src/boti_data/filters/arrow_kernels_comparison.py +82 -0
  42. boti_data-1.2.0/src/boti_data/filters/arrow_kernels_compile.py +217 -0
  43. boti_data-1.2.0/src/boti_data/filters/arrow_kernels_string.py +92 -0
  44. boti_data-1.2.0/src/boti_data/filters/backend_ops.py +228 -0
  45. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/expressions.py +29 -23
  46. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/handler.py +34 -32
  47. boti_data-1.2.0/src/boti_data/filters/utils.py +67 -0
  48. boti_data-1.2.0/src/boti_data/filters/value_parsing.py +314 -0
  49. boti_data-1.2.0/src/boti_data/gateway/_backend_strategies.py +309 -0
  50. boti_data-1.2.0/src/boti_data/gateway/_gateway_init.py +221 -0
  51. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/_series_filters.py +14 -10
  52. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/arrow_adapters.py +88 -77
  53. boti_data-1.2.0/src/boti_data/gateway/build_context.py +70 -0
  54. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/chunking.py +44 -31
  55. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/configured_load.py +20 -33
  56. boti_data-1.2.0/src/boti_data/gateway/core.py +400 -0
  57. boti_data-1.2.0/src/boti_data/gateway/core_load.py +199 -0
  58. boti_data-1.2.0/src/boti_data/gateway/datacube_strategy.py +112 -0
  59. boti_data-1.2.0/src/boti_data/gateway/frame_convert.py +110 -0
  60. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/frame_strategies.py +44 -98
  61. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/incremental.py +5 -0
  62. boti_data-1.2.0/src/boti_data/gateway/load_execution.py +202 -0
  63. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/loaders.py +41 -22
  64. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/normalization.py +8 -5
  65. boti_data-1.2.0/src/boti_data/gateway/parquet_strategy.py +170 -0
  66. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/planning.py +101 -33
  67. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/post_process.py +44 -54
  68. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/return_type.py +75 -69
  69. boti_data-1.2.0/src/boti_data/gateway/select_cache.py +76 -0
  70. boti_data-1.2.0/src/boti_data/gateway/semi_join.py +97 -0
  71. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/sql_guard.py +2 -6
  72. boti_data-1.2.0/src/boti_data/gateway/sql_partitioned_exec.py +364 -0
  73. boti_data-1.2.0/src/boti_data/gateway/sql_size_estimation.py +170 -0
  74. boti_data-1.2.0/src/boti_data/gateway/sql_strategy.py +378 -0
  75. boti_data-1.2.0/src/boti_data/gateway/sql_strategy_configured_async.py +97 -0
  76. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/helper.py +62 -48
  77. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/joins.py +124 -48
  78. boti_data-1.2.0/src/boti_data/parquet/__init__.py +28 -0
  79. boti_data-1.2.0/src/boti_data/parquet/config.py +77 -0
  80. boti_data-1.2.0/src/boti_data/parquet/discovery.py +163 -0
  81. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/parquet/file_info.py +5 -14
  82. boti_data-1.2.0/src/boti_data/parquet/filesystem.py +120 -0
  83. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/parquet/reader.py +9 -5
  84. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/parquet/reader_config.py +5 -4
  85. boti_data-1.2.0/src/boti_data/parquet/resource.py +341 -0
  86. boti_data-1.2.0/src/boti_data/parquet/schema_filters.py +151 -0
  87. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/pipelines/__init__.py +2 -1
  88. boti_data-1.2.0/src/boti_data/pipelines/base.py +208 -0
  89. boti_data-1.2.0/src/boti_data/pipelines/parquet_pipeline.py +312 -0
  90. boti_data-1.2.0/src/boti_data/pipelines/sinks.py +34 -0
  91. boti_data-1.2.0/src/boti_data/pipelines/sinks_common.py +209 -0
  92. boti_data-1.2.0/src/boti_data/pipelines/sinks_parquet.py +127 -0
  93. boti_data-1.2.0/src/boti_data/pipelines/sinks_text.py +336 -0
  94. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/schema.py +38 -33
  95. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/watermark/incremental.py +22 -19
  96. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/watermark/store.py +7 -2
  97. boti_data-1.0.3/README.md → boti_data-1.2.0/src/boti_data.egg-info/PKG-INFO +113 -0
  98. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data.egg-info/SOURCES.txt +35 -0
  99. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data.egg-info/requires.txt +13 -2
  100. boti_data-1.0.3/pyproject.toml +0 -96
  101. boti_data-1.0.3/src/boti_data/__init__.py +0 -142
  102. boti_data-1.0.3/src/boti_data/db/__init__.py +0 -38
  103. boti_data-1.0.3/src/boti_data/db/partitioned_execution.py +0 -415
  104. boti_data-1.0.3/src/boti_data/db/partitioned_planner.py +0 -633
  105. boti_data-1.0.3/src/boti_data/filters/arrow_kernels.py +0 -351
  106. boti_data-1.0.3/src/boti_data/filters/utils.py +0 -482
  107. boti_data-1.0.3/src/boti_data/gateway/_backend_strategies.py +0 -1342
  108. boti_data-1.0.3/src/boti_data/gateway/core.py +0 -880
  109. boti_data-1.0.3/src/boti_data/parquet/__init__.py +0 -8
  110. boti_data-1.0.3/src/boti_data/parquet/resource.py +0 -736
  111. boti_data-1.0.3/src/boti_data/pipelines/base.py +0 -519
  112. boti_data-1.0.3/src/boti_data/pipelines/sinks.py +0 -609
  113. {boti_data-1.0.3 → boti_data-1.2.0}/setup.cfg +0 -0
  114. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_manager.py +0 -0
  115. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_readonly.py +0 -0
  116. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sqlalchemy_async.py +0 -0
  117. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/__init__.py +0 -0
  118. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/composite.py +0 -0
  119. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/__init__.py +0 -0
  120. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/parquet_compiler.py +0 -0
  121. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/__init__.py +0 -0
  122. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/_factory.py +0 -0
  123. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/_payloads.py +0 -0
  124. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/load_request.py +0 -0
  125. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/policies.py +0 -0
  126. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/requests.py +0 -0
  127. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/pipelines/registry.py +0 -0
  128. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/py.typed +0 -0
  129. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/watermark/__init__.py +0 -0
  130. {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data.egg-info/dependency_links.txt +0 -0
  131. {boti_data-1.0.3 → boti_data-1.2.0}/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.0.3
3
+ Version: 1.2.0
4
4
  Summary: Data infrastructure for the Boti ecosystem
5
5
  Author: Luis Valverde
6
6
  Maintainer-email: Luis Valverde <lvalverdeb@gmail.com>
@@ -12,21 +12,26 @@ Project-URL: Issues, https://github.com/lvalverdeb/boti-data/issues
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3 :: Only
19
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Requires-Python: >=3.10
18
+ Requires-Python: >=3.13
21
19
  Description-Content-Type: text/markdown
22
- Requires-Dist: asyncmy<0.3.0,>=0.2.11
23
20
  Requires-Dist: boti<2.0.0,>=1.0.2
24
- Requires-Dist: boti-dask<2.0.0,>=1.0.0
25
- Requires-Dist: dask[dataframe,distributed]<2027.0.0,>=2026.3.0
26
- Requires-Dist: pandas<4.0.0,>=3.0.2
27
- Requires-Dist: polars<2.0.0,>=1.29.0
28
- Requires-Dist: pymysql<2.0.0,>=1.1.2
29
21
  Requires-Dist: sqlalchemy[asyncio]<3.0.0,>=2.0.49
22
+ Provides-Extra: mysql
23
+ Requires-Dist: asyncmy<0.3.0,>=0.2.11; extra == "mysql"
24
+ Requires-Dist: pymysql<2.0.0,>=1.1.2; extra == "mysql"
25
+ Provides-Extra: postgres
26
+ Requires-Dist: asyncpg<0.32.0,>=0.30.0; extra == "postgres"
27
+ Requires-Dist: psycopg[binary]<4.0.0,>=3.2.0; extra == "postgres"
28
+ Provides-Extra: pgvector
29
+ Requires-Dist: pgvector<0.6.0,>=0.3.0; extra == "pgvector"
30
+ Provides-Extra: dataframes
31
+ Requires-Dist: boti-dask<2.0.0,>=1.0.0; extra == "dataframes"
32
+ Requires-Dist: dask[dataframe,distributed]<2027.0.0,>=2026.3.0; extra == "dataframes"
33
+ Requires-Dist: pandas<4.0.0,>=3.0.2; extra == "dataframes"
34
+ Requires-Dist: polars<2.0.0,>=1.29.0; extra == "dataframes"
30
35
 
31
36
  # boti-data
32
37
 
@@ -121,6 +126,27 @@ Or install through the core package extra:
121
126
  pip install "boti[data]"
122
127
  ```
123
128
 
129
+ `boti-data` doesn't bundle a database driver by default — SQLAlchemy resolves the driver package from your connection URL's scheme (`mysql+pymysql://`, `postgresql+asyncpg://`, ...), so install the one matching your database:
130
+
131
+ ```bash
132
+ pip install "boti-data[mysql]" # asyncmy + pymysql
133
+ pip install "boti-data[postgres]" # asyncpg + psycopg[binary]
134
+ ```
135
+
136
+ If a Postgres table has a [pgvector](https://github.com/pgvector/pgvector) `vector` column, install `boti-data[pgvector]` too — this registers pgvector's `Vector` type with SQLAlchemy's reflection so `SqlAlchemyModelBuilder`/`SqlRepository`/`DataGateway` map that column to a usable `list[float]` (with its dimension preserved) instead of an unreflectable `NullType`:
137
+
138
+ ```bash
139
+ pip install "boti-data[pgvector]"
140
+ ```
141
+
142
+ The base install is deliberately light: single-row transactional access (`SqlRepository`/`AsyncSqlRepository`, see below) works out of the box, with no dask/pandas/polars pulled in. The dataframe-shaped, bulk-oriented side of the package (`DataGateway`/`DataHelper`, `ParquetPipeline`, `HybridDataset`, the sink pipelines, ...) needs the `dataframes` extra:
143
+
144
+ ```bash
145
+ pip install "boti-data[dataframes]" # dask + pandas + polars + boti-dask
146
+ ```
147
+
148
+ Every name importable from `boti_data` is loaded lazily on first access, so `import boti_data` alone never touches dask/pandas/polars — only accessing a dataframe-shaped name does.
149
+
124
150
  ## Imports
125
151
 
126
152
  `boti-data` uses the top-level Python package `boti_data`. Dask runtime/session/resilience helpers live in `boti_dask` and are not re-exported here.
@@ -148,9 +174,11 @@ from boti_data import (
148
174
  available_sinks,
149
175
  create_sink,
150
176
  register_sink,
177
+ AsyncSqlRepository,
151
178
  SqlAlchemyModelBuilder,
152
179
  SqlDatabaseConfig,
153
180
  SqlDatabaseResource,
181
+ SqlRepository,
154
182
  )
155
183
  ```
156
184
 
@@ -857,6 +885,61 @@ Use the following as a guide:
857
885
 
858
886
  ## Examples
859
887
 
888
+ ### Repository (lightest-weight, no dataframe dependencies)
889
+
890
+ For single-row transactional access — get/insert/update/delete by primary key — without pulling in dask/pandas/polars at all. Rows come back as plain `dict[str, Any]`, never a DataFrame.
891
+
892
+ `query_only` defaults to `True` on `SqlRepository`/`AsyncSqlRepository` themselves and always wins over whatever the passed-in `config.query_only` says — insert/update/delete raise until you opt in explicitly with `query_only=False`. This is deliberate: a `config` object might be shared with or reused from a context where writes are already enabled, and a repository shouldn't silently inherit write access nobody asked it for.
893
+
894
+ ```python
895
+ from boti_data import SqlDatabaseConfig, SqlRepository
896
+
897
+ config = SqlDatabaseConfig(connection_url="sqlite:///example.db")
898
+
899
+ with SqlRepository(config, "users", query_only=False) as users:
900
+ row = users.insert({"id": 1, "name": "Ada"})
901
+ row = users.get(1) # -> dict | None
902
+ row = users.update(1, {"name": "Grace"})
903
+ deleted = users.delete(1) # -> bool
904
+ ```
905
+
906
+ Omit `query_only=False` (or read-only access is all you need) and writes raise instead of silently succeeding:
907
+
908
+ ```python
909
+ with SqlRepository(config, "users") as users:
910
+ users.get(1) # fine
911
+ users.insert({"id": 2, "name": "Bob"}) # raises SQLAlchemyError
912
+ ```
913
+
914
+ An async counterpart, `AsyncSqlRepository`, mirrors the same four methods as coroutines and is used as an async context manager:
915
+
916
+ ```python
917
+ from boti_data import AsyncSqlRepository
918
+
919
+ async with AsyncSqlRepository(config, "users", query_only=False) as users:
920
+ row = await users.get(1)
921
+ ```
922
+
923
+ ### Nearest-neighbour search (pgvector)
924
+
925
+ `boti_data.db.nearest_neighbors`/`vector_distance` build an `ORDER BY <distance> LIMIT k` statement against a pgvector column, with the query vector bound as a driver parameter — never string-interpolated into the SQL, regardless of how large the vector is. Requires `boti-data[pgvector]` (see [Installation](#installation)) and a table reflected via `SqlAlchemyModelBuilder`/`SqlRepository`/`DataGateway`.
926
+
927
+ ```python
928
+ from boti_data import DataGateway, SqlDatabaseConfig
929
+ from boti_data.db import SqlAlchemyModelBuilder, nearest_neighbors
930
+
931
+ config = SqlDatabaseConfig(connection_url="postgresql+psycopg://user:pass@host/mydb", query_only=True)
932
+ gateway = DataGateway(config)
933
+
934
+ model = SqlAlchemyModelBuilder(gateway.resource.engine, "embeddings").build_model()
935
+ query_vector = [...] # e.g. a 512-float face embedding
936
+
937
+ stmt = nearest_neighbors(model, model.vector, query_vector, k=10, metric="cosine")
938
+ matches = gateway.load(statement=stmt, model=model, as_pandas=True)
939
+ ```
940
+
941
+ Supported `metric` values map directly to pgvector's operators: `"cosine"` (`<=>`), `"l2"` (`<->`), `"inner_product"` (`<#>`), `"l1"` (`<+>`). Add ordinary filtering with `.where(...)` on the statement before passing it to `gateway.load(...)`.
942
+
860
943
  ### SQL resource (low-level)
861
944
 
862
945
  ```python
@@ -1,33 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: boti-data
3
- Version: 1.0.3
4
- Summary: Data infrastructure for the Boti ecosystem
5
- Author: Luis Valverde
6
- Maintainer-email: Luis Valverde <lvalverdeb@gmail.com>
7
- License-Expression: MIT
8
- Project-URL: Homepage, https://github.com/lvalverdeb/boti-data
9
- Project-URL: Repository, https://github.com/lvalverdeb/boti-data
10
- Project-URL: Documentation, https://github.com/lvalverdeb/boti-data#readme
11
- Project-URL: Issues, https://github.com/lvalverdeb/boti-data/issues
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Requires-Python: >=3.10
21
- Description-Content-Type: text/markdown
22
- Requires-Dist: asyncmy<0.3.0,>=0.2.11
23
- Requires-Dist: boti<2.0.0,>=1.0.2
24
- Requires-Dist: boti-dask<2.0.0,>=1.0.0
25
- Requires-Dist: dask[dataframe,distributed]<2027.0.0,>=2026.3.0
26
- Requires-Dist: pandas<4.0.0,>=3.0.2
27
- Requires-Dist: polars<2.0.0,>=1.29.0
28
- Requires-Dist: pymysql<2.0.0,>=1.1.2
29
- Requires-Dist: sqlalchemy[asyncio]<3.0.0,>=2.0.49
30
-
31
1
  # boti-data
32
2
 
33
3
  `boti-data` is the **data access and data transformation layer** of the Boti ecosystem.
@@ -121,6 +91,27 @@ Or install through the core package extra:
121
91
  pip install "boti[data]"
122
92
  ```
123
93
 
94
+ `boti-data` doesn't bundle a database driver by default — SQLAlchemy resolves the driver package from your connection URL's scheme (`mysql+pymysql://`, `postgresql+asyncpg://`, ...), so install the one matching your database:
95
+
96
+ ```bash
97
+ pip install "boti-data[mysql]" # asyncmy + pymysql
98
+ pip install "boti-data[postgres]" # asyncpg + psycopg[binary]
99
+ ```
100
+
101
+ If a Postgres table has a [pgvector](https://github.com/pgvector/pgvector) `vector` column, install `boti-data[pgvector]` too — this registers pgvector's `Vector` type with SQLAlchemy's reflection so `SqlAlchemyModelBuilder`/`SqlRepository`/`DataGateway` map that column to a usable `list[float]` (with its dimension preserved) instead of an unreflectable `NullType`:
102
+
103
+ ```bash
104
+ pip install "boti-data[pgvector]"
105
+ ```
106
+
107
+ The base install is deliberately light: single-row transactional access (`SqlRepository`/`AsyncSqlRepository`, see below) works out of the box, with no dask/pandas/polars pulled in. The dataframe-shaped, bulk-oriented side of the package (`DataGateway`/`DataHelper`, `ParquetPipeline`, `HybridDataset`, the sink pipelines, ...) needs the `dataframes` extra:
108
+
109
+ ```bash
110
+ pip install "boti-data[dataframes]" # dask + pandas + polars + boti-dask
111
+ ```
112
+
113
+ Every name importable from `boti_data` is loaded lazily on first access, so `import boti_data` alone never touches dask/pandas/polars — only accessing a dataframe-shaped name does.
114
+
124
115
  ## Imports
125
116
 
126
117
  `boti-data` uses the top-level Python package `boti_data`. Dask runtime/session/resilience helpers live in `boti_dask` and are not re-exported here.
@@ -148,9 +139,11 @@ from boti_data import (
148
139
  available_sinks,
149
140
  create_sink,
150
141
  register_sink,
142
+ AsyncSqlRepository,
151
143
  SqlAlchemyModelBuilder,
152
144
  SqlDatabaseConfig,
153
145
  SqlDatabaseResource,
146
+ SqlRepository,
154
147
  )
155
148
  ```
156
149
 
@@ -857,6 +850,61 @@ Use the following as a guide:
857
850
 
858
851
  ## Examples
859
852
 
853
+ ### Repository (lightest-weight, no dataframe dependencies)
854
+
855
+ For single-row transactional access — get/insert/update/delete by primary key — without pulling in dask/pandas/polars at all. Rows come back as plain `dict[str, Any]`, never a DataFrame.
856
+
857
+ `query_only` defaults to `True` on `SqlRepository`/`AsyncSqlRepository` themselves and always wins over whatever the passed-in `config.query_only` says — insert/update/delete raise until you opt in explicitly with `query_only=False`. This is deliberate: a `config` object might be shared with or reused from a context where writes are already enabled, and a repository shouldn't silently inherit write access nobody asked it for.
858
+
859
+ ```python
860
+ from boti_data import SqlDatabaseConfig, SqlRepository
861
+
862
+ config = SqlDatabaseConfig(connection_url="sqlite:///example.db")
863
+
864
+ with SqlRepository(config, "users", query_only=False) as users:
865
+ row = users.insert({"id": 1, "name": "Ada"})
866
+ row = users.get(1) # -> dict | None
867
+ row = users.update(1, {"name": "Grace"})
868
+ deleted = users.delete(1) # -> bool
869
+ ```
870
+
871
+ Omit `query_only=False` (or read-only access is all you need) and writes raise instead of silently succeeding:
872
+
873
+ ```python
874
+ with SqlRepository(config, "users") as users:
875
+ users.get(1) # fine
876
+ users.insert({"id": 2, "name": "Bob"}) # raises SQLAlchemyError
877
+ ```
878
+
879
+ An async counterpart, `AsyncSqlRepository`, mirrors the same four methods as coroutines and is used as an async context manager:
880
+
881
+ ```python
882
+ from boti_data import AsyncSqlRepository
883
+
884
+ async with AsyncSqlRepository(config, "users", query_only=False) as users:
885
+ row = await users.get(1)
886
+ ```
887
+
888
+ ### Nearest-neighbour search (pgvector)
889
+
890
+ `boti_data.db.nearest_neighbors`/`vector_distance` build an `ORDER BY <distance> LIMIT k` statement against a pgvector column, with the query vector bound as a driver parameter — never string-interpolated into the SQL, regardless of how large the vector is. Requires `boti-data[pgvector]` (see [Installation](#installation)) and a table reflected via `SqlAlchemyModelBuilder`/`SqlRepository`/`DataGateway`.
891
+
892
+ ```python
893
+ from boti_data import DataGateway, SqlDatabaseConfig
894
+ from boti_data.db import SqlAlchemyModelBuilder, nearest_neighbors
895
+
896
+ config = SqlDatabaseConfig(connection_url="postgresql+psycopg://user:pass@host/mydb", query_only=True)
897
+ gateway = DataGateway(config)
898
+
899
+ model = SqlAlchemyModelBuilder(gateway.resource.engine, "embeddings").build_model()
900
+ query_vector = [...] # e.g. a 512-float face embedding
901
+
902
+ stmt = nearest_neighbors(model, model.vector, query_vector, k=10, metric="cosine")
903
+ matches = gateway.load(statement=stmt, model=model, as_pandas=True)
904
+ ```
905
+
906
+ Supported `metric` values map directly to pgvector's operators: `"cosine"` (`<=>`), `"l2"` (`<->`), `"inner_product"` (`<#>`), `"l1"` (`<+>`). Add ordinary filtering with `.where(...)` on the statement before passing it to `gateway.load(...)`.
907
+
860
908
  ### SQL resource (low-level)
861
909
 
862
910
  ```python
@@ -0,0 +1,141 @@
1
+ [project]
2
+ name = "boti-data"
3
+ version = "1.2.0"
4
+ description = "Data infrastructure for the Boti ecosystem"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "MIT"
8
+ authors = [
9
+ {name = "Luis Valverde"}
10
+ ]
11
+ maintainers = [
12
+ {name = "Luis Valverde", email = "lvalverdeb@gmail.com"}
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ dependencies = [
23
+ "boti>=1.0.2,<2.0.0",
24
+ "sqlalchemy[asyncio]>=2.0.49,<3.0.0",
25
+ ]
26
+
27
+ # Neither driver is imported directly by boti_data's own code — SQLAlchemy
28
+ # lazily imports the DBAPI module named by the connection URL's scheme
29
+ # (e.g. "mysql+pymysql://", "postgresql+asyncpg://") when the engine/dialect
30
+ # is actually constructed. So these are purely "make sure pip installed the
31
+ # right package for the DSN you're using" extras, not code-level optionality.
32
+ [project.optional-dependencies]
33
+ mysql = [
34
+ "asyncmy>=0.2.11,<0.3.0",
35
+ "pymysql>=1.1.2,<2.0.0",
36
+ ]
37
+ postgres = [
38
+ "asyncpg>=0.30.0,<0.32.0",
39
+ "psycopg[binary]>=3.2.0,<4.0.0",
40
+ ]
41
+ # Merely importable, not called directly: importing pgvector.sqlalchemy has the
42
+ # side effect of registering the 'vector' type name into SQLAlchemy's Postgres
43
+ # dialect (sqlalchemy.dialects.postgresql.base.ischema_names), which is what
44
+ # boti_data.db.sql_model_registry's reflection needs to recognize pgvector
45
+ # Vector columns instead of falling back to an unusable NullType.
46
+ pgvector = [
47
+ "pgvector>=0.3.0,<0.6.0",
48
+ ]
49
+ # The dataframe-shaped, bulk-oriented side of the package (DataGateway/
50
+ # DataHelper, ParquetPipeline, HybridDataset, the sink pipelines, etc.) — not
51
+ # needed by the lightweight SqlRepository/AsyncSqlRepository single-row API,
52
+ # which only needs the base "boti-data" install.
53
+ dataframes = [
54
+ "boti-dask>=1.0.0,<2.0.0",
55
+ "dask[dataframe,distributed]>=2026.3.0,<2027.0.0",
56
+ "pandas>=3.0.2,<4.0.0",
57
+ "polars>=1.29.0,<2.0.0",
58
+ ]
59
+
60
+ [project.urls]
61
+ Homepage = "https://github.com/lvalverdeb/boti-data"
62
+ Repository = "https://github.com/lvalverdeb/boti-data"
63
+ Documentation = "https://github.com/lvalverdeb/boti-data#readme"
64
+ Issues = "https://github.com/lvalverdeb/boti-data/issues"
65
+
66
+ [build-system]
67
+ requires = ["setuptools>=80", "wheel"]
68
+ build-backend = "setuptools.build_meta"
69
+
70
+ [dependency-groups]
71
+ dev = [
72
+ # boti-data itself no longer requires these (see [project.optional-dependencies]
73
+ # mysql), but tests/security/test_regressions_engine.py and
74
+ # tests/data/test_sql_manager_registry.py construct real mysql+pymysql/
75
+ # mysql+asyncmy engines and expect them to succeed, not raise a
76
+ # missing-driver error — so the dev/test environment still needs both.
77
+ "asyncmy>=0.2.11,<0.3.0",
78
+ "pymysql>=1.1.2,<2.0.0",
79
+ # Real async-SQLite driver used only by tests/data/test_sql_repository.py
80
+ # for genuine end-to-end AsyncSqlRepository round-trips — not a runtime
81
+ # dependency of boti_data itself, and not part of [mysql]/[postgres]/
82
+ # [dataframes] since SQLite isn't one of boti-data's supported backends.
83
+ "aiosqlite>=0.21.0,<0.23.0",
84
+ # boti-data itself no longer requires these (see [project.optional-dependencies]
85
+ # dataframes), but the bulk of the test suite exercises DataGateway/
86
+ # DataHelper/ParquetPipeline/HybridDataset and friends, which do — so the
87
+ # dev/test environment still needs the full dataframe stack installed.
88
+ "boti-dask>=1.0.0,<2.0.0",
89
+ "dask[dataframe,distributed]>=2026.3.0,<2027.0.0",
90
+ "pandas>=3.0.2,<4.0.0",
91
+ "polars>=1.29.0,<2.0.0",
92
+ # Not part of [project.optional-dependencies] pgvector's runtime need — it's
93
+ # here so tests/data/test_sql_model_registry_pgvector.py can verify the
94
+ # registration side effect actually happens when the package is installed.
95
+ "pgvector>=0.3.0,<0.6.0",
96
+ "mypy>=1.14.0",
97
+ "pytest>=9.0.3",
98
+ "pytest-asyncio>=1.3.0",
99
+ "pytest-benchmark>=5.1.0",
100
+ "pytest-cov>=6.0.0",
101
+ "ruff>=0.11.0",
102
+ ]
103
+
104
+ [tool.ruff]
105
+ line-length = 100
106
+ target-version = "py313"
107
+
108
+ [tool.ruff.lint]
109
+ select = ["E", "F", "W", "I", "UP"]
110
+ ignore = ["E501"]
111
+
112
+ [tool.ruff.lint.per-file-ignores]
113
+ "tests/**" = ["E402", "F841"]
114
+ "src/**" = ["UP007", "E701"]
115
+ # __init__.py has a deliberate non-alphabetical import order to break a circular import cycle
116
+ "src/boti_data/__init__.py" = ["I001"]
117
+
118
+ [tool.pytest.ini_options]
119
+ asyncio_mode = "auto"
120
+ testpaths = ["tests"]
121
+ markers = [
122
+ "perf: performance benchmark (not run in default test suite)",
123
+ "security_regression: focused regression coverage for security fixes and audit findings",
124
+ ]
125
+ filterwarnings = [
126
+ 'ignore:Distributed SQL task payloads will carry the raw DSN credential.*worker_connection_env_var.*:UserWarning',
127
+ 'ignore:Closing managed Dask session with 1 live persisted collection\(s\).*Missing dependency\.:RuntimeWarning',
128
+ ]
129
+
130
+ [tool.setuptools]
131
+ include-package-data = true
132
+
133
+ [tool.setuptools.package-data]
134
+ boti_data = ["py.typed"]
135
+
136
+ [tool.setuptools.package-dir]
137
+ "" = "src"
138
+
139
+ [tool.setuptools.packages.find]
140
+ where = ["src"]
141
+ include = ["boti_data*"]
@@ -0,0 +1,177 @@
1
+ """
2
+ Data modules and interfaces for the Boti pipeline context.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import importlib
8
+ from typing import Any
9
+
10
+ __all__ = [
11
+ "And",
12
+ "AsyncFrameEnricher",
13
+ "AsyncSqlDatabaseResource",
14
+ "AsyncSqlRepository",
15
+ "AttachmentSpec",
16
+ "BaseDataCube",
17
+ "BaseArtifact",
18
+ "BuilderConfig",
19
+ "ConnectionCatalog",
20
+ "CsvSink",
21
+ "CsvSinkConfig",
22
+ "DataFrameOptions",
23
+ "DataFrameParams",
24
+ "DataGateway",
25
+ "DataHelper",
26
+ "DatacubeConfig",
27
+ "DatacubeContract",
28
+ "DefaultBase",
29
+ "EngineRegistry",
30
+ "Expr",
31
+ "FieldMap",
32
+ "FileWatermarkStore",
33
+ "FsspecWatermarkStore",
34
+ "FilterHandler",
35
+ "FrameEnricher",
36
+ "HybridDataset",
37
+ "IncrementalResult",
38
+ "JsonlSink",
39
+ "JsonlSinkConfig",
40
+ "Not",
41
+ "Or",
42
+ "ParquetDataConfig",
43
+ "ParquetLoadRequest",
44
+ "ParquetDataResource",
45
+ "ParquetMaterializationResult",
46
+ "ParquetPipeline",
47
+ "ParquetReader",
48
+ "ParquetSink",
49
+ "RegistryConfig",
50
+ "SchemaValidationError",
51
+ "SinkPipeline",
52
+ "SinkRegistry",
53
+ "SinkWriteResult",
54
+ "SqlLoadRequest",
55
+ "SqlAlchemyModelBuilder",
56
+ "SqlDatabaseConfig",
57
+ "SqlDatabaseResource",
58
+ "SqlPartitionPlan",
59
+ "SqlPartitionSpec",
60
+ "SqlPartitionedLoadRequest",
61
+ "SqlPartitionedLoader",
62
+ "SqlModelRegistry",
63
+ "SqlRepository",
64
+ "TrueExpr",
65
+ "VectorMetric",
66
+ "WatermarkStore",
67
+ "advance_watermark",
68
+ "align_frames_for_join",
69
+ "apply_schema_map",
70
+ "available_sinks",
71
+ "build_incremental_filters",
72
+ "create_sink",
73
+ "ensure_greenlet_available",
74
+ "get_global_registry",
75
+ "indexed_left_join",
76
+ "infer_schema_map",
77
+ "left_join_frames",
78
+ "nearest_neighbors",
79
+ "normalize_dtype_alias",
80
+ "normalize_schema_map",
81
+ "register_sink",
82
+ "validate_schema",
83
+ "vector_distance",
84
+ ]
85
+
86
+ # Every public name is loaded lazily (PEP 562) so that importing boti_data (or
87
+ # any of its dependency-clean pieces, e.g. SqlRepository) never pulls in the
88
+ # dask/pandas/polars/pyarrow chain used by the dataframe-shaped, bulk-oriented
89
+ # side of the package (DataGateway/DataHelper, ParquetPipeline, etc.) unless a
90
+ # consumer actually touches one of those names.
91
+ _LAZY = {
92
+ # db.* — dependency-clean.
93
+ "AsyncSqlDatabaseResource": ("boti_data.db.sql_manager", "AsyncSqlDatabaseResource"),
94
+ "EngineRegistry": ("boti_data.db.sql_manager", "EngineRegistry"),
95
+ "SqlDatabaseConfig": ("boti_data.db.sql_manager", "SqlDatabaseConfig"),
96
+ "SqlDatabaseResource": ("boti_data.db.sql_manager", "SqlDatabaseResource"),
97
+ "BuilderConfig": ("boti_data.db.sql_model_builder", "BuilderConfig"),
98
+ "SqlAlchemyModelBuilder": ("boti_data.db.sql_model_builder", "SqlAlchemyModelBuilder"),
99
+ "DefaultBase": ("boti_data.db.sql_model_registry", "DefaultBase"),
100
+ "RegistryConfig": ("boti_data.db.sql_model_registry", "RegistryConfig"),
101
+ "SqlModelRegistry": ("boti_data.db.sql_model_registry", "SqlModelRegistry"),
102
+ "get_global_registry": ("boti_data.db.sql_model_registry", "get_global_registry"),
103
+ "ensure_greenlet_available": ("boti_data.db.sqlalchemy_async", "ensure_greenlet_available"),
104
+ "SqlPartitionPlan": ("boti_data.db.partitioned_types", "SqlPartitionPlan"),
105
+ "SqlPartitionSpec": ("boti_data.db.partitioned_types", "SqlPartitionSpec"),
106
+ "SqlRepository": ("boti_data.db.sql_repository", "SqlRepository"),
107
+ "AsyncSqlRepository": ("boti_data.db.sql_repository", "AsyncSqlRepository"),
108
+ "nearest_neighbors": ("boti_data.db.vector_search", "nearest_neighbors"),
109
+ "vector_distance": ("boti_data.db.vector_search", "vector_distance"),
110
+ "VectorMetric": ("boti_data.db.vector_search", "VectorMetric"),
111
+ "FieldMap": ("boti_data.field_map", "FieldMap"),
112
+ # dask+pandas — the one heavy import inside the otherwise-clean db package.
113
+ "SqlPartitionedLoader": ("boti_data.db.partitioned_loader", "SqlPartitionedLoader"),
114
+ "SqlPartitionedLoadRequest": ("boti_data.db.partitioned_loader", "SqlPartitionedLoadRequest"),
115
+ # dask/pandas/polars/pyarrow-touching — the dataframe-shaped, bulk-oriented side.
116
+ "ConnectionCatalog": ("boti_data.connection_catalog", "ConnectionCatalog"),
117
+ "ParquetDataConfig": ("boti_data.parquet", "ParquetDataConfig"),
118
+ "ParquetDataResource": ("boti_data.parquet", "ParquetDataResource"),
119
+ "ParquetReader": ("boti_data.parquet", "ParquetReader"),
120
+ "FilterHandler": ("boti_data.filters", "FilterHandler"),
121
+ "Expr": ("boti_data.filters", "Expr"),
122
+ "TrueExpr": ("boti_data.filters", "TrueExpr"),
123
+ "And": ("boti_data.filters", "And"),
124
+ "Or": ("boti_data.filters", "Or"),
125
+ "Not": ("boti_data.filters", "Not"),
126
+ "DataGateway": ("boti_data.gateway", "DataGateway"),
127
+ "ParquetLoadRequest": ("boti_data.gateway", "ParquetLoadRequest"),
128
+ "SqlLoadRequest": ("boti_data.gateway", "SqlLoadRequest"),
129
+ "DataFrameOptions": ("boti_data.gateway", "DataFrameOptions"),
130
+ "DataFrameParams": ("boti_data.gateway", "DataFrameParams"),
131
+ "DataHelper": ("boti_data.helper", "DataHelper"),
132
+ "indexed_left_join": ("boti_data.joins", "indexed_left_join"),
133
+ "left_join_frames": ("boti_data.joins", "left_join_frames"),
134
+ "SchemaValidationError": ("boti_data.schema", "SchemaValidationError"),
135
+ "align_frames_for_join": ("boti_data.schema", "align_frames_for_join"),
136
+ "apply_schema_map": ("boti_data.schema", "apply_schema_map"),
137
+ "infer_schema_map": ("boti_data.schema", "infer_schema_map"),
138
+ "normalize_dtype_alias": ("boti_data.schema", "normalize_dtype_alias"),
139
+ "normalize_schema_map": ("boti_data.schema", "normalize_schema_map"),
140
+ "validate_schema": ("boti_data.schema", "validate_schema"),
141
+ "BaseDataCube": ("boti_data.datacube", "BaseDataCube"),
142
+ "BaseArtifact": ("boti_data.datacube", "BaseArtifact"),
143
+ "DatacubeConfig": ("boti_data.datacube", "DatacubeConfig"),
144
+ "DatacubeContract": ("boti_data.datacube", "DatacubeContract"),
145
+ "HybridDataset": ("boti_data.dataset", "HybridDataset"),
146
+ "AsyncFrameEnricher": ("boti_data.enrichment", "AsyncFrameEnricher"),
147
+ "AttachmentSpec": ("boti_data.enrichment", "AttachmentSpec"),
148
+ "FrameEnricher": ("boti_data.enrichment", "FrameEnricher"),
149
+ "CsvSink": ("boti_data.pipelines", "CsvSink"),
150
+ "CsvSinkConfig": ("boti_data.pipelines", "CsvSinkConfig"),
151
+ "JsonlSink": ("boti_data.pipelines", "JsonlSink"),
152
+ "JsonlSinkConfig": ("boti_data.pipelines", "JsonlSinkConfig"),
153
+ "ParquetMaterializationResult": ("boti_data.pipelines", "ParquetMaterializationResult"),
154
+ "ParquetPipeline": ("boti_data.pipelines", "ParquetPipeline"),
155
+ "ParquetSink": ("boti_data.pipelines", "ParquetSink"),
156
+ "SinkPipeline": ("boti_data.pipelines", "SinkPipeline"),
157
+ "SinkRegistry": ("boti_data.pipelines", "SinkRegistry"),
158
+ "SinkWriteResult": ("boti_data.pipelines", "SinkWriteResult"),
159
+ "available_sinks": ("boti_data.pipelines", "available_sinks"),
160
+ "create_sink": ("boti_data.pipelines", "create_sink"),
161
+ "register_sink": ("boti_data.pipelines", "register_sink"),
162
+ "FileWatermarkStore": ("boti_data.watermark", "FileWatermarkStore"),
163
+ "FsspecWatermarkStore": ("boti_data.watermark", "FsspecWatermarkStore"),
164
+ "IncrementalResult": ("boti_data.watermark", "IncrementalResult"),
165
+ "WatermarkStore": ("boti_data.watermark", "WatermarkStore"),
166
+ "advance_watermark": ("boti_data.watermark", "advance_watermark"),
167
+ "build_incremental_filters": ("boti_data.watermark", "build_incremental_filters"),
168
+ }
169
+
170
+
171
+ def __getattr__(name: str) -> Any:
172
+ if name in _LAZY:
173
+ module_name, attr = _LAZY[name]
174
+ value = getattr(importlib.import_module(module_name), attr)
175
+ globals()[name] = value
176
+ return value
177
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -7,6 +7,7 @@ Provides a canonical ``ArrowSchema`` class that wraps ``pa.Schema`` with:
7
7
  - Dict-compatible constructor for migration from existing schema maps
8
8
  - DataFrame ↔ Table conversion helpers
9
9
  """
10
+
10
11
  from __future__ import annotations
11
12
 
12
13
  from collections.abc import Mapping
@@ -66,10 +67,7 @@ class ArrowSchema:
66
67
  from boti_data.schema import normalize_schema_map
67
68
 
68
69
  normalized = normalize_schema_map(dtype_map)
69
- fields = [
70
- (col, pandas_dtype_to_arrow(dtype))
71
- for col, dtype in normalized.items()
72
- ]
70
+ fields = [(col, pandas_dtype_to_arrow(dtype)) for col, dtype in normalized.items()]
73
71
  return cls(pa.schema(fields))
74
72
 
75
73
  @classmethod
@@ -135,6 +133,7 @@ class ArrowSchema:
135
133
  pandas operations. Handles type mismatches via safe/unsafe casting.
136
134
  """
137
135
  from boti_data.db.arrow_schema_mapper import coerce_arrow_table
136
+
138
137
  return coerce_arrow_table(table, self._schema)
139
138
 
140
139
  def to_arrow_table(self, df: Any) -> pa.Table:
@@ -268,9 +267,11 @@ class ArrowSchema:
268
267
  # Helper functions
269
268
  # ---------------------------------------------------------------------------
270
269
 
270
+
271
271
  def _pandas_dtype_to_arrow(dtype_str: str) -> pa.DataType:
272
272
  """Convert a pandas dtype string to PyArrow type."""
273
273
  from boti_data.db.arrow_schema_mapper import pandas_dtype_to_arrow
274
+
274
275
  return pandas_dtype_to_arrow(dtype_str)
275
276
 
276
277
 
@@ -288,6 +289,7 @@ def _pandas_meta_to_arrow_schema(df: Any) -> pa.Schema:
288
289
  # Convenience functions (drop-in replacements for schema.py functions)
289
290
  # ---------------------------------------------------------------------------
290
291
 
292
+
291
293
  def apply_arrow_schema_map(
292
294
  dataframe: Any,
293
295
  schema_map: Mapping[str, str],
@@ -304,10 +306,12 @@ def apply_arrow_schema_map(
304
306
  # Validate required columns
305
307
  if require_columns:
306
308
  missing = arrow_schema.validate_columns(
307
- dataframe, require_all=True,
309
+ dataframe,
310
+ require_all=True,
308
311
  )
309
312
  if missing:
310
313
  from boti_data.schema import SchemaValidationError
314
+
311
315
  raise SchemaValidationError(f"Missing required column(s): {missing}.")
312
316
 
313
317
  return arrow_schema.coerce_dataframe(dataframe)
@@ -325,8 +329,10 @@ def validate_arrow_schema(
325
329
  """
326
330
  arrow_schema = ArrowSchema.from_dict(expected_schema_map)
327
331
  errors = arrow_schema.validate_dataframe(
328
- dataframe, require_columns=require_columns,
332
+ dataframe,
333
+ require_columns=require_columns,
329
334
  )
330
335
  if errors:
331
336
  from boti_data.schema import SchemaValidationError
337
+
332
338
  raise SchemaValidationError("Schema validation failed:\n" + "\n".join(errors))