datacosmos 0.0.11__py3-none-any.whl → 0.0.12__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.

Potentially problematic release.


This version of datacosmos might be problematic. Click here for more details.

@@ -1,5 +1,6 @@
1
1
  """Unified interface for STAC API, combining Item & Collection operations."""
2
2
 
3
+ from datacosmos.datacosmos_client import DatacosmosClient
3
4
  from datacosmos.stac.collection.collection_client import CollectionClient
4
5
  from datacosmos.stac.item.item_client import ItemClient
5
6
  from datacosmos.stac.storage.storage_client import StorageClient
@@ -8,7 +9,7 @@ from datacosmos.stac.storage.storage_client import StorageClient
8
9
  class STACClient(ItemClient, CollectionClient, StorageClient):
9
10
  """Unified interface for STAC API, combining Item & Collection operations."""
10
11
 
11
- def __init__(self, client):
12
+ def __init__(self, client: DatacosmosClient):
12
13
  """Initialize the STACClient with a DatacosmosClient."""
13
14
  ItemClient.__init__(self, client)
14
15
  CollectionClient.__init__(self, client)
@@ -1,63 +1,42 @@
1
- """Dataclass for retrieving the upload path of a file."""
1
+ """Dataclass for generating the upload key of an asset."""
2
2
 
3
3
  from dataclasses import dataclass
4
- from datetime import datetime
5
4
  from pathlib import Path
6
5
 
7
- import structlog
8
-
9
- from datacosmos.stac.enums.processing_level import ProcessingLevel
10
6
  from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
11
7
 
12
- logger = structlog.get_logger()
13
-
14
8
 
15
9
  @dataclass
16
10
  class UploadPath:
17
- """Dataclass for retrieving the upload path of a file."""
11
+ """Storage key in the form: project/<project-id>/<item-id>/<asset-name>."""
18
12
 
19
- mission: str
20
- level: ProcessingLevel
21
- day: int
22
- month: int
23
- year: int
24
- id: str
25
- path: str
13
+ project_id: str
14
+ item_id: str
15
+ asset_name: str
26
16
 
27
- def __str__(self):
28
- """Return a human-readable string representation of the Path."""
29
- path = f"full/{self.mission.lower()}/{self.level.value.lower()}/{self.year:02}/{self.month:02}/{self.day:02}/{self.id}/{self.path}"
30
- return path.removesuffix("/")
17
+ def __str__(self) -> str:
18
+ """Path in the form: project/<project-id>/<item-id>/<asset-name>."""
19
+ return f"project/{self.project_id}/{self.item_id}/{self.asset_name}".rstrip("/")
31
20
 
32
21
  @classmethod
33
22
  def from_item_path(
34
- cls, item: DatacosmosItem, mission: str, item_path: str
35
- ) -> "Path":
36
- """Create a Path instance from a DatacosmosItem and a path."""
37
- dt = datetime.strptime(item.properties["datetime"], "%Y-%m-%dT%H:%M:%SZ")
38
- path = UploadPath(
39
- mission=mission,
40
- level=ProcessingLevel(item.properties["processing:level"]),
41
- day=dt.day,
42
- month=dt.month,
43
- year=dt.year,
44
- id=item.id,
45
- path=item_path,
46
- )
47
- return cls(**path.__dict__)
23
+ cls,
24
+ item: DatacosmosItem,
25
+ project_id: str,
26
+ asset_name: str,
27
+ ) -> "UploadPath":
28
+ """Create an UploadPath for the given item/asset."""
29
+ return cls(project_id=project_id, item_id=item.id, asset_name=asset_name)
48
30
 
49
31
  @classmethod
50
- def from_path(cls, path: str) -> "Path":
51
- """Create a Path instance from a string path."""
52
- parts = path.split("/")
53
- if len(parts) < 7:
54
- raise ValueError(f"Invalid path {path}")
55
- return cls(
56
- mission=parts[0],
57
- level=ProcessingLevel(parts[1]),
58
- day=int(parts[4]),
59
- month=int(parts[3]),
60
- year=int(parts[2]),
61
- id=parts[5],
62
- path="/".join(parts[6:]),
63
- )
32
+ def from_path(cls, path: str) -> "UploadPath":
33
+ """Reverse-parse a storage key back into its components."""
34
+ parts = Path(path).parts
35
+ if len(parts) < 4 or parts[0] != "project":
36
+ raise ValueError(f"Invalid path: {path}")
37
+
38
+ project_id, item_id, *rest = parts[1:]
39
+ asset_name = "/".join(rest)
40
+ if not asset_name:
41
+ raise ValueError(f"Asset name is missing in path: {path}")
42
+ return cls(project_id=project_id, item_id=item_id, asset_name=asset_name)
@@ -16,6 +16,7 @@ class StorageClient:
16
16
  def upload_item(
17
17
  self,
18
18
  item: DatacosmosItem,
19
+ project_id: str,
19
20
  assets_path: str | None = None,
20
21
  included_assets: list[str] | bool = True,
21
22
  max_workers: int = 4,
@@ -24,6 +25,7 @@ class StorageClient:
24
25
  """Proxy to Uploader.upload_item, without needing to pass client each call."""
25
26
  return self.uploader.upload_item(
26
27
  item=item,
28
+ project_id=project_id,
27
29
  assets_path=assets_path,
28
30
  included_assets=included_assets,
29
31
  max_workers=max_workers,
@@ -13,22 +13,37 @@ from datacosmos.stac.storage.storage_base import StorageBase
13
13
 
14
14
 
15
15
  class Uploader(StorageBase):
16
- """Handles uploading files to Datacosmos storage and registering STAC items."""
16
+ """Upload a STAC item and its assets to Datacosmos storage, then register the item in the STAC API."""
17
17
 
18
18
  def __init__(self, client: DatacosmosClient):
19
- """Handles uploading files to Datacosmos storage and registering STAC items."""
19
+ """Initialize the uploader.
20
+
21
+ Args:
22
+ client (DatacosmosClient): Pre-configured DatacosmosClient.
23
+ """
20
24
  super().__init__(client)
21
25
  self.item_client = ItemClient(client)
22
26
 
23
27
  def upload_item(
24
28
  self,
25
- item: DatacosmosItem,
29
+ item: DatacosmosItem | str,
30
+ project_id: str,
26
31
  assets_path: str | None = None,
27
32
  included_assets: list[str] | bool = True,
28
33
  max_workers: int = 4,
29
34
  time_out: float = 60 * 60 * 1,
30
35
  ) -> DatacosmosItem:
31
- """Upload a STAC item and its assets to Datacosmos."""
36
+ """Upload a STAC item (and optionally its assets) to Datacosmos.
37
+
38
+ `item` can be either:
39
+ • a DatacosmosItem instance, or
40
+ • the path to an item JSON file on disk.
41
+
42
+ If `included_assets` is:
43
+ • True → upload every asset in the item
44
+ • list → upload only the asset keys in that list
45
+ • False → upload nothing; just register the item
46
+ """
32
47
  if not assets_path and not isinstance(item, str):
33
48
  raise ValueError(
34
49
  "assets_path must be provided if item is not the path to an item file."
@@ -37,8 +52,7 @@ class Uploader(StorageBase):
37
52
  if isinstance(item, str):
38
53
  item_filename = item
39
54
  item = self._load_item(item_filename)
40
- if not assets_path:
41
- assets_path = str(Path(item_filename).parent)
55
+ assets_path = assets_path or str(Path(item_filename).parent)
42
56
 
43
57
  assets_path = assets_path or str(Path.cwd())
44
58
 
@@ -50,18 +64,18 @@ class Uploader(StorageBase):
50
64
  else []
51
65
  )
52
66
 
53
- jobs = [(item, asset_key, assets_path) for asset_key in upload_assets]
54
-
67
+ jobs = [
68
+ (item, asset_key, assets_path, project_id) for asset_key in upload_assets
69
+ ]
55
70
  self._run_in_threads(self._upload_asset, jobs, max_workers, time_out)
56
71
 
57
72
  self.item_client.add_item(item)
58
-
59
73
  return item
60
74
 
61
75
  def upload_from_file(
62
76
  self, src: str, dst: str, mime_type: str | None = None
63
77
  ) -> None:
64
- """Uploads a single file to the specified destination path."""
78
+ """Upload a single file to the specified destination path in storage."""
65
79
  url = self.base_url.with_suffix(dst)
66
80
  mime = mime_type or self._guess_mime(src)
67
81
  headers = {"Content-Type": mime}
@@ -71,25 +85,40 @@ class Uploader(StorageBase):
71
85
 
72
86
  @staticmethod
73
87
  def _load_item(item_json_file_path: str) -> DatacosmosItem:
88
+ """Load a DatacosmosItem from a JSON file on disk."""
74
89
  with open(item_json_file_path, "rb") as file:
75
90
  data = file.read().decode("utf-8")
76
91
  return TypeAdapter(DatacosmosItem).validate_json(data)
77
92
 
78
93
  def _upload_asset(
79
- self, item: DatacosmosItem, asset_key: str, assets_path: str
94
+ self, item: DatacosmosItem, asset_key: str, assets_path: str, project_id: str
80
95
  ) -> None:
96
+ """Upload a single asset file and update its href inside the item object.
97
+
98
+ Runs in parallel via _run_in_threads().
99
+ """
81
100
  asset = item.assets[asset_key]
82
- upload_path = UploadPath.from_item_path(item, "", Path(asset.href).name)
101
+
102
+ # Build storage key: project/<project_id>/<item_id>/<asset_name>
103
+ upload_path = UploadPath.from_item_path(
104
+ item,
105
+ project_id,
106
+ Path(asset.href).name,
107
+ )
108
+
83
109
  local_src = Path(assets_path) / asset.href
84
110
  if local_src.exists():
85
111
  src = str(local_src)
86
112
  asset.href = f"file:///{upload_path}"
87
113
  else:
114
+ # fallback: try matching just the filename inside assets_path
88
115
  src = str(Path(assets_path) / Path(asset.href).name)
89
- self._update_asset_href(asset)
116
+
117
+ self._update_asset_href(asset) # turn href into public URL
90
118
  self.upload_from_file(src, str(upload_path), mime_type=asset.type)
91
119
 
92
120
  def _update_asset_href(self, asset: Asset) -> None:
121
+ """Convert the storage key to a public HTTPS URL."""
93
122
  try:
94
123
  url = self.client.config.datacosmos_public_cloud_storage.as_domain_url()
95
124
  new_href = url.with_base(asset.href) # type: ignore
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.11
3
+ Version: 0.0.12
4
4
  Summary: A library for interacting with DataCosmos from Python code
5
5
  Author-email: Open Cosmos <support@open-cosmos.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -11,7 +11,7 @@ datacosmos/config/models/url.py,sha256=bBeulXQ2c-tLJyIoo3sTi9SPsZIyIDn_D2zmkCGWp
11
11
  datacosmos/exceptions/__init__.py,sha256=Crz8W7mOvPUXYcfDVotvjUt_3HKawBpmJA_-uel9UJk,45
12
12
  datacosmos/exceptions/datacosmos_exception.py,sha256=rKjJvQDvCEbxXWWccxB5GI_sth662bW8Yml0hX-vRw4,923
13
13
  datacosmos/stac/__init__.py,sha256=B4x_Mr4X7TzQoYtRC-VzI4W-fEON5WUOaz8cWJbk3Fc,214
14
- datacosmos/stac/stac_client.py,sha256=J4k4aJdakwVK1sorBxeK8KbPtYvjIGa68iqKA_itSgU,654
14
+ datacosmos/stac/stac_client.py,sha256=S0HESbZhlIdS0x_VSCeOSuOFaB50U4CMnTOX_0zLjn8,730
15
15
  datacosmos/stac/collection/__init__.py,sha256=VQMLnsU3sER5kh4YxHrHP7XCA3DG1y0n9yoSmvycOY0,212
16
16
  datacosmos/stac/collection/collection_client.py,sha256=-Nn3yqL4mQS05YAMd0IUmv03hdHKYBtVG2_EqoaAQWc,6064
17
17
  datacosmos/stac/collection/models/__init__.py,sha256=TQaihUS_CM9Eaekm4SbzFTNfv7BmabHv3Z-f37Py5Qs,40
@@ -33,10 +33,10 @@ datacosmos/stac/item/models/item_update.py,sha256=_CpjQn9SsfedfuxlHSiGeptqY4M-p1
33
33
  datacosmos/stac/item/models/raster_band.py,sha256=CoEVs-YyPE5Fse0He9DdOs4dGZpzfCsCuVzOcdXa_UM,354
34
34
  datacosmos/stac/storage/__init__.py,sha256=hivfSpOaoSwCAymgU0rTgvSk9LSPAn1cPLQQ9fLmFX0,151
35
35
  datacosmos/stac/storage/storage_base.py,sha256=5ioMKbEltPEWr4dkhZQiUhdBFEhe7ajIYUd9z3K8elU,1483
36
- datacosmos/stac/storage/storage_client.py,sha256=GeWJoa8ALqelZHvmnop_sSuyU7ntFNFXMFQfplIo0kU,1145
37
- datacosmos/stac/storage/uploader.py,sha256=5W4Wcx2yzdkU9sg93jnwYP0TiZcuxQXB9owfjL2NsBg,3630
36
+ datacosmos/stac/storage/storage_client.py,sha256=4boqQ3zVMrk9X2IXus-Cs429juLe0cUQ0XEzg_y3yOA,1205
37
+ datacosmos/stac/storage/uploader.py,sha256=cUe-6DbcEXcENReDMaQe4etVCW1n2kPMxQlCuB8YnqU,4635
38
38
  datacosmos/stac/storage/dataclasses/__init__.py,sha256=IjcyA8Vod-z1_Gi1FMZhK58Owman0foL25Hs0YtkYYs,43
39
- datacosmos/stac/storage/dataclasses/upload_path.py,sha256=2Nvk51j4s6Cyse9y9sKmN3rQLV7oIMNtokpnt4_qTaw,1895
39
+ datacosmos/stac/storage/dataclasses/upload_path.py,sha256=gbpV67FECFNyXn-yGUSuLvGGWHtibbZq7Qu9yGod3C0,1398
40
40
  datacosmos/utils/__init__.py,sha256=XQbAnoqJrPpnSpEzAbjh84yqYWw8cBM8mNp8ynTG-54,50
41
41
  datacosmos/utils/url.py,sha256=iQwZr6mYRoePqUZg-k3KQSV9o2wju5ZuCa5WS_GyJo4,2114
42
42
  datacosmos/utils/http_response/__init__.py,sha256=BvOWwC5coYqq_kFn8gIw5m54TLpdfJKlW9vgRkfhXiA,33
@@ -44,8 +44,8 @@ datacosmos/utils/http_response/check_api_response.py,sha256=dKWW01jn2_lWV0xpOBAB
44
44
  datacosmos/utils/http_response/models/__init__.py,sha256=Wj8YT6dqw7rAz_rctllxo5Or_vv8DwopvQvBzwCTvpw,45
45
45
  datacosmos/utils/http_response/models/datacosmos_error.py,sha256=Uqi2uM98nJPeCbM7zngV6vHSk97jEAb_nkdDEeUjiQM,740
46
46
  datacosmos/utils/http_response/models/datacosmos_response.py,sha256=oV4n-sue7K1wwiIQeHpxdNU8vxeqF3okVPE2rydw5W0,336
47
- datacosmos-0.0.11.dist-info/licenses/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
48
- datacosmos-0.0.11.dist-info/METADATA,sha256=moE0Kyji_v-O_MSXkBd3idYIjqRzut7qkY9bQ96IFU0,897
49
- datacosmos-0.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
- datacosmos-0.0.11.dist-info/top_level.txt,sha256=ueobs5CNeyDbPMgXPcVV0d0yNdm8CvGtDT3CaksRVtA,11
51
- datacosmos-0.0.11.dist-info/RECORD,,
47
+ datacosmos-0.0.12.dist-info/licenses/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
48
+ datacosmos-0.0.12.dist-info/METADATA,sha256=HvFc03wc5mu0XRmfl5Xw7iP8qxyC7Cmz-VFXQJjxQuo,897
49
+ datacosmos-0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ datacosmos-0.0.12.dist-info/top_level.txt,sha256=ueobs5CNeyDbPMgXPcVV0d0yNdm8CvGtDT3CaksRVtA,11
51
+ datacosmos-0.0.12.dist-info/RECORD,,