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.
- {boti_data-1.0.3/src/boti_data.egg-info → boti_data-1.2.0}/PKG-INFO +94 -11
- boti_data-1.0.3/PKG-INFO → boti_data-1.2.0/README.md +78 -30
- boti_data-1.2.0/pyproject.toml +141 -0
- boti_data-1.2.0/src/boti_data/__init__.py +177 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/arrow_schema.py +12 -6
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/connection_catalog.py +5 -4
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/__init__.py +0 -1
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/artifact.py +19 -21
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/base.py +21 -27
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/contract.py +19 -1
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/datacube/resource.py +1 -4
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/dataset/__init__.py +0 -1
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/dataset/hybrid.py +64 -152
- boti_data-1.2.0/src/boti_data/dataset/hybrid_support.py +133 -0
- boti_data-1.2.0/src/boti_data/db/__init__.py +65 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/arrow_schema_mapper.py +42 -35
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/engine_registry.py +42 -17
- boti_data-1.2.0/src/boti_data/db/partitioned_execution.py +310 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/partitioned_loader.py +61 -36
- boti_data-1.2.0/src/boti_data/db/partitioned_planner.py +383 -0
- boti_data-1.2.0/src/boti_data/db/partitioned_planner_async.py +110 -0
- boti_data-1.2.0/src/boti_data/db/partitioned_planning_math.py +100 -0
- boti_data-1.2.0/src/boti_data/db/partitioned_row_materialization.py +168 -0
- boti_data-1.2.0/src/boti_data/db/partitioned_statements.py +82 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/partitioned_types.py +50 -3
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_config.py +1 -3
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_engine.py +16 -14
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_model_builder.py +21 -12
- boti_data-1.2.0/src/boti_data/db/sql_model_module_injection.py +51 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_model_registry.py +139 -155
- boti_data-1.2.0/src/boti_data/db/sql_model_registry_types.py +51 -0
- boti_data-1.2.0/src/boti_data/db/sql_repository.py +186 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_resource.py +3 -1
- boti_data-1.2.0/src/boti_data/db/vector_search.py +71 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/async_enricher.py +37 -21
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/protocol.py +1 -3
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/specs.py +3 -1
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/transformers.py +3 -1
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/field_map.py +2 -2
- boti_data-1.2.0/src/boti_data/filters/arrow_kernels.py +90 -0
- boti_data-1.2.0/src/boti_data/filters/arrow_kernels_comparison.py +82 -0
- boti_data-1.2.0/src/boti_data/filters/arrow_kernels_compile.py +217 -0
- boti_data-1.2.0/src/boti_data/filters/arrow_kernels_string.py +92 -0
- boti_data-1.2.0/src/boti_data/filters/backend_ops.py +228 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/expressions.py +29 -23
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/handler.py +34 -32
- boti_data-1.2.0/src/boti_data/filters/utils.py +67 -0
- boti_data-1.2.0/src/boti_data/filters/value_parsing.py +314 -0
- boti_data-1.2.0/src/boti_data/gateway/_backend_strategies.py +309 -0
- boti_data-1.2.0/src/boti_data/gateway/_gateway_init.py +221 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/_series_filters.py +14 -10
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/arrow_adapters.py +88 -77
- boti_data-1.2.0/src/boti_data/gateway/build_context.py +70 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/chunking.py +44 -31
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/configured_load.py +20 -33
- boti_data-1.2.0/src/boti_data/gateway/core.py +400 -0
- boti_data-1.2.0/src/boti_data/gateway/core_load.py +199 -0
- boti_data-1.2.0/src/boti_data/gateway/datacube_strategy.py +112 -0
- boti_data-1.2.0/src/boti_data/gateway/frame_convert.py +110 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/frame_strategies.py +44 -98
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/incremental.py +5 -0
- boti_data-1.2.0/src/boti_data/gateway/load_execution.py +202 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/loaders.py +41 -22
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/normalization.py +8 -5
- boti_data-1.2.0/src/boti_data/gateway/parquet_strategy.py +170 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/planning.py +101 -33
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/post_process.py +44 -54
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/return_type.py +75 -69
- boti_data-1.2.0/src/boti_data/gateway/select_cache.py +76 -0
- boti_data-1.2.0/src/boti_data/gateway/semi_join.py +97 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/sql_guard.py +2 -6
- boti_data-1.2.0/src/boti_data/gateway/sql_partitioned_exec.py +364 -0
- boti_data-1.2.0/src/boti_data/gateway/sql_size_estimation.py +170 -0
- boti_data-1.2.0/src/boti_data/gateway/sql_strategy.py +378 -0
- boti_data-1.2.0/src/boti_data/gateway/sql_strategy_configured_async.py +97 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/helper.py +62 -48
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/joins.py +124 -48
- boti_data-1.2.0/src/boti_data/parquet/__init__.py +28 -0
- boti_data-1.2.0/src/boti_data/parquet/config.py +77 -0
- boti_data-1.2.0/src/boti_data/parquet/discovery.py +163 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/parquet/file_info.py +5 -14
- boti_data-1.2.0/src/boti_data/parquet/filesystem.py +120 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/parquet/reader.py +9 -5
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/parquet/reader_config.py +5 -4
- boti_data-1.2.0/src/boti_data/parquet/resource.py +341 -0
- boti_data-1.2.0/src/boti_data/parquet/schema_filters.py +151 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/pipelines/__init__.py +2 -1
- boti_data-1.2.0/src/boti_data/pipelines/base.py +208 -0
- boti_data-1.2.0/src/boti_data/pipelines/parquet_pipeline.py +312 -0
- boti_data-1.2.0/src/boti_data/pipelines/sinks.py +34 -0
- boti_data-1.2.0/src/boti_data/pipelines/sinks_common.py +209 -0
- boti_data-1.2.0/src/boti_data/pipelines/sinks_parquet.py +127 -0
- boti_data-1.2.0/src/boti_data/pipelines/sinks_text.py +336 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/schema.py +38 -33
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/watermark/incremental.py +22 -19
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/watermark/store.py +7 -2
- boti_data-1.0.3/README.md → boti_data-1.2.0/src/boti_data.egg-info/PKG-INFO +113 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data.egg-info/SOURCES.txt +35 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data.egg-info/requires.txt +13 -2
- boti_data-1.0.3/pyproject.toml +0 -96
- boti_data-1.0.3/src/boti_data/__init__.py +0 -142
- boti_data-1.0.3/src/boti_data/db/__init__.py +0 -38
- boti_data-1.0.3/src/boti_data/db/partitioned_execution.py +0 -415
- boti_data-1.0.3/src/boti_data/db/partitioned_planner.py +0 -633
- boti_data-1.0.3/src/boti_data/filters/arrow_kernels.py +0 -351
- boti_data-1.0.3/src/boti_data/filters/utils.py +0 -482
- boti_data-1.0.3/src/boti_data/gateway/_backend_strategies.py +0 -1342
- boti_data-1.0.3/src/boti_data/gateway/core.py +0 -880
- boti_data-1.0.3/src/boti_data/parquet/__init__.py +0 -8
- boti_data-1.0.3/src/boti_data/parquet/resource.py +0 -736
- boti_data-1.0.3/src/boti_data/pipelines/base.py +0 -519
- boti_data-1.0.3/src/boti_data/pipelines/sinks.py +0 -609
- {boti_data-1.0.3 → boti_data-1.2.0}/setup.cfg +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_manager.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sql_readonly.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/db/sqlalchemy_async.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/__init__.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/enrichment/composite.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/__init__.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/filters/parquet_compiler.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/__init__.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/_factory.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/_payloads.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/load_request.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/policies.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/gateway/requests.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/pipelines/registry.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/py.typed +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data/watermark/__init__.py +0 -0
- {boti_data-1.0.3 → boti_data-1.2.0}/src/boti_data.egg-info/dependency_links.txt +0 -0
- {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
|
+
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.
|
|
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,
|
|
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,
|
|
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))
|