prescient-sdk 0.1.0__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.
|
File without changes
|
prescient_sdk/client.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import datetime
|
|
3
|
+
import urllib.parse
|
|
4
|
+
|
|
5
|
+
import msal
|
|
6
|
+
import boto3
|
|
7
|
+
from rasterio.session import AWSSession
|
|
8
|
+
|
|
9
|
+
from prescient_sdk.config import Settings
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("prescient_sdk")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PrescientClient:
|
|
15
|
+
"""
|
|
16
|
+
Client for interacting with the Prescient API.
|
|
17
|
+
|
|
18
|
+
This client is used to authenticate using Azure AD, and for obtaining AWS
|
|
19
|
+
credentials using the Azure AD access token. The client also provides helpers such as
|
|
20
|
+
provding the STAC URL for the Prescient API, and authentication headers for making
|
|
21
|
+
requests to the STAC API.
|
|
22
|
+
|
|
23
|
+
Token expiration (both AWS and Azure) is handled automatically for long running instances
|
|
24
|
+
of the client (in a notebook for example).
|
|
25
|
+
|
|
26
|
+
Default configuration is set using the default values from prescient_sdk.config.
|
|
27
|
+
These can be overridden by setting values explicitly when initializing the client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
settings (Settings, optional): Configuration settings for the client. Defaults to None.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
settings: Settings | None = None,
|
|
37
|
+
):
|
|
38
|
+
# default configuration values are set in the Settings class (prescient_sdk.config)
|
|
39
|
+
if settings is None:
|
|
40
|
+
settings = Settings() # type: ignore
|
|
41
|
+
self.settings: Settings = settings
|
|
42
|
+
|
|
43
|
+
# initialize empty credentials
|
|
44
|
+
self._azure_credentials: dict = {}
|
|
45
|
+
self._aws_credentials: dict = {}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def stac_catalog_url(self) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Get the STAC URL.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
str: The STAC URL.
|
|
54
|
+
"""
|
|
55
|
+
return urllib.parse.urljoin(self.settings.endpoint_url, "stac")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def azure_credentials(self) -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Get the Azure credentials for the client.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
dict: Azure access token::
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
"token_type": "string",
|
|
67
|
+
"scope": "string",
|
|
68
|
+
"expires_in": "int",
|
|
69
|
+
"ext_expires_in": "int",
|
|
70
|
+
"access_token": "string",
|
|
71
|
+
"refresh_token": "string",
|
|
72
|
+
"id_token": "string",
|
|
73
|
+
"client_info": "string",
|
|
74
|
+
"id_token_claims": {
|
|
75
|
+
"aud": "string",
|
|
76
|
+
"iss": "string",
|
|
77
|
+
"iat": "int",
|
|
78
|
+
"nbf": "int",
|
|
79
|
+
"exp": "int",
|
|
80
|
+
"aio": "string",
|
|
81
|
+
"name": "string",
|
|
82
|
+
"nonce": "string",
|
|
83
|
+
"oid": "string",
|
|
84
|
+
"preferred_username": "string",
|
|
85
|
+
"rh": "string",
|
|
86
|
+
"roles": ["string"],
|
|
87
|
+
"sub": "string",
|
|
88
|
+
"tid": "string",
|
|
89
|
+
"uti": "string",
|
|
90
|
+
"ver": "string",
|
|
91
|
+
},
|
|
92
|
+
"token_source": "string",
|
|
93
|
+
}
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If the response status code is not 200, or if the access token is not in the response.
|
|
96
|
+
"""
|
|
97
|
+
# return cached credentials if they exist and are not expired
|
|
98
|
+
if "expiration" in self._azure_credentials and (
|
|
99
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
100
|
+
< self._azure_credentials["expiration"]
|
|
101
|
+
):
|
|
102
|
+
return self._azure_credentials
|
|
103
|
+
|
|
104
|
+
authority_url = urllib.parse.urljoin(
|
|
105
|
+
self.settings.azure_auth_url, self.settings.azure_tenant_id
|
|
106
|
+
)
|
|
107
|
+
app = msal.PublicClientApplication(
|
|
108
|
+
client_id=self.settings.azure_client_id, authority=authority_url
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# trigger auth or auth refresh flow
|
|
112
|
+
time_zero = datetime.datetime.now(datetime.timezone.utc)
|
|
113
|
+
if not self._azure_credentials:
|
|
114
|
+
# aquire creds interactively if none have been fetched yet
|
|
115
|
+
self._azure_credentials = app.acquire_token_interactive(scopes=[])
|
|
116
|
+
else:
|
|
117
|
+
# refresh creds if they have been fetched before and are expired
|
|
118
|
+
self._azure_credentials = app.acquire_token_by_refresh_token(
|
|
119
|
+
refresh_token=self._azure_credentials["refresh_token"], scopes=[]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# check that a nonzero length token has been obtained
|
|
123
|
+
token: str = self._azure_credentials.get("id_token", "")
|
|
124
|
+
if token == "":
|
|
125
|
+
raise ValueError(f"Failed to obtain Azure token: {self._azure_credentials}")
|
|
126
|
+
|
|
127
|
+
# set expiration time of the token
|
|
128
|
+
self._azure_credentials["expiration"] = time_zero + datetime.timedelta(
|
|
129
|
+
seconds=self._azure_credentials["expires_in"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return self._azure_credentials
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def headers(self) -> dict:
|
|
136
|
+
"""
|
|
137
|
+
Get headers for a request, including the auth header with a bearer token.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
dict: The headers.
|
|
141
|
+
"""
|
|
142
|
+
return {
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
"Accept": "application/json",
|
|
145
|
+
"Authorization": f"Bearer {self.azure_credentials['id_token']}",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def aws_credentials(self):
|
|
150
|
+
"""Get AWS credentials using Azure AD access token
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
dict: AWS temporary credentials::
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
"AccessKeyId": "string",
|
|
157
|
+
"SecretAccessKey": "string",
|
|
158
|
+
"SessionToken": "string",
|
|
159
|
+
"Expiration": datetime(2015, 1, 1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: If the AWS credentials response is empty
|
|
164
|
+
"""
|
|
165
|
+
if (
|
|
166
|
+
"Expiration" in self._aws_credentials
|
|
167
|
+
and datetime.datetime.now(datetime.timezone.utc)
|
|
168
|
+
< self._aws_credentials["Expiration"]
|
|
169
|
+
):
|
|
170
|
+
return self._aws_credentials
|
|
171
|
+
|
|
172
|
+
access_token = self.azure_credentials.get("id_token")
|
|
173
|
+
sts_client = boto3.client("sts", region_name=self.settings.aws_region)
|
|
174
|
+
|
|
175
|
+
# exchange token with aws temp creds
|
|
176
|
+
response: dict = sts_client.assume_role_with_web_identity(
|
|
177
|
+
DurationSeconds=3600, # 1 hour
|
|
178
|
+
RoleArn=self.settings.aws_role,
|
|
179
|
+
RoleSessionName="prescient-s3-access",
|
|
180
|
+
WebIdentityToken=access_token,
|
|
181
|
+
)
|
|
182
|
+
self._aws_credentials = response.get("Credentials", {})
|
|
183
|
+
|
|
184
|
+
if not self._aws_credentials:
|
|
185
|
+
raise ValueError(f"Failed to obtain AWS creds: {response}")
|
|
186
|
+
|
|
187
|
+
# convert datetime to UTC for later comparison
|
|
188
|
+
self._aws_credentials["Expiration"] = self._aws_credentials[
|
|
189
|
+
"Expiration"
|
|
190
|
+
].astimezone(datetime.timezone.utc)
|
|
191
|
+
|
|
192
|
+
return self._aws_credentials
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def session(self) -> AWSSession:
|
|
196
|
+
"""
|
|
197
|
+
Get an AWS session to be used when authenticating rasterio
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
AWSSession: Rasterio AWS session
|
|
201
|
+
"""
|
|
202
|
+
return AWSSession(
|
|
203
|
+
aws_access_key_id=self.aws_credentials["AccessKeyId"],
|
|
204
|
+
aws_secret_access_key=self.aws_credentials["SecretAccessKey"],
|
|
205
|
+
aws_session_token=self.aws_credentials["SessionToken"]
|
|
206
|
+
)
|
prescient_sdk/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
"""
|
|
8
|
+
Default configuration for the Prescient SDK.
|
|
9
|
+
|
|
10
|
+
Configuration is handled using [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
|
|
11
|
+
|
|
12
|
+
Order of precedence for configuration values:
|
|
13
|
+
|
|
14
|
+
1. Environment variables are always highest precedence and will override any other configuration values
|
|
15
|
+
2. `.env` file: if a `.env` file is present in the root of the project, it will be used
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
endpoint_url: str = Field()
|
|
19
|
+
|
|
20
|
+
aws_region: str = Field()
|
|
21
|
+
aws_role: str = Field(min_length=20)
|
|
22
|
+
|
|
23
|
+
azure_tenant_id: str
|
|
24
|
+
azure_client_id: str
|
|
25
|
+
azure_auth_url: str
|
|
26
|
+
azure_auth_token_path: str
|
|
27
|
+
|
|
28
|
+
model_config = SettingsConfigDict(
|
|
29
|
+
env_file="config.env",
|
|
30
|
+
env_file_encoding="utf-8",
|
|
31
|
+
env_prefix="PRESCIENT_",
|
|
32
|
+
case_sensitive=False,
|
|
33
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: prescient-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python SDK for integrating with Prescient services
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: boto3>=1.35.19
|
|
8
|
+
Requires-Dist: msal>=1.31.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.5.2
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# prescient-sdk
|
|
13
|
+
A Python SDK for integrating with Prescient services
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Quickstart
|
|
17
|
+
|
|
18
|
+
## Local Development
|
|
19
|
+
|
|
20
|
+
The project is set up using [uv](https://docs.astral.sh/uv/) for package management. To develop locally:
|
|
21
|
+
|
|
22
|
+
1. Install uv following [these](https://docs.astral.sh/uv/getting-started/installation/) intructions
|
|
23
|
+
|
|
24
|
+
1. (Optional) Use uv to set up your local env.
|
|
25
|
+
|
|
26
|
+
Note that this will happen automatically when you run tests or any python application using uv, so it is not necessary
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
uv sync
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
1. Run the tests
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
uv run pytest
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
or
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
make run-tests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Configuration methods are discussed in the Jupyter Notebook [configuration.ipynb](./docs/examples/configuration.ipynb)
|
|
47
|
+
|
|
48
|
+
## Code Formatting/Linting
|
|
49
|
+
|
|
50
|
+
Code format is set using the [Ruff](https://docs.astral.sh/ruff/) formatter. To run this formatter:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
make format
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Adding or removing dependencies
|
|
57
|
+
|
|
58
|
+
Add or remove dependencies using [UV](https://docs.astral.sh/uv/concepts/dependencies/).
|
|
59
|
+
|
|
60
|
+
In the simplest case, you can add a new dependency like this:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
uv add <some-dependency>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To add a dev dependency:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
uv add <some-dependency> --dev
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To remove a dependency:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
uv remove <some-dependency>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For more complex features, see the [uv documentation](https://docs.astral.sh/uv/)
|
|
79
|
+
|
|
80
|
+
## Build the documentation
|
|
81
|
+
|
|
82
|
+
Public facing documentation is built using [jupyter-books](https://jupyterbook.org/en/stable/intro.html).
|
|
83
|
+
|
|
84
|
+
The [docs](./docs) folder contains the layout for the public facing documentation.
|
|
85
|
+
|
|
86
|
+
You can build the documentation locally, and access the built html pages in a local browser:
|
|
87
|
+
|
|
88
|
+
1. Build the docs:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
make build-docs
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
1. Open the html in a browser:
|
|
95
|
+
|
|
96
|
+
After building the docs, the path to the index.html file will be logged, and should look something like `docs/_build/html/index.html`
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
prescient_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
prescient_sdk/client.py,sha256=7o6vP67-evUSYhhI_ZktdRrXJUNRPTBYnHC9VzQOgcA,7141
|
|
3
|
+
prescient_sdk/config.py,sha256=kKSMqtst7yvs02tszGk1ejcjSpinpIfztbCphvG9Q34,947
|
|
4
|
+
prescient_sdk-0.1.0.dist-info/METADATA,sha256=jfS3BAnzQLsFD3SwtFRCvSH9LMw6wJMjFcGlLzIzvJI,2180
|
|
5
|
+
prescient_sdk-0.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
6
|
+
prescient_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=XSE_ZgGPmTkM8I6i2rJMIXsgMAJlaQh2qdNe5u7v0S4,1069
|
|
7
|
+
prescient_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 sparkgeo.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|