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).

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, bump version in pyproject.toml, then publish a GitHub release tagged vX.Y.Z. The workflow refuses to publish if the tag and the packaged version disagree. 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.3.0.tar.gz (165.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.3.0-py3-none-any.whl (33.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pyskylight-0.3.0.tar.gz
  • Upload date:
  • Size: 165.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.3.0.tar.gz
Algorithm Hash digest
SHA256 4128236c7f84205a924155e80ea3c3992f3b23df09cb052e312158f9d848d70f
MD5 fd899246b9932fc55f5e7bf0e6d9ba16
BLAKE2b-256 12d84768d1321167f6490113674c2c2196306662afe69c9b5aa75a55a488cf99

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: pyskylight-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 33.1 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.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c35279752eb39cd35dd1452ae663eaf72520623ba33b59cdb2b7a6bbc9d880b
MD5 48beaaaccbdcbf2f93b1342f9103d2da
BLAKE2b-256 ad21da069394b3e8c1979206f0bb3803e2b2fc34f9fb2acda1d2236f3b8139c6

See more details on using hashes here.

Provenance

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

0.5.0

2 files

0.4.0

2 files

This release

0.3.0 This release

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