Skip to content

Commit 25bfda9

Browse files
committed
Extend xr.open_dataset
1 parent f1ae32e commit 25bfda9

File tree

8 files changed

+180
-130
lines changed

8 files changed

+180
-130
lines changed

.gitignore

Lines changed: 4 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,5 @@
1-
# Byte-compiled / optimized / DLL files
2-
__pycache__/
3-
*.py[cod]
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-
pip-wheel-metadata/
24-
share/python-wheels/
25-
*.egg-info/
26-
.installed.cfg
27-
*.egg
28-
MANIFEST
29-
30-
# PyInstaller
31-
# Usually these files are written by a python script from a template
32-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
33-
*.manifest
34-
*.spec
35-
36-
# Installer logs
37-
pip-log.txt
38-
pip-delete-this-directory.txt
39-
40-
# Unit test / coverage reports
41-
htmlcov/
42-
.tox/
43-
.nox/
1+
__pycache__
442
.coverage
45-
.coverage.*
46-
.cache
47-
nosetests.xml
48-
coverage.xml
49-
*.cover
50-
*.py,cover
51-
.hypothesis/
52-
.pytest_cache/
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-
target/
76-
77-
# Jupyter Notebook
78-
.ipynb_checkpoints
79-
80-
# IPython
81-
profile_default/
82-
ipython_config.py
83-
84-
# pyenv
85-
.python-version
86-
87-
# pipenv
88-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
90-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
91-
# install all needed dependencies.
92-
#Pipfile.lock
93-
94-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
95-
__pypackages__/
96-
97-
# Celery stuff
98-
celerybeat-schedule
99-
celerybeat.pid
100-
101-
# SageMath parsed files
102-
*.sage.py
103-
104-
# Environments
105-
.env
106-
.venv
107-
env/
108-
venv/
109-
ENV/
110-
env.bak/
111-
venv.bak/
112-
113-
# Spyder project settings
114-
.spyderproject
115-
.spyproject
116-
117-
# Rope project settings
118-
.ropeproject
119-
120-
# mkdocs documentation
121-
/site
122-
123-
# mypy
124-
.mypy_cache/
125-
.dmypy.json
126-
dmypy.json
127-
128-
# Pyre type checker
129-
.pyre/
3+
htmlcov
4+
dist
5+
.direnv

.pre-commit-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
repos:
3+
- repo: https://github.com/psf/black
4+
rev: "22.12.0"
5+
hooks:
6+
- id: black
7+
additional_dependencies: ['click==8.0.4']
8+
- repo: https://github.com/pycqa/flake8
9+
rev: 6.0.0
10+
hooks:
11+
- id: flake8
12+
language_version: python3

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,30 @@
1-
# xarray-pystac-backend
2-
For extending xarray.open_dataset to accept pystac objects
1+
# xpystac
2+
For extending `xarray.open_dataset` to accept pystac objects
3+
4+
## Example
5+
6+
```python
7+
from pystac_client import Client
8+
import xarray as xr
9+
10+
11+
client = Client.open("https://earth-search.aws.element84.com/v0")
12+
13+
search = client.search(
14+
intersects=dict(type="Point", coordinates=[-105.78, 35.79]),
15+
collections=['sentinel-s2-l2a-cogs'],
16+
datetime="2020-04-01/2020-05-01",
17+
)
18+
19+
xr.open_dataset(search, engine="stac")
20+
```
21+
22+
## Install
23+
24+
```bash
25+
pip install git+https://github.com/jsignell/xpystac
26+
```
27+
28+
## Prior Art
29+
30+
This work is inspired by https://github.com/TomAugspurger/staccontainers

pyproject.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[build-system]
2+
requires = ["flit_core >=2,<4"]
3+
build-backend = "flit_core.buildapi"
4+
5+
[project]
6+
name = "xpystac"
7+
author = "Julia Signell"
8+
author-email = "[email protected]"
9+
classifiers = [ "License :: OSI Approved :: MIT License",]
10+
dependencies = [
11+
"xarray",
12+
"pystac>=1.0.0b3",
13+
]
14+
requires-python = ">=3.8"
15+
description = "Extend xarray.open_dataset to accept pystac objects"
16+
license = {text = "MIT"}
17+
readme = "README.md"
18+
dynamic = ["version"]
19+
20+
[project.optional-dependencies]
21+
dev = [
22+
"pytest",
23+
"pre-commit",
24+
]
25+
26+
[project.entry-points."xarray.backends"]
27+
stac = "xpystac.xarray_plugin:STACBackend"

setup.cfg

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[flake8]
2+
# References:
3+
# https://flake8.readthedocs.io/en/latest/user/configuration.html
4+
# https://flake8.readthedocs.io/en/latest/user/error-codes.html
5+
# https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
6+
exclude = __init__.py
7+
ignore =
8+
E20,
9+
E231,
10+
E241,
11+
E26,
12+
E4,
13+
E721,
14+
E731,
15+
E741,
16+
W503,
17+
W504,
18+
F811,
19+
max-line-length = 120

xpystac/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.0.1"

xpystac/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import importlib
2+
3+
4+
def _import_optional_dependency(name):
5+
try:
6+
module = importlib.import_module(name)
7+
except ImportError as e:
8+
raise ImportError(f"Missing optional dependency '{name}'") from e
9+
return module

xpystac/xarray_plugin.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import functools
2+
3+
import pystac
4+
import xarray
5+
from xarray.backends import BackendEntrypoint
6+
7+
from .utils import _import_optional_dependency
8+
9+
10+
@functools.singledispatch
11+
def to_xarray(item, **kwargs) -> xarray.Dataset:
12+
if hasattr(item, "get_all_items"):
13+
item_collection = item.get_all_items()
14+
return to_xarray(item_collection)
15+
raise TypeError
16+
17+
18+
@to_xarray.register(pystac.Item)
19+
@to_xarray.register(pystac.ItemCollection)
20+
def _(
21+
obj: pystac.Item | pystac.ItemCollection,
22+
drop_variables: str | list[str] = None,
23+
**kwargs,
24+
) -> xarray.Dataset:
25+
stackstac = _import_optional_dependency("stackstac")
26+
if drop_variables is not None:
27+
raise KeyError("``drop_variables`` not implemented for pystac items")
28+
return stackstac.stack(obj, **kwargs).to_dataset(dim="band", promote_attrs=True)
29+
30+
31+
@to_xarray.register
32+
def _(obj: pystac.Asset, **kwargs) -> xarray.Dataset:
33+
open_kwargs = obj.extra_fields.get("xarray:open_kwargs", {})
34+
35+
if obj.media_type == pystac.MediaType.JSON and "index" in obj.roles:
36+
requests = _import_optional_dependency("requests")
37+
fsspec = _import_optional_dependency("fsspec")
38+
r = requests.get(obj.href)
39+
r.raise_for_status()
40+
try:
41+
import planetary_computer
42+
43+
refs = planetary_computer.sign(r.json())
44+
except ImportError:
45+
refs = r.json()
46+
mapper = fsspec.get_mapper("reference://", fo=refs)
47+
default_kwargs = {"engine": "zarr", "consolidated": False, "chunks": {}}
48+
return xarray.open_dataset(mapper, **default_kwargs, **open_kwargs, **kwargs)
49+
50+
if obj.media_type == pystac.MediaType.COG:
51+
default_kwargs = {"engine", "rasterio"}
52+
elif obj.media_type == "application/vnd+zarr":
53+
default_kwargs = {"engine": "zarr"}
54+
else:
55+
default_kwargs = {}
56+
57+
ds = xarray.open_dataset(obj.href, **default_kwargs, **open_kwargs, **kwargs)
58+
return ds
59+
60+
61+
class STACBackend(BackendEntrypoint):
62+
def open_dataset(
63+
self,
64+
obj,
65+
*,
66+
drop_variables: str | list[str] = None,
67+
**kwargs,
68+
):
69+
return to_xarray(obj, drop_variables=drop_variables, **kwargs)
70+
71+
open_dataset_parameters = ["obj", "drop_variables"]
72+
73+
def guess_can_open(self, obj):
74+
return isinstance(obj, (pystac.Asset, pystac.Item, pystac.ItemCollection))
75+
76+
description = "Open pystac objects in Xarray"
77+
78+
url = "https://github.com/jsignell/xpystac"

0 commit comments

Comments
 (0)