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
@@ -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
+ )
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.