Skip to content

Commit ab7a8a6

Browse files
committed
Cleanup
1 parent 52529d7 commit ab7a8a6

File tree

4 files changed

+217
-205
lines changed

4 files changed

+217
-205
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""Planetary computer credential providers."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from dataclasses import dataclass
7+
from datetime import datetime
8+
from pathlib import Path
9+
from typing import TYPE_CHECKING
10+
from urllib.parse import ParseResult, urlparse, urlunparse
11+
12+
if TYPE_CHECKING:
13+
import sys
14+
15+
import requests
16+
17+
from obstore.store import AzureCredential, AzureSASToken
18+
19+
if sys.version_info >= (3, 11):
20+
from typing import Self
21+
else:
22+
from typing_extensions import Self
23+
24+
_SETTINGS_ENV_STR = "~/.planetarycomputer/settings.env"
25+
_SETTINGS_ENV_FILE = Path(_SETTINGS_ENV_STR).expanduser()
26+
27+
_DEFAULT_SAS_TOKEN_ENDPOINT = "https://planetarycomputer.microsoft.com/api/sas/v1/token" # noqa: S105
28+
29+
__all__ = ["PlanetaryComputerCredentialProvider"]
30+
31+
32+
class PlanetaryComputerCredentialProvider:
33+
"""A CredentialProvider for [AzureStore][obstore.store.AzureStore] for accessing Planetary Computer.""" # noqa: E501
34+
35+
def __init__(
36+
self,
37+
*,
38+
account_name: str | None = None,
39+
container_name: str | None = None,
40+
sas_url: str | None = None,
41+
session: requests.Session | None = None,
42+
subscription_key: str | None = None,
43+
url: str | None = None,
44+
) -> None:
45+
"""Construct a new PlanetaryComputerCredentialProvider."""
46+
import requests
47+
import requests.adapters
48+
import urllib3
49+
import urllib3.util.retry
50+
51+
self.settings = _Settings.load(
52+
subscription_key=subscription_key,
53+
sas_url=sas_url,
54+
)
55+
56+
if session is None:
57+
# Upstream docstring in case we want to expose these values publicly
58+
# retry_total: The number of allowable retry attempts for REST API calls.
59+
# Use retry_total=0 to disable retries. A backoff factor to apply
60+
# between attempts.
61+
# retry_backoff_factor: A backoff factor to apply between attempts
62+
# after the second try (most errors are resolved immediately by a second
63+
# try without a delay). Retry policy will sleep for:
64+
65+
# ``{backoff factor} * (2 ** ({number of total retries} - 1))`` seconds.
66+
# If the backoff_factor is 0.1, then the retry will sleep for
67+
# [0.0s, 0.2s, 0.4s, ...] between retries. The default value is 0.8.
68+
retry_total = 10
69+
retry_backoff_factor = 0.8
70+
71+
session = requests.Session()
72+
retry = urllib3.util.retry.Retry(
73+
total=retry_total,
74+
backoff_factor=retry_backoff_factor, # type: ignore (invalid upstream typing)
75+
status_forcelist=[429, 500, 502, 503, 504],
76+
)
77+
78+
adapter = requests.adapters.HTTPAdapter(max_retries=retry)
79+
session.mount("http://", adapter)
80+
session.mount("https://", adapter)
81+
self.session = session
82+
else:
83+
self.session = session
84+
85+
if url is not None:
86+
assert container_name is None and account_name is None
87+
88+
parsed_url = urlparse(url.rstrip("/"))
89+
90+
self.account, self.container = parse_blob_url(parsed_url)
91+
else:
92+
assert container_name is not None and account_name is not None
93+
self.account = account_name
94+
self.container = container_name
95+
96+
def __call__(self) -> AzureCredential:
97+
"""Fetch a new token."""
98+
return _get_token_sync(
99+
account_name=self.account,
100+
container_name=self.container,
101+
settings=self.settings,
102+
session=self.session,
103+
)
104+
105+
106+
def _get_token_sync(
107+
*,
108+
account_name: str,
109+
container_name: str,
110+
settings: _Settings,
111+
session: requests.Session,
112+
) -> AzureSASToken:
113+
"""Get a token for a container in a storage account.
114+
115+
Returns:
116+
SASToken: the generated token
117+
118+
"""
119+
token_request_url = settings.token_request_url(
120+
account_name=account_name,
121+
container_name=container_name,
122+
)
123+
124+
response = session.get(
125+
token_request_url,
126+
headers=(
127+
{"Ocp-Apim-Subscription-Key": settings.subscription_key}
128+
if settings.subscription_key
129+
else None
130+
),
131+
)
132+
response.raise_for_status()
133+
134+
d = response.json()
135+
expires_at = datetime.fromisoformat(d["msft:expiry"].replace("Z", "+00:00"))
136+
137+
return {
138+
"sas_token": d["token"],
139+
"expires_at": expires_at,
140+
}
141+
142+
143+
def parse_blob_url(parsed_url: ParseResult) -> tuple[str, str]:
144+
"""Find the account and container in a blob URL.
145+
146+
Returns:
147+
Tuple of the account name and container name
148+
149+
"""
150+
try:
151+
account_name = parsed_url.netloc.split(".")[0]
152+
path_blob = parsed_url.path.lstrip("/").split("/", 1)
153+
container_name = path_blob[-2]
154+
except Exception as failed_parse:
155+
msg = f"Invalid blob URL: {urlunparse(parsed_url)}"
156+
raise ValueError(msg) from failed_parse
157+
158+
return account_name, container_name
159+
160+
161+
@dataclass
162+
class _Settings:
163+
"""Planetary Computer configuration settings."""
164+
165+
subscription_key: str | None
166+
sas_url: str
167+
168+
@classmethod
169+
def load(cls, *, subscription_key: str | None, sas_url: str | None) -> Self:
170+
"""Load settings values.
171+
172+
Order of precedence:
173+
174+
1. Passed in values by the user.
175+
2. Environment variables
176+
3. Dotenv file
177+
4. Defaults
178+
179+
"""
180+
return cls(
181+
subscription_key=subscription_key or _subscription_key_default(),
182+
sas_url=sas_url or _sas_url_default(),
183+
)
184+
185+
def token_request_url(
186+
self,
187+
*,
188+
account_name: str,
189+
container_name: str,
190+
) -> str:
191+
return f"{self.sas_url}/{account_name}/{container_name}"
192+
193+
194+
def _from_env(key: str) -> str | None:
195+
value = os.environ.get(key)
196+
if value is not None:
197+
return value
198+
199+
if _SETTINGS_ENV_FILE.exists():
200+
try:
201+
import dotenv
202+
except ImportError as e:
203+
msg = f"python-dotenv dependency required to read from {_SETTINGS_ENV_STR}"
204+
raise ImportError(msg) from e
205+
206+
values = dotenv.dotenv_values(_SETTINGS_ENV_FILE)
207+
return values.get(key)
208+
209+
return None
210+
211+
212+
def _subscription_key_default() -> str | None:
213+
return _from_env("PC_SDK_SUBSCRIPTION_KEY")
214+
215+
216+
def _sas_url_default() -> str:
217+
return _from_env("PC_SDK_SAS_URL") or _DEFAULT_SAS_TOKEN_ENDPOINT

obstore/python/obstore/auth/planetary_computer/__init__.py

-1
This file was deleted.

obstore/python/obstore/auth/planetary_computer/_credential_provider.py

-129
This file was deleted.

0 commit comments

Comments
 (0)