freeastroapi
Official Python client for FreeAstroAPI, with sync and async transports and typed methods for every public endpoint.
Install
pip install freeastroapi
# or
uv add freeastroapi
Quick start
import os
from freeastroapi import FreeAstro
with FreeAstro(api_key=os.environ["FREEASTRO_API_KEY"]) as client:
cities = client.geo.search("Paris", limit=3)
print(cities.results[0].timezone)
Async usage exposes the same resources:
import os
from freeastroapi import AsyncFreeAstro
async with AsyncFreeAstro(api_key=os.environ["FREEASTRO_API_KEY"]) as client:
cities = await client.geo.search("Paris", limit=3)
Resources
Every public operation in the FreeAstroAPI OpenAPI document has a typed method on both clients. Resources follow the API paths:
from freeastroapi import FreeAstro, schemas
with FreeAstro(api_key="YOUR_API_KEY") as client:
# POST /api/v1/natal/calculate?include_minor=true
chart = client.natal.calculate(
{"year": 1990, "month": 6, "day": 15, "hour": 14, "minute": 30, "city": "Paris"},
include_minor=True,
)
# POST /api/v2/vedic/panchang
request: schemas.PanchangRequestV2 = {"year": 2026, "month": 1, "day": 14, "hour": 6, "minute": 0, "city": "Delhi"}
panchang = client.vedic.panchang(request)
# POST /api/v1/western/progressions/secondary/exact-aspects
hits = client.western.progressions.secondary_exact_aspects(progressions_request)
# GET /api/v1/chinese/calendar/{date}
day = client.chinese.calendar("2026-02-17", lang="en")
# GET /api/v3/horoscope/daily/sign
horoscope = client.horoscope.daily.sign(sign="aries")
Path parameters are positional, the JSON body (when there is one) comes next, and query parameters are keyword-only. Every method also takes options=RequestOptions(...).
When an operation exists in several API versions, the plain method name calls the latest version and older versions carry a suffix: client.vedic.chart calls /api/v2/vedic/chart and client.vedic.chart_v1 calls /api/v1/vedic/chart; client.horoscope.daily.sign_v1 / sign_v2 reach the older daily horoscopes.
Resource groups: ai, chinese, ephemeris, geo, horoscope, moon, natal, numerology, sky_events, space, transits, vedic, and western (with astrocartography, directions, electional, natal, progressions, signs, transits and more). freeastroapi.OPERATIONS lists every operation with its client method.
Types
Request bodies and responses are typed with TypedDicts generated from the spec's component schemas, in freeastroapi.schemas. Every JSON operation has a typed response. They are plain dictionaries at runtime: responses are returned as parsed JSON without validation, so a response with a missing or null field, an unknown field or an unknown enum value never raises.
Operations with non-JSON responses send the matching Accept header and return str (SVG, Markdown) or bytes (PNG, PDF). The chart endpoints (client.natal.chart, client.natal.chart_* such as chart_synastry, client.natal.experimental.solar_return and .transits, client.space.chart_svg, client.vedic.kp_render, client.vedic.visual.chart) return SVG as text; for PNG output read the body as bytes:
png = client.request(
"POST", "/api/v2/vedic/kp/render", json={**birth, "format": "png"}, accept="image/png", response_type="bytes"
)
client.natal.report(job_id) returns the report job as JSON; for the finished PDF call client.request("GET", f"/api/v1/natal/report/{job_id}", params={"format": "pdf"}, accept="application/pdf", response_type="bytes").
v0.1 methods
client.geo.search, client.moon.phase, client.moon.month, client.sky_events.list and client.western.signs.* keep their v0.1 signatures and return Pydantic v2 models whose fields follow the spec (for example MoonPhaseResponse.interpretation is a MoonInterpretationData with key, title, body, tone and tags, and next_phases has new_moon, first_quarter, full_moon and last_quarter).
Decoding into these models never raises on a response from the API: unknown fields are kept in model_extra, and when a value does not match its declared type (a missing or null required field, an unknown enum value) the model is still built: valid fields are converted as usual, a missing required field is None and any other mismatched value is kept as received. freeastroapi.models.decode(Model, data) applies the same rules to your own data.
Use client.request(...) for any other endpoint.
Western sign example
from freeastroapi import FreeAstro, RequestOptions, WesternSignRequest
birth = WesternSignRequest(
year=1990,
month=6,
day=15,
city="Paris",
)
with FreeAstro(api_key="YOUR_API_KEY") as client:
sun = client.western.signs.sun(
birth,
options=RequestOptions(idempotency_key="stable-logical-request-key"),
)
Retries
By default a request is retried up to 2 times (RetryConfig(retries=2)), with exponential backoff and full jitter: before retry n (from 0) the client waits a random time between 0 and min(8, 0.5 * 2**n) seconds.
- HTTP 429 is retried for every operation: the request was rejected before it ran. The wait is the
Retry-Afterheader (seconds or an HTTP date), elseretry_after_msfrom the error body, else the backoff. If the suggested wait is longer than 30 seconds (RetryConfig.max_retry_after), the client does not wait and raisesRateLimitErrorat once, with the wait inretry_after. - HTTP 408, 500, 502, 503, 504, network errors and timeouts are retried for every operation except the side-effecting ones in
freeastroapi.SIDE_EFFECT_OPERATIONS:v1_natal_calculate(may create a paid report job),v1_ai_generate,v2_vedic_chat,v2_western_chat,v2_vedic_qa,v1_western_transits_timelineandv1_western_transits_insights. Those are retried only when the request carries an idempotency key (sent as theIdempotency-Keyheader) or opts in withRequestOptions(retry=True). ARetry-Afteron these responses is honoured the same way. - Other 4xx responses are never retried.
client.request(...) follows the same rules, matching the path against the known operations; a path that matches none is treated as side-effecting unless the method is GET, HEAD or OPTIONS.
from freeastroapi import FreeAstro, RequestOptions, RetryConfig
client = FreeAstro(api_key="YOUR_API_KEY", retry=RetryConfig(retries=4)) # retry=None disables retries
client.vedic.chat(body, options=RequestOptions(idempotency_key="chat-turn-42")) # retried on 5xx
client.natal.calculate(body, options=RequestOptions(max_retries=0)) # no retries for this call
client.geo.search("Paris", options=RequestOptions(retry=False)) # no retries, not even on 429
A RetryConfig passed in RequestOptions(retry=...) replaces the client's settings for that request and also opts it in.
Errors
All API failures derive from FreeAstroError. Specific subclasses cover authentication, permission, validation, conflict, rate limit, server, network, and timeout failures. Each error carries status, code (the stable error code from the body's error, code or detail.error), request_id and details (the parsed error body, the spec's ErrorResponse); the message is the body's detail message.
Development
uv sync
uv run python scripts/generate.py --check
uv run ruff check .
uv run mypy
uv run pytest # unit tests, generator tests
uv run python scripts/with_prism.py -- uv run pytest -m contract # needs Node.js for Prism
uv run python -m build
src/freeastroapi/_generated.py and src/freeastroapi/schemas.py are generated from spec/freeastro.json by scripts/generate.py (uv run python scripts/generate.py); --check fails when they are out of date. The generator's own tests (tests/test_generator.py) run it on a small synthetic spec (tests/fixtures/synthetic-spec.json) covering component names that are not identifiers, error response schemas, 201 responses and non-JSON bodies, and type-check the output with mypy --strict.
Tests
| Command | What it runs |
|---|---|
pytest |
Unit tests with a mocked transport, including the generator tests. Contract and live tests are skipped. |
python scripts/with_prism.py -- pytest -m contract |
Starts Prism (pinned version, via npx) in mock mode on spec/freeastro.json and calls every operation through the SDK (tests/contract/). Prism rejects any request that does not match the spec, and each response must validate against the method's return type. With Prism already running: PRISM_URL=http://127.0.0.1:4010 pytest -m contract. |
FREEASTRO_TEST_API_KEY=... pytest -m live |
Five read-only calls against production (tests/live/). |
python3 -m unittest discover -s scripts -p "test_*.py" |
Tests for scripts/spec_sync.py. |
Request values for the contract tests come from python3 scripts/spec_sync.py samples spec/freeastro.json, which builds a minimal valid request for every operation from its schema. For the hand-written v0.1 methods the contract tests also check that Prism's spec-valid data decodes into the Pydantic models without falling back to lenient decoding, so the models stay in line with the spec.
Maintaining
How the spec stays in sync
Production (https://api.freeastroapi.com/openapi.json) is the source of truth: the API is deployed by hand, so the live document is what customers call.
Sync specworkflow (.github/workflows/sync-spec.yml, daily and on demand) fetches the live spec withscripts/spec_sync.py fetch, which validates it and writes it normalised (sorted keys except schemaproperties, which keep the API's field order; 2-space indent) so diffs only show real changes.- If it differs from
spec/freeastro.json, the workflow regenerates the SDK, runs the full check (generate--check, ruff, mypy, pytest, build) and the Prism contract tests, and opens or updates a pull request from thespec-sync/autobranch, labelledspec-sync. If generation or a check fails, the workflow run fails and no pull request is opened. - The pull request body is the output of
scripts/spec_sync.py diff: added, removed and changed operations and schemas. Breaking changes (removed operations, parameters or request fields, newly required request fields or parameters, removed request enum values, removed response fields or content types, removed schemas) are listed first and the title starts withBREAKING:. The script never picks the version; you do. - If production goes back to matching
main, the next run closes the stale pull request.
Pull requests opened with GITHUB_TOKEN do not trigger other workflows, so CI does not start on its own for spec-sync/auto; the sync run already ran the same checks. To get CI status on the pull request, run Actions → CI → Run workflow on the spec-sync/auto branch, or close and reopen the pull request.
Run the same steps locally:
cp spec/freeastro.json /tmp/previous-spec.json
python3 scripts/spec_sync.py fetch
python3 scripts/spec_sync.py diff /tmp/previous-spec.json spec/freeastro.json
uv run python scripts/generate.py && uv run pytest && uv run python scripts/with_prism.py -- uv run pytest -m contract
The Live smoke test workflow (.github/workflows/live-smoke.yml, daily and on demand) runs pytest -m live with the FREEASTRO_TEST_API_KEY repository secret: geo search, moon phase, western sun sign, a natal chart and a vedic panchang, without retries (5 requests a day). Without the secret it is skipped with a notice.
scripts/spec_sync.py and scripts/with_prism.py are standard-library Python and identical in every FreeAstroAPI SDK repository; copy them, tests/fixtures/synthetic-spec.json and the three workflows when starting a new SDK, then swap in that language's generate, check and test commands.
Releasing
After merging a spec-sync pull request (or any release-worthy change):
- Pick the version: breaking changes need a major bump (a minor bump while the SDK is 0.x), new operations or types a minor bump, anything else a patch.
- Bump
versioninpyproject.tomland__version__insrc/freeastroapi/__init__.py, and add aCHANGELOG.mdentry. - Commit, then push a matching tag:
git tag v0.3.0 && git push origin v0.3.0.
The Release workflow checks that the tag matches pyproject.toml, runs the checks, builds, and publishes to PyPI with trusted publishing.
Issues and contributions are welcome on GitHub.
Licensed under Apache License 2.0.
Metadata
Release files for freeastroapi 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| freeastroapi-0.3.0.tar.gz | 310.0 kB | With provenance | Details for freeastroapi-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| freeastroapi-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for freeastroapi-0.3.0-py3-none-any.whl |
Total release size: 406.9 kB
Release files / freeastroapi-0.3.0.tar.gz
| Download URL | freeastroapi-0.3.0.tar.gz |
|---|---|
| Size | 310.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7444f295f580cbf24d1a88d2eaec34bef867b36852fbd21b8579e98f09d5f1c5
|
|
BLAKE2b-256 checksum How to use checksums |
1594600ca2ce7c53defb498c20a8eb7effcc221df6798fbb9cdc042d30bf9877
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / freeastroapi-0.3.0-py3-none-any.whl
| Download URL | freeastroapi-0.3.0-py3-none-any.whl |
|---|---|
| Size | 96.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d59e76f0b744d41cc9b68de3353f3a33d73795214f92761f3436f6d3af4c8d3c
|
|
BLAKE2b-256 checksum How to use checksums |
540146e28c344a5abbfdce8f7a91384dcfd10a77dd5935c54e828efde72f7d73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logSecurity for freeastroapi 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for freeastroapi-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.