Skip to content

Commit c1ae775

Browse files
robert3005claude
andauthored
Push hugging face storage resolution into vortex-cloud rather than vortex-python (#9199)
We should treat hf:// urls as any other object store, right now we are keeping all of it in vortex-python which is not quite as versatile as having a full fledged object store. fixes #5379 --------- Signed-off-by: Robert Kruszewski <robert@spiraldb.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 27859f7 commit c1ae775

30 files changed

Lines changed: 1282 additions & 172 deletions

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ glob = "0.3.2"
170170
goldenfile = "1"
171171
half = { version = "2.7.1", features = ["std", "num-traits"] }
172172
hashbrown = "0.17.1"
173+
http = "1.5.0"
173174
humansize = "2.1.3"
174175
indicatif = "0.18.0"
175176
insta = "1.43"
@@ -207,6 +208,7 @@ parquet-variant = "58.3"
207208
parquet-variant-compute = "58.3"
208209
paste = "1.0.15"
209210
pco = "1.0.1"
211+
percent-encoding = "2.3.2"
210212
pin-project-lite = "0.2.15"
211213
primitive-types = { version = "0.14.0" }
212214
proc-macro2 = "1.0.95"

docs/api/python/datasets.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ transforms.
1919

2020
Hub repositories are streamed in place: files are read with HTTP range requests, so only the
2121
projected columns and matching rows are ever transferred. Private and gated repositories
22-
authenticate with the ``token`` argument or the locally saved login. Files are downloaded (with
23-
the usual Hub caching) only when ``streaming=False`` or ``local_files_only=True``.
22+
authenticate with the ``token`` argument, ``HF_TOKEN``, or the locally saved login — see
23+
:doc:`store/huggingface` for the full precedence. Files are downloaded (with the usual Hub
24+
caching) only when ``streaming=False`` or ``local_files_only=True``.
2425

2526
.. code-block:: python
2627

docs/api/python/store.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Vortex arrays support reading and writing to many object storage systems:
1111
store/gcs
1212
store/azure
1313
store/http
14+
store/huggingface
1415
store/local
1516
store/memory
1617
store/opendal
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
==================
2+
Hugging Face Hub
3+
==================
4+
5+
Vortex reads Hugging Face Hub repositories over ``hf://`` URLs. A Hub repository is a set of files
6+
behind an HTTP endpoint that honours range requests, so no cloud SDK is involved and no extra build
7+
feature is needed.
8+
9+
.. list-table::
10+
:header-rows: 1
11+
12+
* - URL
13+
- Repository kind
14+
* - ``hf://datasets/<owner>/<name>[@<revision>][/<path>]``
15+
- Dataset
16+
* - ``hf://spaces/<owner>/<name>[@<revision>][/<path>]``
17+
- Space
18+
* - ``hf://<owner>/<name>[@<revision>][/<path>]``
19+
- Model
20+
21+
``<revision>`` is a branch, tag or commit, defaulting to ``main``. A revision containing ``/`` must
22+
be percent-encoded, e.g. ``hf://datasets/org/name@refs%2Fconvert%2Fparquet/data/train.vortex``.
23+
24+
Configuration comes from the same environment variables ``huggingface_hub`` reads:
25+
26+
.. list-table::
27+
:header-rows: 1
28+
29+
* - Variable
30+
- Meaning
31+
* - ``HF_TOKEN``
32+
- API token for private and gated repositories. Falls back to the token file at
33+
``HF_TOKEN_PATH``, then ``$HF_HOME/token``, then ``$HOME/.cache/huggingface/token``.
34+
* - ``HF_ENDPOINT``
35+
- Hub endpoint, defaulting to ``https://huggingface.co``.
36+
37+
Reading from the Hub
38+
====================
39+
40+
Pass an ``hf://`` URL directly. Public repositories need no credentials; private and gated ones
41+
authenticate from ``HF_TOKEN`` or the saved login:
42+
43+
.. code-block:: python
44+
45+
import vortex as vx
46+
47+
vxf = vx.open("hf://datasets/org/name/data/train.vortex")
48+
for batch in vxf.to_arrow():
49+
...
50+
51+
:class:`vortex.store.HfStore`
52+
=============================
53+
54+
.. py:class:: vortex.store.HfStore(repo_id, *, repo_type="dataset", revision=None, token=None, endpoint=None)
55+
56+
A Hugging Face Hub object store, rooted at one repository and revision.
57+
58+
A URL is enough for most reads, so reach for this class only for the two things a URL cannot
59+
express: a token held in a variable rather than the environment, and a read that must stay
60+
anonymous even though the environment offers credentials.
61+
62+
Because the store is rooted at the repository and revision, the path passed alongside it is a
63+
path *within* the repository.
64+
65+
:param repo_id: The repository, as ``"<owner>/<name>"``.
66+
:param repo_type: ``"dataset"``, ``"model"`` or ``"space"``. Defaults to ``"dataset"``.
67+
:param revision: A branch, tag or commit. Defaults to ``main``. Unlike in a URL, a revision
68+
containing ``/`` is passed literally — the store percent-encodes it.
69+
:param token: ``None`` (the default) or ``True`` authenticates from ``HF_TOKEN`` or the saved
70+
login; ``False`` forces an anonymous read even when credentials are available; a string is
71+
used as the token directly.
72+
:param endpoint: Hub endpoint. Defaults to ``HF_ENDPOINT``, then ``https://huggingface.co``.
73+
74+
.. code-block:: python
75+
76+
import vortex as vx
77+
from vortex.store import HfStore
78+
79+
store = HfStore("org/name", revision="refs/convert/parquet", token="hf_...")
80+
81+
# With `store=`, the path is a path within the repository.
82+
vxf = vx.open("data/train.vortex", store=store)
83+
84+
Listing
85+
=======
86+
87+
The Hub does not implement WebDAV ``PROPFIND``, which is how object-store HTTP listing works, so a
88+
Hub store cannot list a prefix. Opening a known path works, since that is a ``HEAD`` plus ranged
89+
``GET``. To expand a glob, list the repository through the Hub's own API first — which is what
90+
``vortex.datasets.load_dataset`` does — and then open each path it returns.
91+
92+
Hugging Face Datasets
93+
=====================
94+
95+
``vortex.datasets.load_dataset`` builds on this to load Vortex files from the Hub as Hugging Face
96+
``Datasets`` objects, expanding globs and pushing projections, filters and row limits into each
97+
scan. See :doc:`../datasets`.

docs/api/python/store/opendal.rst

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Vortex can read from and write to Tencent Cloud COS, Alibaba Cloud OSS, and Tenc
66
GooseFS through `OpenDAL <https://opendal.apache.org/>`_, which provides native service
77
support.
88

9-
These stores are available only when Vortex is built with the ``opendal`` feature
10-
(e.g. ``maturin develop --features opendal`` or ``cargo build -p vortex-jni --features opendal``).
9+
The Python bindings always include these stores. Other consumers opt in with the ``opendal``
10+
Cargo feature (e.g. ``cargo build -p vortex-jni --features opendal``).
1111

1212
.. list-table::
1313
:header-rows: 1
@@ -39,9 +39,6 @@ These stores are available only when Vortex is built with the ``opendal`` featur
3939
:func:`vortex.io.read_url` / :func:`vortex.io.write` via the ``store=`` argument,
4040
exactly like the built-in S3/Azure/GCS stores.
4141

42-
The class is only available when Vortex is built with the ``opendal`` feature; on
43-
a default build, instantiating it raises :class:`ImportError`.
44-
4542
:param bucket: COS bucket name (e.g. ``"my-bucket"``).
4643
:param endpoint: COS endpoint (e.g. ``"https://cos.ap-guangzhou.myqcloud.com"``).
4744
:param secret_id: Optional Tencent Cloud secret id. Maps to the ``TENCENTCLOUD_SECRET_ID``
@@ -62,9 +59,6 @@ These stores are available only when Vortex is built with the ``opendal`` featur
6259
:func:`vortex.io.read_url` / :func:`vortex.io.write` via the ``store=`` argument,
6360
exactly like the built-in S3/Azure/GCS stores.
6461

65-
The class is only available when Vortex is built with the ``opendal`` feature; on
66-
a default build, instantiating it raises :class:`ImportError`.
67-
6862
:param master_addr: GooseFS master address(es). Single master:
6963
``"10.0.0.1:9200"``. HA (comma-separated):
7064
``"10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200"``.

docs/conf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
# classes are fully documented in `opendal.rst`; the private paths are intentionally not.
5858
("py:class", "vortex.store._cos.CosStore"),
5959
("py:class", "vortex.store._goosefs.GoosefsStore"),
60+
# `vortex.store.CosStore` / `GoosefsStore` / `HfStore` are the native classes re-exported
61+
# through private modules, so annotations resolve to their `vortex._lib` module paths. The
62+
# public classes are fully documented in `opendal.rst` / `huggingface.rst`; the native paths
63+
# are intentionally not.
64+
("py:class", "vortex._lib.CosStore"),
65+
("py:class", "vortex._lib.GoosefsStore"),
66+
("py:class", "vortex._lib.HfStore"),
6067
]
6168

6269
doctest_global_setup = "import pyarrow; import vortex; import vortex as vx; import random; random.seed(a=0)"

vortex-cloud/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ categories = { workspace = true }
1717
all-features = true
1818

1919
[dependencies]
20+
http = { workspace = true, optional = true }
2021
object_store = { workspace = true, features = ["fs"] }
2122
object_store_opendal = { workspace = true, optional = true }
2223
opendal = { workspace = true, optional = true }
2324
parking_lot = { workspace = true, optional = true }
25+
percent-encoding = { workspace = true, optional = true }
2426
tracing = { workspace = true, optional = true }
2527
url = { workspace = true }
2628
vortex-utils = { workspace = true }
2729

30+
[dev-dependencies]
31+
rstest = { workspace = true }
32+
tempfile = { workspace = true }
33+
2834
[features]
2935
default = []
3036
# The URL -> ObjectStore registry, plus the cloud backends it resolves URLs to. Kept optional so
@@ -36,6 +42,9 @@ registry = [
3642
"object_store/gcp",
3743
"object_store/http",
3844
]
45+
# The Hugging Face Hub, the `hf://` scheme. Served over `object_store`'s HTTP store, so it adds no
46+
# cloud SDK of its own.
47+
hf = ["dep:http", "dep:percent-encoding", "object_store/http"]
3948
# Tencent Cloud COS, the `cos://` scheme.
4049
cos = [
4150
"dep:opendal",

0 commit comments

Comments
 (0)