across-server-openapi-python 0.0.1__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.
- across/__init__.py +0 -0
- across/sdk/__init__.py +0 -0
- across/sdk/v1/__init__.py +251 -0
- across/sdk/v1/abstract_credential_storage.py +16 -0
- across/sdk/v1/api/__init__.py +20 -0
- across/sdk/v1/api/auth_api.py +1097 -0
- across/sdk/v1/api/filter_api.py +623 -0
- across/sdk/v1/api/group_api.py +586 -0
- across/sdk/v1/api/group_invite_api.py +1165 -0
- across/sdk/v1/api/group_role_api.py +2070 -0
- across/sdk/v1/api/instrument_api.py +633 -0
- across/sdk/v1/api/internal_api.py +571 -0
- across/sdk/v1/api/observation_api.py +940 -0
- across/sdk/v1/api/observatory_api.py +670 -0
- across/sdk/v1/api/permission_api.py +285 -0
- across/sdk/v1/api/role_api.py +552 -0
- across/sdk/v1/api/schedule_api.py +1914 -0
- across/sdk/v1/api/service_account_api.py +2353 -0
- across/sdk/v1/api/telescope_api.py +667 -0
- across/sdk/v1/api/tle_api.py +317 -0
- across/sdk/v1/api/tools_api.py +812 -0
- across/sdk/v1/api/user_api.py +2263 -0
- across/sdk/v1/api_client.py +801 -0
- across/sdk/v1/api_client_wrapper.py +239 -0
- across/sdk/v1/api_response.py +21 -0
- across/sdk/v1/configuration.py +605 -0
- across/sdk/v1/exceptions.py +216 -0
- across/sdk/v1/models/__init__.py +103 -0
- across/sdk/v1/models/access_token_response.py +89 -0
- across/sdk/v1/models/across_server_routes_v1_group_invite_schemas_group_invite.py +104 -0
- across/sdk/v1/models/across_server_routes_v1_group_role_schemas_group_role.py +124 -0
- across/sdk/v1/models/across_server_routes_v1_group_role_schemas_service_account.py +106 -0
- across/sdk/v1/models/across_server_routes_v1_group_role_schemas_user.py +95 -0
- across/sdk/v1/models/across_server_routes_v1_group_schemas_group.py +111 -0
- across/sdk/v1/models/across_server_routes_v1_group_schemas_user.py +105 -0
- across/sdk/v1/models/across_server_routes_v1_role_schemas_user.py +96 -0
- across/sdk/v1/models/across_server_routes_v1_system_service_account_schemas_service_account.py +90 -0
- across/sdk/v1/models/across_server_routes_v1_system_service_account_schemas_service_account_secret.py +92 -0
- across/sdk/v1/models/across_server_routes_v1_user_schemas_group.py +101 -0
- across/sdk/v1/models/across_server_routes_v1_user_schemas_group_invite.py +99 -0
- across/sdk/v1/models/across_server_routes_v1_user_schemas_group_role.py +99 -0
- across/sdk/v1/models/across_server_routes_v1_user_schemas_user.py +135 -0
- across/sdk/v1/models/across_server_routes_v1_user_service_account_schemas_service_account.py +111 -0
- across/sdk/v1/models/across_server_routes_v1_user_service_account_schemas_service_account_secret.py +118 -0
- across/sdk/v1/models/alt_az_constraint.py +130 -0
- across/sdk/v1/models/bandpass.py +149 -0
- across/sdk/v1/models/bandpass_type.py +155 -0
- across/sdk/v1/models/constrained_date.py +93 -0
- across/sdk/v1/models/constraint_reason.py +89 -0
- across/sdk/v1/models/constraint_type.py +43 -0
- across/sdk/v1/models/coordinate.py +100 -0
- across/sdk/v1/models/date_range.py +90 -0
- across/sdk/v1/models/depth_unit.py +39 -0
- across/sdk/v1/models/earth_limb_constraint.py +124 -0
- across/sdk/v1/models/energy_bandpass.py +121 -0
- across/sdk/v1/models/energy_unit.py +40 -0
- across/sdk/v1/models/ephemeris_type.py +39 -0
- across/sdk/v1/models/filter.py +135 -0
- across/sdk/v1/models/frequency_bandpass.py +121 -0
- across/sdk/v1/models/frequency_unit.py +40 -0
- across/sdk/v1/models/grant_type.py +37 -0
- across/sdk/v1/models/ground_parameters.py +91 -0
- across/sdk/v1/models/group_invite_create.py +87 -0
- across/sdk/v1/models/group_invite_group_details.py +94 -0
- across/sdk/v1/models/group_read.py +91 -0
- across/sdk/v1/models/group_role_create.py +89 -0
- across/sdk/v1/models/group_role_read.py +99 -0
- across/sdk/v1/models/http_validation_error.py +95 -0
- across/sdk/v1/models/id_name_schema.py +96 -0
- across/sdk/v1/models/instrument.py +163 -0
- across/sdk/v1/models/instrument_constraints_inner.py +237 -0
- across/sdk/v1/models/ivoa_obs_category.py +39 -0
- across/sdk/v1/models/ivoa_obs_tracking_type.py +38 -0
- across/sdk/v1/models/jpl_parameters.py +87 -0
- across/sdk/v1/models/moon_angle_constraint.py +124 -0
- across/sdk/v1/models/nullable_date_range.py +100 -0
- across/sdk/v1/models/observation.py +248 -0
- across/sdk/v1/models/observation_create.py +249 -0
- across/sdk/v1/models/observation_status.py +40 -0
- across/sdk/v1/models/observation_type.py +39 -0
- across/sdk/v1/models/observatory.py +145 -0
- across/sdk/v1/models/observatory_ephemeris_type.py +96 -0
- across/sdk/v1/models/observatory_type.py +37 -0
- across/sdk/v1/models/page_observation.py +116 -0
- across/sdk/v1/models/page_schedule.py +116 -0
- across/sdk/v1/models/parameters.py +164 -0
- across/sdk/v1/models/permission.py +89 -0
- across/sdk/v1/models/point.py +89 -0
- across/sdk/v1/models/role.py +100 -0
- across/sdk/v1/models/role_base.py +89 -0
- across/sdk/v1/models/saa_polygon_constraint.py +101 -0
- across/sdk/v1/models/schedule.py +144 -0
- across/sdk/v1/models/schedule_cadence.py +99 -0
- across/sdk/v1/models/schedule_create.py +123 -0
- across/sdk/v1/models/schedule_create_many.py +97 -0
- across/sdk/v1/models/schedule_fidelity.py +37 -0
- across/sdk/v1/models/schedule_status.py +38 -0
- across/sdk/v1/models/service_account_create.py +101 -0
- across/sdk/v1/models/service_account_secret.py +103 -0
- across/sdk/v1/models/service_account_update.py +106 -0
- across/sdk/v1/models/spice_parameters.py +89 -0
- across/sdk/v1/models/sun_angle_constraint.py +124 -0
- across/sdk/v1/models/system_service_account.py +121 -0
- across/sdk/v1/models/system_service_account_secret.py +123 -0
- across/sdk/v1/models/telescope.py +135 -0
- across/sdk/v1/models/telescope_instrument.py +163 -0
- across/sdk/v1/models/tle.py +99 -0
- across/sdk/v1/models/tle_create.py +94 -0
- across/sdk/v1/models/tle_parameters.py +89 -0
- across/sdk/v1/models/unit_value.py +94 -0
- across/sdk/v1/models/user_create.py +93 -0
- across/sdk/v1/models/user_info.py +95 -0
- across/sdk/v1/models/user_update.py +106 -0
- across/sdk/v1/models/validation_error.py +99 -0
- across/sdk/v1/models/validation_error_loc_inner.py +138 -0
- across/sdk/v1/models/visibility_result.py +97 -0
- across/sdk/v1/models/visibility_type.py +38 -0
- across/sdk/v1/models/visibility_window.py +99 -0
- across/sdk/v1/models/wavelength_bandpass.py +142 -0
- across/sdk/v1/models/wavelength_unit.py +39 -0
- across/sdk/v1/models/window.py +96 -0
- across/sdk/v1/py.typed +0 -0
- across/sdk/v1/rest.py +258 -0
- across_server_openapi_python-0.0.1.dist-info/METADATA +326 -0
- across_server_openapi_python-0.0.1.dist-info/RECORD +128 -0
- across_server_openapi_python-0.0.1.dist-info/WHEEL +5 -0
- across_server_openapi_python-0.0.1.dist-info/licenses/LICENSE +42 -0
- across_server_openapi_python-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import across.sdk.v1 as sdk
|
|
11
|
+
from across.sdk.v1 import rest
|
|
12
|
+
|
|
13
|
+
from .abstract_credential_storage import CredentialStorage as ICredStorage
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("ACROSS_API_CLIENT_WRAPPER")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiClientWrapper(sdk.ApiClient):
|
|
19
|
+
_client = None
|
|
20
|
+
_cred_store: ICredStorage | None = None
|
|
21
|
+
_exp: datetime | None = None
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
configuration: sdk.Configuration,
|
|
26
|
+
creds_store: ICredStorage | None = None,
|
|
27
|
+
*args,
|
|
28
|
+
**kwargs,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(configuration, *args, **kwargs)
|
|
31
|
+
|
|
32
|
+
self._cred_store = creds_store
|
|
33
|
+
self._lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_client(
|
|
37
|
+
cls,
|
|
38
|
+
configuration: sdk.Configuration,
|
|
39
|
+
creds: ICredStorage | None = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Retrieve (or lazily initialize) a singleton API client.
|
|
43
|
+
|
|
44
|
+
This method ensures that only one client instance is created per class.
|
|
45
|
+
If no client exists, it will initialize one using the provided
|
|
46
|
+
`configuration` and optional `creds` (credentials storage).
|
|
47
|
+
|
|
48
|
+
Credentials resolution order:
|
|
49
|
+
1. Use `configuration.username` and `configuration.password` if they are provided.
|
|
50
|
+
2. If not provided and `creds` is passed in, fetch credentials from it.
|
|
51
|
+
3. Otherwise, fall back to environment variables:
|
|
52
|
+
- `ACROSS_SERVER_ID`
|
|
53
|
+
- `ACROSS_SERVER_SECRET`
|
|
54
|
+
|
|
55
|
+
⚠️ Note:
|
|
56
|
+
This is a **singleton** accessor. Once the client is created,
|
|
57
|
+
subsequent calls to `get_client` will reuse the same instance,
|
|
58
|
+
regardless of whether a different `configuration` or `creds` is passed.
|
|
59
|
+
To reset or replace the client, `_client` must be cleared explicitly.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
configuration (sdk.Configuration): The base configuration object
|
|
63
|
+
for the SDK client.
|
|
64
|
+
creds (ICredStorage | None, optional): Optional credential storage
|
|
65
|
+
provider for resolving `id` and `secret`. Defaults to None.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ApiClientWrapper: A singleton instance of the API client wrapper.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
from my_sdk import Configuration, ApiClientWrapper, CredentialStorage, SomeApi
|
|
72
|
+
from my_creds import CredStorage
|
|
73
|
+
|
|
74
|
+
config = Configuration(host="https://api.example.com")
|
|
75
|
+
creds: CredentialStorage = CredStorage()
|
|
76
|
+
|
|
77
|
+
client = ApiClientWrapper.get_client(configuration=config, creds=creds)
|
|
78
|
+
|
|
79
|
+
response = SomeApi(client).some_method()
|
|
80
|
+
print(response)
|
|
81
|
+
"""
|
|
82
|
+
if cls._client is None:
|
|
83
|
+
if not configuration.username and not configuration.password:
|
|
84
|
+
# Use the creds store if it is passed in, otherwise use env vars
|
|
85
|
+
if creds:
|
|
86
|
+
configuration.username = creds.id(force=True)
|
|
87
|
+
configuration.password = creds.secret(force=True)
|
|
88
|
+
else:
|
|
89
|
+
configuration.username = os.getenv("ACROSS_SERVER_ID")
|
|
90
|
+
configuration.password = os.getenv("ACROSS_SERVER_SECRET")
|
|
91
|
+
|
|
92
|
+
cls._client = ApiClientWrapper(
|
|
93
|
+
configuration=configuration,
|
|
94
|
+
creds_store=creds,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return cls._client
|
|
98
|
+
|
|
99
|
+
def call_api(self, *args, **kwargs) -> rest.RESTResponse:
|
|
100
|
+
if args[0].lower() != "get":
|
|
101
|
+
self.refresh()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return super().call_api(*args, **kwargs)
|
|
105
|
+
except sdk.ApiException as err:
|
|
106
|
+
if err.status == 401:
|
|
107
|
+
logger.debug("Access token is unauthenticated or it has expired.")
|
|
108
|
+
|
|
109
|
+
refreshed = self.refresh_token()
|
|
110
|
+
|
|
111
|
+
# Attempt the call again
|
|
112
|
+
if refreshed:
|
|
113
|
+
return super().call_api(*args, **kwargs)
|
|
114
|
+
else:
|
|
115
|
+
raise err
|
|
116
|
+
else:
|
|
117
|
+
raise err
|
|
118
|
+
|
|
119
|
+
def refresh(self) -> None:
|
|
120
|
+
if not self.configuration.access_token:
|
|
121
|
+
logger.debug('No access_token, refreshing')
|
|
122
|
+
self.refresh_token()
|
|
123
|
+
|
|
124
|
+
if self._is_token_invalid(self.configuration.access_token):
|
|
125
|
+
logger.debug('Expired access_token, refreshing')
|
|
126
|
+
self.refresh_token()
|
|
127
|
+
|
|
128
|
+
if self._cred_store:
|
|
129
|
+
with self._lock:
|
|
130
|
+
if self._should_rotate(self._cred_store):
|
|
131
|
+
res = sdk.InternalApi(super()).service_account_rotate_key(
|
|
132
|
+
service_account_id=self._cred_store.id()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self._set_exp(res.expiration)
|
|
136
|
+
self._cred_store.update_key(res.secret_key)
|
|
137
|
+
self.configuration.password = res.secret_key
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _decode_jwt_part(self, encoded_part) -> dict[str, Any]:
|
|
142
|
+
"""Decodes a Base64Url-encoded JWT part and returns the decoded JSON as a dictionary."""
|
|
143
|
+
# Add padding characters if missing, as Base64Url encoding might omit them.
|
|
144
|
+
# Base64 requires padding to be a multiple of 4.
|
|
145
|
+
missing_padding = len(encoded_part) % 4
|
|
146
|
+
if missing_padding != 0:
|
|
147
|
+
encoded_part += '=' * (4 - missing_padding)
|
|
148
|
+
|
|
149
|
+
# Decode from Base64Url to bytes to UTF-8
|
|
150
|
+
decoded_bytes = base64.urlsafe_b64decode(encoded_part).decode('utf-8')
|
|
151
|
+
|
|
152
|
+
if decoded_bytes is None:
|
|
153
|
+
logger.debug("Could not decode jwt payload as bytes")
|
|
154
|
+
return {}
|
|
155
|
+
|
|
156
|
+
decoded_json = json.loads(decoded_bytes)
|
|
157
|
+
return decoded_json
|
|
158
|
+
|
|
159
|
+
def _is_token_invalid(self, jwt_token):
|
|
160
|
+
"""Returns True when the token is expired, malformed, or missing expiration"""
|
|
161
|
+
# JWT contains 3 parts, we're looking for the middle part; the payload with the exp key
|
|
162
|
+
if not isinstance(jwt_token, str):
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
jwt_parts = jwt_token.split('.')
|
|
166
|
+
|
|
167
|
+
if len(jwt_parts) != 3:
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
payload_encoded = jwt_parts[1]
|
|
171
|
+
|
|
172
|
+
if payload_encoded is None:
|
|
173
|
+
logger.debug("Token missing payload")
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
payload = self._decode_jwt_part(payload_encoded)
|
|
177
|
+
token_exp = payload.get('exp')
|
|
178
|
+
current_timestamp = time.time()
|
|
179
|
+
|
|
180
|
+
if token_exp is None:
|
|
181
|
+
logger.debug("Token missing exp")
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
# Add 30 seconds to avoid boundary condition expiry while request is in flight
|
|
185
|
+
if token_exp < current_timestamp + 30:
|
|
186
|
+
logger.debug("Token is expired")
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def refresh_token(self) -> bool:
|
|
193
|
+
if self.configuration.username and self.configuration.password:
|
|
194
|
+
logger.debug("Refreshing access token...")
|
|
195
|
+
|
|
196
|
+
# Instantiate with super to avoid infinite recursion through call_api
|
|
197
|
+
token = sdk.AuthApi(super()).token(
|
|
198
|
+
grant_type=sdk.GrantType.CLIENT_CREDENTIALS
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
self.configuration.access_token = token.access_token
|
|
202
|
+
|
|
203
|
+
logger.debug("Successfully refreshed token!")
|
|
204
|
+
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
def _should_rotate(self, cred_store: ICredStorage) -> bool:
|
|
210
|
+
now = datetime.now(timezone.utc)
|
|
211
|
+
|
|
212
|
+
expiration = self._expiration(cred_store)
|
|
213
|
+
|
|
214
|
+
if expiration:
|
|
215
|
+
will_expire_soon = expiration <= now + timedelta(
|
|
216
|
+
days=cred_store.days_before_exp
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return will_expire_soon
|
|
220
|
+
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
def _expiration(self, cred_store: ICredStorage) -> datetime | None:
|
|
224
|
+
if self._exp:
|
|
225
|
+
return self._exp
|
|
226
|
+
|
|
227
|
+
res = sdk.InternalApi(super()).get_service_account(
|
|
228
|
+
service_account_id=cred_store.id()
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self._set_exp(res.expiration)
|
|
232
|
+
|
|
233
|
+
return self._exp
|
|
234
|
+
|
|
235
|
+
def _set_exp(self, date: datetime):
|
|
236
|
+
if date.tzinfo is None:
|
|
237
|
+
self._exp = date.replace(tzinfo=timezone.utc)
|
|
238
|
+
else:
|
|
239
|
+
self._exp = date
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""API response object."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Generic, Mapping, TypeVar
|
|
5
|
+
from pydantic import Field, StrictInt, StrictBytes, BaseModel
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
class ApiResponse(BaseModel, Generic[T]):
|
|
10
|
+
"""
|
|
11
|
+
API response object
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
status_code: StrictInt = Field(description="HTTP status code")
|
|
15
|
+
headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers")
|
|
16
|
+
data: T = Field(description="Deserialized data given the data type")
|
|
17
|
+
raw_data: StrictBytes = Field(description="Raw data (HTTP response body)")
|
|
18
|
+
|
|
19
|
+
model_config = {
|
|
20
|
+
"arbitrary_types_allowed": True
|
|
21
|
+
}
|