orbsim-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbsim-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the OrbSim API
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: pydantic>=2.8.0
9
+
10
+ # OrbSim SDK
11
+
12
+ Typed Python client for the OrbSim API.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install /path/to/orbsim/sdk
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from orbsim_sdk import OrbitSource, OrbsimClient, SatelliteCreate, TLEDefinition
24
+
25
+ client = OrbsimClient("http://127.0.0.1:8000/api")
26
+ auth = client.login("admin@example.com", "supersecret123", token_name="SDK Session")
27
+ projects = client.list_projects()
28
+
29
+ satellite = client.create_satellite(
30
+ projects[0].id,
31
+ SatelliteCreate(
32
+ name="ISS Copy",
33
+ source=OrbitSource.TLE,
34
+ tle=TLEDefinition(
35
+ line1="1 25544U 98067A 26099.50000000 .00016717 00000+0 30747-3 0 9991",
36
+ line2="2 25544 51.6400 54.1200 0003870 123.4567 321.6543 15.50000000123456",
37
+ ),
38
+ ),
39
+ )
40
+
41
+ state = client.get_satellite_state(projects[0].id, satellite.id, "2026-04-09T12:00:00Z")
42
+ print(state.latitude_deg, state.longitude_deg, state.altitude_m)
43
+ ```
44
+
45
+ ## Async Quick Start
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ from orbsim_sdk import AsyncOrbsimClient
51
+
52
+
53
+ async def main():
54
+ async with AsyncOrbsimClient("http://127.0.0.1:8000/api") as client:
55
+ await client.login("admin@example.com", "supersecret123", token_name="Async SDK Session")
56
+ projects = await client.list_projects()
57
+ print([project.name for project in projects])
58
+
59
+
60
+ asyncio.run(main())
61
+ ```
@@ -0,0 +1,52 @@
1
+ # OrbSim SDK
2
+
3
+ Typed Python client for the OrbSim API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install /path/to/orbsim/sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from orbsim_sdk import OrbitSource, OrbsimClient, SatelliteCreate, TLEDefinition
15
+
16
+ client = OrbsimClient("http://127.0.0.1:8000/api")
17
+ auth = client.login("admin@example.com", "supersecret123", token_name="SDK Session")
18
+ projects = client.list_projects()
19
+
20
+ satellite = client.create_satellite(
21
+ projects[0].id,
22
+ SatelliteCreate(
23
+ name="ISS Copy",
24
+ source=OrbitSource.TLE,
25
+ tle=TLEDefinition(
26
+ line1="1 25544U 98067A 26099.50000000 .00016717 00000+0 30747-3 0 9991",
27
+ line2="2 25544 51.6400 54.1200 0003870 123.4567 321.6543 15.50000000123456",
28
+ ),
29
+ ),
30
+ )
31
+
32
+ state = client.get_satellite_state(projects[0].id, satellite.id, "2026-04-09T12:00:00Z")
33
+ print(state.latitude_deg, state.longitude_deg, state.altitude_m)
34
+ ```
35
+
36
+ ## Async Quick Start
37
+
38
+ ```python
39
+ import asyncio
40
+
41
+ from orbsim_sdk import AsyncOrbsimClient
42
+
43
+
44
+ async def main():
45
+ async with AsyncOrbsimClient("http://127.0.0.1:8000/api") as client:
46
+ await client.login("admin@example.com", "supersecret123", token_name="Async SDK Session")
47
+ projects = await client.list_projects()
48
+ print([project.name for project in projects])
49
+
50
+
51
+ asyncio.run(main())
52
+ ```
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "orbsim-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the OrbSim API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "httpx>=0.27.0",
13
+ "pydantic>=2.8.0",
14
+ ]
15
+
16
+ [tool.setuptools]
17
+ package-dir = {"" = "src"}
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,93 @@
1
+ from .client import AsyncOrbsimClient, OrbsimClient
2
+ from .exceptions import OrbsimAPIError, OrbsimSDKError
3
+ from .models import (
4
+ AdminDashboard,
5
+ AdminProjectInfo,
6
+ AnomalyKind,
7
+ AttitudeMode,
8
+ AttitudeSample,
9
+ AuthTokenResponse,
10
+ BootstrapAdminRequest,
11
+ CartesianState,
12
+ Constellation,
13
+ ConstellationCreate,
14
+ ConstellationDesignCreate,
15
+ ConstellationDesignResponse,
16
+ ConstellationDesignType,
17
+ ContactInterval,
18
+ GroundStation,
19
+ GroundStationCreate,
20
+ HealthResponse,
21
+ KeplerianDefinition,
22
+ LinkBudgetRequest,
23
+ LinkBudgetResponse,
24
+ LinkKind,
25
+ LoginRequest,
26
+ NetworkResponse,
27
+ OrbitalStateResponse,
28
+ OrbitSource,
29
+ Project,
30
+ ProjectCreate,
31
+ PublicContactQuery,
32
+ PublicSatelliteQuery,
33
+ PublicTLE,
34
+ RegisterRequest,
35
+ Satellite,
36
+ SatelliteCreate,
37
+ SatelliteSubsystemState,
38
+ TLEDefinition,
39
+ TimeRangeQuery,
40
+ TokenCreateRequest,
41
+ TokenCreateResponse,
42
+ TokenInfo,
43
+ UserProfile,
44
+ WalkerDeltaDefinition,
45
+ )
46
+
47
+ __all__ = [
48
+ "AdminDashboard",
49
+ "AdminProjectInfo",
50
+ "AnomalyKind",
51
+ "AttitudeMode",
52
+ "AttitudeSample",
53
+ "AuthTokenResponse",
54
+ "BootstrapAdminRequest",
55
+ "CartesianState",
56
+ "Constellation",
57
+ "ConstellationCreate",
58
+ "ConstellationDesignCreate",
59
+ "ConstellationDesignResponse",
60
+ "ConstellationDesignType",
61
+ "ContactInterval",
62
+ "GroundStation",
63
+ "GroundStationCreate",
64
+ "HealthResponse",
65
+ "KeplerianDefinition",
66
+ "LinkBudgetRequest",
67
+ "LinkBudgetResponse",
68
+ "LinkKind",
69
+ "LoginRequest",
70
+ "NetworkResponse",
71
+ "OrbitalStateResponse",
72
+ "OrbitSource",
73
+ "AsyncOrbsimClient",
74
+ "OrbsimAPIError",
75
+ "OrbsimClient",
76
+ "OrbsimSDKError",
77
+ "Project",
78
+ "ProjectCreate",
79
+ "PublicContactQuery",
80
+ "PublicSatelliteQuery",
81
+ "PublicTLE",
82
+ "RegisterRequest",
83
+ "Satellite",
84
+ "SatelliteCreate",
85
+ "SatelliteSubsystemState",
86
+ "TLEDefinition",
87
+ "TimeRangeQuery",
88
+ "TokenCreateRequest",
89
+ "TokenCreateResponse",
90
+ "TokenInfo",
91
+ "UserProfile",
92
+ "WalkerDeltaDefinition",
93
+ ]
@@ -0,0 +1,393 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, TypeVar
5
+
6
+ import httpx
7
+ from pydantic import BaseModel
8
+
9
+ from .exceptions import OrbsimAPIError
10
+ from .models import (
11
+ AdminDashboard,
12
+ AdminProjectInfo,
13
+ AttitudeSample,
14
+ AuthTokenResponse,
15
+ BootstrapAdminRequest,
16
+ Constellation,
17
+ ConstellationCreate,
18
+ ConstellationDesignCreate,
19
+ ConstellationDesignResponse,
20
+ ContactInterval,
21
+ GroundStation,
22
+ GroundStationCreate,
23
+ HealthResponse,
24
+ LinkBudgetRequest,
25
+ LinkBudgetResponse,
26
+ LoginRequest,
27
+ NetworkResponse,
28
+ OrbitalStateResponse,
29
+ Project,
30
+ ProjectCreate,
31
+ PublicContactQuery,
32
+ PublicSatelliteQuery,
33
+ PublicTLE,
34
+ RegisterRequest,
35
+ Satellite,
36
+ SatelliteCreate,
37
+ SatelliteSubsystemState,
38
+ TimeRangeQuery,
39
+ TokenCreateRequest,
40
+ TokenCreateResponse,
41
+ TokenInfo,
42
+ UserProfile,
43
+ )
44
+
45
+ ModelT = TypeVar("ModelT", bound=BaseModel)
46
+
47
+
48
+ class _OrbsimClientBase:
49
+ def _model(self, model_type: type[ModelT], data: Any) -> ModelT:
50
+ return model_type.model_validate(data)
51
+
52
+ def _models(self, model_type: type[ModelT], data: list[Any]) -> list[ModelT]:
53
+ return [self._model(model_type, item) for item in data]
54
+
55
+
56
+ class OrbsimClient(_OrbsimClientBase):
57
+ def __init__(
58
+ self,
59
+ base_url: str,
60
+ token: str | None = None,
61
+ timeout: float = 30.0,
62
+ client: httpx.Client | None = None,
63
+ ) -> None:
64
+ self.base_url = base_url.rstrip("/")
65
+ self._client = client or httpx.Client(base_url=self.base_url, timeout=timeout)
66
+ self._owns_client = client is None
67
+ if token:
68
+ self.set_token(token)
69
+
70
+ def close(self) -> None:
71
+ if self._owns_client:
72
+ self._client.close()
73
+
74
+ def __enter__(self) -> "OrbsimClient":
75
+ return self
76
+
77
+ def __exit__(self, exc_type, exc, tb) -> None:
78
+ self.close()
79
+
80
+ @property
81
+ def token(self) -> str | None:
82
+ auth = self._client.headers.get("Authorization", "")
83
+ if auth.startswith("Bearer "):
84
+ return auth.removeprefix("Bearer ")
85
+ return None
86
+
87
+ def set_token(self, token: str | None) -> None:
88
+ if token:
89
+ self._client.headers["Authorization"] = f"Bearer {token}"
90
+ else:
91
+ self._client.headers.pop("Authorization", None)
92
+
93
+ def _request(self, method: str, path: str, *, params: dict[str, Any] | None = None, payload: BaseModel | dict[str, Any] | None = None) -> Any:
94
+ json_payload: dict[str, Any] | None = None
95
+ if payload is not None:
96
+ json_payload = payload.model_dump(mode="json", exclude_none=True) if isinstance(payload, BaseModel) else payload
97
+ response = self._client.request(method, path, params=params, json=json_payload)
98
+ if response.is_error:
99
+ try:
100
+ detail = response.json().get("detail", response.text)
101
+ except Exception:
102
+ detail = response.text
103
+ raise OrbsimAPIError(response.status_code, detail)
104
+ if not response.content:
105
+ return None
106
+ return response.json()
107
+
108
+ def bootstrap_admin(self, email: str, password: str, full_name: str) -> AuthTokenResponse:
109
+ result = self._model(AuthTokenResponse, self._request("POST", "/auth/bootstrap-admin", payload=BootstrapAdminRequest(email=email, password=password, full_name=full_name)))
110
+ self.set_token(result.token)
111
+ return result
112
+
113
+ def register(self, email: str, password: str, full_name: str) -> AuthTokenResponse:
114
+ result = self._model(AuthTokenResponse, self._request("POST", "/auth/register", payload=RegisterRequest(email=email, password=password, full_name=full_name)))
115
+ self.set_token(result.token)
116
+ return result
117
+
118
+ def login(self, email: str, password: str, token_name: str = "Default API Token") -> AuthTokenResponse:
119
+ result = self._model(AuthTokenResponse, self._request("POST", "/auth/login", payload=LoginRequest(email=email, password=password, token_name=token_name)))
120
+ self.set_token(result.token)
121
+ return result
122
+
123
+ def me(self) -> UserProfile:
124
+ return self._model(UserProfile, self._request("GET", "/auth/me"))
125
+
126
+ def list_tokens(self) -> list[TokenInfo]:
127
+ return self._models(TokenInfo, self._request("GET", "/auth/tokens"))
128
+
129
+ def create_token(self, name: str) -> TokenCreateResponse:
130
+ return self._model(TokenCreateResponse, self._request("POST", "/auth/tokens", payload=TokenCreateRequest(name=name)))
131
+
132
+ def revoke_own_token(self, token_id: str) -> TokenInfo:
133
+ return self._model(TokenInfo, self._request("DELETE", f"/auth/tokens/{token_id}"))
134
+
135
+ def health(self) -> HealthResponse:
136
+ return self._model(HealthResponse, self._request("GET", "/health"))
137
+
138
+ def get_public_tle(self, query: PublicSatelliteQuery) -> PublicTLE:
139
+ return self._model(PublicTLE, self._request("POST", "/public/tle", payload=query))
140
+
141
+ def get_public_state(self, query: PublicSatelliteQuery, timestamp: datetime | str) -> OrbitalStateResponse:
142
+ return self._model(OrbitalStateResponse, self._request("POST", "/public/state", params={"timestamp": timestamp}, payload=query))
143
+
144
+ def get_public_contacts(self, query: PublicContactQuery) -> list[ContactInterval]:
145
+ return self._models(ContactInterval, self._request("POST", "/public/contacts", payload=query))
146
+
147
+ def create_project(self, payload: ProjectCreate) -> Project:
148
+ return self._model(Project, self._request("POST", "/projects", payload=payload))
149
+
150
+ def list_projects(self) -> list[Project]:
151
+ return self._models(Project, self._request("GET", "/projects"))
152
+
153
+ def delete_project(self, project_id: str) -> Project:
154
+ return self._model(Project, self._request("DELETE", f"/projects/{project_id}"))
155
+
156
+ def create_satellite(self, project_id: str, payload: SatelliteCreate) -> Satellite:
157
+ return self._model(Satellite, self._request("POST", f"/projects/{project_id}/satellites", payload=payload))
158
+
159
+ def list_satellites(self, project_id: str) -> list[Satellite]:
160
+ return self._models(Satellite, self._request("GET", f"/projects/{project_id}/satellites"))
161
+
162
+ def delete_satellite(self, project_id: str, satellite_id: str) -> Satellite:
163
+ return self._model(Satellite, self._request("DELETE", f"/projects/{project_id}/satellites/{satellite_id}"))
164
+
165
+ def create_groundstation(self, project_id: str, payload: GroundStationCreate) -> GroundStation:
166
+ return self._model(GroundStation, self._request("POST", f"/projects/{project_id}/groundstations", payload=payload))
167
+
168
+ def list_groundstations(self, project_id: str) -> list[GroundStation]:
169
+ return self._models(GroundStation, self._request("GET", f"/projects/{project_id}/groundstations"))
170
+
171
+ def delete_groundstation(self, project_id: str, groundstation_id: str) -> GroundStation:
172
+ return self._model(GroundStation, self._request("DELETE", f"/projects/{project_id}/groundstations/{groundstation_id}"))
173
+
174
+ def get_satellite_state(self, project_id: str, satellite_id: str, timestamp: datetime | str) -> OrbitalStateResponse:
175
+ return self._model(OrbitalStateResponse, self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/state", params={"timestamp": timestamp}))
176
+
177
+ def get_satellite_states(self, project_id: str, satellite_id: str, payload: TimeRangeQuery) -> list[OrbitalStateResponse]:
178
+ return self._models(OrbitalStateResponse, self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/states", payload=payload))
179
+
180
+ def get_satellite_attitude(self, project_id: str, satellite_id: str, timestamp: datetime | str) -> AttitudeSample:
181
+ return self._model(AttitudeSample, self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/attitude", params={"timestamp": timestamp}))
182
+
183
+ def get_satellite_subsystems(self, project_id: str, satellite_id: str, timestamp: datetime | str) -> SatelliteSubsystemState:
184
+ return self._model(SatelliteSubsystemState, self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/subsystems", params={"timestamp": timestamp}))
185
+
186
+ def get_contacts(self, project_id: str, satellite_id: str, groundstation_id: str, payload: TimeRangeQuery) -> list[ContactInterval]:
187
+ return self._models(ContactInterval, self._request("POST", f"/projects/{project_id}/contacts/{satellite_id}/{groundstation_id}", payload=payload))
188
+
189
+ def get_link_budget(self, project_id: str, payload: LinkBudgetRequest) -> LinkBudgetResponse:
190
+ return self._model(LinkBudgetResponse, self._request("POST", f"/projects/{project_id}/links/budget", payload=payload))
191
+
192
+ def create_constellation(self, project_id: str, payload: ConstellationCreate) -> Constellation:
193
+ return self._model(Constellation, self._request("POST", f"/projects/{project_id}/constellations", payload=payload))
194
+
195
+ def design_constellation(self, project_id: str, payload: ConstellationDesignCreate) -> ConstellationDesignResponse:
196
+ return self._model(ConstellationDesignResponse, self._request("POST", f"/projects/{project_id}/constellations/design", payload=payload))
197
+
198
+ def list_constellations(self, project_id: str) -> list[Constellation]:
199
+ return self._models(Constellation, self._request("GET", f"/projects/{project_id}/constellations"))
200
+
201
+ def delete_constellation(self, project_id: str, constellation_id: str) -> Constellation:
202
+ return self._model(Constellation, self._request("DELETE", f"/projects/{project_id}/constellations/{constellation_id}"))
203
+
204
+ def get_constellation_network(self, project_id: str, constellation_id: str, timestamp: datetime | str, kind: str = "rf") -> NetworkResponse:
205
+ return self._model(NetworkResponse, self._request("POST", f"/projects/{project_id}/constellations/{constellation_id}/network", params={"timestamp": timestamp, "kind": kind}))
206
+
207
+ def admin_dashboard(self) -> AdminDashboard:
208
+ return self._model(AdminDashboard, self._request("GET", "/admin/dashboard"))
209
+
210
+ def admin_list_users(self) -> list[UserProfile]:
211
+ return self._models(UserProfile, self._request("GET", "/admin/users"))
212
+
213
+ def admin_list_projects(self) -> list[AdminProjectInfo]:
214
+ return self._models(AdminProjectInfo, self._request("GET", "/admin/projects"))
215
+
216
+ def admin_user_tokens(self, user_id: str) -> list[TokenInfo]:
217
+ return self._models(TokenInfo, self._request("GET", f"/admin/users/{user_id}/tokens"))
218
+
219
+ def admin_toggle_admin(self, user_id: str) -> UserProfile:
220
+ return self._model(UserProfile, self._request("POST", f"/admin/users/{user_id}/toggle-admin"))
221
+
222
+ def admin_revoke_token(self, token_id: str) -> TokenInfo:
223
+ return self._model(TokenInfo, self._request("POST", f"/admin/tokens/{token_id}/revoke"))
224
+
225
+
226
+ class AsyncOrbsimClient(_OrbsimClientBase):
227
+ def __init__(
228
+ self,
229
+ base_url: str,
230
+ token: str | None = None,
231
+ timeout: float = 30.0,
232
+ client: httpx.AsyncClient | None = None,
233
+ ) -> None:
234
+ self.base_url = base_url.rstrip("/")
235
+ self._client = client or httpx.AsyncClient(base_url=self.base_url, timeout=timeout)
236
+ self._owns_client = client is None
237
+ if token:
238
+ self.set_token(token)
239
+
240
+ async def aclose(self) -> None:
241
+ if self._owns_client:
242
+ await self._client.aclose()
243
+
244
+ async def __aenter__(self) -> "AsyncOrbsimClient":
245
+ return self
246
+
247
+ async def __aexit__(self, exc_type, exc, tb) -> None:
248
+ await self.aclose()
249
+
250
+ @property
251
+ def token(self) -> str | None:
252
+ auth = self._client.headers.get("Authorization", "")
253
+ if auth.startswith("Bearer "):
254
+ return auth.removeprefix("Bearer ")
255
+ return None
256
+
257
+ def set_token(self, token: str | None) -> None:
258
+ if token:
259
+ self._client.headers["Authorization"] = f"Bearer {token}"
260
+ else:
261
+ self._client.headers.pop("Authorization", None)
262
+
263
+ async def _request(self, method: str, path: str, *, params: dict[str, Any] | None = None, payload: BaseModel | dict[str, Any] | None = None) -> Any:
264
+ json_payload: dict[str, Any] | None = None
265
+ if payload is not None:
266
+ json_payload = payload.model_dump(mode="json", exclude_none=True) if isinstance(payload, BaseModel) else payload
267
+ response = await self._client.request(method, path, params=params, json=json_payload)
268
+ if response.is_error:
269
+ try:
270
+ detail = response.json().get("detail", response.text)
271
+ except Exception:
272
+ detail = response.text
273
+ raise OrbsimAPIError(response.status_code, detail)
274
+ if not response.content:
275
+ return None
276
+ return response.json()
277
+
278
+ async def bootstrap_admin(self, email: str, password: str, full_name: str) -> AuthTokenResponse:
279
+ result = self._model(AuthTokenResponse, await self._request("POST", "/auth/bootstrap-admin", payload=BootstrapAdminRequest(email=email, password=password, full_name=full_name)))
280
+ self.set_token(result.token)
281
+ return result
282
+
283
+ async def register(self, email: str, password: str, full_name: str) -> AuthTokenResponse:
284
+ result = self._model(AuthTokenResponse, await self._request("POST", "/auth/register", payload=RegisterRequest(email=email, password=password, full_name=full_name)))
285
+ self.set_token(result.token)
286
+ return result
287
+
288
+ async def login(self, email: str, password: str, token_name: str = "Default API Token") -> AuthTokenResponse:
289
+ result = self._model(AuthTokenResponse, await self._request("POST", "/auth/login", payload=LoginRequest(email=email, password=password, token_name=token_name)))
290
+ self.set_token(result.token)
291
+ return result
292
+
293
+ async def me(self) -> UserProfile:
294
+ return self._model(UserProfile, await self._request("GET", "/auth/me"))
295
+
296
+ async def list_tokens(self) -> list[TokenInfo]:
297
+ return self._models(TokenInfo, await self._request("GET", "/auth/tokens"))
298
+
299
+ async def create_token(self, name: str) -> TokenCreateResponse:
300
+ return self._model(TokenCreateResponse, await self._request("POST", "/auth/tokens", payload=TokenCreateRequest(name=name)))
301
+
302
+ async def revoke_own_token(self, token_id: str) -> TokenInfo:
303
+ return self._model(TokenInfo, await self._request("DELETE", f"/auth/tokens/{token_id}"))
304
+
305
+ async def health(self) -> HealthResponse:
306
+ return self._model(HealthResponse, await self._request("GET", "/health"))
307
+
308
+ async def get_public_tle(self, query: PublicSatelliteQuery) -> PublicTLE:
309
+ return self._model(PublicTLE, await self._request("POST", "/public/tle", payload=query))
310
+
311
+ async def get_public_state(self, query: PublicSatelliteQuery, timestamp: datetime | str) -> OrbitalStateResponse:
312
+ return self._model(OrbitalStateResponse, await self._request("POST", "/public/state", params={"timestamp": timestamp}, payload=query))
313
+
314
+ async def get_public_contacts(self, query: PublicContactQuery) -> list[ContactInterval]:
315
+ return self._models(ContactInterval, await self._request("POST", "/public/contacts", payload=query))
316
+
317
+ async def create_project(self, payload: ProjectCreate) -> Project:
318
+ return self._model(Project, await self._request("POST", "/projects", payload=payload))
319
+
320
+ async def list_projects(self) -> list[Project]:
321
+ return self._models(Project, await self._request("GET", "/projects"))
322
+
323
+ async def delete_project(self, project_id: str) -> Project:
324
+ return self._model(Project, await self._request("DELETE", f"/projects/{project_id}"))
325
+
326
+ async def create_satellite(self, project_id: str, payload: SatelliteCreate) -> Satellite:
327
+ return self._model(Satellite, await self._request("POST", f"/projects/{project_id}/satellites", payload=payload))
328
+
329
+ async def list_satellites(self, project_id: str) -> list[Satellite]:
330
+ return self._models(Satellite, await self._request("GET", f"/projects/{project_id}/satellites"))
331
+
332
+ async def delete_satellite(self, project_id: str, satellite_id: str) -> Satellite:
333
+ return self._model(Satellite, await self._request("DELETE", f"/projects/{project_id}/satellites/{satellite_id}"))
334
+
335
+ async def create_groundstation(self, project_id: str, payload: GroundStationCreate) -> GroundStation:
336
+ return self._model(GroundStation, await self._request("POST", f"/projects/{project_id}/groundstations", payload=payload))
337
+
338
+ async def list_groundstations(self, project_id: str) -> list[GroundStation]:
339
+ return self._models(GroundStation, await self._request("GET", f"/projects/{project_id}/groundstations"))
340
+
341
+ async def delete_groundstation(self, project_id: str, groundstation_id: str) -> GroundStation:
342
+ return self._model(GroundStation, await self._request("DELETE", f"/projects/{project_id}/groundstations/{groundstation_id}"))
343
+
344
+ async def get_satellite_state(self, project_id: str, satellite_id: str, timestamp: datetime | str) -> OrbitalStateResponse:
345
+ return self._model(OrbitalStateResponse, await self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/state", params={"timestamp": timestamp}))
346
+
347
+ async def get_satellite_states(self, project_id: str, satellite_id: str, payload: TimeRangeQuery) -> list[OrbitalStateResponse]:
348
+ return self._models(OrbitalStateResponse, await self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/states", payload=payload))
349
+
350
+ async def get_satellite_attitude(self, project_id: str, satellite_id: str, timestamp: datetime | str) -> AttitudeSample:
351
+ return self._model(AttitudeSample, await self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/attitude", params={"timestamp": timestamp}))
352
+
353
+ async def get_satellite_subsystems(self, project_id: str, satellite_id: str, timestamp: datetime | str) -> SatelliteSubsystemState:
354
+ return self._model(SatelliteSubsystemState, await self._request("POST", f"/projects/{project_id}/satellites/{satellite_id}/subsystems", params={"timestamp": timestamp}))
355
+
356
+ async def get_contacts(self, project_id: str, satellite_id: str, groundstation_id: str, payload: TimeRangeQuery) -> list[ContactInterval]:
357
+ return self._models(ContactInterval, await self._request("POST", f"/projects/{project_id}/contacts/{satellite_id}/{groundstation_id}", payload=payload))
358
+
359
+ async def get_link_budget(self, project_id: str, payload: LinkBudgetRequest) -> LinkBudgetResponse:
360
+ return self._model(LinkBudgetResponse, await self._request("POST", f"/projects/{project_id}/links/budget", payload=payload))
361
+
362
+ async def create_constellation(self, project_id: str, payload: ConstellationCreate) -> Constellation:
363
+ return self._model(Constellation, await self._request("POST", f"/projects/{project_id}/constellations", payload=payload))
364
+
365
+ async def design_constellation(self, project_id: str, payload: ConstellationDesignCreate) -> ConstellationDesignResponse:
366
+ return self._model(ConstellationDesignResponse, await self._request("POST", f"/projects/{project_id}/constellations/design", payload=payload))
367
+
368
+ async def list_constellations(self, project_id: str) -> list[Constellation]:
369
+ return self._models(Constellation, await self._request("GET", f"/projects/{project_id}/constellations"))
370
+
371
+ async def delete_constellation(self, project_id: str, constellation_id: str) -> Constellation:
372
+ return self._model(Constellation, await self._request("DELETE", f"/projects/{project_id}/constellations/{constellation_id}"))
373
+
374
+ async def get_constellation_network(self, project_id: str, constellation_id: str, timestamp: datetime | str, kind: str = "rf") -> NetworkResponse:
375
+ return self._model(NetworkResponse, await self._request("POST", f"/projects/{project_id}/constellations/{constellation_id}/network", params={"timestamp": timestamp, "kind": kind}))
376
+
377
+ async def admin_dashboard(self) -> AdminDashboard:
378
+ return self._model(AdminDashboard, await self._request("GET", "/admin/dashboard"))
379
+
380
+ async def admin_list_users(self) -> list[UserProfile]:
381
+ return self._models(UserProfile, await self._request("GET", "/admin/users"))
382
+
383
+ async def admin_list_projects(self) -> list[AdminProjectInfo]:
384
+ return self._models(AdminProjectInfo, await self._request("GET", "/admin/projects"))
385
+
386
+ async def admin_user_tokens(self, user_id: str) -> list[TokenInfo]:
387
+ return self._models(TokenInfo, await self._request("GET", f"/admin/users/{user_id}/tokens"))
388
+
389
+ async def admin_toggle_admin(self, user_id: str) -> UserProfile:
390
+ return self._model(UserProfile, await self._request("POST", f"/admin/users/{user_id}/toggle-admin"))
391
+
392
+ async def admin_revoke_token(self, token_id: str) -> TokenInfo:
393
+ return self._model(TokenInfo, await self._request("POST", f"/admin/tokens/{token_id}/revoke"))
@@ -0,0 +1,9 @@
1
+ class OrbsimSDKError(Exception):
2
+ """Base SDK exception."""
3
+
4
+
5
+ class OrbsimAPIError(OrbsimSDKError):
6
+ def __init__(self, status_code: int, detail: str):
7
+ self.status_code = status_code
8
+ self.detail = detail
9
+ super().__init__(f"OrbSim API error {status_code}: {detail}")
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, Field, HttpUrl
8
+
9
+
10
+ class OrbitSource(str, Enum):
11
+ TLE = "tle"
12
+ EPHEMERIS = "ephemeris"
13
+ KEPLERIAN = "keplerian"
14
+
15
+
16
+ class AnomalyKind(str, Enum):
17
+ TRUE = "true"
18
+ MEAN = "mean"
19
+ ECCENTRIC = "eccentric"
20
+
21
+
22
+ class AttitudeMode(str, Enum):
23
+ LVLH = "lvlh"
24
+ NADIR = "nadir"
25
+ SUN_POINTING = "sun_pointing"
26
+ INERTIAL = "inertial"
27
+
28
+
29
+ class LinkKind(str, Enum):
30
+ RF = "rf"
31
+ OPTICAL = "optical"
32
+
33
+
34
+ class ConstellationDesignType(str, Enum):
35
+ WALKER_DELTA = "walker_delta"
36
+
37
+
38
+ class LoginRequest(BaseModel):
39
+ email: str
40
+ password: str
41
+ token_name: str = "Default API Token"
42
+
43
+
44
+ class RegisterRequest(BaseModel):
45
+ email: str
46
+ password: str
47
+ full_name: str
48
+
49
+
50
+ class BootstrapAdminRequest(RegisterRequest):
51
+ pass
52
+
53
+
54
+ class UserProfile(BaseModel):
55
+ id: str
56
+ email: str
57
+ full_name: str
58
+ is_admin: bool
59
+ is_active: bool
60
+ created_at: datetime
61
+
62
+
63
+ class AuthTokenResponse(BaseModel):
64
+ token: str
65
+ token_type: Literal["bearer"] = "bearer"
66
+ user: UserProfile
67
+
68
+
69
+ class TokenCreateRequest(BaseModel):
70
+ name: str
71
+
72
+
73
+ class TokenInfo(BaseModel):
74
+ id: str
75
+ name: str
76
+ token_prefix: str
77
+ created_at: datetime
78
+ last_used_at: datetime | None = None
79
+ revoked_at: datetime | None = None
80
+
81
+
82
+ class TokenCreateResponse(BaseModel):
83
+ token: str
84
+ token_info: TokenInfo
85
+
86
+
87
+ class ProjectCreate(BaseModel):
88
+ name: str
89
+ description: str | None = None
90
+
91
+
92
+ class Project(BaseModel):
93
+ id: str
94
+ name: str
95
+ description: str | None = None
96
+ created_at: datetime
97
+
98
+
99
+ class CartesianState(BaseModel):
100
+ timestamp: datetime
101
+ position_m: list[float] = Field(min_length=3, max_length=3)
102
+ velocity_mps: list[float] = Field(min_length=3, max_length=3)
103
+
104
+
105
+ class TLEDefinition(BaseModel):
106
+ line1: str
107
+ line2: str
108
+
109
+
110
+ class KeplerianDefinition(BaseModel):
111
+ semi_major_axis_m: float
112
+ eccentricity: float
113
+ inclination_deg: float
114
+ raan_deg: float
115
+ arg_perigee_deg: float
116
+ anomaly_deg: float
117
+ anomaly_kind: AnomalyKind = AnomalyKind.TRUE
118
+ epoch: datetime
119
+
120
+
121
+ class SatelliteSubsystemState(BaseModel):
122
+ power_w: float = 0.0
123
+ battery_soc: float = 1.0
124
+ thermal_c: float = 20.0
125
+ payload_active: bool = False
126
+ comms_margin_db: float = 0.0
127
+
128
+
129
+ class SatelliteCreate(BaseModel):
130
+ name: str
131
+ source: OrbitSource
132
+ tle: TLEDefinition | None = None
133
+ ephemeris: list[CartesianState] | None = None
134
+ keplerian: KeplerianDefinition | None = None
135
+ attitude_mode: AttitudeMode = AttitudeMode.NADIR
136
+ dry_mass_kg: float = 100.0
137
+ subsystems: SatelliteSubsystemState = Field(default_factory=SatelliteSubsystemState)
138
+
139
+
140
+ class Satellite(BaseModel):
141
+ id: str
142
+ name: str
143
+ source: OrbitSource
144
+ tle: TLEDefinition | None = None
145
+ ephemeris: list[CartesianState] | None = None
146
+ keplerian: KeplerianDefinition | None = None
147
+ attitude_mode: AttitudeMode
148
+ dry_mass_kg: float
149
+ subsystems: SatelliteSubsystemState
150
+
151
+
152
+ class GroundStationCreate(BaseModel):
153
+ name: str
154
+ latitude_deg: float
155
+ longitude_deg: float
156
+ altitude_m: float = 0.0
157
+ min_elevation_deg: float = 5.0
158
+
159
+
160
+ class GroundStation(BaseModel):
161
+ id: str
162
+ name: str
163
+ latitude_deg: float
164
+ longitude_deg: float
165
+ altitude_m: float = 0.0
166
+ min_elevation_deg: float = 5.0
167
+
168
+
169
+ class TimeRangeQuery(BaseModel):
170
+ start: datetime
171
+ end: datetime
172
+ step_seconds: int = 60
173
+
174
+
175
+ class AttitudeSample(BaseModel):
176
+ timestamp: datetime
177
+ quaternion_xyzw: list[float]
178
+ euler_deg: list[float]
179
+ mode: AttitudeMode
180
+
181
+
182
+ class OrbitalStateResponse(BaseModel):
183
+ satellite_id: str
184
+ timestamp: datetime
185
+ position_m: list[float]
186
+ velocity_mps: list[float]
187
+ inertial_position_m: list[float] | None = None
188
+ inertial_velocity_mps: list[float] | None = None
189
+ altitude_m: float
190
+ latitude_deg: float
191
+ longitude_deg: float
192
+
193
+
194
+ class ContactInterval(BaseModel):
195
+ start: datetime
196
+ end: datetime
197
+ max_elevation_deg: float
198
+ satellite_id: str
199
+ groundstation_id: str
200
+
201
+
202
+ class LinkBudgetRequest(BaseModel):
203
+ source_satellite_id: str
204
+ target_satellite_id: str | None = None
205
+ groundstation_id: str | None = None
206
+ kind: LinkKind
207
+ timestamp: datetime
208
+ frequency_hz: float = 8.2e9
209
+ tx_power_dbw: float = 10.0
210
+ tx_gain_dbi: float = 25.0
211
+ rx_gain_dbi: float = 25.0
212
+ bandwidth_hz: float = 1e6
213
+ system_temperature_k: float = 500.0
214
+ optical_wavelength_nm: float = 1550.0
215
+ aperture_m: float = 0.12
216
+
217
+
218
+ class LinkBudgetResponse(BaseModel):
219
+ kind: LinkKind
220
+ range_m: float
221
+ free_space_loss_db: float
222
+ rx_power_dbw: float
223
+ snr_db: float
224
+ visible: bool
225
+
226
+
227
+ class ConstellationCreate(BaseModel):
228
+ name: str
229
+ satellite_ids: list[str]
230
+ isl_max_range_m: float = 3_500_000.0
231
+ max_degree: int = 4
232
+
233
+
234
+ class WalkerDeltaDefinition(BaseModel):
235
+ total_satellites: int
236
+ plane_count: int
237
+ relative_spacing: int
238
+ semi_major_axis_m: float
239
+ eccentricity: float
240
+ inclination_deg: float
241
+ arg_perigee_deg: float = 0.0
242
+ start_raan_deg: float = 0.0
243
+ start_anomaly_deg: float = 0.0
244
+ anomaly_kind: AnomalyKind = AnomalyKind.MEAN
245
+ satellite_name_prefix: str | None = None
246
+ attitude_mode: AttitudeMode = AttitudeMode.NADIR
247
+ dry_mass_kg: float = 100.0
248
+ subsystems: SatelliteSubsystemState = Field(default_factory=SatelliteSubsystemState)
249
+
250
+
251
+ class ConstellationDesignCreate(BaseModel):
252
+ name: str
253
+ type: ConstellationDesignType
254
+ walker_delta: WalkerDeltaDefinition | None = None
255
+ isl_max_range_m: float = 3_500_000.0
256
+ max_degree: int = 4
257
+
258
+
259
+ class Constellation(BaseModel):
260
+ id: str
261
+ name: str
262
+ satellite_ids: list[str]
263
+ isl_max_range_m: float
264
+ max_degree: int
265
+
266
+
267
+ class ConstellationDesignResponse(BaseModel):
268
+ constellation: Constellation
269
+ satellites: list[Satellite]
270
+
271
+
272
+ class PublicSatelliteQuery(BaseModel):
273
+ norad_id: str | None = None
274
+ name: str | None = None
275
+ source_url: HttpUrl | None = None
276
+
277
+
278
+ class PublicTLE(BaseModel):
279
+ name: str
280
+ line1: str
281
+ line2: str
282
+ source: str
283
+
284
+
285
+ class PublicContactQuery(BaseModel):
286
+ satellite: PublicSatelliteQuery
287
+ groundstation: GroundStationCreate
288
+ start: datetime
289
+ end: datetime
290
+ step_seconds: int = 60
291
+
292
+
293
+ class AdminDashboard(BaseModel):
294
+ user_count: int
295
+ project_count: int
296
+ token_count: int
297
+ active_token_count: int
298
+
299
+
300
+ class HealthResponse(BaseModel):
301
+ status: Literal["ok"]
302
+ orekit_available: bool
303
+
304
+
305
+ class AdminProjectInfo(BaseModel):
306
+ id: str
307
+ name: str
308
+ description: str | None = None
309
+ created_at: datetime
310
+ owner_email: str
311
+
312
+
313
+ class NetworkResponse(BaseModel):
314
+ timestamp: datetime
315
+ kind: str
316
+ nodes: dict[str, OrbitalStateResponse]
317
+ adjacency: dict[str, list[dict[str, Any]]]
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbsim-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the OrbSim API
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: pydantic>=2.8.0
9
+
10
+ # OrbSim SDK
11
+
12
+ Typed Python client for the OrbSim API.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install /path/to/orbsim/sdk
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from orbsim_sdk import OrbitSource, OrbsimClient, SatelliteCreate, TLEDefinition
24
+
25
+ client = OrbsimClient("http://127.0.0.1:8000/api")
26
+ auth = client.login("admin@example.com", "supersecret123", token_name="SDK Session")
27
+ projects = client.list_projects()
28
+
29
+ satellite = client.create_satellite(
30
+ projects[0].id,
31
+ SatelliteCreate(
32
+ name="ISS Copy",
33
+ source=OrbitSource.TLE,
34
+ tle=TLEDefinition(
35
+ line1="1 25544U 98067A 26099.50000000 .00016717 00000+0 30747-3 0 9991",
36
+ line2="2 25544 51.6400 54.1200 0003870 123.4567 321.6543 15.50000000123456",
37
+ ),
38
+ ),
39
+ )
40
+
41
+ state = client.get_satellite_state(projects[0].id, satellite.id, "2026-04-09T12:00:00Z")
42
+ print(state.latitude_deg, state.longitude_deg, state.altitude_m)
43
+ ```
44
+
45
+ ## Async Quick Start
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ from orbsim_sdk import AsyncOrbsimClient
51
+
52
+
53
+ async def main():
54
+ async with AsyncOrbsimClient("http://127.0.0.1:8000/api") as client:
55
+ await client.login("admin@example.com", "supersecret123", token_name="Async SDK Session")
56
+ projects = await client.list_projects()
57
+ print([project.name for project in projects])
58
+
59
+
60
+ asyncio.run(main())
61
+ ```
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/orbsim_sdk/__init__.py
4
+ src/orbsim_sdk/client.py
5
+ src/orbsim_sdk/exceptions.py
6
+ src/orbsim_sdk/models.py
7
+ src/orbsim_sdk.egg-info/PKG-INFO
8
+ src/orbsim_sdk.egg-info/SOURCES.txt
9
+ src/orbsim_sdk.egg-info/dependency_links.txt
10
+ src/orbsim_sdk.egg-info/requires.txt
11
+ src/orbsim_sdk.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.27.0
2
+ pydantic>=2.8.0
@@ -0,0 +1 @@
1
+ orbsim_sdk