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.
Files changed (128) hide show
  1. across/__init__.py +0 -0
  2. across/sdk/__init__.py +0 -0
  3. across/sdk/v1/__init__.py +251 -0
  4. across/sdk/v1/abstract_credential_storage.py +16 -0
  5. across/sdk/v1/api/__init__.py +20 -0
  6. across/sdk/v1/api/auth_api.py +1097 -0
  7. across/sdk/v1/api/filter_api.py +623 -0
  8. across/sdk/v1/api/group_api.py +586 -0
  9. across/sdk/v1/api/group_invite_api.py +1165 -0
  10. across/sdk/v1/api/group_role_api.py +2070 -0
  11. across/sdk/v1/api/instrument_api.py +633 -0
  12. across/sdk/v1/api/internal_api.py +571 -0
  13. across/sdk/v1/api/observation_api.py +940 -0
  14. across/sdk/v1/api/observatory_api.py +670 -0
  15. across/sdk/v1/api/permission_api.py +285 -0
  16. across/sdk/v1/api/role_api.py +552 -0
  17. across/sdk/v1/api/schedule_api.py +1914 -0
  18. across/sdk/v1/api/service_account_api.py +2353 -0
  19. across/sdk/v1/api/telescope_api.py +667 -0
  20. across/sdk/v1/api/tle_api.py +317 -0
  21. across/sdk/v1/api/tools_api.py +812 -0
  22. across/sdk/v1/api/user_api.py +2263 -0
  23. across/sdk/v1/api_client.py +801 -0
  24. across/sdk/v1/api_client_wrapper.py +239 -0
  25. across/sdk/v1/api_response.py +21 -0
  26. across/sdk/v1/configuration.py +605 -0
  27. across/sdk/v1/exceptions.py +216 -0
  28. across/sdk/v1/models/__init__.py +103 -0
  29. across/sdk/v1/models/access_token_response.py +89 -0
  30. across/sdk/v1/models/across_server_routes_v1_group_invite_schemas_group_invite.py +104 -0
  31. across/sdk/v1/models/across_server_routes_v1_group_role_schemas_group_role.py +124 -0
  32. across/sdk/v1/models/across_server_routes_v1_group_role_schemas_service_account.py +106 -0
  33. across/sdk/v1/models/across_server_routes_v1_group_role_schemas_user.py +95 -0
  34. across/sdk/v1/models/across_server_routes_v1_group_schemas_group.py +111 -0
  35. across/sdk/v1/models/across_server_routes_v1_group_schemas_user.py +105 -0
  36. across/sdk/v1/models/across_server_routes_v1_role_schemas_user.py +96 -0
  37. across/sdk/v1/models/across_server_routes_v1_system_service_account_schemas_service_account.py +90 -0
  38. across/sdk/v1/models/across_server_routes_v1_system_service_account_schemas_service_account_secret.py +92 -0
  39. across/sdk/v1/models/across_server_routes_v1_user_schemas_group.py +101 -0
  40. across/sdk/v1/models/across_server_routes_v1_user_schemas_group_invite.py +99 -0
  41. across/sdk/v1/models/across_server_routes_v1_user_schemas_group_role.py +99 -0
  42. across/sdk/v1/models/across_server_routes_v1_user_schemas_user.py +135 -0
  43. across/sdk/v1/models/across_server_routes_v1_user_service_account_schemas_service_account.py +111 -0
  44. across/sdk/v1/models/across_server_routes_v1_user_service_account_schemas_service_account_secret.py +118 -0
  45. across/sdk/v1/models/alt_az_constraint.py +130 -0
  46. across/sdk/v1/models/bandpass.py +149 -0
  47. across/sdk/v1/models/bandpass_type.py +155 -0
  48. across/sdk/v1/models/constrained_date.py +93 -0
  49. across/sdk/v1/models/constraint_reason.py +89 -0
  50. across/sdk/v1/models/constraint_type.py +43 -0
  51. across/sdk/v1/models/coordinate.py +100 -0
  52. across/sdk/v1/models/date_range.py +90 -0
  53. across/sdk/v1/models/depth_unit.py +39 -0
  54. across/sdk/v1/models/earth_limb_constraint.py +124 -0
  55. across/sdk/v1/models/energy_bandpass.py +121 -0
  56. across/sdk/v1/models/energy_unit.py +40 -0
  57. across/sdk/v1/models/ephemeris_type.py +39 -0
  58. across/sdk/v1/models/filter.py +135 -0
  59. across/sdk/v1/models/frequency_bandpass.py +121 -0
  60. across/sdk/v1/models/frequency_unit.py +40 -0
  61. across/sdk/v1/models/grant_type.py +37 -0
  62. across/sdk/v1/models/ground_parameters.py +91 -0
  63. across/sdk/v1/models/group_invite_create.py +87 -0
  64. across/sdk/v1/models/group_invite_group_details.py +94 -0
  65. across/sdk/v1/models/group_read.py +91 -0
  66. across/sdk/v1/models/group_role_create.py +89 -0
  67. across/sdk/v1/models/group_role_read.py +99 -0
  68. across/sdk/v1/models/http_validation_error.py +95 -0
  69. across/sdk/v1/models/id_name_schema.py +96 -0
  70. across/sdk/v1/models/instrument.py +163 -0
  71. across/sdk/v1/models/instrument_constraints_inner.py +237 -0
  72. across/sdk/v1/models/ivoa_obs_category.py +39 -0
  73. across/sdk/v1/models/ivoa_obs_tracking_type.py +38 -0
  74. across/sdk/v1/models/jpl_parameters.py +87 -0
  75. across/sdk/v1/models/moon_angle_constraint.py +124 -0
  76. across/sdk/v1/models/nullable_date_range.py +100 -0
  77. across/sdk/v1/models/observation.py +248 -0
  78. across/sdk/v1/models/observation_create.py +249 -0
  79. across/sdk/v1/models/observation_status.py +40 -0
  80. across/sdk/v1/models/observation_type.py +39 -0
  81. across/sdk/v1/models/observatory.py +145 -0
  82. across/sdk/v1/models/observatory_ephemeris_type.py +96 -0
  83. across/sdk/v1/models/observatory_type.py +37 -0
  84. across/sdk/v1/models/page_observation.py +116 -0
  85. across/sdk/v1/models/page_schedule.py +116 -0
  86. across/sdk/v1/models/parameters.py +164 -0
  87. across/sdk/v1/models/permission.py +89 -0
  88. across/sdk/v1/models/point.py +89 -0
  89. across/sdk/v1/models/role.py +100 -0
  90. across/sdk/v1/models/role_base.py +89 -0
  91. across/sdk/v1/models/saa_polygon_constraint.py +101 -0
  92. across/sdk/v1/models/schedule.py +144 -0
  93. across/sdk/v1/models/schedule_cadence.py +99 -0
  94. across/sdk/v1/models/schedule_create.py +123 -0
  95. across/sdk/v1/models/schedule_create_many.py +97 -0
  96. across/sdk/v1/models/schedule_fidelity.py +37 -0
  97. across/sdk/v1/models/schedule_status.py +38 -0
  98. across/sdk/v1/models/service_account_create.py +101 -0
  99. across/sdk/v1/models/service_account_secret.py +103 -0
  100. across/sdk/v1/models/service_account_update.py +106 -0
  101. across/sdk/v1/models/spice_parameters.py +89 -0
  102. across/sdk/v1/models/sun_angle_constraint.py +124 -0
  103. across/sdk/v1/models/system_service_account.py +121 -0
  104. across/sdk/v1/models/system_service_account_secret.py +123 -0
  105. across/sdk/v1/models/telescope.py +135 -0
  106. across/sdk/v1/models/telescope_instrument.py +163 -0
  107. across/sdk/v1/models/tle.py +99 -0
  108. across/sdk/v1/models/tle_create.py +94 -0
  109. across/sdk/v1/models/tle_parameters.py +89 -0
  110. across/sdk/v1/models/unit_value.py +94 -0
  111. across/sdk/v1/models/user_create.py +93 -0
  112. across/sdk/v1/models/user_info.py +95 -0
  113. across/sdk/v1/models/user_update.py +106 -0
  114. across/sdk/v1/models/validation_error.py +99 -0
  115. across/sdk/v1/models/validation_error_loc_inner.py +138 -0
  116. across/sdk/v1/models/visibility_result.py +97 -0
  117. across/sdk/v1/models/visibility_type.py +38 -0
  118. across/sdk/v1/models/visibility_window.py +99 -0
  119. across/sdk/v1/models/wavelength_bandpass.py +142 -0
  120. across/sdk/v1/models/wavelength_unit.py +39 -0
  121. across/sdk/v1/models/window.py +96 -0
  122. across/sdk/v1/py.typed +0 -0
  123. across/sdk/v1/rest.py +258 -0
  124. across_server_openapi_python-0.0.1.dist-info/METADATA +326 -0
  125. across_server_openapi_python-0.0.1.dist-info/RECORD +128 -0
  126. across_server_openapi_python-0.0.1.dist-info/WHEEL +5 -0
  127. across_server_openapi_python-0.0.1.dist-info/licenses/LICENSE +42 -0
  128. 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
+ }