freeastroapi 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freeastroapi-0.3.0/.github/workflows/ci.yml +55 -0
- freeastroapi-0.3.0/.github/workflows/live-smoke.yml +45 -0
- freeastroapi-0.3.0/.github/workflows/sync-spec.yml +99 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/.gitignore +1 -0
- freeastroapi-0.3.0/CHANGELOG.md +30 -0
- freeastroapi-0.3.0/PKG-INFO +235 -0
- freeastroapi-0.3.0/README.md +204 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/pyproject.toml +5 -1
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/scripts/generate.py +115 -25
- freeastroapi-0.3.0/scripts/spec_sync.py +633 -0
- freeastroapi-0.3.0/scripts/test_spec_sync.py +181 -0
- freeastroapi-0.3.0/scripts/with_prism.py +86 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/spec/freeastro.json +58690 -39390
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/src/freeastroapi/__init__.py +12 -2
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/src/freeastroapi/_generated.py +1726 -1658
- freeastroapi-0.3.0/src/freeastroapi/client.py +406 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/src/freeastroapi/errors.py +56 -14
- freeastroapi-0.3.0/src/freeastroapi/models.py +369 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/src/freeastroapi/resources.py +21 -15
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/src/freeastroapi/schemas.py +3694 -204
- freeastroapi-0.3.0/tests/contract/test_contract.py +178 -0
- freeastroapi-0.3.0/tests/fixtures/synthetic-spec.json +445 -0
- freeastroapi-0.3.0/tests/live/test_smoke.py +55 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/tests/test_client.py +6 -2
- freeastroapi-0.3.0/tests/test_generator.py +179 -0
- freeastroapi-0.3.0/tests/test_lenient_decoding.py +175 -0
- freeastroapi-0.3.0/tests/test_retries.py +215 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/uv.lock +1 -1
- freeastroapi-0.2.0/.github/workflows/ci.yml +0 -30
- freeastroapi-0.2.0/CHANGELOG.md +0 -18
- freeastroapi-0.2.0/PKG-INFO +0 -158
- freeastroapi-0.2.0/README.md +0 -127
- freeastroapi-0.2.0/src/freeastroapi/client.py +0 -309
- freeastroapi-0.2.0/src/freeastroapi/models.py +0 -221
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/.github/workflows/release.yml +0 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/LICENSE +0 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/src/freeastroapi/py.typed +0 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/tests/test_operations.py +0 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/tests/test_resources.py +0 -0
- {freeastroapi-0.2.0 → freeastroapi-0.3.0}/tests/test_transport.py +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
# Lets maintainers run CI on the bot-opened spec-sync/auto branch (pull requests opened
|
|
8
|
+
# with GITHUB_TOKEN do not trigger workflows): Actions -> CI -> Run workflow.
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
cache: pip
|
|
27
|
+
- run: python -m pip install --upgrade pip
|
|
28
|
+
- run: python -m pip install -e ".[dev]"
|
|
29
|
+
- run: python scripts/generate.py --check
|
|
30
|
+
- run: ruff check .
|
|
31
|
+
- run: mypy
|
|
32
|
+
# Unit and generator tests; contract and live tests skip without PRISM_URL / API key.
|
|
33
|
+
- run: pytest
|
|
34
|
+
- run: python -m build
|
|
35
|
+
|
|
36
|
+
contract:
|
|
37
|
+
# Every SDK method against a Stoplight Prism mock of spec/freeastro.json.
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
41
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
cache: pip
|
|
45
|
+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
46
|
+
with:
|
|
47
|
+
node-version: 22
|
|
48
|
+
- run: python -m pip install -e ".[dev]"
|
|
49
|
+
- name: Spec tool tests
|
|
50
|
+
run: python -m unittest discover -s scripts -p "test_*.py"
|
|
51
|
+
- name: Contract tests
|
|
52
|
+
run: python scripts/with_prism.py -- pytest -m contract
|
|
53
|
+
- name: Prism log
|
|
54
|
+
if: failure()
|
|
55
|
+
run: tail -n 300 prism.log || true
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Live smoke test
|
|
2
|
+
|
|
3
|
+
# Five cheap read-only calls against production with the FREEASTRO_TEST_API_KEY secret
|
|
4
|
+
# (geo search, moon phase, western sun sign, natal chart, vedic panchang). Skipped with a
|
|
5
|
+
# notice when the secret is not set. Retries are off to protect the key's daily quota.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "11 7 * * *"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: live-smoke
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
smoke:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
timeout-minutes: 10
|
|
23
|
+
env:
|
|
24
|
+
FREEASTRO_TEST_API_KEY: ${{ secrets.FREEASTRO_TEST_API_KEY }}
|
|
25
|
+
steps:
|
|
26
|
+
- name: Check for the API key
|
|
27
|
+
id: key
|
|
28
|
+
run: |
|
|
29
|
+
if [ -z "$FREEASTRO_TEST_API_KEY" ]; then
|
|
30
|
+
echo "::notice title=Live smoke test skipped::Add the FREEASTRO_TEST_API_KEY repository secret to run it."
|
|
31
|
+
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
32
|
+
else
|
|
33
|
+
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
34
|
+
fi
|
|
35
|
+
- if: steps.key.outputs.present == 'true'
|
|
36
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
37
|
+
- if: steps.key.outputs.present == 'true'
|
|
38
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.12"
|
|
41
|
+
cache: pip
|
|
42
|
+
- if: steps.key.outputs.present == 'true'
|
|
43
|
+
run: python -m pip install -e ".[dev]"
|
|
44
|
+
- if: steps.key.outputs.present == 'true'
|
|
45
|
+
run: pytest -m live
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: Sync spec
|
|
2
|
+
|
|
3
|
+
# Pulls the production OpenAPI document daily. When it differs from spec/freeastro.json, the
|
|
4
|
+
# SDK is regenerated and fully checked here (pull requests opened with GITHUB_TOKEN do not
|
|
5
|
+
# trigger CI), then a pull request is opened or updated from the spec-sync/auto branch.
|
|
6
|
+
# A failing generator or check fails this workflow instead of opening a broken PR.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: "53 5 * * *"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: sync-spec
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
env:
|
|
22
|
+
SPEC_URL: https://api.freeastroapi.com/openapi.json
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
sync:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 30
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
30
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.12"
|
|
33
|
+
cache: pip
|
|
34
|
+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
35
|
+
with:
|
|
36
|
+
node-version: 22
|
|
37
|
+
- run: python -m pip install -e ".[dev]"
|
|
38
|
+
|
|
39
|
+
- name: Fetch the production spec
|
|
40
|
+
run: |
|
|
41
|
+
cp spec/freeastro.json "$RUNNER_TEMP/previous-spec.json"
|
|
42
|
+
python scripts/spec_sync.py fetch "$SPEC_URL" --out spec/freeastro.json
|
|
43
|
+
|
|
44
|
+
- name: Diff against the vendored spec
|
|
45
|
+
id: diff
|
|
46
|
+
run: |
|
|
47
|
+
python scripts/spec_sync.py diff "$RUNNER_TEMP/previous-spec.json" spec/freeastro.json \
|
|
48
|
+
--markdown "$RUNNER_TEMP/spec-diff.md" --github-output "$GITHUB_OUTPUT" > /dev/null
|
|
49
|
+
cat "$RUNNER_TEMP/spec-diff.md" >> "$GITHUB_STEP_SUMMARY"
|
|
50
|
+
|
|
51
|
+
- name: Regenerate
|
|
52
|
+
if: steps.diff.outputs.changed == 'true'
|
|
53
|
+
run: python scripts/generate.py
|
|
54
|
+
|
|
55
|
+
- name: Check (generated files, lint, types, tests, build)
|
|
56
|
+
if: steps.diff.outputs.changed == 'true'
|
|
57
|
+
run: |
|
|
58
|
+
python scripts/generate.py --check
|
|
59
|
+
ruff check .
|
|
60
|
+
mypy
|
|
61
|
+
pytest
|
|
62
|
+
python -m build
|
|
63
|
+
|
|
64
|
+
- name: Contract tests
|
|
65
|
+
if: steps.diff.outputs.changed == 'true'
|
|
66
|
+
run: python scripts/with_prism.py -- pytest -m contract
|
|
67
|
+
|
|
68
|
+
- name: Prism log
|
|
69
|
+
if: failure() && steps.diff.outputs.changed == 'true'
|
|
70
|
+
run: tail -n 300 prism.log || true
|
|
71
|
+
|
|
72
|
+
- name: Pull request body
|
|
73
|
+
run: |
|
|
74
|
+
{
|
|
75
|
+
cat "$RUNNER_TEMP/spec-diff.md"
|
|
76
|
+
echo
|
|
77
|
+
echo "---"
|
|
78
|
+
echo "Opened by [sync-spec.yml]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) from \`$SPEC_URL\`."
|
|
79
|
+
echo "Generation, ruff, mypy, pytest, the build and the Prism contract tests passed in that run."
|
|
80
|
+
echo
|
|
81
|
+
echo "To release after merging: bump \`version\` in \`pyproject.toml\` and \`__version__\` in"
|
|
82
|
+
echo "\`src/freeastroapi/__init__.py\` (major/minor for breaking changes, minor for new operations,"
|
|
83
|
+
echo "patch otherwise), update \`CHANGELOG.md\`, then push a matching \`v*\` tag."
|
|
84
|
+
} > "$RUNNER_TEMP/pr-body.md"
|
|
85
|
+
|
|
86
|
+
# With no changes against main this closes a stale spec-sync PR (delete-branch).
|
|
87
|
+
- name: Open or update the pull request
|
|
88
|
+
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
89
|
+
with:
|
|
90
|
+
branch: spec-sync/auto
|
|
91
|
+
delete-branch: true
|
|
92
|
+
add-paths: |
|
|
93
|
+
spec/freeastro.json
|
|
94
|
+
src/freeastroapi/_generated.py
|
|
95
|
+
src/freeastroapi/schemas.py
|
|
96
|
+
commit-message: ${{ steps.diff.outputs.title }}
|
|
97
|
+
title: ${{ steps.diff.outputs.title }}
|
|
98
|
+
body-path: ${{ runner.temp }}/pr-body.md
|
|
99
|
+
labels: spec-sync
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Vendor the new spec (142 operations, 975 component schemas): every JSON operation now has a typed response (`TypedDict`s in `freeastroapi.schemas`) instead of `Any`; chart endpoints declare SVG and PNG, and `client.natal.report` documents its PDF form.
|
|
6
|
+
- Typed errors follow the spec's `ErrorResponse`: `code` also comes from the body's `error` or `detail.error`, the message from `detail.message`, and validation errors (`HTTPValidationError`) name the first failing field. `RateLimitError.retry_after` falls back to `retry_after_ms` from the body.
|
|
7
|
+
- One retry policy, shared by every FreeAstroAPI SDK: up to 2 retries with exponential backoff (0.5 s, factor 2, full jitter, 8 s cap). HTTP 429 is retried for every operation, honouring `Retry-After` and `retry_after_ms`; a suggested wait over 30 s raises `RateLimitError` at once. HTTP 408/5xx, network errors and timeouts are retried for every operation except those in the new `SIDE_EFFECT_OPERATIONS` (`v1_natal_calculate`, `v1_ai_generate`, `v2_vedic_chat`, `v2_western_chat`, `v2_vedic_qa`, `v1_western_transits_timeline`, `v1_western_transits_insights`), which need an idempotency key or `RequestOptions(retry=True)`. Calculation POSTs are now retried by default; other 4xx never are. `RetryConfig` defaults change (`base_delay=0.5`, `max_delay=8.0`, new `max_retry_after=30.0`) and `RequestOptions` gains `max_retries`.
|
|
8
|
+
- Lenient response decoding: the v0.1 Pydantic models (geo, Moon, sky events, Western signs) follow the spec (new Moon fields such as `zodiac`, `rise_set`, `interpretation` with `key`/`title`/`body`/`tone`/`tags`, `traditional_moon`, `sign_timeline`; open `calendar_basis` and `angle`; sign `interpretation` and `resolved_time` objects) and never raise on a response: missing or `null` required fields, unknown fields and unknown enum values decode. `freeastroapi.models.decode` is exported.
|
|
9
|
+
- `request()` with the default `response_type="auto"` returns `bytes` for binary content types (PDF, PNG) instead of decoding them as text.
|
|
10
|
+
- Operations with non-JSON responses send the matching `Accept` header and return `str` (SVG, Markdown, other `text/*`) or `bytes` (PNG, PDF and other binary types). `request` and `request_raw` take new `accept` and `response_type` arguments (`response_type="bytes"` reads PNG chart renders); `ResponseType` is exported.
|
|
11
|
+
- The generator handles component names that are not identifiers (`Model-Input` becomes `schemas.Model_Input`), names that clash with `typing`, 2xx responses other than 200 and `+json` content types, and fails on request bodies it cannot encode.
|
|
12
|
+
- Add the daily `Sync spec` workflow, `scripts/spec_sync.py` (fetch, normalise, diff, request samples), Prism contract tests (`pytest -m contract`) and a nightly live smoke test (`pytest -m live`).
|
|
13
|
+
- `spec/freeastro.json` is stored normalised (sorted keys); generated files are reordered accordingly.
|
|
14
|
+
|
|
15
|
+
## 0.2.0
|
|
16
|
+
|
|
17
|
+
- Cover every operation in the public FreeAstroAPI OpenAPI document (142 operations) on both `FreeAstro` and `AsyncFreeAstro`: Western, Vedic (v1 and v2), Chinese BaZi, numerology, natal charts and reports, transits, ephemeris, horoscopes (v1-v3), AI reports, space charts, geo, Moon and sky events.
|
|
18
|
+
- Generate resources and `TypedDict` request/response types (`freeastroapi.schemas`) from the vendored spec (`spec/freeastro.json`) with `scripts/generate.py`.
|
|
19
|
+
- The plain method name calls the latest API version; older versions are available with a `_v1`/`_v2` suffix.
|
|
20
|
+
- Export `OPERATIONS`, a list of every operation and its client method.
|
|
21
|
+
- Add a tag-triggered release workflow that publishes to PyPI with trusted publishing.
|
|
22
|
+
- v0.1 methods and Pydantic models are unchanged.
|
|
23
|
+
|
|
24
|
+
## 0.1.0
|
|
25
|
+
|
|
26
|
+
- Add synchronous `FreeAstro` and asynchronous `AsyncFreeAstro` clients.
|
|
27
|
+
- Add Pydantic v2 models for Geo Search, Moon Phase, Moon Month, Sky Events, and four Western sign operations.
|
|
28
|
+
- Add a generic request escape hatch for additional documented endpoints.
|
|
29
|
+
- Add capped exponential retries with `Retry-After` and idempotency-aware POST behavior.
|
|
30
|
+
- Add typed API, network, timeout, validation, authentication, permission, conflict, rate-limit, and server errors.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: freeastroapi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official Python SDK for FreeAstroAPI
|
|
5
|
+
Project-URL: Documentation, https://www.freeastroapi.com/docs/utilities/sdk
|
|
6
|
+
Project-URL: Homepage, https://www.freeastroapi.com
|
|
7
|
+
Project-URL: Issues, https://github.com/GabrielRw/freeastroapi-python-sdk/issues
|
|
8
|
+
Project-URL: Source, https://github.com/GabrielRw/freeastroapi-python-sdk
|
|
9
|
+
Author: FreeAstroAPI
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,astrology,freeastroapi,python,sdk
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy<2,>=1.11; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio<2,>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest<10,>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff<1,>=0.9; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# `freeastroapi`
|
|
33
|
+
|
|
34
|
+
[](https://github.com/GabrielRw/freeastroapi-python-sdk/actions/workflows/ci.yml)
|
|
35
|
+
|
|
36
|
+
Official Python client for [FreeAstroAPI](https://www.freeastroapi.com), with sync and async transports and typed methods for every public endpoint.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install freeastroapi
|
|
42
|
+
# or
|
|
43
|
+
uv add freeastroapi
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
from freeastroapi import FreeAstro
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
with FreeAstro(api_key=os.environ["FREEASTRO_API_KEY"]) as client:
|
|
55
|
+
cities = client.geo.search("Paris", limit=3)
|
|
56
|
+
print(cities.results[0].timezone)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Async usage exposes the same resources:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import os
|
|
63
|
+
|
|
64
|
+
from freeastroapi import AsyncFreeAstro
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async with AsyncFreeAstro(api_key=os.environ["FREEASTRO_API_KEY"]) as client:
|
|
68
|
+
cities = await client.geo.search("Paris", limit=3)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Resources
|
|
72
|
+
|
|
73
|
+
Every public operation in the [FreeAstroAPI OpenAPI document](spec/freeastro.json) has a typed method on both clients. Resources follow the API paths:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from freeastroapi import FreeAstro, schemas
|
|
77
|
+
|
|
78
|
+
with FreeAstro(api_key="YOUR_API_KEY") as client:
|
|
79
|
+
# POST /api/v1/natal/calculate?include_minor=true
|
|
80
|
+
chart = client.natal.calculate(
|
|
81
|
+
{"year": 1990, "month": 6, "day": 15, "hour": 14, "minute": 30, "city": "Paris"},
|
|
82
|
+
include_minor=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# POST /api/v2/vedic/panchang
|
|
86
|
+
request: schemas.PanchangRequestV2 = {"year": 2026, "month": 1, "day": 14, "hour": 6, "minute": 0, "city": "Delhi"}
|
|
87
|
+
panchang = client.vedic.panchang(request)
|
|
88
|
+
|
|
89
|
+
# POST /api/v1/western/progressions/secondary/exact-aspects
|
|
90
|
+
hits = client.western.progressions.secondary_exact_aspects(progressions_request)
|
|
91
|
+
|
|
92
|
+
# GET /api/v1/chinese/calendar/{date}
|
|
93
|
+
day = client.chinese.calendar("2026-02-17", lang="en")
|
|
94
|
+
|
|
95
|
+
# GET /api/v3/horoscope/daily/sign
|
|
96
|
+
horoscope = client.horoscope.daily.sign(sign="aries")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
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(...)`.
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
### Types
|
|
106
|
+
|
|
107
|
+
Request bodies and responses are typed with `TypedDict`s 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.
|
|
108
|
+
|
|
109
|
+
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:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
png = client.request(
|
|
113
|
+
"POST", "/api/v2/vedic/kp/render", json={**birth, "format": "png"}, accept="image/png", response_type="bytes"
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`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")`.
|
|
118
|
+
|
|
119
|
+
### v0.1 methods
|
|
120
|
+
|
|
121
|
+
`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`).
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
Use `client.request(...)` for any other endpoint.
|
|
126
|
+
|
|
127
|
+
## Western sign example
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from freeastroapi import FreeAstro, RequestOptions, WesternSignRequest
|
|
131
|
+
|
|
132
|
+
birth = WesternSignRequest(
|
|
133
|
+
year=1990,
|
|
134
|
+
month=6,
|
|
135
|
+
day=15,
|
|
136
|
+
city="Paris",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
with FreeAstro(api_key="YOUR_API_KEY") as client:
|
|
140
|
+
sun = client.western.signs.sun(
|
|
141
|
+
birth,
|
|
142
|
+
options=RequestOptions(idempotency_key="stable-logical-request-key"),
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Retries
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
- **HTTP 429** is retried for every operation: the request was rejected before it ran. The wait is the `Retry-After` header (seconds or an HTTP date), else `retry_after_ms` from 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 raises `RateLimitError` at once, with the wait in `retry_after`.
|
|
151
|
+
- **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_timeline` and `v1_western_transits_insights`. Those are retried only when the request carries an idempotency key (sent as the `Idempotency-Key` header) or opts in with `RequestOptions(retry=True)`. A `Retry-After` on these responses is honoured the same way.
|
|
152
|
+
- **Other 4xx responses** are never retried.
|
|
153
|
+
|
|
154
|
+
`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.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from freeastroapi import FreeAstro, RequestOptions, RetryConfig
|
|
158
|
+
|
|
159
|
+
client = FreeAstro(api_key="YOUR_API_KEY", retry=RetryConfig(retries=4)) # retry=None disables retries
|
|
160
|
+
|
|
161
|
+
client.vedic.chat(body, options=RequestOptions(idempotency_key="chat-turn-42")) # retried on 5xx
|
|
162
|
+
client.natal.calculate(body, options=RequestOptions(max_retries=0)) # no retries for this call
|
|
163
|
+
client.geo.search("Paris", options=RequestOptions(retry=False)) # no retries, not even on 429
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A `RetryConfig` passed in `RequestOptions(retry=...)` replaces the client's settings for that request and also opts it in.
|
|
167
|
+
|
|
168
|
+
## Errors
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
uv sync
|
|
176
|
+
uv run python scripts/generate.py --check
|
|
177
|
+
uv run ruff check .
|
|
178
|
+
uv run mypy
|
|
179
|
+
uv run pytest # unit tests, generator tests
|
|
180
|
+
uv run python scripts/with_prism.py -- uv run pytest -m contract # needs Node.js for Prism
|
|
181
|
+
uv run python -m build
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`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`.
|
|
185
|
+
|
|
186
|
+
### Tests
|
|
187
|
+
|
|
188
|
+
| Command | What it runs |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `pytest` | Unit tests with a mocked transport, including the generator tests. Contract and live tests are skipped. |
|
|
191
|
+
| `python scripts/with_prism.py -- pytest -m contract` | Starts [Prism](https://github.com/stoplightio/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`. |
|
|
192
|
+
| `FREEASTRO_TEST_API_KEY=... pytest -m live` | Five read-only calls against production (`tests/live/`). |
|
|
193
|
+
| `python3 -m unittest discover -s scripts -p "test_*.py"` | Tests for `scripts/spec_sync.py`. |
|
|
194
|
+
|
|
195
|
+
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.
|
|
196
|
+
|
|
197
|
+
## Maintaining
|
|
198
|
+
|
|
199
|
+
### How the spec stays in sync
|
|
200
|
+
|
|
201
|
+
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.
|
|
202
|
+
|
|
203
|
+
1. **`Sync spec` workflow** (`.github/workflows/sync-spec.yml`, daily and on demand) fetches the live spec with `scripts/spec_sync.py fetch`, which validates it and writes it normalised (sorted keys except schema `properties`, which keep the API's field order; 2-space indent) so diffs only show real changes.
|
|
204
|
+
2. 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 the `spec-sync/auto` branch, labelled `spec-sync`. If generation or a check fails, the workflow run fails and no pull request is opened.
|
|
205
|
+
3. 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 with `BREAKING:`. The script never picks the version; you do.
|
|
206
|
+
4. If production goes back to matching `main`, the next run closes the stale pull request.
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
Run the same steps locally:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
cp spec/freeastro.json /tmp/previous-spec.json
|
|
214
|
+
python3 scripts/spec_sync.py fetch
|
|
215
|
+
python3 scripts/spec_sync.py diff /tmp/previous-spec.json spec/freeastro.json
|
|
216
|
+
uv run python scripts/generate.py && uv run pytest && uv run python scripts/with_prism.py -- uv run pytest -m contract
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
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.
|
|
220
|
+
|
|
221
|
+
`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.
|
|
222
|
+
|
|
223
|
+
### Releasing
|
|
224
|
+
|
|
225
|
+
After merging a spec-sync pull request (or any release-worthy change):
|
|
226
|
+
|
|
227
|
+
1. 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.
|
|
228
|
+
2. Bump `version` in `pyproject.toml` and `__version__` in `src/freeastroapi/__init__.py`, and add a `CHANGELOG.md` entry.
|
|
229
|
+
3. Commit, then push a matching tag: `git tag v0.3.0 && git push origin v0.3.0`.
|
|
230
|
+
|
|
231
|
+
The `Release` workflow checks that the tag matches `pyproject.toml`, runs the checks, builds, and publishes to PyPI with trusted publishing.
|
|
232
|
+
|
|
233
|
+
Issues and contributions are welcome on [GitHub](https://github.com/GabrielRw/freeastroapi-python-sdk).
|
|
234
|
+
|
|
235
|
+
Licensed under Apache License 2.0.
|