tilebox-storage 0.46.0__py3-none-any.whl → 0.48.0__py3-none-any.whl
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.
- tilebox/storage/__init__.py +12 -0
- tilebox/storage/aio.py +92 -12
- tilebox/storage/granule.py +27 -2
- {tilebox_storage-0.46.0.dist-info → tilebox_storage-0.48.0.dist-info}/METADATA +2 -1
- tilebox_storage-0.48.0.dist-info/RECORD +7 -0
- tilebox_storage-0.46.0.dist-info/RECORD +0 -7
- {tilebox_storage-0.46.0.dist-info → tilebox_storage-0.48.0.dist-info}/WHEEL +0 -0
tilebox/storage/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
from tilebox.storage.aio import ASFStorageClient as _ASFStorageClient
|
|
4
4
|
from tilebox.storage.aio import CopernicusStorageClient as _CopernicusStorageClient
|
|
5
|
+
from tilebox.storage.aio import LocalFileSystemStorageClient as _LocalFileSystemStorageClient
|
|
5
6
|
from tilebox.storage.aio import UmbraStorageClient as _UmbraStorageClient
|
|
6
7
|
from tilebox.storage.aio import USGSLandsatStorageClient as _USGSLandsatStorageClient
|
|
7
8
|
|
|
@@ -66,3 +67,14 @@ class USGSLandsatStorageClient(_USGSLandsatStorageClient):
|
|
|
66
67
|
"""
|
|
67
68
|
super().__init__(cache_directory)
|
|
68
69
|
self._syncify()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LocalFileSystemStorageClient(_LocalFileSystemStorageClient):
|
|
73
|
+
def __init__(self, root: Path) -> None:
|
|
74
|
+
"""A tilebox storage client for accessing data on a local file system, or a mounted network file system.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
root: The root directory of the file system to access.
|
|
78
|
+
"""
|
|
79
|
+
super().__init__(root)
|
|
80
|
+
self._syncify()
|
tilebox/storage/aio.py
CHANGED
|
@@ -7,6 +7,7 @@ import zipfile
|
|
|
7
7
|
from asyncio import Queue, QueueEmpty
|
|
8
8
|
from collections.abc import AsyncIterator
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from pathlib import PurePosixPath as ObjectPath
|
|
10
11
|
from typing import Any, TypeAlias
|
|
11
12
|
|
|
12
13
|
import anyio
|
|
@@ -23,13 +24,14 @@ from _tilebox.grpc.aio.syncify import Syncifiable
|
|
|
23
24
|
from tilebox.storage.granule import (
|
|
24
25
|
ASFStorageGranule,
|
|
25
26
|
CopernicusStorageGranule,
|
|
27
|
+
LocationStorageGranule,
|
|
26
28
|
UmbraStorageGranule,
|
|
27
29
|
USGSLandsatStorageGranule,
|
|
28
30
|
)
|
|
29
31
|
from tilebox.storage.providers import login
|
|
30
32
|
|
|
31
33
|
try:
|
|
32
|
-
from IPython.display import HTML, Image, display
|
|
34
|
+
from IPython.display import HTML, Image, display
|
|
33
35
|
except ImportError:
|
|
34
36
|
# IPython is not available, so we can't display the quicklook image
|
|
35
37
|
# but let's define stubs for the type checker
|
|
@@ -240,6 +242,10 @@ def _display_quicklook(image_data: bytes | Path, width: int, height: int, image_
|
|
|
240
242
|
|
|
241
243
|
|
|
242
244
|
class StorageClient(Syncifiable):
|
|
245
|
+
"""Base class for all storage clients."""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class CachingStorageClient(StorageClient):
|
|
243
249
|
def __init__(self, cache_directory: Path | None) -> None:
|
|
244
250
|
self._cache = cache_directory
|
|
245
251
|
|
|
@@ -259,8 +265,8 @@ class StorageClient(Syncifiable):
|
|
|
259
265
|
|
|
260
266
|
async def list_object_paths(store: ObjectStore, prefix: str) -> list[str]:
|
|
261
267
|
objects = await obs.list(store, prefix).collect_async()
|
|
262
|
-
prefix_path =
|
|
263
|
-
return sorted(str(
|
|
268
|
+
prefix_path = ObjectPath(prefix)
|
|
269
|
+
return sorted(str(ObjectPath(obj["path"]).relative_to(prefix_path)) for obj in objects)
|
|
264
270
|
|
|
265
271
|
|
|
266
272
|
async def download_objects( # noqa: PLR0913
|
|
@@ -299,7 +305,7 @@ async def _download_worker(
|
|
|
299
305
|
async def _download_object(
|
|
300
306
|
store: ObjectStore, prefix: str, obj: str, output_dir: Path, show_progress: bool = True
|
|
301
307
|
) -> Path:
|
|
302
|
-
key = str(
|
|
308
|
+
key = str(ObjectPath(prefix) / obj)
|
|
303
309
|
output_path = output_dir / obj
|
|
304
310
|
if output_path.exists(): # already cached
|
|
305
311
|
return output_path
|
|
@@ -322,7 +328,7 @@ async def _download_object(
|
|
|
322
328
|
return output_path
|
|
323
329
|
|
|
324
330
|
|
|
325
|
-
class ASFStorageClient(
|
|
331
|
+
class ASFStorageClient(CachingStorageClient):
|
|
326
332
|
def __init__(self, user: str, password: str, cache_directory: Path = Path.home() / ".cache" / "tilebox") -> None:
|
|
327
333
|
"""A tilebox storage client that downloads data from the Alaska Satellite Facility.
|
|
328
334
|
|
|
@@ -414,7 +420,7 @@ class ASFStorageClient(StorageClient):
|
|
|
414
420
|
"""
|
|
415
421
|
granule = ASFStorageGranule.from_data(datapoint)
|
|
416
422
|
if Image is None:
|
|
417
|
-
raise ImportError("IPython is not available, please use
|
|
423
|
+
raise ImportError("IPython is not available, please use download_quicklook instead.")
|
|
418
424
|
quicklook = await self._download_quicklook(datapoint)
|
|
419
425
|
_display_quicklook(quicklook, width, height, f"<code>Image {quicklook.name} © ASF {granule.time.year}</code>")
|
|
420
426
|
|
|
@@ -438,7 +444,7 @@ def _umbra_s3_prefix(datapoint: xr.Dataset | UmbraStorageGranule) -> str:
|
|
|
438
444
|
return f"sar-data/tasks/{granule.location}/"
|
|
439
445
|
|
|
440
446
|
|
|
441
|
-
class UmbraStorageClient(
|
|
447
|
+
class UmbraStorageClient(CachingStorageClient):
|
|
442
448
|
_STORAGE_PROVIDER = "Umbra"
|
|
443
449
|
_BUCKET = "umbra-open-data-catalog"
|
|
444
450
|
_REGION = "us-west-2"
|
|
@@ -538,7 +544,7 @@ def _copernicus_s3_prefix(datapoint: xr.Dataset | CopernicusStorageGranule) -> s
|
|
|
538
544
|
return granule.location.removeprefix("/eodata/")
|
|
539
545
|
|
|
540
546
|
|
|
541
|
-
class CopernicusStorageClient(
|
|
547
|
+
class CopernicusStorageClient(CachingStorageClient):
|
|
542
548
|
_STORAGE_PROVIDER = "CopernicusDataspace"
|
|
543
549
|
_BUCKET = "eodata"
|
|
544
550
|
_ENDPOINT_URL = "https://eodata.dataspace.copernicus.eu"
|
|
@@ -609,7 +615,7 @@ class CopernicusStorageClient(StorageClient):
|
|
|
609
615
|
granule = CopernicusStorageGranule.from_data(datapoint)
|
|
610
616
|
# special handling for Sentinel-5P, where the location is not a folder but a single file
|
|
611
617
|
if granule.location.endswith(".nc"):
|
|
612
|
-
return [
|
|
618
|
+
return [str(ObjectPath(granule.granule_name))]
|
|
613
619
|
|
|
614
620
|
return await list_object_paths(self._store, _copernicus_s3_prefix(granule))
|
|
615
621
|
|
|
@@ -723,7 +729,7 @@ class CopernicusStorageClient(StorageClient):
|
|
|
723
729
|
ValueError: If no quicklook is available for the given datapoint.
|
|
724
730
|
"""
|
|
725
731
|
if Image is None:
|
|
726
|
-
raise ImportError("IPython is not available, please use
|
|
732
|
+
raise ImportError("IPython is not available, please use download_quicklook instead.")
|
|
727
733
|
granule = CopernicusStorageGranule.from_data(datapoint)
|
|
728
734
|
quicklook = await self._download_quicklook(granule)
|
|
729
735
|
_display_quicklook(quicklook, width, height, f"<code>{granule.granule_name} © ESA {granule.time.year}</code>")
|
|
@@ -749,7 +755,7 @@ def _landsat_s3_prefix(datapoint: xr.Dataset | USGSLandsatStorageGranule) -> str
|
|
|
749
755
|
return granule.location.removeprefix("s3://usgs-landsat/")
|
|
750
756
|
|
|
751
757
|
|
|
752
|
-
class USGSLandsatStorageClient(
|
|
758
|
+
class USGSLandsatStorageClient(CachingStorageClient):
|
|
753
759
|
"""
|
|
754
760
|
A client for downloading USGS Landsat data from the usgs-landsat and usgs-landsat-ard S3 bucket.
|
|
755
761
|
|
|
@@ -882,7 +888,7 @@ class USGSLandsatStorageClient(StorageClient):
|
|
|
882
888
|
ValueError: If no quicklook is available for the given datapoint.
|
|
883
889
|
"""
|
|
884
890
|
if Image is None:
|
|
885
|
-
raise ImportError("IPython is not available, please use
|
|
891
|
+
raise ImportError("IPython is not available, please use download_quicklook instead.")
|
|
886
892
|
quicklook = await self._download_quicklook(datapoint)
|
|
887
893
|
_display_quicklook(quicklook, width, height, f"<code>Image {quicklook.name} © USGS</code>")
|
|
888
894
|
|
|
@@ -900,3 +906,77 @@ class USGSLandsatStorageClient(StorageClient):
|
|
|
900
906
|
|
|
901
907
|
await download_objects(self._store, prefix, [granule.thumbnail], output_folder, show_progress=False)
|
|
902
908
|
return output_folder / granule.thumbnail
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
class LocalFileSystemStorageClient(StorageClient):
|
|
912
|
+
def __init__(self, root: Path) -> None:
|
|
913
|
+
"""A tilebox storage client for accessing data on a local file system, or a mounted network file system.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
root: The root directory of the file system to access.
|
|
917
|
+
"""
|
|
918
|
+
super().__init__()
|
|
919
|
+
self._root = Path(root)
|
|
920
|
+
|
|
921
|
+
async def list_objects(self, datapoint: xr.Dataset | LocationStorageGranule) -> list[str]:
|
|
922
|
+
"""List all available objects for a given datapoint."""
|
|
923
|
+
granule = LocationStorageGranule.from_data(datapoint)
|
|
924
|
+
granule_path = self._root / granule.location
|
|
925
|
+
return [p.relative_to(granule_path).as_posix() for p in granule_path.rglob("**/*") if p.is_file()]
|
|
926
|
+
|
|
927
|
+
async def download(
|
|
928
|
+
self,
|
|
929
|
+
datapoint: xr.Dataset | LocationStorageGranule,
|
|
930
|
+
) -> Path:
|
|
931
|
+
"""No-op download method, as the data is already on the local file system.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
datapoint: The datapoint to locate the data for in the local file system.
|
|
935
|
+
|
|
936
|
+
Returns:
|
|
937
|
+
The path to the data on the local file system.
|
|
938
|
+
"""
|
|
939
|
+
granule = LocationStorageGranule.from_data(datapoint)
|
|
940
|
+
granule_path = self._root / granule.location
|
|
941
|
+
if not granule_path.exists():
|
|
942
|
+
raise ValueError(f"Data not found on the local file system: {granule_path}")
|
|
943
|
+
return granule_path
|
|
944
|
+
|
|
945
|
+
async def _download_quicklook(self, datapoint: xr.Dataset | LocationStorageGranule) -> Path:
|
|
946
|
+
granule = LocationStorageGranule.from_data(datapoint)
|
|
947
|
+
if granule.thumbnail is None:
|
|
948
|
+
raise ValueError(f"No quicklook available for {granule.location}")
|
|
949
|
+
quicklook_path = self._root / granule.thumbnail
|
|
950
|
+
if not quicklook_path.exists():
|
|
951
|
+
raise ValueError(f"Quicklook not found on the local file system: {quicklook_path}")
|
|
952
|
+
return quicklook_path
|
|
953
|
+
|
|
954
|
+
async def download_quicklook(self, datapoint: xr.Dataset | LocationStorageGranule) -> Path:
|
|
955
|
+
"""No-op download_quicklook method, as the quicklook image is already on the local file system.
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
datapoint: The datapoint to locate the quicklook image for in the local file system.
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
The path to the data on the local file system.
|
|
962
|
+
|
|
963
|
+
Raises:
|
|
964
|
+
ValueError: If no quicklook image is available for the given datapoint, or if the quicklook image is not
|
|
965
|
+
found on the local file system.
|
|
966
|
+
"""
|
|
967
|
+
return await self._download_quicklook(datapoint)
|
|
968
|
+
|
|
969
|
+
async def quicklook(
|
|
970
|
+
self, datapoint: xr.Dataset | LocationStorageGranule, width: int = 600, height: int = 600
|
|
971
|
+
) -> None:
|
|
972
|
+
"""Display the quicklook image for a given datapoint.
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
datapoint: The datapoint to display the quicklook for.
|
|
976
|
+
width: Display width of the image in pixels. Defaults to 600.
|
|
977
|
+
height: Display height of the image in pixels. Defaults to 600.
|
|
978
|
+
"""
|
|
979
|
+
quicklook_path = await self._download_quicklook(datapoint)
|
|
980
|
+
if Image is None:
|
|
981
|
+
raise ImportError("IPython is not available, please use download_quicklook instead.")
|
|
982
|
+
_display_quicklook(quicklook_path, width, height, None)
|
tilebox/storage/granule.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from pathlib import
|
|
3
|
+
from pathlib import PurePosixPath as ObjectPath
|
|
4
4
|
|
|
5
5
|
import xarray as xr
|
|
6
6
|
|
|
@@ -103,7 +103,7 @@ def _thumbnail_relative_to_eodata_location(thumbnail_url: str, location: str) ->
|
|
|
103
103
|
url_path = thumbnail_url.split("?path=")[-1]
|
|
104
104
|
url_path = url_path.removeprefix("/")
|
|
105
105
|
location = location.removeprefix("/eodata/")
|
|
106
|
-
return str(
|
|
106
|
+
return str(ObjectPath(url_path).relative_to(location))
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
@dataclass
|
|
@@ -183,3 +183,28 @@ class USGSLandsatStorageGranule:
|
|
|
183
183
|
dataset.location.item().replace("s3://usgs-landsat-ard/", "s3://usgs-landsat/"),
|
|
184
184
|
thumbnail,
|
|
185
185
|
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class LocationStorageGranule:
|
|
190
|
+
location: str
|
|
191
|
+
thumbnail: str | None = None
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_data(cls, dataset: "xr.Dataset | LocationStorageGranule") -> "LocationStorageGranule":
|
|
195
|
+
"""Extract the granule information from a datapoint given as xarray dataset."""
|
|
196
|
+
if isinstance(dataset, LocationStorageGranule):
|
|
197
|
+
return dataset
|
|
198
|
+
|
|
199
|
+
if "location" not in dataset:
|
|
200
|
+
raise ValueError("The given dataset has no location information.")
|
|
201
|
+
|
|
202
|
+
thumbnail = None
|
|
203
|
+
if "thumbnail" in dataset:
|
|
204
|
+
thumbnail = dataset.thumbnail.item()
|
|
205
|
+
elif "overview" in dataset:
|
|
206
|
+
thumbnail = dataset.overview.item()
|
|
207
|
+
elif "quicklook" in dataset:
|
|
208
|
+
thumbnail = dataset.quicklook.item()
|
|
209
|
+
|
|
210
|
+
return cls(dataset.location.item(), thumbnail)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilebox-storage
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.48.0
|
|
4
4
|
Summary: Storage client for Tilebox
|
|
5
5
|
Project-URL: Homepage, https://tilebox.com
|
|
6
6
|
Project-URL: Documentation, https://docs.tilebox.com/
|
|
@@ -20,6 +20,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
20
20
|
Classifier: Topic :: Software Development
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: aiofile>=3.8
|
|
23
|
+
Requires-Dist: boto3>=1.37.0
|
|
23
24
|
Requires-Dist: folium>=0.15
|
|
24
25
|
Requires-Dist: httpx>=0.27
|
|
25
26
|
Requires-Dist: obstore>=0.8.0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tilebox/storage/__init__.py,sha256=1oyjSgUcmPpD01mlL_2FSIShRU1uYnZd79UigAwuYxc,3759
|
|
2
|
+
tilebox/storage/aio.py,sha256=mFFWb-WdaaIzRnn0zoRsKWvEhbHNhl7_uEIvUc-9I8E,41818
|
|
3
|
+
tilebox/storage/granule.py,sha256=wdW-TYWA3Ha5R36KGpW0M17vxP-UY7S7cFTTuY1lNSI,7045
|
|
4
|
+
tilebox/storage/providers.py,sha256=vOTxSj2VIQhbFyvxu_eOcPmBGETDaijRoCWi9heUwRs,1832
|
|
5
|
+
tilebox_storage-0.48.0.dist-info/METADATA,sha256=0pu3jMyaUuTVXClSh4f-5pwUfP_-DUgKZG_nsPhcADc,4132
|
|
6
|
+
tilebox_storage-0.48.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
tilebox_storage-0.48.0.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
tilebox/storage/__init__.py,sha256=nQYsEKee3lBCDi_rmISGd-kKgqDV75ogiadbpLKLGww,3290
|
|
2
|
-
tilebox/storage/aio.py,sha256=kNahmyUUXeFMgA-XvBXq3MCqBkZw-8BPLr7n2HLf5gA,38383
|
|
3
|
-
tilebox/storage/granule.py,sha256=RPw3UkiIwGwQEqmiuxy2tbWAMrjoMYNNigXimB4jJGI,6179
|
|
4
|
-
tilebox/storage/providers.py,sha256=vOTxSj2VIQhbFyvxu_eOcPmBGETDaijRoCWi9heUwRs,1832
|
|
5
|
-
tilebox_storage-0.46.0.dist-info/METADATA,sha256=IR1nWsAlhWmCZkd0xsnOlHdpubT4xTotW4ukY_YxmyI,4103
|
|
6
|
-
tilebox_storage-0.46.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
tilebox_storage-0.46.0.dist-info/RECORD,,
|
|
File without changes
|