Metadata-Version: 2.5
Name: cyclecalcs
Version: 1.0.0
Summary: A thin client for the CycleCalcs astronomy API: moon phase, sunrise and sunset, planet visibility, eclipses and observing windows. No key, no signup, no dependencies.
Project-URL: Homepage, https://www.cyclecalcs.com/api.html
Project-URL: Documentation, https://www.cyclecalcs.com/api/reference.html
Project-URL: Changelog, https://www.cyclecalcs.com/api/versioning.html
Project-URL: Contact, https://www.cyclecalcs.com/about.html
Author-email: CycleCalcs <info@cyclecalcs.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api-client,astronomy,eclipse,ephemeris,moon-phase,observing,sunrise-sunset,twilight
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# cyclecalcs

A thin Python client for the [CycleCalcs astronomy API](https://www.cyclecalcs.com/api.html):
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

```bash
pip install cyclecalcs
```

## Use

```python
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:

```python
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:

```python
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.

```python
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:

```python
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`.

```python
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:

```python
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:

```python
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:

```python
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](https://www.cyclecalcs.com/api/terms.html)
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

- The [API itself](https://www.cyclecalcs.com/api.html), plain HTTP with a live playground
- An [MCP server](https://www.cyclecalcs.com/api/mcp.html) for Claude and other AI clients
- [Embeddable widgets](https://www.cyclecalcs.com/embed.html) for a web page, and
  `cyclecalcs-widget` on npm for a framework
- [Bulk datasets](https://www.cyclecalcs.com/press.html) as CC0 CSV, for offline work

## Licence

MIT, scoped to this package directory. See `LICENSE`.
