Skip to main content

cyclecalcs

A thin Python client for the CycleCalcs astronomy API: moon phase, sunrise and sunset, planet visibility, eclipses, twilight and the genuinely dark moonless window of a night. The API answers the question rather than the coordinates, so you get "Waning Gibbous, 95 percent lit, sets at 06:14", not a longitude to decode.

Free, no account, no API key. No dependencies: it is urllib underneath, so installing it adds nothing to your lockfile. Positions are good to well under a degree for the Sun, Moon and planets, roughly 1700 to 2200. Astronomy only: no astrology, and no claim that anything in the sky affects events on Earth.

Install

pip install cyclecalcs

Use

from cyclecalcs import CycleCalcs

cc = CycleCalcs()

tonight = cc.today(lat=51.5074, lon=-0.1278, tz="Europe/London")
print(tonight.data["moon"]["phase"]["name"])      # 'Waning Gibbous'
print(tonight.data["night"]["verdict"])           # 'Good. No moonlight ...'

There is one method per endpoint, named for its route, and every parameter is a keyword argument that is left out of the request when you leave it out of the call:

cc.moon(at="2026-08-27T22:00Z")                   # /v2/moon
cc.rise_set(body="mars", lat=39.74, lon=-104.99)  # /v2/rise-set
cc.dark_window(lat=38.72, lon=-9.14, nights=14)   # /v2/dark-window
cc.eclipses(lat=39.74, lon=-104.99, count=2)      # /v2/eclipses

Dates and times can be strings or datetime objects. An aware datetime names an instant and is converted to UTC. A naive one names a wall clock and is sent unmarked, which the API reads as UTC on its own but as local time in the zone when you also pass tz= or place=. If you mean an instant, pass an aware datetime. Lists are joined the way the API expects:

from datetime import datetime
cc.positions(bodies=["Venus", "Mars"], at=datetime(2026, 8, 20, 22, 0))

The whole envelope, not just the answer

Every response carries nine keys and means all of them. data is the answer; the rest is how to use it honestly.

r = cc.sun(lat=78.22, lon=15.63)          # Svalbard, in summer

r.data                                    # the answer
r.warnings                                # [{'code': 'polar_day', ...}]
r.links                                   # self, spec, docs, page, explain
r.meta                                    # api_version, engine, accuracy, cache class
r.attribution                             # one credit line; keep it where required
r.etag                                    # for a conditional request later
r.rate_limit.remaining                    # what is left in the current window

warnings is where the API tells you things that did not change the status code, such as polar_day when the Sun never set. Some are only reported when you ask for them: an unrecognised parameter shows up as unknown_parameter under verbosity="full" and is silent otherwise. A client that discards warnings throws away the part that explains the answer.

r["day_length"] is shorthand for r.data["day_length"].

Ranges and paging

Twenty-one of the twenty-nine routes take start and end, and the fourteen that walk a grid take step as well. When a series is longer than one response, links.next carries the rest:

for page in cc.moon(start="2026-01-01", end="2026-03-31", step="1d", limit=30).pages():
    for row in page.data["series"]:
        ...

The key holding the rows is named for what it holds and so varies by endpoint: series on moon, days on sun and twilight, rows on sidereal-time, phases on phases, nights on dark-window. page.has_next says whether another follows.

Ask for more than a request can carry and it refuses with a 400 naming the cap, rather than truncating the answer silently. Two bounds can apply: your tier's row cap, and the hard response-size ceiling, which on a wide endpoint binds first. When the size ceiling is the one that bound, the refusal usually says so in bound_by.

except BadRequest as err:
    err.extensions["cap"]   # {'requested': 2193, 'maximum': 262,
                            #  'unit': 'rows', 'bound_by': 'response_size'}

Conditional requests

Pass an ETag back and an unchanged answer costs you a 304 and no body:

first = cc.moon(at="2026-08-27T22:00Z")
later = cc.moon(at="2026-08-27T22:00Z", if_none_match=first.etag)
later.not_modified        # True
later.data                # None: you already have it

When it fails

Failures raise, and every field of the API's RFC 9457 problem document survives on the exception:

from cyclecalcs import BadRequest, RateLimited

try:
    cc.sun(lat=999, lon=0)
except BadRequest as err:
    err.code          # 'BAD_LATITUDE'
    err.parameter     # 'lat'
    err.hint          # what to send instead
    err.extensions    # {} here. Carries 'cap' on a refused range,
                      # or 'candidates' on an ambiguous local time.

try:
    ...
except RateLimited as err:
    err.retry_after   # seconds, from the API

TransportError covers anything that never became an HTTP response.

Limits, and the paid tiers

The direct API is keyless and free: 300 requests a minute, 2,000 an hour and 5,000 a day, per caller. Those are a published floor and may only ever rise.

A RapidAPI subscription raises the rate and range ceilings and changes no answer. Pass the key and the client talks to the gateway instead:

cc = CycleCalcs(rapidapi_key="...")

The key travels in a header, never in a URL, on either host.

What it sends

One GET per call, to www.cyclecalcs.com (or the RapidAPI gateway if you gave a key). No cookies, no identifiers, no analytics. Requests carry a client=python tag so the API can see how its own traffic splits; it is never echoed, never part of a cache key, and CycleCalcs(client_tag=None) removes it.

Rights

The numbers are computed astronomical facts: store them, publish them, redistribute them, build commercial products on them, no licence and no attribution required. The terms say so in one page.

One exception rides in the response rather than in a licence file: a place lookup returns GeoNames data under CC BY 4.0, and the credit arrives in response.attribution. Keep it wherever you show the result.

Also available

Licence

MIT, scoped to this package directory. See LICENSE.

Download files

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

Source Distribution

cyclecalcs-1.0.0.tar.gz (26.1 kB view details)

Uploaded Source

Built Distribution

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

cyclecalcs-1.0.0-py3-none-any.whl (22.7 kB view details)

Uploaded Python 3

File details

Details for the file cyclecalcs-1.0.0.tar.gz.

File metadata

  • Download URL: cyclecalcs-1.0.0.tar.gz
  • Upload date:
  • Size: 26.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for cyclecalcs-1.0.0.tar.gz
Algorithm Hash digest
SHA256 74afff7c14ee313ee83611cb58d05cfa63414382f25abb2ee002ff8f0be6b54d
MD5 5463b15d51627535b8fdd449d3bae49c
BLAKE2b-256 273b5d017e3a51d7dee582b9fd76370f6c2a205a6f74822faf4ef02a1e3972e9

See more details on using hashes here.

File details

Details for the file cyclecalcs-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: cyclecalcs-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 22.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for cyclecalcs-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 acc3399a01e00437013e68e73d23b786ec57e3befcdf338f00c38a3ac1ea9aad
MD5 de1e58a659f6315aff19a967ceb964a1
BLAKE2b-256 ef27d2ac275c9f11a89a92b3a0d5fe0e6247df7ea892e5f6b857f3d0619f3401

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

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