datacosmos 0.0.10__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,32 +1,51 @@
1
- """Module defining a structured URL configuration model.
1
+ """Structured URL configuration model for the Datacosmos SDK.
2
2
 
3
- Ensures that URLs contain required components such as protocol, host,
4
- port, and path.
3
+ The model accepts *either* a mapping of fields
4
+ (protocol, host, port, path) **or** a raw URL string such as
5
+ ``"https://example.com/api/v1"`` and converts it into a fully featured
6
+ :class:`datacosmos.utils.url.URL` instance via :py:meth:`as_domain_url`.
5
7
  """
6
8
 
7
- from pydantic import BaseModel
9
+ from urllib.parse import urlparse
10
+
11
+ from pydantic import BaseModel, model_validator
8
12
 
9
13
  from datacosmos.utils.url import URL as DomainURL
10
14
 
11
15
 
12
16
  class URL(BaseModel):
13
- """Generic configuration model for a URL.
14
-
15
- This class provides attributes to store URL components and a method
16
- to convert them into a `DomainURL` instance.
17
- """
17
+ """Generic configuration model for a URL."""
18
18
 
19
19
  protocol: str
20
20
  host: str
21
21
  port: int
22
22
  path: str
23
23
 
24
- def as_domain_url(self) -> DomainURL:
25
- """Convert the URL instance to a `DomainURL` object.
24
+ @model_validator(mode="before")
25
+ @classmethod
26
+ def _coerce_from_string(cls, data):
27
+ """Convert a raw URL string into the dict expected by the model."""
28
+ if isinstance(data, cls):
29
+ return data
30
+
31
+ if isinstance(data, str):
32
+ parts = urlparse(data)
33
+ if not parts.scheme or not parts.hostname:
34
+ raise ValueError(f"'{data}' is not a valid absolute URL")
26
35
 
27
- Returns:
28
- DomainURL: A domain-specific URL object.
29
- """
36
+ default_port = 443 if parts.scheme == "https" else 80
37
+
38
+ return {
39
+ "protocol": parts.scheme,
40
+ "host": parts.hostname,
41
+ "port": parts.port or default_port,
42
+ "path": parts.path.rstrip("/"),
43
+ }
44
+
45
+ return data
46
+
47
+ def as_domain_url(self) -> DomainURL:
48
+ """Convert this Pydantic model to the utility `DomainURL` object."""
30
49
  return DomainURL(
31
50
  protocol=self.protocol,
32
51
  host=self.host,
@@ -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.10
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
@@ -7,11 +7,11 @@ datacosmos/config/models/authentication_config.py,sha256=01Q90-yupbJ5orYDtatZIm9
7
7
  datacosmos/config/models/local_user_account_authentication_config.py,sha256=8WApn720MBXMKQa6w7bCd7Z37GRmYR-I7mBUgUI20lQ,701
8
8
  datacosmos/config/models/m2m_authentication_config.py,sha256=n76N4bakpPPycTOeKpiM8pazYtNqiJGMzZXmI_ogbHM,847
9
9
  datacosmos/config/models/no_authentication_config.py,sha256=x5xikSGPuqQbrf_S2oIWXo5XxAORci2sSE5KyJvZHVw,312
10
- datacosmos/config/models/url.py,sha256=fwr2C06e_RDS8AWxOV_orVxMWhc57bzYoWSjFxQbkwg,835
10
+ datacosmos/config/models/url.py,sha256=bBeulXQ2c-tLJyIoo3sTi9SPsZIyIDn_D2zmkCGWp9s,1597
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.10.dist-info/licenses/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
48
- datacosmos-0.0.10.dist-info/METADATA,sha256=9LtN2v3bq397TTrDhL4MZ-euAt6S8YixPT6nXqy2UZs,897
49
- datacosmos-0.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
- datacosmos-0.0.10.dist-info/top_level.txt,sha256=ueobs5CNeyDbPMgXPcVV0d0yNdm8CvGtDT3CaksRVtA,11
51
- datacosmos-0.0.10.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,,