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

@@ -12,8 +12,9 @@ import yaml
12
12
  from pydantic import field_validator
13
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
14
14
 
15
- from config.models.m2m_authentication_config import M2MAuthenticationConfig
16
- from config.models.url import URL
15
+ from datacosmos.config.models.authentication_config import AuthenticationConfig
16
+ from datacosmos.config.models.m2m_authentication_config import M2MAuthenticationConfig
17
+ from datacosmos.config.models.url import URL
17
18
 
18
19
 
19
20
  class Config(BaseSettings):
@@ -25,10 +26,9 @@ class Config(BaseSettings):
25
26
  extra="allow",
26
27
  )
27
28
 
28
- authentication: Optional[M2MAuthenticationConfig] = None
29
+ authentication: Optional[AuthenticationConfig] = None
29
30
  stac: Optional[URL] = None
30
31
  datacosmos_cloud_storage: Optional[URL] = None
31
- mission_id: int = 0
32
32
 
33
33
  DEFAULT_AUTH_TYPE: ClassVar[str] = "m2m"
34
34
  DEFAULT_AUTH_TOKEN_URL: ClassVar[str] = "https://login.open-cosmos.com/oauth/token"
@@ -94,15 +94,15 @@ class Config(BaseSettings):
94
94
  environment=os.getenv("ENVIRONMENT", "test"),
95
95
  )
96
96
 
97
- @field_validator("authentication", mode="before")
97
+ @field_validator("authentication", mode="after")
98
98
  @classmethod
99
99
  def validate_authentication(
100
- cls, auth_data: Optional[dict]
100
+ cls, auth: Optional[M2MAuthenticationConfig]
101
101
  ) -> M2MAuthenticationConfig:
102
102
  """Ensure authentication is provided and apply defaults.
103
103
 
104
104
  Args:
105
- auth_data (Optional[dict]): The authentication config as a dictionary.
105
+ auth (Optional[M2MAuthenticationConfig]): The authentication config.
106
106
 
107
107
  Returns:
108
108
  M2MAuthenticationConfig: The validated authentication configuration.
@@ -110,11 +110,10 @@ class Config(BaseSettings):
110
110
  Raises:
111
111
  ValueError: If authentication is missing or required fields are not set.
112
112
  """
113
- if not auth_data:
114
- return cls.apply_auth_defaults(M2MAuthenticationConfig())
115
-
116
- auth = cls.parse_auth_config(auth_data)
117
- auth = cls.apply_auth_defaults(auth)
113
+ if auth is None:
114
+ auth = cls.apply_auth_defaults(M2MAuthenticationConfig())
115
+ else:
116
+ auth = cls.apply_auth_defaults(auth)
118
117
 
119
118
  cls.check_required_auth_fields(auth)
120
119
  return auth
@@ -0,0 +1,15 @@
1
+ """Authentication configuration options."""
2
+
3
+ from typing import Union
4
+
5
+ from datacosmos.config.models.local_user_account_authentication_config import (
6
+ LocalUserAccountAuthenticationConfig,
7
+ )
8
+ from datacosmos.config.models.m2m_authentication_config import M2MAuthenticationConfig
9
+ from datacosmos.config.models.no_authentication_config import NoAuthenticationConfig
10
+
11
+ AuthenticationConfig = Union[
12
+ NoAuthenticationConfig,
13
+ LocalUserAccountAuthenticationConfig,
14
+ M2MAuthenticationConfig,
15
+ ]
@@ -0,0 +1,26 @@
1
+ """Configuration for local user account authentication.
2
+
3
+ When this is chosen, the user will be prompted to log in using their OPS credentials.
4
+ This will be used for running scripts locally.
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class LocalUserAccountAuthenticationConfig(BaseModel):
13
+ """Configuration for local user account authentication.
14
+
15
+ When this is chosen, the user will be prompted to log in using their OPS credentials.
16
+ This will be used for running scripts locally.
17
+ """
18
+
19
+ type: Literal["local"]
20
+ client_id: str
21
+ authorization_endpoint: str
22
+ token_endpoint: str
23
+ redirect_port: int
24
+ scopes: str
25
+ audience: str
26
+ cache_file: str
@@ -0,0 +1,17 @@
1
+ """No authentication configuration.
2
+
3
+ This is used when running scripts in tests.
4
+ """
5
+
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class NoAuthenticationConfig(BaseModel):
12
+ """No authentication configuration.
13
+
14
+ This is used when running scripts in tests.
15
+ """
16
+
17
+ type: Literal["none"]
@@ -1,8 +1,4 @@
1
- """DatacosmosClient handles authenticated interactions with the Datacosmos API.
2
-
3
- Automatically manages token refreshing and provides HTTP convenience
4
- methods.
5
- """
1
+ """Client to interact with the Datacosmos API with authentication and request handling."""
6
2
 
7
3
  from datetime import datetime, timedelta, timezone
8
4
  from typing import Any, Optional
@@ -12,30 +8,64 @@ from oauthlib.oauth2 import BackendApplicationClient
12
8
  from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
13
9
  from requests_oauthlib import OAuth2Session
14
10
 
15
- from config.config import Config
11
+ from datacosmos.config.config import Config
16
12
  from datacosmos.exceptions.datacosmos_exception import DatacosmosException
17
13
 
18
14
 
19
15
  class DatacosmosClient:
20
16
  """Client to interact with the Datacosmos API with authentication and request handling."""
21
17
 
22
- def __init__(self, config: Optional[Config] = None):
18
+ def __init__(
19
+ self,
20
+ config: Optional[Config | Any] = None,
21
+ http_session: Optional[requests.Session | OAuth2Session] = None,
22
+ ):
23
23
  """Initialize the DatacosmosClient.
24
24
 
25
25
  Args:
26
- config (Optional[Config]): Configuration object.
26
+ config (Optional[Config]): Configuration object (only needed when SDK creates its own session).
27
+ http_session (Optional[requests.Session]): Pre-authenticated session.
27
28
  """
28
- if config:
29
- self.config = config
30
- else:
29
+ if http_session is not None:
30
+ self._http_client = http_session
31
+ self._owns_session = False
32
+ if isinstance(http_session, OAuth2Session):
33
+ token_data = http_session.token
34
+ elif isinstance(http_session, requests.Session):
35
+ auth_header = http_session.headers.get("Authorization", "")
36
+ if not auth_header.startswith("Bearer "):
37
+ raise DatacosmosException(
38
+ "Injected requests.Session must include a 'Bearer' token in its headers"
39
+ )
40
+ token_data = {"access_token": auth_header.split(" ", 1)[1]}
41
+ else:
42
+ raise DatacosmosException(
43
+ f"Unsupported session type: {type(http_session)}"
44
+ )
31
45
  try:
32
- self.config = Config.from_yaml()
33
- except ValueError:
34
- self.config = Config.from_env()
46
+ self.token = token_data.get("access_token")
47
+ self.token_expiry = token_data.get("expires_at") or token_data.get(
48
+ "expires_in"
49
+ )
50
+ except Exception:
51
+ raise DatacosmosException(
52
+ "Failed to extract token from injected session"
53
+ )
35
54
 
36
- self.token = None
37
- self.token_expiry = None
38
- self._http_client = self._authenticate_and_initialize_client()
55
+ self.config = config
56
+ else:
57
+ if config:
58
+ self.config = config
59
+ else:
60
+ try:
61
+ self.config = Config.from_yaml()
62
+ except ValueError:
63
+ self.config = Config.from_env()
64
+
65
+ self._owns_session = True
66
+ self.token = None
67
+ self.token_expiry = None
68
+ self._http_client = self._authenticate_and_initialize_client()
39
69
 
40
70
  def _authenticate_and_initialize_client(self) -> requests.Session:
41
71
  """Authenticate and initialize the HTTP client with a valid token."""
@@ -68,8 +98,10 @@ class DatacosmosClient:
68
98
  ) from e
69
99
 
70
100
  def _refresh_token_if_needed(self):
71
- """Refresh the token if it has expired."""
72
- if not self.token or self.token_expiry <= datetime.now(timezone.utc):
101
+ """Refresh the token if it has expired (only if SDK created it)."""
102
+ if self._owns_session and (
103
+ not self.token or self.token_expiry <= datetime.now(timezone.utc)
104
+ ):
73
105
  self._http_client = self._authenticate_and_initialize_client()
74
106
 
75
107
  def request(
@@ -8,7 +8,6 @@ import structlog
8
8
 
9
9
  from datacosmos.stac.enums.processing_level import ProcessingLevel
10
10
  from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
11
- from datacosmos.utils.missions import get_mission_id
12
11
 
13
12
  logger = structlog.get_logger()
14
13
 
@@ -35,11 +34,6 @@ class UploadPath:
35
34
  cls, item: DatacosmosItem, mission: str, item_path: str
36
35
  ) -> "Path":
37
36
  """Create a Path instance from a DatacosmosItem and a path."""
38
- for asset in item.assets.values():
39
- if mission == "":
40
- mission = cls._get_mission_name(asset.href)
41
- else:
42
- break
43
37
  dt = datetime.strptime(item.properties["datetime"], "%Y-%m-%dT%H:%M:%SZ")
44
38
  path = UploadPath(
45
39
  mission=mission,
@@ -67,27 +61,3 @@ class UploadPath:
67
61
  id=parts[5],
68
62
  path="/".join(parts[6:]),
69
63
  )
70
-
71
- @classmethod
72
- def _get_mission_name(cls, href: str) -> str:
73
- mission = ""
74
- # bruteforce mission name from asset path
75
- # traverse the path and check if any part is a mission name (generates a mission id)
76
- href_parts = href.split("/")
77
- for idx, part in enumerate(href_parts):
78
- try:
79
- # when an id is found, then the mission name is valid
80
- get_mission_id(
81
- part, "test"
82
- ) # using test as it is more wide and anything on prod should exists on test
83
- except KeyError:
84
- continue
85
- # validate the mission name by checking if the path is correct
86
- # using the same logic as the __str__ method
87
- mission = part.lower()
88
- h = "/".join(["full", *href_parts[idx:]])
89
- p = UploadPath.from_path("/".join([mission, *href_parts[idx + 1 :]]))
90
- if str(p) != h:
91
- raise ValueError(f"Could not find mission name in asset path {href}")
92
- break
93
- return mission
@@ -9,7 +9,6 @@ from datacosmos.datacosmos_client import DatacosmosClient
9
9
  from datacosmos.stac.item.item_client import ItemClient
10
10
  from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
11
11
  from datacosmos.uploader.dataclasses.upload_path import UploadPath
12
- from datacosmos.utils.missions import get_mission_name
13
12
 
14
13
 
15
14
  class DatacosmosUploader:
@@ -17,14 +16,10 @@ class DatacosmosUploader:
17
16
 
18
17
  def __init__(self, client: DatacosmosClient):
19
18
  """Initialize the uploader with DatacosmosClient."""
20
- mission_id = client.config.mission_id
21
- environment = client.config.environment
19
+ self.environment = client.config.environment
22
20
 
23
21
  self.datacosmos_client = client
24
22
  self.item_client = ItemClient(client)
25
- self.mission_name = (
26
- get_mission_name(mission_id, environment) if mission_id != 0 else ""
27
- )
28
23
  self.base_url = client.config.datacosmos_cloud_storage.as_domain_url()
29
24
 
30
25
  def upload_and_register_item(self, item_json_file_path: str) -> None:
@@ -92,9 +87,9 @@ class DatacosmosUploader:
92
87
  except Exception: # nosec
93
88
  pass # Ignore if item doesn't exist
94
89
 
95
- def _get_upload_path(self, item: DatacosmosItem) -> str:
90
+ def _get_upload_path(self, item: DatacosmosItem, mission_name: str = "") -> str:
96
91
  """Constructs the storage upload path based on the item and mission name."""
97
- return UploadPath.from_item_path(item, self.mission_name, "")
92
+ return UploadPath.from_item_path(item, mission_name, "")
98
93
 
99
94
  def _update_item_assets(self, item: DatacosmosItem) -> None:
100
95
  """Updates the item's assets with uploaded file URLs."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.6
3
+ Version: 0.0.8
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
@@ -1,10 +1,13 @@
1
- config/__init__.py,sha256=KCsaTb9-ZgFui1GM8wZFIPLJy0D0O8l8Z1Sv3NRD9UM,140
2
- config/config.py,sha256=vtimmFY2zOXV3OjT7sS5P0p1sW_-ecB5VCF6cseSk4g,7165
3
- config/models/__init__.py,sha256=r3lThPkyKjBjUZXRNscFzOrmn_-m_i9DvG3RePfCFYc,41
4
- config/models/m2m_authentication_config.py,sha256=n76N4bakpPPycTOeKpiM8pazYtNqiJGMzZXmI_ogbHM,847
5
- config/models/url.py,sha256=fwr2C06e_RDS8AWxOV_orVxMWhc57bzYoWSjFxQbkwg,835
6
1
  datacosmos/__init__.py,sha256=dVHKpbz5FVtfoJAWHRdsUENG6H-vs4UrkuwnIvOGJr4,66
7
- datacosmos/datacosmos_client.py,sha256=sivVYf45QEHTkUO62fnb1fnObKVmUngTR1Ga-ZRnoQE,4967
2
+ datacosmos/datacosmos_client.py,sha256=3BurTz1fPk1Dzp8B5xt5gZZrFiqk1AT5oaqKeYmXPec,6517
3
+ datacosmos/config/__init__.py,sha256=KCsaTb9-ZgFui1GM8wZFIPLJy0D0O8l8Z1Sv3NRD9UM,140
4
+ datacosmos/config/config.py,sha256=qj9e68enxX61Iw8lqEXUUCNJ2O7vy-1j9nAoBIB-4Ao,7219
5
+ datacosmos/config/models/__init__.py,sha256=r3lThPkyKjBjUZXRNscFzOrmn_-m_i9DvG3RePfCFYc,41
6
+ datacosmos/config/models/authentication_config.py,sha256=01Q90-yupbJ5orYDtatZIm9EaL7roQ-oUMoZfFMRzIM,499
7
+ datacosmos/config/models/local_user_account_authentication_config.py,sha256=8WApn720MBXMKQa6w7bCd7Z37GRmYR-I7mBUgUI20lQ,701
8
+ datacosmos/config/models/m2m_authentication_config.py,sha256=n76N4bakpPPycTOeKpiM8pazYtNqiJGMzZXmI_ogbHM,847
9
+ datacosmos/config/models/no_authentication_config.py,sha256=x5xikSGPuqQbrf_S2oIWXo5XxAORci2sSE5KyJvZHVw,312
10
+ datacosmos/config/models/url.py,sha256=fwr2C06e_RDS8AWxOV_orVxMWhc57bzYoWSjFxQbkwg,835
8
11
  datacosmos/exceptions/__init__.py,sha256=Crz8W7mOvPUXYcfDVotvjUt_3HKawBpmJA_-uel9UJk,45
9
12
  datacosmos/exceptions/datacosmos_exception.py,sha256=rKjJvQDvCEbxXWWccxB5GI_sth662bW8Yml0hX-vRw4,923
10
13
  datacosmos/stac/__init__.py,sha256=B4x_Mr4X7TzQoYtRC-VzI4W-fEON5WUOaz8cWJbk3Fc,214
@@ -29,20 +32,18 @@ datacosmos/stac/item/models/eo_band.py,sha256=YC3Scn_wFhIo51pIVcJeuJienF7JGWoEv3
29
32
  datacosmos/stac/item/models/item_update.py,sha256=_CpjQn9SsfedfuxlHSiGeptqY4M-p15t9YX__mBRueI,2088
30
33
  datacosmos/stac/item/models/raster_band.py,sha256=CoEVs-YyPE5Fse0He9DdOs4dGZpzfCsCuVzOcdXa_UM,354
31
34
  datacosmos/uploader/__init__.py,sha256=ZtfCVJ_pWKKh2F1r_NArnbG3_JtpcEiXcA_tmSwSKmQ,128
32
- datacosmos/uploader/datacosmos_uploader.py,sha256=QFFzR9Z2KFu_G5EcmvEn251IiwbPAfZSrOYZ_vC3NSg,4393
35
+ datacosmos/uploader/datacosmos_uploader.py,sha256=FDBUSZ9mpieXi3dms3RM17aIrnRuAKd0rFkD7Jwl1pY,4195
33
36
  datacosmos/uploader/dataclasses/__init__.py,sha256=IjcyA8Vod-z1_Gi1FMZhK58Owman0foL25Hs0YtkYYs,43
34
- datacosmos/uploader/dataclasses/upload_path.py,sha256=X8zkfw3_FO9qTiKHu-nL_uDmQJYfaov6e4Y2-f-opaU,3204
37
+ datacosmos/uploader/dataclasses/upload_path.py,sha256=5QadynHxkJrnOk1lyPtLyiVAHdzBshEuhjA9hwVF0NI,1903
35
38
  datacosmos/utils/__init__.py,sha256=XQbAnoqJrPpnSpEzAbjh84yqYWw8cBM8mNp8ynTG-54,50
36
- datacosmos/utils/constants.py,sha256=f7pOqCpdXk7WFGoaTyuCpr65jb-TtfhoVGuYTz3_T6Y,272
37
- datacosmos/utils/missions.py,sha256=7GOnrjxB8V11C_Jr3HHI4vpXifgkOSeirNjIDx17C58,940
38
39
  datacosmos/utils/url.py,sha256=iQwZr6mYRoePqUZg-k3KQSV9o2wju5ZuCa5WS_GyJo4,2114
39
40
  datacosmos/utils/http_response/__init__.py,sha256=BvOWwC5coYqq_kFn8gIw5m54TLpdfJKlW9vgRkfhXiA,33
40
41
  datacosmos/utils/http_response/check_api_response.py,sha256=dKWW01jn2_lWV0xpOBABhEP42CFSsx9dP0iSxykbN54,1186
41
42
  datacosmos/utils/http_response/models/__init__.py,sha256=Wj8YT6dqw7rAz_rctllxo5Or_vv8DwopvQvBzwCTvpw,45
42
43
  datacosmos/utils/http_response/models/datacosmos_error.py,sha256=Uqi2uM98nJPeCbM7zngV6vHSk97jEAb_nkdDEeUjiQM,740
43
44
  datacosmos/utils/http_response/models/datacosmos_response.py,sha256=oV4n-sue7K1wwiIQeHpxdNU8vxeqF3okVPE2rydw5W0,336
44
- datacosmos-0.0.6.dist-info/licenses/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
45
- datacosmos-0.0.6.dist-info/METADATA,sha256=62t4WIjxXxYdgVWDyJTcLnBYS9LHU1yuS872Zu8YqbQ,896
46
- datacosmos-0.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- datacosmos-0.0.6.dist-info/top_level.txt,sha256=Iu5b533Fmdfz0rFKTnuBPjSUOQL2lEkTfHxsokP72s4,18
48
- datacosmos-0.0.6.dist-info/RECORD,,
45
+ datacosmos-0.0.8.dist-info/licenses/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
46
+ datacosmos-0.0.8.dist-info/METADATA,sha256=tK8nQIG9_5zJ-gCQPQB7S2q6z7HabdgYdpPLfkinP9g,896
47
+ datacosmos-0.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ datacosmos-0.0.8.dist-info/top_level.txt,sha256=ueobs5CNeyDbPMgXPcVV0d0yNdm8CvGtDT3CaksRVtA,11
49
+ datacosmos-0.0.8.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- """Package for storing constants."""
2
-
3
- TEST_MISSION_NAMES = {
4
- 55: "MENUT",
5
- 56: "PHISAT-2",
6
- 57: "HAMMER",
7
- 63: "MANTIS",
8
- 64: "PLATERO",
9
- }
10
- PROD_MISSION_NAMES = {
11
- 23: "MENUT",
12
- 29: "MANTIS",
13
- 35: "PHISAT-2",
14
- 37: "PLATERO",
15
- 48: "HAMMER",
16
- }
@@ -1,27 +0,0 @@
1
- """Package for storing mission specific information."""
2
-
3
- from datacosmos.utils.constants import PROD_MISSION_NAMES, TEST_MISSION_NAMES
4
-
5
-
6
- def get_mission_name(mission: int, env: str) -> str:
7
- """Get the mission name from the mission number."""
8
- if env == "test" or env == "local":
9
- return TEST_MISSION_NAMES[mission]
10
- elif env == "prod":
11
- return PROD_MISSION_NAMES[mission]
12
- else:
13
- raise ValueError(f"Unsupported environment: {env}")
14
-
15
-
16
- def get_mission_id(mission_name: str, env: str) -> int:
17
- """Get the mission number from the mission name."""
18
- if env == "test" or env == "local":
19
- return {v.upper(): k for k, v in TEST_MISSION_NAMES.items()}[
20
- mission_name.upper()
21
- ]
22
- elif env == "prod":
23
- return {v.upper(): k for k, v in PROD_MISSION_NAMES.items()}[
24
- mission_name.upper()
25
- ]
26
- else:
27
- raise ValueError(f"Unsupported environment: {env}")
File without changes
File without changes
File without changes