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.6.0.tar.gz (174.3 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.6.0-py3-none-any.whl (36.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pyskylight-0.6.0.tar.gz
Algorithm Hash digest
SHA256 a029db40579f0543d425950771c9bf1cede51c787e564aaca409617bcc8a64a2
MD5 1aeb3c25d800ac73adf2dba34530c00b
BLAKE2b-256 390d1d76666dbaeadfcbd6c2bd038319c51058000a340e5f4196d4a34896e844

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyskylight-0.6.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.6.0-py3-none-any.whl.

File metadata

  • Download URL: pyskylight-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 36.7 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.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f1862ea8b275c12248959b654a585f460c60629b39472f0bf0ebcca78a6d5b0b
MD5 0c0747327c1f03146726fbdbf671ff59
BLAKE2b-256 2ea4aa3c6c680db6347c0b2e3e62e04703473df79be3eca9cfe1231cdd7c805b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyskylight-0.6.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

This release

0.6.0 This release

2 files

0.5.0

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