datacosmos 0.0.19__tar.gz → 0.0.21__tar.gz
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.
- {datacosmos-0.0.19 → datacosmos-0.0.21}/PKG-INFO +1 -1
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/auth/factory.py +24 -38
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/config.py +13 -26
- datacosmos-0.0.21/datacosmos/stac/storage/storage_base.py +77 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/storage/uploader.py +57 -30
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos.egg-info/PKG-INFO +1 -1
- {datacosmos-0.0.19 → datacosmos-0.0.21}/pyproject.toml +1 -1
- datacosmos-0.0.19/datacosmos/stac/storage/storage_base.py +0 -40
- {datacosmos-0.0.19 → datacosmos-0.0.21}/LICENSE.md +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/README.md +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/auth/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/auth/base_authenticator.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/auth/local_authenticator.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/auth/local_token_fetcher.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/auth/m2m_authenticator.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/auth/token.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/auth/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/constants.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/loaders/yaml_source.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/authentication_config.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/m2m_authentication_config.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/no_authentication_config.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/url.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/datacosmos_client.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/exceptions/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/exceptions/datacosmos_error.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/exceptions/stac_validation_error.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/collection/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/collection/collection_client.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/collection/models/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/collection/models/collection_update.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/constants/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/enums/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/enums/processing_level.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/enums/product_type.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/enums/season.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/item_client.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/asset.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/datacosmos_item.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/eo_band.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/item_update.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/raster_band.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/stac_client.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/storage/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/storage/storage_client.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/check_api_response.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/models/__init__.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/url.py +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos.egg-info/SOURCES.txt +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos.egg-info/dependency_links.txt +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos.egg-info/requires.txt +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos.egg-info/top_level.txt +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/setup.cfg +0 -0
- {datacosmos-0.0.19 → datacosmos-0.0.21}/tests/test_pass.py +0 -0
|
@@ -5,11 +5,6 @@ This module normalizes the `authentication` config into a concrete model:
|
|
|
5
5
|
- `apply_auth_defaults` fills sensible defaults per auth type without inventing secrets.
|
|
6
6
|
- `check_required_auth_fields` enforces the minimum required inputs.
|
|
7
7
|
- `normalize_authentication` runs the whole pipeline.
|
|
8
|
-
|
|
9
|
-
Design notes:
|
|
10
|
-
- Auth models accept partial data (fields are Optional with None defaults).
|
|
11
|
-
- We DO NOT pass `None` explicitly when constructing models here.
|
|
12
|
-
- Required-ness is enforced centrally by `check_required_auth_fields`, not by model init.
|
|
13
8
|
"""
|
|
14
9
|
|
|
15
10
|
from typing import Optional, Union, cast
|
|
@@ -33,49 +28,45 @@ AuthModel = Union[M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig]
|
|
|
33
28
|
|
|
34
29
|
|
|
35
30
|
def parse_auth_config(raw: dict | AuthModel | None) -> Optional[AuthModel]:
|
|
36
|
-
"""Turn a raw dict (e.g., from YAML) into a concrete auth model.
|
|
37
|
-
|
|
38
|
-
- If `raw` is already an auth model (M2M or local), return it unchanged.
|
|
39
|
-
- If `raw` is a dict, choose/validate the type using `raw['type']`
|
|
40
|
-
(or DEFAULT_AUTH_TYPE), then construct the corresponding model.
|
|
41
|
-
For missing fields we *may* apply non-secret defaults (endpoints, etc.).
|
|
42
|
-
"""
|
|
43
|
-
if raw is None or isinstance(
|
|
44
|
-
raw, (M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig)
|
|
45
|
-
):
|
|
31
|
+
"""Turn a raw dict (e.g., from YAML/env) into a concrete auth model."""
|
|
32
|
+
if isinstance(raw, (M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig)):
|
|
46
33
|
return cast(Optional[AuthModel], raw)
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
if raw is None:
|
|
36
|
+
raw_data = {}
|
|
37
|
+
else:
|
|
38
|
+
raw_data = raw.copy()
|
|
39
|
+
|
|
40
|
+
if raw is None and not raw_data:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
auth_type = _normalize_auth_type(raw_data.get("type") or DEFAULT_AUTH_TYPE)
|
|
49
44
|
|
|
50
45
|
if auth_type == "local":
|
|
51
46
|
return LocalUserAccountAuthenticationConfig(
|
|
52
47
|
type="local",
|
|
53
|
-
client_id=
|
|
54
|
-
authorization_endpoint=
|
|
48
|
+
client_id=raw_data.get("client_id"),
|
|
49
|
+
authorization_endpoint=raw_data.get(
|
|
55
50
|
"authorization_endpoint", DEFAULT_LOCAL_AUTHORIZATION_ENDPOINT
|
|
56
51
|
),
|
|
57
|
-
token_endpoint=
|
|
58
|
-
redirect_port=
|
|
59
|
-
scopes=
|
|
60
|
-
audience=
|
|
61
|
-
cache_file=
|
|
52
|
+
token_endpoint=raw_data.get("token_endpoint", DEFAULT_LOCAL_TOKEN_ENDPOINT),
|
|
53
|
+
redirect_port=raw_data.get("redirect_port", DEFAULT_LOCAL_REDIRECT_PORT),
|
|
54
|
+
scopes=raw_data.get("scopes", DEFAULT_LOCAL_SCOPES),
|
|
55
|
+
audience=raw_data.get("audience", DEFAULT_AUTH_AUDIENCE),
|
|
56
|
+
cache_file=raw_data.get("cache_file", DEFAULT_LOCAL_CACHE_FILE),
|
|
62
57
|
)
|
|
63
58
|
|
|
64
59
|
return M2MAuthenticationConfig(
|
|
65
60
|
type="m2m",
|
|
66
|
-
token_url=
|
|
67
|
-
audience=
|
|
68
|
-
client_id=
|
|
69
|
-
client_secret=
|
|
61
|
+
token_url=raw_data.get("token_url", DEFAULT_AUTH_TOKEN_URL),
|
|
62
|
+
audience=raw_data.get("audience", DEFAULT_AUTH_AUDIENCE),
|
|
63
|
+
client_id=raw_data.get("client_id"),
|
|
64
|
+
client_secret=raw_data.get("client_secret"),
|
|
70
65
|
)
|
|
71
66
|
|
|
72
67
|
|
|
73
68
|
def apply_auth_defaults(auth: AuthModel | None) -> AuthModel:
|
|
74
|
-
"""Fill in any missing defaults by type (non-secret values only).
|
|
75
|
-
|
|
76
|
-
If `auth` is None, construct a default "shell" based on DEFAULT_AUTH_TYPE,
|
|
77
|
-
without passing None for unknown credentials.
|
|
78
|
-
"""
|
|
69
|
+
"""Fill in any missing defaults by type (non-secret values only)."""
|
|
79
70
|
if auth is None:
|
|
80
71
|
default_type = _normalize_auth_type(DEFAULT_AUTH_TYPE)
|
|
81
72
|
if default_type == "local":
|
|
@@ -101,7 +92,6 @@ def apply_auth_defaults(auth: AuthModel | None) -> AuthModel:
|
|
|
101
92
|
auth.audience = auth.audience or DEFAULT_AUTH_AUDIENCE
|
|
102
93
|
return auth
|
|
103
94
|
|
|
104
|
-
# Local defaults (Pydantic already coerces types; only set when missing)
|
|
105
95
|
auth.type = auth.type or "local"
|
|
106
96
|
auth.authorization_endpoint = (
|
|
107
97
|
auth.authorization_endpoint or DEFAULT_LOCAL_AUTHORIZATION_ENDPOINT
|
|
@@ -116,11 +106,7 @@ def apply_auth_defaults(auth: AuthModel | None) -> AuthModel:
|
|
|
116
106
|
|
|
117
107
|
|
|
118
108
|
def check_required_auth_fields(auth: AuthModel) -> None:
|
|
119
|
-
"""Enforce required fields per auth type.
|
|
120
|
-
|
|
121
|
-
- m2m requires client_id and client_secret.
|
|
122
|
-
- local requires client_id.
|
|
123
|
-
"""
|
|
109
|
+
"""Enforce required fields per auth type."""
|
|
124
110
|
if isinstance(auth, M2MAuthenticationConfig):
|
|
125
111
|
missing = [f for f in ("client_id", "client_secret") if not getattr(auth, f)]
|
|
126
112
|
if missing:
|
|
@@ -7,7 +7,7 @@ and supports environment variable-based overrides.
|
|
|
7
7
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from pydantic import field_validator
|
|
10
|
+
from pydantic import Field, field_validator
|
|
11
11
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
12
|
|
|
13
13
|
from datacosmos.config.auth.factory import normalize_authentication, parse_auth_config
|
|
@@ -18,6 +18,10 @@ from datacosmos.config.constants import (
|
|
|
18
18
|
)
|
|
19
19
|
from datacosmos.config.loaders.yaml_source import yaml_settings_source
|
|
20
20
|
from datacosmos.config.models.authentication_config import AuthenticationConfig
|
|
21
|
+
from datacosmos.config.models.local_user_account_authentication_config import (
|
|
22
|
+
LocalUserAccountAuthenticationConfig,
|
|
23
|
+
)
|
|
24
|
+
from datacosmos.config.models.m2m_authentication_config import M2MAuthenticationConfig
|
|
21
25
|
from datacosmos.config.models.url import URL
|
|
22
26
|
|
|
23
27
|
|
|
@@ -31,9 +35,14 @@ class Config(BaseSettings):
|
|
|
31
35
|
)
|
|
32
36
|
|
|
33
37
|
authentication: Optional[AuthenticationConfig] = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
stac: URL = Field(default_factory=lambda: URL(**DEFAULT_STAC))
|
|
40
|
+
datacosmos_cloud_storage: URL = Field(
|
|
41
|
+
default_factory=lambda: URL(**DEFAULT_STORAGE)
|
|
42
|
+
)
|
|
43
|
+
datacosmos_public_cloud_storage: URL = Field(
|
|
44
|
+
default_factory=lambda: URL(**DEFAULT_STORAGE)
|
|
45
|
+
)
|
|
37
46
|
|
|
38
47
|
@classmethod
|
|
39
48
|
def settings_customise_sources(cls, *args, **kwargs):
|
|
@@ -65,13 +74,6 @@ class Config(BaseSettings):
|
|
|
65
74
|
def _parse_authentication(cls, raw):
|
|
66
75
|
if raw is None:
|
|
67
76
|
return None
|
|
68
|
-
from datacosmos.config.models.local_user_account_authentication_config import (
|
|
69
|
-
LocalUserAccountAuthenticationConfig,
|
|
70
|
-
)
|
|
71
|
-
from datacosmos.config.models.m2m_authentication_config import (
|
|
72
|
-
M2MAuthenticationConfig,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
77
|
if isinstance(
|
|
76
78
|
raw, (M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig)
|
|
77
79
|
):
|
|
@@ -84,18 +86,3 @@ class Config(BaseSettings):
|
|
|
84
86
|
@classmethod
|
|
85
87
|
def _validate_authentication(cls, auth: Optional[AuthenticationConfig]):
|
|
86
88
|
return normalize_authentication(auth)
|
|
87
|
-
|
|
88
|
-
@field_validator("stac", mode="before")
|
|
89
|
-
@classmethod
|
|
90
|
-
def _default_stac(cls, value: URL | None) -> URL:
|
|
91
|
-
return value or URL(**DEFAULT_STAC)
|
|
92
|
-
|
|
93
|
-
@field_validator("datacosmos_cloud_storage", mode="before")
|
|
94
|
-
@classmethod
|
|
95
|
-
def _default_cloud_storage(cls, value: URL | None) -> URL:
|
|
96
|
-
return value or URL(**DEFAULT_STORAGE)
|
|
97
|
-
|
|
98
|
-
@field_validator("datacosmos_public_cloud_storage", mode="before")
|
|
99
|
-
@classmethod
|
|
100
|
-
def _default_public_cloud_storage(cls, value: URL | None) -> URL:
|
|
101
|
-
return value or URL(**DEFAULT_STORAGE)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Base class providing common storage helpers (threading, MIME guess, futures)."""
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
from concurrent.futures import Future, ThreadPoolExecutor, wait
|
|
5
|
+
from typing import Any, Callable, Dict, Iterable, List, Tuple
|
|
6
|
+
|
|
7
|
+
from datacosmos.datacosmos_client import DatacosmosClient
|
|
8
|
+
from datacosmos.exceptions.datacosmos_error import DatacosmosError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StorageBase:
|
|
12
|
+
"""Base class providing common storage helpers (threading, MIME guess, futures)."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: DatacosmosClient):
|
|
15
|
+
"""Base class providing common storage helpers (threading, MIME guess, futures)."""
|
|
16
|
+
self.client = client
|
|
17
|
+
self.base_url = client.config.datacosmos_cloud_storage.as_domain_url()
|
|
18
|
+
|
|
19
|
+
def _guess_mime(self, src: str) -> str:
|
|
20
|
+
mime, _ = mimetypes.guess_type(src)
|
|
21
|
+
return mime or "application/octet-stream"
|
|
22
|
+
|
|
23
|
+
def run_in_threads(
|
|
24
|
+
self,
|
|
25
|
+
fn: Callable[..., Any],
|
|
26
|
+
jobs: Iterable[Tuple[Any, ...]],
|
|
27
|
+
max_workers: int,
|
|
28
|
+
timeout: float,
|
|
29
|
+
) -> Tuple[List[Any], List[Dict[str, Any]]]:
|
|
30
|
+
"""Run the callable `fn(*args)` over the iterable of jobs in parallel threads.
|
|
31
|
+
|
|
32
|
+
Collects successes and failures without aborting the batch on individual errors.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
fn: The function to execute.
|
|
36
|
+
jobs: An iterable of tuples, where each tuple is unpacked as fn(*args).
|
|
37
|
+
max_workers: Maximum number of threads to use.
|
|
38
|
+
timeout: Timeout for the entire batch.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A tuple containing (successes: List[Any], failures: List[Dict[str, Any]]).
|
|
42
|
+
Failures include the exception and job arguments.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
DatacosmosError: If the entire batch times out.
|
|
46
|
+
"""
|
|
47
|
+
futures: List[Future] = []
|
|
48
|
+
|
|
49
|
+
executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
for args in jobs:
|
|
53
|
+
futures.append(executor.submit(fn, *args))
|
|
54
|
+
|
|
55
|
+
# Wait until all futures are done or the timeout is reached
|
|
56
|
+
done, not_done = wait(futures, timeout=timeout)
|
|
57
|
+
|
|
58
|
+
successes = []
|
|
59
|
+
failures = []
|
|
60
|
+
|
|
61
|
+
for future in done:
|
|
62
|
+
try:
|
|
63
|
+
result = future.result()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
failures.append({"error": str(e), "exception": e})
|
|
66
|
+
else:
|
|
67
|
+
successes.append(result)
|
|
68
|
+
|
|
69
|
+
if not_done:
|
|
70
|
+
# The executor's shutdown wait must be skipped to allow cancellation
|
|
71
|
+
raise DatacosmosError("Batch processing failed: operation timed out.")
|
|
72
|
+
|
|
73
|
+
return successes, failures
|
|
74
|
+
finally:
|
|
75
|
+
# Shutdown without waiting to enable timeout handling
|
|
76
|
+
# The wait call already established which jobs finished
|
|
77
|
+
executor.shutdown(wait=False)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Handles uploading files to Datacosmos storage and registering STAC items."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from pydantic import TypeAdapter
|
|
6
7
|
|
|
@@ -13,7 +14,7 @@ from datacosmos.stac.storage.storage_base import StorageBase
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Uploader(StorageBase):
|
|
16
|
-
"""Upload a STAC item and its assets to Datacosmos storage
|
|
17
|
+
"""Upload a STAC item and its assets to Datacosmos storage and register the item in the STAC API."""
|
|
17
18
|
|
|
18
19
|
def __init__(self, client: DatacosmosClient):
|
|
19
20
|
"""Initialize the uploader.
|
|
@@ -32,17 +33,25 @@ class Uploader(StorageBase):
|
|
|
32
33
|
included_assets: list[str] | bool = True,
|
|
33
34
|
max_workers: int = 4,
|
|
34
35
|
time_out: float = 60 * 60 * 1,
|
|
35
|
-
) -> DatacosmosItem:
|
|
36
|
-
"""Upload a STAC item (and optionally its assets) to Datacosmos.
|
|
36
|
+
) -> tuple[DatacosmosItem, list[str], list[dict[str, Any]]]:
|
|
37
|
+
"""Upload a STAC item (and optionally its assets) to Datacosmos in parallel threads.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
Args:
|
|
40
|
+
item (DatacosmosItem | str):
|
|
41
|
+
- a DatacosmosItem instance, or
|
|
42
|
+
- the path to an item JSON file on disk.
|
|
43
|
+
project_id (str): The project ID to upload assets to.
|
|
44
|
+
assets_path (str | None): Base directory where local asset files are located.
|
|
45
|
+
included_assets (list[str] | bool):
|
|
46
|
+
- True → upload every asset in the item.
|
|
47
|
+
- list[str] → upload only the asset keys in that list.
|
|
48
|
+
- False → skip asset upload; just register the item.
|
|
49
|
+
max_workers (int): Maximum number of parallel threads for asset upload.
|
|
50
|
+
time_out (float): Timeout in seconds for the entire asset batch upload.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
tuple[DatacosmosItem, list[str], list[dict[str, Any]]]:
|
|
54
|
+
The updated DatacosmosItem, a list of asset keys that were uploaded successfully, and a list of upload failures.
|
|
46
55
|
"""
|
|
47
56
|
if not assets_path and not isinstance(item, str):
|
|
48
57
|
raise ValueError(
|
|
@@ -54,23 +63,45 @@ class Uploader(StorageBase):
|
|
|
54
63
|
item = self._load_item(item_filename)
|
|
55
64
|
assets_path = assets_path or str(Path(item_filename).parent)
|
|
56
65
|
|
|
66
|
+
if not isinstance(item, DatacosmosItem):
|
|
67
|
+
raise TypeError(f"item must be a DatacosmosItem, got {type(item).__name__}")
|
|
68
|
+
|
|
57
69
|
assets_path = assets_path or str(Path.cwd())
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
if included_assets is False:
|
|
72
|
+
upload_assets: list[str] = []
|
|
73
|
+
elif included_assets is True:
|
|
74
|
+
upload_assets = list(item.assets.keys())
|
|
75
|
+
elif isinstance(included_assets, list):
|
|
76
|
+
upload_assets = included_assets
|
|
77
|
+
else:
|
|
78
|
+
upload_assets = []
|
|
66
79
|
|
|
67
80
|
jobs = [
|
|
68
81
|
(item, asset_key, assets_path, project_id) for asset_key in upload_assets
|
|
69
82
|
]
|
|
70
|
-
self._run_in_threads(self._upload_asset, jobs, max_workers, time_out)
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
if not jobs:
|
|
85
|
+
self.item_client.add_item(item)
|
|
86
|
+
return item, [], []
|
|
87
|
+
|
|
88
|
+
successes, failures = self.run_in_threads(
|
|
89
|
+
self._upload_asset, jobs, max_workers, time_out
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Register the item if the overall process didn't time out
|
|
93
|
+
# and there was at least one successful upload.
|
|
94
|
+
if successes:
|
|
95
|
+
self.item_client.add_item(item)
|
|
96
|
+
|
|
97
|
+
return item, successes, failures
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _load_item(item_json_file_path: str) -> DatacosmosItem:
|
|
101
|
+
"""Load a DatacosmosItem from a JSON file on disk."""
|
|
102
|
+
with open(item_json_file_path, "rb") as file:
|
|
103
|
+
data = file.read().decode("utf-8")
|
|
104
|
+
return TypeAdapter(DatacosmosItem).validate_json(data)
|
|
74
105
|
|
|
75
106
|
def upload_from_file(
|
|
76
107
|
self, src: str, dst: str, mime_type: str | None = None
|
|
@@ -83,19 +114,13 @@ class Uploader(StorageBase):
|
|
|
83
114
|
response = self.client.put(url, data=f, headers=headers)
|
|
84
115
|
response.raise_for_status()
|
|
85
116
|
|
|
86
|
-
@staticmethod
|
|
87
|
-
def _load_item(item_json_file_path: str) -> DatacosmosItem:
|
|
88
|
-
"""Load a DatacosmosItem from a JSON file on disk."""
|
|
89
|
-
with open(item_json_file_path, "rb") as file:
|
|
90
|
-
data = file.read().decode("utf-8")
|
|
91
|
-
return TypeAdapter(DatacosmosItem).validate_json(data)
|
|
92
|
-
|
|
93
117
|
def _upload_asset(
|
|
94
118
|
self, item: DatacosmosItem, asset_key: str, assets_path: str, project_id: str
|
|
95
|
-
) ->
|
|
119
|
+
) -> str:
|
|
96
120
|
"""Upload a single asset file and update its href inside the item object.
|
|
97
121
|
|
|
98
|
-
|
|
122
|
+
Returns:
|
|
123
|
+
str: The asset_key upon successful upload.
|
|
99
124
|
"""
|
|
100
125
|
asset = item.assets[asset_key]
|
|
101
126
|
|
|
@@ -117,6 +142,8 @@ class Uploader(StorageBase):
|
|
|
117
142
|
self._update_asset_href(asset) # turn href into public URL
|
|
118
143
|
self.upload_from_file(src, str(upload_path), mime_type=asset.type)
|
|
119
144
|
|
|
145
|
+
return asset_key
|
|
146
|
+
|
|
120
147
|
def _update_asset_href(self, asset: Asset) -> None:
|
|
121
148
|
"""Convert the storage key to a public HTTPS URL."""
|
|
122
149
|
try:
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"""Base class providing common storage helpers (threading, MIME guess, futures)."""
|
|
2
|
-
|
|
3
|
-
import mimetypes
|
|
4
|
-
from concurrent.futures import ThreadPoolExecutor, wait
|
|
5
|
-
|
|
6
|
-
from datacosmos.datacosmos_client import DatacosmosClient
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class StorageBase:
|
|
10
|
-
"""Base class providing common storage helpers (threading, MIME guess, futures)."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, client: DatacosmosClient):
|
|
13
|
-
"""Base class providing common storage helpers (threading, MIME guess, futures)."""
|
|
14
|
-
self.client = client
|
|
15
|
-
self.base_url = client.config.datacosmos_cloud_storage.as_domain_url()
|
|
16
|
-
|
|
17
|
-
def _guess_mime(self, src: str) -> str:
|
|
18
|
-
mime, _ = mimetypes.guess_type(src)
|
|
19
|
-
return mime or "application/octet-stream"
|
|
20
|
-
|
|
21
|
-
def _run_in_threads(self, fn, fn_args, max_workers: int, timeout: float):
|
|
22
|
-
"""Run the callable `fn(*args)` over the iterable of jobs in parallel threads.
|
|
23
|
-
|
|
24
|
-
`jobs` should be a list of tuples, each tuple unpacked as fn(*args).
|
|
25
|
-
"""
|
|
26
|
-
futures = []
|
|
27
|
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
28
|
-
for args in fn_args:
|
|
29
|
-
futures.append(executor.submit(fn, *args))
|
|
30
|
-
done, not_done = wait(futures, timeout=timeout)
|
|
31
|
-
errors = []
|
|
32
|
-
for future in done:
|
|
33
|
-
try:
|
|
34
|
-
future.result()
|
|
35
|
-
except Exception as e:
|
|
36
|
-
errors.append(e)
|
|
37
|
-
for future in not_done:
|
|
38
|
-
future.cancel()
|
|
39
|
-
if errors:
|
|
40
|
-
raise errors[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/m2m_authentication_config.py
RENAMED
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/config/models/no_authentication_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/collection/models/collection_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/stac/item/models/catalog_search_parameters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/check_api_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/models/datacosmos_error.py
RENAMED
|
File without changes
|
{datacosmos-0.0.19 → datacosmos-0.0.21}/datacosmos/utils/http_response/models/datacosmos_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|