Skip to content

Commit 54bc685

Browse files
authored
Define reader usable by Xarray (#1)
* Define reader usable by Xarray * Use uv in workflow
1 parent a1fddbb commit 54bc685

File tree

6 files changed

+162
-52
lines changed

6 files changed

+162
-52
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,17 @@ jobs:
2828
- uses: actions/checkout@v4
2929
with:
3030
fetch-depth: 0 # grab all branches and tags
31+
- name: Install uv
32+
uses: astral-sh/setup-uv@v7
33+
with:
34+
enable-cache: true
3135
- name: Set up Python
3236
uses: actions/setup-python@v5
3337
with:
3438
python-version: ${{ matrix.python-version }}
35-
cache: 'pip'
36-
- name: Install Hatch
37-
run: |
38-
python -m pip install --upgrade pip
39-
pip install hatch
40-
- name: Set Up Hatch Env
41-
run: |
42-
hatch env create upstream
43-
hatch env run -e upstream list-env
4439
- name: Run Tests
45-
env:
46-
HYPOTHESIS_PROFILE: ci
4740
run: |
48-
hatch env run --env upstream run-coverage
41+
uv run --all-groups pytest --cov-config=pyproject.toml --cov=pkg --cov-report xml --cov=src
4942
- name: Upload coverage
5043
uses: codecov/codecov-action@v5
5144
with:

pyproject.toml

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Documentation = "https://github.com/virtual-zarr/obspec-utils#readme"
3737
Issues = "https://github.com/virtual-zarr/obspec-utils/issues"
3838
Source = "https://github.com/virtual-zarr/obspec-utils"
3939

40-
[project.optional-dependencies]
40+
[dependency-groups]
4141
test = [
4242
"coverage",
4343
"pytest",
@@ -47,12 +47,18 @@ test = [
4747
"rich",
4848
"mypy",
4949
"pytest-xdist",
50+
"minio",
51+
]
52+
xarray = [
53+
"xarray",
54+
"h5netcdf",
55+
]
56+
fsspec = [
57+
"s3fs",
58+
"fsspec",
5059
]
51-
52-
[dependency-groups]
5360
dev = [
5461
"ipykernel>=6.29.5",
55-
"pip>=25.0.1",
5662
]
5763

5864
[tool.hatch.metadata]
@@ -64,41 +70,6 @@ version.source = "vcs"
6470
[tool.hatch.build]
6571
hooks.vcs.version-file = "src/obspec_utils/_version.py"
6672

67-
[tool.hatch.envs.types]
68-
extra-dependencies = [
69-
"mypy>=1.0.0",
70-
]
71-
[tool.hatch.envs.types.scripts]
72-
check = "mypy --install-types --non-interactive {args:src/obspec_utils tests}"
73-
74-
75-
[tool.hatch.envs.test]
76-
features = ["test"]
77-
78-
[tool.hatch.envs.test.scripts]
79-
run-coverage = "pytest --cov-config=pyproject.toml --cov=pkg --cov-report xml --cov=src --junitxml=junit.xml -o junit_family=legacy"
80-
run-coverage-html = "pytest --cov-config=pyproject.toml --cov=pkg --cov-report html --cov=src"
81-
run-pytest = "run-coverage --no-cov"
82-
run-verbose = "run-coverage --verbose"
83-
run-mypy = "mypy src"
84-
list-env = "pip list"
85-
86-
[tool.hatch.envs.upstream]
87-
python = "3.13"
88-
dependencies = [
89-
'obstore @ git+https://github.com/developmentseed/obstore@main#subdirectory=obstore',
90-
'obspec @ git+https://github.com/developmentseed/obspec',
91-
]
92-
features = ["test"]
93-
94-
[tool.hatch.envs.upstream.scripts]
95-
run-coverage = "pytest --cov-config=pyproject.toml --cov=pkg --cov-report xml --cov=src --junitxml=junit.xml -o junit_family=legacy"
96-
run-coverage-html = "pytest --cov-config=pyproject.toml --cov=pkg --cov-report html --cov=src"
97-
run-pytest = "run-coverage --no-cov"
98-
run-verbose = "run-coverage --verbose"
99-
run-mypy = "mypy src"
100-
list-env = "pip list"
101-
10273
[tool.coverage.run]
10374
source_pkgs = ["obspec_utils", "tests"]
10475
branch = true

src/obspec_utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from ._version import __version__
2+
from .file_handlers import ObstoreReader
23

3-
__all__ = ["__version__"]
4+
__all__ = ["__version__", "ObstoreReader"]

src/obspec_utils/file_handlers.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import obstore as obs
6+
7+
if TYPE_CHECKING:
8+
from obstore import ReadableFile
9+
from obstore.store import ObjectStore
10+
11+
12+
class ObstoreReader:
13+
_reader: ReadableFile
14+
15+
def __init__(self, store: ObjectStore, path: str) -> None:
16+
"""
17+
Create an obstore file reader that implements the read, readall, seek, and tell methods, which
18+
can be used in libraries that expect file-like objects.
19+
20+
This wrapper is necessary in order to return Python bytes types rather than obstore Bytes buffers.
21+
22+
Parameters
23+
----------
24+
store
25+
[ObjectStore][obstore.store.ObjectStore] for reading the file.
26+
path
27+
The path to the file within the store. This should not include the prefix.
28+
"""
29+
self._reader = obs.open_reader(store, path)
30+
31+
def read(self, size: int, /) -> bytes:
32+
return self._reader.read(size).to_bytes()
33+
34+
def readall(self) -> bytes:
35+
return self._reader.read().to_bytes()
36+
37+
def seek(self, offset: int, whence: int = 0, /):
38+
# TODO: Check on default for whence
39+
return self._reader.seek(offset, whence)
40+
41+
def tell(self) -> int:
42+
return self._reader.tell()

tests/conftest.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import xarray as xr
2+
3+
import json
4+
import time
5+
6+
import pytest
7+
from pathlib import Path
8+
9+
10+
@pytest.fixture(scope="session")
11+
def container():
12+
import docker
13+
14+
client = docker.from_env()
15+
port = 9000
16+
minio_container = client.containers.run(
17+
"quay.io/minio/minio",
18+
"server /data",
19+
detach=True,
20+
ports={f"{port}/tcp": port},
21+
environment={
22+
"MINIO_ACCESS_KEY": "minioadmin",
23+
"MINIO_SECRET_KEY": "minioadmin",
24+
},
25+
)
26+
time.sleep(3) # give it time to boot
27+
# enter
28+
yield {
29+
"port": port,
30+
"endpoint": f"http://localhost:{port}",
31+
"username": "minioadmin",
32+
"password": "minioadmin",
33+
}
34+
# exit
35+
minio_container.stop()
36+
minio_container.remove()
37+
38+
39+
@pytest.fixture(scope="session")
40+
def minio_bucket(container):
41+
# Setup with guidance from https://medium.com/@sant1/using-minio-with-docker-and-python-cbbad397cb5d
42+
from minio import Minio
43+
44+
bucket = "my-bucket"
45+
filename = "test.nc"
46+
# Initialize MinIO client
47+
client = Minio(
48+
"localhost:9000",
49+
access_key=container["username"],
50+
secret_key=container["password"],
51+
secure=False,
52+
)
53+
client.make_bucket(bucket)
54+
policy = {
55+
"Version": "2012-10-17",
56+
"Statement": [
57+
{
58+
"Effect": "Allow",
59+
"Principal": {"AWS": "*"},
60+
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
61+
"Resource": "arn:aws:s3:::my-bucket",
62+
},
63+
{
64+
"Effect": "Allow",
65+
"Principal": {"AWS": "*"},
66+
"Action": [
67+
"s3:GetObject",
68+
"s3:GetObjectRetention",
69+
"s3:GetObjectLegalHold",
70+
],
71+
"Resource": "arn:aws:s3:::my-bucket/*",
72+
},
73+
],
74+
}
75+
client.set_bucket_policy(bucket, json.dumps(policy))
76+
yield {
77+
"port": container["port"],
78+
"endpoint": container["endpoint"],
79+
"username": container["username"],
80+
"password": container["password"],
81+
"bucket": bucket,
82+
"file": filename,
83+
"client": client,
84+
}
85+
86+
87+
@pytest.fixture
88+
def local_netcdf4_file(tmp_path: Path) -> str:
89+
"""Create a NetCDF4 file with data in multiple groups."""
90+
filepath = tmp_path / "test.nc"
91+
ds1 = xr.DataArray([1, 2, 3], name="foo").to_dataset()
92+
ds1.to_netcdf(filepath)
93+
return str(filepath)

tests/test_xarray.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import xarray as xr
2+
from obspec_utils import ObstoreReader
3+
from obstore.store import LocalStore
4+
5+
6+
def test_local_reader(local_netcdf4_file) -> None:
7+
ds_fsspec = xr.open_dataset(local_netcdf4_file, engine="h5netcdf")
8+
reader = ObstoreReader(store=LocalStore(), path=local_netcdf4_file)
9+
ds_obstore = xr.open_dataset(reader, engine="h5netcdf")
10+
xr.testing.assert_allclose(ds_fsspec, ds_obstore)

0 commit comments

Comments
 (0)