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.
@@ -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 # type: ignore[assignment]
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 = Path(prefix)
263
- return sorted(str(Path(obj["path"]).relative_to(prefix_path)) for obj in objects)
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(Path(prefix) / obj)
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(StorageClient):
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 download_preview instead.")
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(StorageClient):
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(StorageClient):
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 [Path(granule.granule_name).name]
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 download_preview instead.")
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(StorageClient):
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 download_preview instead.")
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)
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from datetime import datetime
3
- from pathlib import Path
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(Path(url_path).relative_to(location))
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.46.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,,