arrowbricks 0.1.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.1.0/.github/workflows/ci.yml +22 -0
- arrowbricks-0.1.0/.github/workflows/release.yml +32 -0
- arrowbricks-0.1.0/.gitignore +218 -0
- arrowbricks-0.1.0/AGENTS.md +44 -0
- arrowbricks-0.1.0/LICENSE +21 -0
- arrowbricks-0.1.0/PKG-INFO +114 -0
- arrowbricks-0.1.0/README.md +101 -0
- arrowbricks-0.1.0/prek.toml +30 -0
- arrowbricks-0.1.0/pyproject.toml +44 -0
- arrowbricks-0.1.0/src/arrowbricks/__init__.py +23 -0
- arrowbricks-0.1.0/src/arrowbricks/_streaming.py +263 -0
- arrowbricks-0.1.0/src/arrowbricks/client.py +374 -0
- arrowbricks-0.1.0/src/arrowbricks/cursor.py +261 -0
- arrowbricks-0.1.0/tests/conftest.py +110 -0
- arrowbricks-0.1.0/tests/test_client.py +165 -0
- arrowbricks-0.1.0/tests/test_cursor.py +161 -0
- arrowbricks-0.1.0/tests/test_streaming.py +39 -0
- arrowbricks-0.1.0/uv.lock +379 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.13", "3.14"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: uv sync --all-extras
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ty check src tests
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*.*.*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v7
|
|
12
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.14"
|
|
15
|
+
- run: uv sync --all-extras
|
|
16
|
+
- run: uv run ruff check .
|
|
17
|
+
- run: uv run ty check src tests
|
|
18
|
+
- run: uv run pytest -q
|
|
19
|
+
|
|
20
|
+
publish:
|
|
21
|
+
needs: test
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment: pypi
|
|
24
|
+
permissions:
|
|
25
|
+
id-token: write # PyPI trusted publishing (OIDC) -- no stored API token
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v7
|
|
28
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.14"
|
|
31
|
+
- run: uv build
|
|
32
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# arrowbricks
|
|
2
|
+
|
|
3
|
+
Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands the result back as Arrow (a `Cursor` shaped like `databricks-sql-python`'s) or streaming NDJSON. See `README.md` for the user-facing API; this file is about working *on* the package.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
- `src/arrowbricks/client.py` -- pure REST client (auth, statement submission/polling, backpressure-bounded concurrent chunk download). No Arrow dependency at all, intentionally: someone who only wants `execute_json_statement` or `upload_volume_file`/`delete_volume_file` shouldn't need arro3 pulled in conceptually either, even though it's a hard dependency of the package as a whole. Retries are a small hand-rolled `_retry_call` loop, not a dependency (see "Design invariants" below).
|
|
8
|
+
- `src/arrowbricks/_streaming.py` -- Arrow (de)serialization via arro3, always: `ReplayableArrowChunk`, `write_ipc_stream` (always uncompressed, see below), heartbeat helpers, chunk fetching, and `stream_query_json` (arro3's `write_ndjson`). One Arrow engine, no pluggable backend -- unlike duckbricks, there's nothing here to make pluggable; arro3 *is* the whole point.
|
|
9
|
+
- `src/arrowbricks/cursor.py` -- `Connection`/`Cursor`, the DB-API-ish surface (`execute`/`execute_streamed`, `fetchone`/`fetchmany`/`fetchall`, `fetchall_arrow`/`fetchmany_arrow`). `_ResultSet` buffers at the Arrow-Table level (not materialized Python rows) so the Arrow-native fetch methods stay zero-copy; row-based fetches materialize lazily off that buffer.
|
|
10
|
+
- `tests/` -- respx mocks the Databricks REST endpoints (warehouse status, statement submit, chunk-link resolution, external-link byte download); no real warehouse or credentials needed to run the suite.
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync --all-extras
|
|
16
|
+
uv run pytest -q
|
|
17
|
+
uv run ruff check .
|
|
18
|
+
uv run ty check src tests
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
One-time setup per clone: `prek install` (needs `uv tool install prek` first if not already on PATH).
|
|
22
|
+
|
|
23
|
+
## Design invariants -- don't casually undo these
|
|
24
|
+
|
|
25
|
+
- **No cloud-SDK dependency.** Auth is `token: str` or `token_provider: Callable[[], str | Awaitable[str]]`. Do not add `azure-identity`/`boto3`/etc. as a real dependency -- that belongs in the caller's app.
|
|
26
|
+
- **No hardcoded catalog/schema.** `catalog`/`schema` default to `None` everywhere. This package has zero knowledge of any specific Databricks workspace's naming.
|
|
27
|
+
- **Chunk order is not fetch order.** `DatabricksClient` fetches chunks concurrently (bounded, with backpressure) and they can complete out of order. `_ResultSet`/`stream_query_json` both hold a `pending: dict[int, chunk]` reorder buffer keyed by `chunk_index`, releasing in order as the next expected index shows up. If you touch either, keep a test proving order survives out-of-order arrival (see `test_fetchall_preserves_order_despite_out_of_order_chunks`, `test_stream_query_json_preserves_order_despite_out_of_order_chunks`).
|
|
28
|
+
- **Chunks are fetched lazily, not all upfront.** `_ResultSet` only pulls the next chunk from `_chunk_aiter` when the caller's `fetchone`/`fetchmany`/`fetchall` actually needs more rows than are already buffered. Don't "simplify" this into draining the whole chunk iterator inside `execute()` -- that defeats the point of a paginated cursor.
|
|
29
|
+
- **No silent row caps.** There's no `ABSOLUTE_ROW_LIMIT`-style ceiling baked in. If a caller wants one, that's `row_limit`, which they pass explicitly.
|
|
30
|
+
- **No retry dependency.** `client.py`'s `_retry_call` is a ~10-line hand-rolled exponential-backoff loop, replacing tenacity on purpose -- it's the only retry pattern in the whole client, so a dependency for it wasn't worth it.
|
|
31
|
+
- **One Arrow engine, no pluggable backend.** Unlike duckbricks (which supports nanoarrow *or* arro3 *or* bring-your-own), arrowbricks is arro3-only by design -- that's the entire "single responsibility" pitch. Don't add a backend-abstraction layer back in; if a caller needs a different Arrow engine, that's duckbricks' `set_arrow_backend()`, not this package.
|
|
32
|
+
- **`write_ipc_stream` (and everything built on it) always writes uncompressed Arrow-IPC bodies.** `aio.write_ipc_stream(..., compression=None)` explicitly, everywhere. arro3's own default (`compression="LZ4"`) is transparently decompressed by DuckDB's Arrow reader but not necessarily by other Arrow IPC readers -- `duckdb-wasm`'s browser-side decoder silently fails to parse LZ4-compressed bodies (this was a real bug in duckbricks 0.3.0, fixed in 0.3.1 -- see its CHANGELOG/git history). Never remove the explicit `compression=None`.
|
|
33
|
+
|
|
34
|
+
## Testing
|
|
35
|
+
|
|
36
|
+
Mock the Databricks endpoints with `respx` (see `tests/conftest.py`'s `mock_warehouse` fixture) rather than hitting a real warehouse. The fixture builds real Arrow-IPC chunk bytes via arro3 directly, so tests exercise the actual Arrow IPC round trip, not a stand-in. Pass `reverse_arrival=True` to force genuine out-of-order chunk completion when a test needs to prove ordering survives it.
|
|
37
|
+
|
|
38
|
+
## Releasing
|
|
39
|
+
|
|
40
|
+
1. Bump `version` in `pyproject.toml`.
|
|
41
|
+
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
42
|
+
3. `.github/workflows/release.yml` runs the test job, then builds and publishes to PyPI via trusted publishing (OIDC) -- no stored token.
|
|
43
|
+
|
|
44
|
+
One-time, outside this repo: register this GitHub repo + `release.yml` workflow as a **trusted publisher** on the `arrowbricks` PyPI project (PyPI project settings -> Publishing). Without that, the `publish` job's OIDC exchange fails even though tests pass.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BMS Suisse AG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arrowbricks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
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. Single Arrow engine (arro3), no DuckDB.
|
|
5
|
+
Project-URL: Repository, https://github.com/bmsuisse/arrowbricks
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: arro3-core>=0.8
|
|
10
|
+
Requires-Dist: arro3-io>=0.8
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# arrowbricks
|
|
15
|
+
|
|
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. One Arrow engine ([arro3](https://github.com/kylebarron/arro3)), no DuckDB, no pandas/pyarrow.
|
|
17
|
+
|
|
18
|
+
- Single responsibility: Databricks to Arrow via arro3. No embedded query engine -- that's [duckbricks](https://github.com/bmsuisse/duckbricks), built on top of this.
|
|
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
|
+
- Chunks are fetched lazily 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
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install arrowbricks
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Dependencies: `httpx` + `arro3-core` + `arro3-io`. That's the whole tree.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from arrowbricks import connect
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
conn = connect(
|
|
41
|
+
host="adb-1234567890.1.azuredatabricks.net",
|
|
42
|
+
warehouse_id="abcd1234efgh5678",
|
|
43
|
+
token="dapi...", # or token_provider=... -- see Auth below
|
|
44
|
+
)
|
|
45
|
+
cursor = conn.cursor()
|
|
46
|
+
|
|
47
|
+
await cursor.execute("SELECT * FROM my_catalog.my_schema.my_table LIMIT 100")
|
|
48
|
+
async for row in cursor:
|
|
49
|
+
print(row)
|
|
50
|
+
|
|
51
|
+
await cursor.execute("SELECT * FROM my_catalog.my_schema.my_table LIMIT 100")
|
|
52
|
+
table = await cursor.fetchall_arrow() # an arro3 Table
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For streaming NDJSON (e.g. a FastAPI SSE endpoint, first row out as soon as its chunk arrives):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from arrowbricks import HEARTBEAT, DatabricksClient, stream_query_json
|
|
62
|
+
|
|
63
|
+
client = DatabricksClient(host=..., warehouse_id=..., token=...)
|
|
64
|
+
|
|
65
|
+
async for item in stream_query_json(client, "SELECT * FROM my_catalog.my_schema.big_table"):
|
|
66
|
+
if item is HEARTBEAT:
|
|
67
|
+
continue # forward as an SSE keep-alive comment, e.g.
|
|
68
|
+
print(item) # one ready-to-send JSON string per row
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Why not `databricks-sql-connector`?
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
## Why not `duckbricks`?
|
|
76
|
+
|
|
77
|
+
[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.
|
|
78
|
+
|
|
79
|
+
## Auth
|
|
80
|
+
|
|
81
|
+
`connect`/`DatabricksClient` take either:
|
|
82
|
+
|
|
83
|
+
- `token: str` -- a static personal access token or pre-issued OAuth token, or
|
|
84
|
+
- `token_provider` -- a callable (sync or async) returning a token string, called on every request.
|
|
85
|
+
|
|
86
|
+
arrowbricks has no opinion on *how* you get a token and no cloud-SDK dependency of its own. If your provider is expensive to call, cache/refresh inside it -- arrowbricks does no caching on your behalf.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
conn = connect(host=..., warehouse_id=..., token_provider=my_token_provider)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
- `connect(host, warehouse_id, *, token=None, token_provider=None, ...) -> Connection`
|
|
95
|
+
- `Connection.cursor() -> Cursor`
|
|
96
|
+
- `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`.
|
|
97
|
+
- `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.
|
|
98
|
+
- `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`
|
|
99
|
+
- `Cursor.fetchmany_arrow(size) -> arro3.core.Table`, `Cursor.fetchall_arrow() -> arro3.core.Table`
|
|
100
|
+
- `Cursor` is an async iterator, yielding one row (tuple) at a time.
|
|
101
|
+
- `Cursor.description` -- DB-API-style `[(name, type_name, None, None, None, None, None), ...]` after `execute()`.
|
|
102
|
+
- `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).
|
|
103
|
+
- `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, ...)` -- the lower-level client `Connection` wraps. `client.execute_json_statement(sql, ...)` for plain JSON rows with no Arrow parse at all; `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API.
|
|
104
|
+
- `write_ipc_stream(table_or_chunk, buf)` -- thin wrapper around `arro3.io.write_ipc_stream` that always writes uncompressed bodies (see below).
|
|
105
|
+
|
|
106
|
+
`Cursor.execute`/`execute_streamed`/`stream_query_json` all accept `catalog`, `schema`, `row_limit`, `offset`, and `total_timeout_s`.
|
|
107
|
+
|
|
108
|
+
## A note on Arrow IPC compression
|
|
109
|
+
|
|
110
|
+
`write_ipc_stream` (and everything in this package that serializes Arrow-IPC bytes) always writes **uncompressed** bodies. arro3's own default (`compression="LZ4"`) is transparently decompressed by DuckDB's Arrow reader, but not necessarily by every other Arrow IPC reader -- notably, `duckdb-wasm`'s browser-side decoder silently fails to parse LZ4-compressed bodies. If you're producing bytes that might be consumed by something other than a Python DuckDB connection, this default matters.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# arrowbricks
|
|
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. One Arrow engine ([arro3](https://github.com/kylebarron/arro3)), no DuckDB, no pandas/pyarrow.
|
|
4
|
+
|
|
5
|
+
- Single responsibility: Databricks to Arrow via arro3. No embedded query engine -- that's [duckbricks](https://github.com/bmsuisse/duckbricks), built on top of this.
|
|
6
|
+
- Bring-your-own-auth -- a static token or your own token-refresh callable. No cloud-SDK dependency baked in.
|
|
7
|
+
- Result-order preserved even though chunks can complete out of order over the network.
|
|
8
|
+
- Chunks are fetched lazily as `fetchone`/`fetchmany`/`fetchall` actually need them, not all upfront.
|
|
9
|
+
- 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
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install arrowbricks
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Dependencies: `httpx` + `arro3-core` + `arro3-io`. That's the whole tree.
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from arrowbricks import connect
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def main():
|
|
27
|
+
conn = connect(
|
|
28
|
+
host="adb-1234567890.1.azuredatabricks.net",
|
|
29
|
+
warehouse_id="abcd1234efgh5678",
|
|
30
|
+
token="dapi...", # or token_provider=... -- see Auth below
|
|
31
|
+
)
|
|
32
|
+
cursor = conn.cursor()
|
|
33
|
+
|
|
34
|
+
await cursor.execute("SELECT * FROM my_catalog.my_schema.my_table LIMIT 100")
|
|
35
|
+
async for row in cursor:
|
|
36
|
+
print(row)
|
|
37
|
+
|
|
38
|
+
await cursor.execute("SELECT * FROM my_catalog.my_schema.my_table LIMIT 100")
|
|
39
|
+
table = await cursor.fetchall_arrow() # an arro3 Table
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For streaming NDJSON (e.g. a FastAPI SSE endpoint, first row out as soon as its chunk arrives):
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from arrowbricks import HEARTBEAT, DatabricksClient, stream_query_json
|
|
49
|
+
|
|
50
|
+
client = DatabricksClient(host=..., warehouse_id=..., token=...)
|
|
51
|
+
|
|
52
|
+
async for item in stream_query_json(client, "SELECT * FROM my_catalog.my_schema.big_table"):
|
|
53
|
+
if item is HEARTBEAT:
|
|
54
|
+
continue # forward as an SSE keep-alive comment, e.g.
|
|
55
|
+
print(item) # one ready-to-send JSON string per row
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why not `databricks-sql-connector`?
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
## Why not `duckbricks`?
|
|
63
|
+
|
|
64
|
+
[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.
|
|
65
|
+
|
|
66
|
+
## Auth
|
|
67
|
+
|
|
68
|
+
`connect`/`DatabricksClient` take either:
|
|
69
|
+
|
|
70
|
+
- `token: str` -- a static personal access token or pre-issued OAuth token, or
|
|
71
|
+
- `token_provider` -- a callable (sync or async) returning a token string, called on every request.
|
|
72
|
+
|
|
73
|
+
arrowbricks has no opinion on *how* you get a token and no cloud-SDK dependency of its own. If your provider is expensive to call, cache/refresh inside it -- arrowbricks does no caching on your behalf.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
conn = connect(host=..., warehouse_id=..., token_provider=my_token_provider)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
- `connect(host, warehouse_id, *, token=None, token_provider=None, ...) -> Connection`
|
|
82
|
+
- `Connection.cursor() -> Cursor`
|
|
83
|
+
- `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`.
|
|
84
|
+
- `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.
|
|
85
|
+
- `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`
|
|
86
|
+
- `Cursor.fetchmany_arrow(size) -> arro3.core.Table`, `Cursor.fetchall_arrow() -> arro3.core.Table`
|
|
87
|
+
- `Cursor` is an async iterator, yielding one row (tuple) at a time.
|
|
88
|
+
- `Cursor.description` -- DB-API-style `[(name, type_name, None, None, None, None, None), ...]` after `execute()`.
|
|
89
|
+
- `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).
|
|
90
|
+
- `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, ...)` -- the lower-level client `Connection` wraps. `client.execute_json_statement(sql, ...)` for plain JSON rows with no Arrow parse at all; `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API.
|
|
91
|
+
- `write_ipc_stream(table_or_chunk, buf)` -- thin wrapper around `arro3.io.write_ipc_stream` that always writes uncompressed bodies (see below).
|
|
92
|
+
|
|
93
|
+
`Cursor.execute`/`execute_streamed`/`stream_query_json` all accept `catalog`, `schema`, `row_limit`, `offset`, and `total_timeout_s`.
|
|
94
|
+
|
|
95
|
+
## A note on Arrow IPC compression
|
|
96
|
+
|
|
97
|
+
`write_ipc_stream` (and everything in this package that serializes Arrow-IPC bytes) always writes **uncompressed** bodies. arro3's own default (`compression="LZ4"`) is transparently decompressed by DuckDB's Arrow reader, but not necessarily by every other Arrow IPC reader -- notably, `duckdb-wasm`'s browser-side decoder silently fails to parse LZ4-compressed bodies. If you're producing bytes that might be consumed by something other than a Python DuckDB connection, this default matters.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# prek.toml — pre-commit hook configuration
|
|
2
|
+
# Install git hook: prek install
|
|
3
|
+
# Format all files: prek run --all-files
|
|
4
|
+
|
|
5
|
+
[[repos]]
|
|
6
|
+
repo = "https://github.com/astral-sh/ruff-pre-commit"
|
|
7
|
+
rev = "v0.15.1"
|
|
8
|
+
hooks = [
|
|
9
|
+
{ id = "ruff-check", args = ["--fix"] },
|
|
10
|
+
{ id = "ruff-format" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[repos]]
|
|
14
|
+
repo = "local"
|
|
15
|
+
hooks = [
|
|
16
|
+
{ id = "ty", name = "ty check", language = "system", entry = "uv run --frozen ty check src tests", pass_filenames = false, files = '\.py$' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[repos]]
|
|
20
|
+
repo = "https://github.com/pre-commit/pre-commit-hooks"
|
|
21
|
+
rev = "v5.0.0"
|
|
22
|
+
hooks = [
|
|
23
|
+
{ id = "trailing-whitespace" },
|
|
24
|
+
{ id = "end-of-file-fixer" },
|
|
25
|
+
{ id = "check-yaml" },
|
|
26
|
+
{ id = "check-toml" },
|
|
27
|
+
{ id = "check-added-large-files", args = ["--maxkb=1000"] },
|
|
28
|
+
{ id = "check-merge-conflict" },
|
|
29
|
+
{ id = "detect-private-key" },
|
|
30
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "arrowbricks"
|
|
3
|
+
version = "0.1.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. Single Arrow engine (arro3), no DuckDB."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.27",
|
|
10
|
+
"arro3-core>=0.8",
|
|
11
|
+
"arro3-io>=0.8",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Repository = "https://github.com/bmsuisse/arrowbricks"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/arrowbricks"]
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-asyncio>=0.24",
|
|
28
|
+
"respx>=0.21",
|
|
29
|
+
"ruff>=0.6",
|
|
30
|
+
"ty>=0.0.65",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
asyncio_mode = "auto"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 120
|
|
38
|
+
target-version = "py311"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "S", "B"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint.per-file-ignores]
|
|
44
|
+
"tests/*" = ["S101", "S106"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ._streaming import (
|
|
2
|
+
HEARTBEAT,
|
|
3
|
+
QueryTimeout,
|
|
4
|
+
ReplayableArrowChunk,
|
|
5
|
+
await_with_heartbeat,
|
|
6
|
+
stream_query_json,
|
|
7
|
+
write_ipc_stream,
|
|
8
|
+
)
|
|
9
|
+
from .client import DatabricksClient
|
|
10
|
+
from .cursor import Connection, Cursor, connect
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"HEARTBEAT",
|
|
14
|
+
"Connection",
|
|
15
|
+
"Cursor",
|
|
16
|
+
"DatabricksClient",
|
|
17
|
+
"QueryTimeout",
|
|
18
|
+
"ReplayableArrowChunk",
|
|
19
|
+
"await_with_heartbeat",
|
|
20
|
+
"connect",
|
|
21
|
+
"stream_query_json",
|
|
22
|
+
"write_ipc_stream",
|
|
23
|
+
]
|