Skip to content

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:

from skynet_sdk.ephem import target_position, visibility

These wrap astropy.coordinates for the patterns Skynet uses internally. For ad-hoc coordinate work, use astropy directly.

Next steps