arrowbricks 0.2.0__tar.gz → 1.0.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.
- {arrowbricks-0.2.0 → arrowbricks-1.0.0}/PKG-INFO +53 -32
- {arrowbricks-0.2.0 → arrowbricks-1.0.0}/README.md +45 -24
- arrowbricks-1.0.0/pyproject.toml +81 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/.gitignore +5 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/Cargo.lock +2597 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/Cargo.toml +56 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/README.md +130 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/examples/duckdb_query.py +37 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/examples/fastapi_sse.py +54 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/rustfmt.toml +1 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/src/client.rs +717 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/src/heartbeat.rs +361 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/src/lib.rs +704 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/src/pipeline.rs +532 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests/wiremock_pipeline.rs +308 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests/wiremock_volume_files.rs +87 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_execute_json.py +101 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_parameters.py +99 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py +150 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_streaming.py +200 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_token_provider.py +179 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_volume_files.py +64 -0
- arrowbricks-1.0.0/rust/arrowbricks_core/tests_py/test_write_ipc_stream.py +36 -0
- {arrowbricks-0.2.0 → arrowbricks-1.0.0}/src/arrowbricks/__init__.py +0 -2
- arrowbricks-1.0.0/src/arrowbricks/_core.pyi +87 -0
- arrowbricks-1.0.0/src/arrowbricks/_streaming.py +143 -0
- arrowbricks-1.0.0/src/arrowbricks/client.py +85 -0
- arrowbricks-1.0.0/src/arrowbricks/cursor.py +225 -0
- arrowbricks-1.0.0/src/arrowbricks/py.typed +0 -0
- arrowbricks-0.2.0/.github/workflows/ci.yml +0 -22
- arrowbricks-0.2.0/.github/workflows/release.yml +0 -32
- arrowbricks-0.2.0/.gitignore +0 -218
- arrowbricks-0.2.0/AGENTS.md +0 -46
- arrowbricks-0.2.0/examples/azure_auth.py +0 -66
- arrowbricks-0.2.0/examples/basic.py +0 -40
- arrowbricks-0.2.0/examples/cursor_paging.py +0 -44
- arrowbricks-0.2.0/examples/fastapi_sse.py +0 -49
- arrowbricks-0.2.0/examples/fastapi_sse_pivot.py +0 -78
- arrowbricks-0.2.0/prek.toml +0 -30
- arrowbricks-0.2.0/pyproject.toml +0 -44
- arrowbricks-0.2.0/src/arrowbricks/_streaming.py +0 -281
- arrowbricks-0.2.0/src/arrowbricks/client.py +0 -374
- arrowbricks-0.2.0/src/arrowbricks/cursor.py +0 -322
- arrowbricks-0.2.0/tests/conftest.py +0 -110
- arrowbricks-0.2.0/tests/test_client.py +0 -165
- arrowbricks-0.2.0/tests/test_cursor.py +0 -257
- arrowbricks-0.2.0/tests/test_result_set.py +0 -74
- arrowbricks-0.2.0/tests/test_streaming.py +0 -82
- arrowbricks-0.2.0/uv.lock +0 -379
- {arrowbricks-0.2.0 → arrowbricks-1.0.0}/LICENSE +0 -0
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arrowbricks
|
|
3
|
-
Version: 0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
License-Expression: MIT
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Requires-Dist: arro3-core>=0.8 ; extra == 'arro3'
|
|
5
|
+
Provides-Extra: arro3
|
|
7
6
|
License-File: LICENSE
|
|
7
|
+
Summary: Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a DB-API-ish Cursor (fetchone/fetchmany/fetchall/fetchall_arrow) or NDJSON streaming. Rust/PyO3 core throughout -- zero required runtime dependencies.
|
|
8
|
+
License-Expression: MIT
|
|
8
9
|
Requires-Python: >=3.11
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Requires-Dist: httpx>=0.27
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
10
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
11
|
+
Project-URL: Repository, https://github.com/bmsuisse/arrowbricks
|
|
13
12
|
|
|
14
13
|
# arrowbricks
|
|
15
14
|
|
|
16
|
-
Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a `Cursor` shaped like [`databricks-sql-python`](https://github.com/databricks/databricks-sql-python)'s (`execute`, `fetchone`/`fetchmany`/`fetchall`, `fetchall_arrow`/`fetchmany_arrow`), or `stream_query_json` for streaming NDJSON.
|
|
15
|
+
Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a `Cursor` shaped like [`databricks-sql-python`](https://github.com/databricks/databricks-sql-python)'s (`execute`, `fetchone`/`fetchmany`/`fetchall`, `fetchall_arrow`/`fetchmany_arrow`), or `stream_query_json` for streaming NDJSON.
|
|
17
16
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
17
|
+
- **Rust core.** Statement submit/poll, bounded-concurrency chunk fetch, the reorder buffer, and Arrow-IPC decode all run in a [PyO3](https://pyo3.rs)/[arrow-rs](https://github.com/apache/arrow-rs) extension bundled in this same package -- 1.6x-2.5x faster than a pure-Python/asyncio client on a multi-chunk result, scaling further with chunk count and concurrency where asyncio+GIL plateaus.
|
|
18
|
+
- **Zero required dependencies.** `pip install arrowbricks` and go.
|
|
19
|
+
- **Bring-your-own-auth** -- a static token or your own token-refresh callable. No cloud-SDK dependency baked in.
|
|
20
|
+
- **Result order preserved** even though chunks can complete out of order over the network.
|
|
21
|
+
- **Lazy fetching** -- chunks are pulled only as `fetchone`/`fetchmany`/`fetchall` actually need them, not all upfront.
|
|
22
|
+
- **Heartbeats** between slow chunks (`execute_streamed`/`stream_query_json`), so a caller streaming this over e.g. SSE never goes silent during a cold warehouse start.
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ Runs SQL against a Databricks SQL warehouse via the Statement Execution API and
|
|
|
27
27
|
pip install arrowbricks
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Ships as precompiled platform wheels (Linux/macOS/Windows) -- no Rust toolchain needed, and nothing else to install for most of the API. Row-tuple fetches (`fetchone`/`fetchmany`/`fetchall`) need one optional extra: `pip install arrowbricks[arro3]` -- see [Arrow vs. row-tuple fetches](#arrow-vs-row-tuple-fetches) below.
|
|
31
31
|
|
|
32
32
|
## Quickstart
|
|
33
33
|
|
|
@@ -49,7 +49,7 @@ async def main():
|
|
|
49
49
|
print(row)
|
|
50
50
|
|
|
51
51
|
await cursor.execute("SELECT * FROM my_catalog.my_schema.my_table LIMIT 100")
|
|
52
|
-
table = await cursor.fetchall_arrow() # an arro3
|
|
52
|
+
table = await cursor.fetchall_arrow() # an Arrow table (arro3/pyarrow/DuckDB-compatible)
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
asyncio.run(main())
|
|
@@ -78,14 +78,6 @@ combined heartbeat/timeout budget across both the wait and the download, or
|
|
|
78
78
|
[`examples/azure_auth.py`](examples/azure_auth.py) for a caching
|
|
79
79
|
`token_provider` built on Azure AD (`DefaultAzureCredential`).
|
|
80
80
|
|
|
81
|
-
## Why not `databricks-sql-connector`?
|
|
82
|
-
|
|
83
|
-
The [official driver](https://github.com/databricks/databricks-sql-python) is the right choice if you need full DB-API 2.0 compatibility over Databricks' Thrift/ODBC-style protocol. If you just want a query result as Arrow/JSON in your own async app, it drags in a lot for that: `pandas`, `thrift`, `openpyxl`, `pybreaker`, `pyjwt`, `oauthlib`, `lz4`, `requests`, `urllib3` as hard dependencies. arrowbricks talks to the plain REST Statement Execution API instead, and its whole dependency tree is `httpx` + `arro3-core` + `arro3-io`. The `Cursor` API is deliberately shaped like the official driver's so switching between them is mostly a constructor change, but arrowbricks is async throughout (`execute`, `fetchone`, etc. are all coroutines) -- there's no sync escape hatch.
|
|
84
|
-
|
|
85
|
-
## Why not `duckbricks`?
|
|
86
|
-
|
|
87
|
-
[duckbricks](https://github.com/bmsuisse/duckbricks) does the same Databricks-to-Arrow work, then goes further: it uses a real embedded DuckDB engine to materialize results into your own DuckDB connection/table (`feed_select_to_duckdb_table`), or push a DuckDB query's result *up* to Databricks (`feed_duckdb_table_to_databricks`). If you need that -- a real local SQL engine sitting on top, not just "run this query, get Arrow/JSON back" -- use duckbricks; it depends on arrowbricks for the Databricks/Arrow half. If you don't need DuckDB at all, arrowbricks alone is the smaller, single-responsibility half.
|
|
88
|
-
|
|
89
81
|
## Auth
|
|
90
82
|
|
|
91
83
|
`connect`/`DatabricksClient` take either:
|
|
@@ -103,24 +95,53 @@ conn = connect(host=..., warehouse_id=..., token_provider=my_token_provider)
|
|
|
103
95
|
|
|
104
96
|
- `connect(host, warehouse_id, *, token=None, token_provider=None, ...) -> Connection`
|
|
105
97
|
- `Connection.cursor() -> Cursor`
|
|
106
|
-
- `Connection.client -> DatabricksClient` -- the same client `cursor()` uses, for lower-level access (e.g. `stream_query_json`, `
|
|
98
|
+
- `Connection.client -> DatabricksClient` -- the same client `cursor()` uses, for lower-level access (e.g. `stream_query_json`, `upload_volume_file`).
|
|
107
99
|
- `Cursor.execute(sql, parameters=None, *, row_limit=None, offset=None, catalog=None, schema=None, total_timeout_s=None) -> Cursor` -- submits and waits for the statement, like a real DB-API cursor. `parameters`, if given, is Databricks' own named-parameter format -- `[{"name": ..., "value": ..., "type": ...}]` bound against `:name` markers in `sql`.
|
|
108
100
|
- `Cursor.execute_streamed(...)` -- same args, but an async generator yielding `HEARTBEAT` while waiting on a slow cold start, then the ready `Cursor` -- for bridging e.g. an SSE connection. Its timeout/heartbeats stop the moment the statement is ready, *before* any chunk has been downloaded -- see `fetchall_streamed` below for the download phase itself.
|
|
109
|
-
- `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`
|
|
110
|
-
- `Cursor.fetchmany_arrow(size) ->
|
|
101
|
+
- `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`, and iterating a `Cursor` directly -- row tuples; needs the `arro3` extra.
|
|
102
|
+
- `Cursor.fetchmany_arrow(size) -> Table`, `Cursor.fetchall_arrow() -> Table` -- an Arrow table (implements `__arrow_c_stream__`, so arro3/pyarrow/DuckDB can all consume it directly, zero-copy).
|
|
111
103
|
- `Cursor.fetchall_streamed(*, total_timeout_s=None)` / `Cursor.fetchall_arrow_streamed(*, total_timeout_s=None)` -- like `fetchall()`/`fetchall_arrow()`, but yield `HEARTBEAT` while pulling chunks instead of blocking silently, then the final rows/Table -- for a caller downloading a large result over SSE who needs heartbeats (and a timeout) through the *download*, not just the initial wait. Compose with `execute_streamed` and a shared deadline if you want one combined budget across both phases (see `examples/fastapi_sse_pivot.py`).
|
|
112
|
-
- `Cursor` is an async iterator, yielding one row (tuple) at a time.
|
|
113
104
|
- `Cursor.description` -- DB-API-style `[(name, type_name, None, None, None, None, None), ...]` after `execute()`.
|
|
114
105
|
- `stream_query_json(client, sql, **kwargs)` -- yields `HEARTBEAT`, then each row as a JSON string, as soon as its chunk arrives. Timestamps come out as full ISO-8601, every column key is always present (`"col":null` for a null value, never an omitted key).
|
|
115
|
-
- `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, ...)` -- the lower-level client `Connection` wraps. `client.
|
|
116
|
-
- `write_ipc_stream(
|
|
106
|
+
- `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, ...)` -- the lower-level client `Connection` wraps. `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API.
|
|
107
|
+
- `write_ipc_stream(table, buf)` -- writes any Arrow-C-Data-Interface-compatible object as an uncompressed Arrow-IPC stream (see below).
|
|
117
108
|
|
|
118
109
|
`Cursor.execute`/`execute_streamed`/`stream_query_json` all accept `catalog`, `schema`, `row_limit`, `offset`, and `total_timeout_s`.
|
|
119
110
|
|
|
111
|
+
## Arrow vs. row-tuple fetches
|
|
112
|
+
|
|
113
|
+
Everything above works with zero dependencies installed *except* row-tuple fetches. `fetchall_arrow`/`fetchmany_arrow` return an Arrow table straight from the Rust core -- the faster path if your code can consume Arrow directly (DuckDB, pyarrow, polars, a Parquet writer, ...):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import duckdb
|
|
117
|
+
|
|
118
|
+
table = await cursor.fetchall_arrow()
|
|
119
|
+
duckdb.sql("SELECT count(*) FROM table").show() # DuckDB reads it zero-copy
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`fetchone`/`fetchmany`/`fetchall` (and iterating a `Cursor` directly) materialize actual Python tuples instead -- `("id", "label")`-style rows you can index into, print, or pass to code that doesn't know about Arrow at all. That conversion needs `arro3-core` (`pip install arrowbricks[arro3]`):
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
await cursor.execute("SELECT id, label FROM my_catalog.my_schema.my_table")
|
|
126
|
+
async for row in cursor: # or: rows = await cursor.fetchall()
|
|
127
|
+
print(row[0], row[1])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Calling a row-tuple method without `arro3-core` installed raises a `ModuleNotFoundError` naming the exact install command, rather than failing silently or with a confusing traceback.
|
|
131
|
+
|
|
132
|
+
## Rust core
|
|
133
|
+
|
|
134
|
+
`rust/arrowbricks_core` is the crate implementing the hot path above, built into this same `arrowbricks` wheel as a compiled submodule -- not a separate PyPI package. See [its own README](rust/arrowbricks_core/README.md) for the crate-level design, plus standalone DuckDB and FastAPI SSE examples against the compiled extension directly.
|
|
135
|
+
|
|
136
|
+
## Why not `databricks-sql-connector`?
|
|
137
|
+
|
|
138
|
+
The [official driver](https://github.com/databricks/databricks-sql-python) is the right choice if you need full DB-API 2.0 compatibility over Databricks' Thrift/ODBC-style protocol. If you just want a query result as Arrow/JSON in your own async app, it drags in a lot for that: `pandas`, `thrift`, `openpyxl`, `pybreaker`, `pyjwt`, `oauthlib`, `lz4`, `requests`, `urllib3` as hard dependencies. arrowbricks talks to the plain REST Statement Execution API instead, with a Rust core and zero required dependencies of its own. The `Cursor` API is deliberately shaped like the official driver's so switching between them is mostly a constructor change, but arrowbricks is async throughout (`execute`, `fetchone`, etc. are all coroutines) -- there's no sync escape hatch.
|
|
139
|
+
|
|
120
140
|
## A note on Arrow IPC compression
|
|
121
141
|
|
|
122
|
-
`write_ipc_stream` (and everything in this package that serializes Arrow-IPC bytes) always writes **uncompressed** bodies. arro3's own default
|
|
142
|
+
`write_ipc_stream` (and everything in this package that serializes Arrow-IPC bytes) always writes **uncompressed** bodies. A compressed body (arro3's own default is `compression="LZ4"`) is transparently decompressed by some Arrow readers (e.g. DuckDB's) but not necessarily by every other Arrow IPC reader -- notably, `duckdb-wasm`'s browser-side decoder silently fails to parse LZ4-compressed bodies. Since arrowbricks' bytes might end up read by anything, plain uncompressed is the safe default.
|
|
123
143
|
|
|
124
144
|
## License
|
|
125
145
|
|
|
126
|
-
MIT
|
|
146
|
+
MIT
|
|
147
|
+
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# arrowbricks
|
|
2
2
|
|
|
3
|
-
Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a `Cursor` shaped like [`databricks-sql-python`](https://github.com/databricks/databricks-sql-python)'s (`execute`, `fetchone`/`fetchmany`/`fetchall`, `fetchall_arrow`/`fetchmany_arrow`), or `stream_query_json` for streaming NDJSON.
|
|
3
|
+
Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a `Cursor` shaped like [`databricks-sql-python`](https://github.com/databricks/databricks-sql-python)'s (`execute`, `fetchone`/`fetchmany`/`fetchall`, `fetchall_arrow`/`fetchmany_arrow`), or `stream_query_json` for streaming NDJSON.
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
5
|
+
- **Rust core.** Statement submit/poll, bounded-concurrency chunk fetch, the reorder buffer, and Arrow-IPC decode all run in a [PyO3](https://pyo3.rs)/[arrow-rs](https://github.com/apache/arrow-rs) extension bundled in this same package -- 1.6x-2.5x faster than a pure-Python/asyncio client on a multi-chunk result, scaling further with chunk count and concurrency where asyncio+GIL plateaus.
|
|
6
|
+
- **Zero required dependencies.** `pip install arrowbricks` and go.
|
|
7
|
+
- **Bring-your-own-auth** -- a static token or your own token-refresh callable. No cloud-SDK dependency baked in.
|
|
8
|
+
- **Result order preserved** even though chunks can complete out of order over the network.
|
|
9
|
+
- **Lazy fetching** -- chunks are pulled only as `fetchone`/`fetchmany`/`fetchall` actually need them, not all upfront.
|
|
10
|
+
- **Heartbeats** between slow chunks (`execute_streamed`/`stream_query_json`), so a caller streaming this over e.g. SSE never goes silent during a cold warehouse start.
|
|
10
11
|
|
|
11
12
|
## Install
|
|
12
13
|
|
|
@@ -14,7 +15,7 @@ Runs SQL against a Databricks SQL warehouse via the Statement Execution API and
|
|
|
14
15
|
pip install arrowbricks
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Ships as precompiled platform wheels (Linux/macOS/Windows) -- no Rust toolchain needed, and nothing else to install for most of the API. Row-tuple fetches (`fetchone`/`fetchmany`/`fetchall`) need one optional extra: `pip install arrowbricks[arro3]` -- see [Arrow vs. row-tuple fetches](#arrow-vs-row-tuple-fetches) below.
|
|
18
19
|
|
|
19
20
|
## Quickstart
|
|
20
21
|
|
|
@@ -36,7 +37,7 @@ async def main():
|
|
|
36
37
|
print(row)
|
|
37
38
|
|
|
38
39
|
await cursor.execute("SELECT * FROM my_catalog.my_schema.my_table LIMIT 100")
|
|
39
|
-
table = await cursor.fetchall_arrow() # an arro3
|
|
40
|
+
table = await cursor.fetchall_arrow() # an Arrow table (arro3/pyarrow/DuckDB-compatible)
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
asyncio.run(main())
|
|
@@ -65,14 +66,6 @@ combined heartbeat/timeout budget across both the wait and the download, or
|
|
|
65
66
|
[`examples/azure_auth.py`](examples/azure_auth.py) for a caching
|
|
66
67
|
`token_provider` built on Azure AD (`DefaultAzureCredential`).
|
|
67
68
|
|
|
68
|
-
## Why not `databricks-sql-connector`?
|
|
69
|
-
|
|
70
|
-
The [official driver](https://github.com/databricks/databricks-sql-python) is the right choice if you need full DB-API 2.0 compatibility over Databricks' Thrift/ODBC-style protocol. If you just want a query result as Arrow/JSON in your own async app, it drags in a lot for that: `pandas`, `thrift`, `openpyxl`, `pybreaker`, `pyjwt`, `oauthlib`, `lz4`, `requests`, `urllib3` as hard dependencies. arrowbricks talks to the plain REST Statement Execution API instead, and its whole dependency tree is `httpx` + `arro3-core` + `arro3-io`. The `Cursor` API is deliberately shaped like the official driver's so switching between them is mostly a constructor change, but arrowbricks is async throughout (`execute`, `fetchone`, etc. are all coroutines) -- there's no sync escape hatch.
|
|
71
|
-
|
|
72
|
-
## Why not `duckbricks`?
|
|
73
|
-
|
|
74
|
-
[duckbricks](https://github.com/bmsuisse/duckbricks) does the same Databricks-to-Arrow work, then goes further: it uses a real embedded DuckDB engine to materialize results into your own DuckDB connection/table (`feed_select_to_duckdb_table`), or push a DuckDB query's result *up* to Databricks (`feed_duckdb_table_to_databricks`). If you need that -- a real local SQL engine sitting on top, not just "run this query, get Arrow/JSON back" -- use duckbricks; it depends on arrowbricks for the Databricks/Arrow half. If you don't need DuckDB at all, arrowbricks alone is the smaller, single-responsibility half.
|
|
75
|
-
|
|
76
69
|
## Auth
|
|
77
70
|
|
|
78
71
|
`connect`/`DatabricksClient` take either:
|
|
@@ -90,24 +83,52 @@ conn = connect(host=..., warehouse_id=..., token_provider=my_token_provider)
|
|
|
90
83
|
|
|
91
84
|
- `connect(host, warehouse_id, *, token=None, token_provider=None, ...) -> Connection`
|
|
92
85
|
- `Connection.cursor() -> Cursor`
|
|
93
|
-
- `Connection.client -> DatabricksClient` -- the same client `cursor()` uses, for lower-level access (e.g. `stream_query_json`, `
|
|
86
|
+
- `Connection.client -> DatabricksClient` -- the same client `cursor()` uses, for lower-level access (e.g. `stream_query_json`, `upload_volume_file`).
|
|
94
87
|
- `Cursor.execute(sql, parameters=None, *, row_limit=None, offset=None, catalog=None, schema=None, total_timeout_s=None) -> Cursor` -- submits and waits for the statement, like a real DB-API cursor. `parameters`, if given, is Databricks' own named-parameter format -- `[{"name": ..., "value": ..., "type": ...}]` bound against `:name` markers in `sql`.
|
|
95
88
|
- `Cursor.execute_streamed(...)` -- same args, but an async generator yielding `HEARTBEAT` while waiting on a slow cold start, then the ready `Cursor` -- for bridging e.g. an SSE connection. Its timeout/heartbeats stop the moment the statement is ready, *before* any chunk has been downloaded -- see `fetchall_streamed` below for the download phase itself.
|
|
96
|
-
- `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`
|
|
97
|
-
- `Cursor.fetchmany_arrow(size) ->
|
|
89
|
+
- `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`, and iterating a `Cursor` directly -- row tuples; needs the `arro3` extra.
|
|
90
|
+
- `Cursor.fetchmany_arrow(size) -> Table`, `Cursor.fetchall_arrow() -> Table` -- an Arrow table (implements `__arrow_c_stream__`, so arro3/pyarrow/DuckDB can all consume it directly, zero-copy).
|
|
98
91
|
- `Cursor.fetchall_streamed(*, total_timeout_s=None)` / `Cursor.fetchall_arrow_streamed(*, total_timeout_s=None)` -- like `fetchall()`/`fetchall_arrow()`, but yield `HEARTBEAT` while pulling chunks instead of blocking silently, then the final rows/Table -- for a caller downloading a large result over SSE who needs heartbeats (and a timeout) through the *download*, not just the initial wait. Compose with `execute_streamed` and a shared deadline if you want one combined budget across both phases (see `examples/fastapi_sse_pivot.py`).
|
|
99
|
-
- `Cursor` is an async iterator, yielding one row (tuple) at a time.
|
|
100
92
|
- `Cursor.description` -- DB-API-style `[(name, type_name, None, None, None, None, None), ...]` after `execute()`.
|
|
101
93
|
- `stream_query_json(client, sql, **kwargs)` -- yields `HEARTBEAT`, then each row as a JSON string, as soon as its chunk arrives. Timestamps come out as full ISO-8601, every column key is always present (`"col":null` for a null value, never an omitted key).
|
|
102
|
-
- `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, ...)` -- the lower-level client `Connection` wraps. `client.
|
|
103
|
-
- `write_ipc_stream(
|
|
94
|
+
- `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, ...)` -- the lower-level client `Connection` wraps. `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API.
|
|
95
|
+
- `write_ipc_stream(table, buf)` -- writes any Arrow-C-Data-Interface-compatible object as an uncompressed Arrow-IPC stream (see below).
|
|
104
96
|
|
|
105
97
|
`Cursor.execute`/`execute_streamed`/`stream_query_json` all accept `catalog`, `schema`, `row_limit`, `offset`, and `total_timeout_s`.
|
|
106
98
|
|
|
99
|
+
## Arrow vs. row-tuple fetches
|
|
100
|
+
|
|
101
|
+
Everything above works with zero dependencies installed *except* row-tuple fetches. `fetchall_arrow`/`fetchmany_arrow` return an Arrow table straight from the Rust core -- the faster path if your code can consume Arrow directly (DuckDB, pyarrow, polars, a Parquet writer, ...):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import duckdb
|
|
105
|
+
|
|
106
|
+
table = await cursor.fetchall_arrow()
|
|
107
|
+
duckdb.sql("SELECT count(*) FROM table").show() # DuckDB reads it zero-copy
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`fetchone`/`fetchmany`/`fetchall` (and iterating a `Cursor` directly) materialize actual Python tuples instead -- `("id", "label")`-style rows you can index into, print, or pass to code that doesn't know about Arrow at all. That conversion needs `arro3-core` (`pip install arrowbricks[arro3]`):
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
await cursor.execute("SELECT id, label FROM my_catalog.my_schema.my_table")
|
|
114
|
+
async for row in cursor: # or: rows = await cursor.fetchall()
|
|
115
|
+
print(row[0], row[1])
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Calling a row-tuple method without `arro3-core` installed raises a `ModuleNotFoundError` naming the exact install command, rather than failing silently or with a confusing traceback.
|
|
119
|
+
|
|
120
|
+
## Rust core
|
|
121
|
+
|
|
122
|
+
`rust/arrowbricks_core` is the crate implementing the hot path above, built into this same `arrowbricks` wheel as a compiled submodule -- not a separate PyPI package. See [its own README](rust/arrowbricks_core/README.md) for the crate-level design, plus standalone DuckDB and FastAPI SSE examples against the compiled extension directly.
|
|
123
|
+
|
|
124
|
+
## Why not `databricks-sql-connector`?
|
|
125
|
+
|
|
126
|
+
The [official driver](https://github.com/databricks/databricks-sql-python) is the right choice if you need full DB-API 2.0 compatibility over Databricks' Thrift/ODBC-style protocol. If you just want a query result as Arrow/JSON in your own async app, it drags in a lot for that: `pandas`, `thrift`, `openpyxl`, `pybreaker`, `pyjwt`, `oauthlib`, `lz4`, `requests`, `urllib3` as hard dependencies. arrowbricks talks to the plain REST Statement Execution API instead, with a Rust core and zero required dependencies of its own. The `Cursor` API is deliberately shaped like the official driver's so switching between them is mostly a constructor change, but arrowbricks is async throughout (`execute`, `fetchone`, etc. are all coroutines) -- there's no sync escape hatch.
|
|
127
|
+
|
|
107
128
|
## A note on Arrow IPC compression
|
|
108
129
|
|
|
109
|
-
`write_ipc_stream` (and everything in this package that serializes Arrow-IPC bytes) always writes **uncompressed** bodies. arro3's own default
|
|
130
|
+
`write_ipc_stream` (and everything in this package that serializes Arrow-IPC bytes) always writes **uncompressed** bodies. A compressed body (arro3's own default is `compression="LZ4"`) is transparently decompressed by some Arrow readers (e.g. DuckDB's) but not necessarily by every other Arrow IPC reader -- notably, `duckdb-wasm`'s browser-side decoder silently fails to parse LZ4-compressed bodies. Since arrowbricks' bytes might end up read by anything, plain uncompressed is the safe default.
|
|
110
131
|
|
|
111
132
|
## License
|
|
112
133
|
|
|
113
|
-
MIT
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "arrowbricks"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a DB-API-ish Cursor (fetchone/fetchmany/fetchall/fetchall_arrow) or NDJSON streaming. Rust/PyO3 core throughout -- zero required runtime dependencies."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
# No hard runtime dependencies -- httpx and arro3-io are fully eliminated
|
|
10
|
+
# (see rust/arrowbricks_core's own reqwest client + Arrow-IPC/NDJSON
|
|
11
|
+
# writer). arro3-core is optional (see below): only Cursor.fetchone/
|
|
12
|
+
# fetchmany/fetchall's row materialization needs it -- fetchall_arrow,
|
|
13
|
+
# execute_arrow, stream_query_json, upload_volume_file/delete_volume_file,
|
|
14
|
+
# and Cursor.description all work with zero dependencies installed.
|
|
15
|
+
dependencies = []
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
# pyo3-arrow's own Table.column()/schema methods are designed to hand back
|
|
19
|
+
# the caller's *real* arro3.core objects for further arro3-ecosystem
|
|
20
|
+
# interop (not a value pyo3-arrow can materialize itself), so
|
|
21
|
+
# fetchone/fetchmany/fetchall need this installed. `pip install
|
|
22
|
+
# arrowbricks[arro3]` -- or handle rows as Arrow via fetchall_arrow/
|
|
23
|
+
# fetchmany_arrow instead, which don't need it at all.
|
|
24
|
+
arro3 = ["arro3-core>=0.8"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Repository = "https://github.com/bmsuisse/arrowbricks"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["maturin>=1.0,<2.0"]
|
|
31
|
+
build-backend = "maturin"
|
|
32
|
+
|
|
33
|
+
# The compiled hot path (rust/arrowbricks_core) builds into this same wheel as
|
|
34
|
+
# arrowbricks._core -- not a separately published package -- so `pip install
|
|
35
|
+
# arrowbricks` is the only install step. python-source points at the existing
|
|
36
|
+
# src/ layout (arrowbricks/__init__.py, client.py, cursor.py, _streaming.py);
|
|
37
|
+
# manifest-path points back at the Cargo.toml living under rust/, since it
|
|
38
|
+
# isn't in the same directory as this pyproject.toml.
|
|
39
|
+
[tool.maturin]
|
|
40
|
+
python-source = "src"
|
|
41
|
+
module-name = "arrowbricks._core"
|
|
42
|
+
manifest-path = "rust/arrowbricks_core/Cargo.toml"
|
|
43
|
+
# `license-files` above declares LICENSE in metadata, but maturin's sdist
|
|
44
|
+
# build doesn't bundle a repo-root file just because it's referenced there --
|
|
45
|
+
# it needs to be told explicitly, or PyPI rejects the upload with "License-File
|
|
46
|
+
# LICENSE does not exist in distribution file" (a real failure the first time
|
|
47
|
+
# v1.0.0 released: all 5 wheels published fine, only the sdist hit this).
|
|
48
|
+
include = ["LICENSE"]
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
# arro3-core/arro3-io are test-only -- tests/conftest.py uses them to build
|
|
52
|
+
# synthetic Arrow-IPC chunk bytes so tests exercise a real Arrow IPC round
|
|
53
|
+
# trip. The package itself has no runtime dependency on them (or anything
|
|
54
|
+
# else) -- see rust/arrowbricks_core's own Arrow/NDJSON encoding.
|
|
55
|
+
dev = [
|
|
56
|
+
"arro3-core>=0.8",
|
|
57
|
+
"arro3-io>=0.8",
|
|
58
|
+
"pytest>=8.0",
|
|
59
|
+
"pytest-asyncio>=0.24",
|
|
60
|
+
"ruff>=0.6",
|
|
61
|
+
"ty>=0.0.65",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
# Bare `pytest`/`pytest -q` (no explicit path) stays scoped to the Cursor-
|
|
67
|
+
# level suite -- rust/arrowbricks_core/tests_py (the compiled extension's own
|
|
68
|
+
# PyO3-level tests) is invoked explicitly by path where it's needed (CI's
|
|
69
|
+
# build job, or by hand), not auto-discovered every run.
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
line-length = 120
|
|
74
|
+
target-version = "py311"
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["E", "F", "I", "UP", "S", "B"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.per-file-ignores]
|
|
80
|
+
"tests/*" = ["S101", "S106"]
|
|
81
|
+
"rust/arrowbricks_core/tests_py/*" = ["S101", "S106"]
|