Quickstart — Python SDK¶
The same "list telescopes, create an observation, watch it run"
walkthrough as the curl quickstart, using the
Python SDK's typed schemas with httpx for HTTP.
The Python SDK ships typed Pydantic schemas; it doesn't yet ship a
generated HTTP client. The pattern most Skynet Python consumers use
is httpx + the SDK schemas — that's what we'll do here. For the
full treatment (catalog/non-sidereal targets, exposure sizing, asset
download) see Creating observations.
Prereqs¶
- Python 3.10+ and
pip install skynet-sdk httpx. - An access token. See Auth.
Setup¶
import os
import httpx
from skynet_sdk.schemas import (
ObservationCreate,
Target,
FixedPositionCreate,
EquatorialCoordinatesCreate,
OpticalImagingRequestCreate,
)
from skynet_sdk.schemas.observation.base import OpticalImagingConfiguration
from skynet_sdk.enums import (
ObservationType,
TargetPositionType,
CoordinateType,
ObservationTrackingMode,
DitherStrategy,
)
API = "https://api.skynetgo.org/v1"
TOKEN = os.environ["SKYNET_TOKEN"]
client = httpx.Client(
base_url=API,
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30.0,
)
Walkthrough¶
An observation is created in one POST from a complete
ObservationCreate — there's no draft-then-publish handshake.
def main() -> None:
# 1. Who am I? (slug is the entity in the create path)
me = client.get("/me").raise_for_status().json()
slug = me["slug"]
print(f"Hello, {me['username']}")
# 2. A telescope you can submit to
scopes = client.get("/me/observing-access/telescopes").raise_for_status().json()
telescope_id = scopes[0]["id"]
# 3. A grant that funds it + the imager and a filter
grants = client.get(
"/observing-grants", params={"telescopeId": telescope_id},
).raise_for_status().json()
grant_id = grants["items"][0]["id"]
detail = client.get(
f"/users/{slug}/telescopes/{telescope_id}/detail",
).raise_for_status().json()
imager = next(i for i in detail["instruments"] if i["instrumentType"] == "opticalImager")
filter_id = imager["filterWheels"][0]["options"][0]["filters"][0]["id"]
# 4. Build and create the observation in one call
obs = ObservationCreate(
name="Py Quickstart M51",
target=Target(
name="M51",
position=FixedPositionCreate(
position_type=TargetPositionType.fixed,
coordinates=EquatorialCoordinatesCreate(
coordinate_type=CoordinateType.equatorial,
ra_deg=202.4696, dec_deg=47.1952,
),
),
),
optical_imaging_configuration=OpticalImagingConfiguration(
tracking_mode=ObservationTrackingMode.sidereal,
dither_strategy=DitherStrategy.none,
temporal_offset_sec=0.0,
max_tiles=1, tile_overlap=0.0,
),
requests=[OpticalImagingRequestCreate(
request_type=ObservationType.optical_imaging, order=0,
filter_specifier_ids=[filter_id],
exposure_time_sec=60.0, sample_count=1,
)],
observing_grant_ids=[grant_id], # pins the telescope
instrument_ids=[imager["id"]],
)
created = client.post(
f"/users/{slug}/observations",
# by_alias → camelCase wire format; exclude_unset → don't post stale defaults
content=obs.model_dump_json(by_alias=True, exclude_unset=True),
headers={"Content-Type": "application/json"},
).raise_for_status().json()
obs_id = created["id"]
print(f"Created observation {obs_id} (status={created['status']})")
# 5. Watch it run
for _ in range(5):
snap = client.get(f"/observations/{obs_id}").raise_for_status().json()
print(f" status={snap['status']} progress={snap['progressFraction']}")
if __name__ == "__main__":
main()
Serialization
The SDK models are snake_case; the wire format is camelCase. Dump with
model_dump_json(by_alias=True, exclude_unset=True) — by_alias emits
camelCase, exclude_unset drops the defaults you didn't set. See
Conventions.
Why not a generated client?¶
The TS SDK leans on openapi-typescript to auto-generate typed
clients from /openapi.json. There's no equivalent in the Python
SDK today — most Python integrations use httpx directly (as above)
or the WebSocket protocol for streaming work, where the schemas are
the bigger win than a generated HTTP client. If a generated Python
client would help you, flag it; the spec is served at /openapi.json.
Validating responses with the SDK schemas¶
When you want type safety on the response side, validate against the SDK's Pydantic models:
from skynet_sdk.schemas import Observation
raw = client.get(f"/observations/{obs_id}").raise_for_status().json()
observation = Observation.model_validate(raw)
print(observation.name, observation.status)
For polymorphic responses (Device, Instrument, ObservationRequest, …), use the base type's validator — Pydantic picks the concrete subclass from the discriminator:
from skynet_sdk.schemas import Device
devices = client.get("/devices").raise_for_status().json()
for raw_device in devices["items"]:
device = Device.model_validate(raw_device) # concrete: Camera, Mount, …
print(device.device_type, device.id)
Coordinate math¶
For coordinate transforms and ephemerides, the Python SDK exposes
ephem:
These wrap astropy.coordinates for the patterns Skynet uses
internally. For ad-hoc coordinate work, use astropy directly.
Next steps¶
- Creating observations — catalog and non-sidereal targets, exposure sizing, asset download.
- For realtime subscriptions, see WebSocket streams.
- For the TS equivalent, see quickstart — TypeScript SDK.