Skip to main content

pyskylight

An async Python client for the Skylight API — calendars, chores, lists, rewards, and frames.

Unofficial. Not affiliated with or endorsed by Skylight. The API is reverse-engineered from observed traffic and may change without notice. Use it only with accounts you own.

Install

uv add pyskylight
pip install pyskylight

Quick start

import asyncio

from pyskylight import PasswordAuth, Skylight


async def main() -> None:
    async with Skylight(PasswordAuth("me@example.com", "hunter2")) as skylight:
        frame = (await skylight.get_frames())[0]

        for chore in await skylight.get_chores(frame.id, after="2025-08-25", before="2025-08-31"):
            print(chore.summary, chore.start, "done" if chore.completed else "todo")

        for family_list in await skylight.get_lists(frame.id):
            print(family_list.label, family_list.kind)


asyncio.run(main())

Skylight creates and owns an aiohttp.ClientSession unless you pass one in:

async with aiohttp.ClientSession() as session:
    skylight = Skylight(PasswordAuth(email, password, session=session), session=session)

Authentication

Skylight uses OAuth 2.0 authorization code + PKCE, with credentials entered into a server-rendered Rails login form. PasswordAuth drives that whole flow headlessly and refreshes the access token before it expires:

  1. GET /oauth/authorize → redirect to /auth/session/new, which carries a Rails CSRF token in <meta name="csrf-token"> and sets a skylightcloud_session cookie.
  2. POST /auth/session with authenticity_token, email, password.
  3. GET /oauth/authorize again (now authenticated) → redirect to https://ourskylight.com/welcome?code=...&state=....
  4. POST /oauth/token exchanges the code plus the PKCE code_verifier for an access token and a refresh token.

pyskylight never follows the final redirect — it reads the authorization code out of the Location header — and the Rails session cookie is confined to a private cookie jar.

If you already captured a token, skip the flow:

from pyskylight import Skylight, TokenAuth

skylight = Skylight(TokenAuth("<access token>"))

TokenAuth cannot refresh; a rejected token raises AuthenticationError.

Sign out with await auth.revoke().

Common operations

# Family profiles ("categories" in the API)
categories = await skylight.get_categories(frame_id)

# Chores
chore = await skylight.create_chore(
    frame_id,
    "Take out recycling",
    categories[0].id,  # a chore must belong to a profile
    start="2025-09-01",
    start_time="10:00",
    recurring=True,
    recurrence_set="RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO;WKST=SU",
)
await skylight.complete_chore(frame_id, chore.chore_id, instance_date="2025-09-01")
await skylight.delete_chore(frame_id, chore.chore_id, apply_to=ApplyTo.ALL)  # recurring only

# Lists
grocery = await skylight.get_list(frame_id, list_id)  # items + sections resolved
await skylight.create_list_item(frame_id, grocery.id, "Milk")

# Calendar
events = await skylight.get_calendar_events(
    frame_id, date_min="2025-09-01", date_max="2025-09-30", timezone="America/Los_Angeles"
)

Recurring chores are returned one resource per occurrence. Chore.id is the occurrence id ("<chore_id>-<date>"); pass Chore.chore_id — the group attribute — when updating, deleting, or completing.

A few endpoints don't follow the usual shapes, and pyskylight normalizes them:

groups = await skylight.get_all_chores(frame_id)  # ChoreGroups, bucketed
groups.chores["late"], groups.chores["today"], groups.routines["today_timed"]
groups.all  # flattened

balances = await skylight.get_reward_points(frame_id)  # plain array upstream
frames = await skylight.get_calendar_frames()  # a list, despite the path

Some endpoints reject requests that omit an optional-looking parameter, so pyskylight makes those required: get_countdowns(frame_id, timezone), get_nudges(frame_id, after, before), get_meal_sittings(frame_id, date_min, date_max).

Display settings belong to the device, not the frame: update_frame() accepts them and silently applies nothing, while update_device() works. Write calls send flat bodies, not JSON:API documents, and several have sharp edges the published spec does not mention — "complete" rather than "completed", apply_to being forbidden on one-time chores, move_chore taking a neighbour instead of an index. All of it is verified against a live test frame and written up in docs/api-notes.md.

Models and unmodeled fields

The upstream schema is observed, not specified, so every model keeps its raw resource:

chore.attributes["a_field_pyskylight_does_not_know_about"]

Fully typed models, all verified against live responses: Frame, Category, Chore, TaskBoxItem, SkylightList, ListItem, Device, CalendarEvent, SourceCalendar, Reward, RewardPoint, Nudge, User. Thin models (id plus .attributes) where the account used for verification had no data to capture: Alarm. Endpoints whose shape is entirely unknown (meals, photos, Plus, activities) return the decoded JSON untouched.

Anything not wrapped is still reachable:

data = await skylight.request("GET", f"/api/frames/{frame_id}/month_in_review")

Errors

Exception When
AuthenticationError Login failed, or the token was rejected and could not be refreshed
NotAuthorizedError HTTP 401/403 after one refresh attempt
NotFoundError HTTP 404
RateLimitError HTTP 429
ApiError Any other unsuccessful status

All derive from SkylightError. 304 Not Modified and 204 No Content return None (empty lists for list endpoints).

Logging

Every request is logged at DEBUG on the pyskylight.client logger, as method, path, status and duration:

DEBUG pyskylight.client: GET /api/frames/5455113/categories -> 200 in 74ms

Deliberately nothing else. Bodies carry chore summaries and calendar entries, and headers carry the bearer token; neither belongs in a log someone is about to paste into a bug report. When a request fails, ApiError.url names it too.

import logging

logging.getLogger("pyskylight").setLevel(logging.DEBUG)

Development

This project uses uv. One command sets up a virtualenv with the locked dependency versions:

uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check . && uv run mypy pyskylight

Dev tools live in the dev dependency group, which uv sync installs by default and which stays out of the published wheel. uv.lock is committed and CI runs --frozen, so a new upstream release can't turn a green branch red on its own; run uv lock --upgrade to pick up newer versions deliberately.

Enable the git hooks (ruff, mypy, pytest, lockfile freshness, and a guard against committing credentials) once:

uv run pre-commit install

Test against another interpreter with uv run --python 3.10 pytest, and build with uv build.

CI runs the suite on 3.10–3.13 with branch coverage (floor: 97%), the linters, the pre-commit hooks, a build with twine check, and a job that installs the declared dependency floor (aiohttp==3.9.0) to check that claim is true.

Releasing

Publishing runs from CI via PyPI Trusted Publishing, so no API token is stored in the repository. One-time setup on PyPI (Account → Publishing): owner dknowles2, repository pyskylight, workflow release.yml, environment pypi.

To cut a release, publish a GitHub release tagged vX.Y.Z. That is the whole process — there is no version to bump, because hatch-vcs takes it from the tag.

A build from an untagged commit is versioned from the last tag with a .devN suffix and a local +g<sha> segment, which PyPI refuses outright — so a stray publish cannot masquerade as a real release. The workflow still checks the tag against the built version, which now catches a shallow clone rather than a forgotten edit. workflow_dispatch publishes to TestPyPI for a dry run.

Sources

The endpoint surface comes from two reverse-engineering efforts; see docs/api-notes.md for how they differ and which one pyskylight follows where they disagree.

License

Apache-2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyskylight-0.5.0.tar.gz (172.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyskylight-0.5.0-py3-none-any.whl (36.0 kB view details)

Uploaded Python 3

File details

Details for the file pyskylight-0.5.0.tar.gz.

File metadata

  • Download URL: pyskylight-0.5.0.tar.gz
  • Upload date:
  • Size: 172.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyskylight-0.5.0.tar.gz
Algorithm Hash digest
SHA256 f2caf8a9b5a5fae807bc02222d441728b3f228118e53a01f91a3f58f03e26a46
MD5 c6e1e7f4d92fbc2f53be50574be14475
BLAKE2b-256 38d944c5c421756a070dd3da75a55f4a493ce1043824fd274a2e1c20aacc232f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyskylight-0.5.0.tar.gz:

Publisher: release.yml on dknowles2/pyskylight

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyskylight-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: pyskylight-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 36.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyskylight-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0f8e1e7935c2ce1b742e71dd90e58573a0ddbfc2728ca3e500cb5c4efaa3cdd3
MD5 6df4fb888e3f7b6c0e704a7952008922
BLAKE2b-256 4dcccc2d4e9b227f39f800281e10f12d479e62cadd58932ec363a98d279a4526

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyskylight-0.5.0-py3-none-any.whl:

Publisher: release.yml on dknowles2/pyskylight

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.6.0

2 files

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page