Metadata-Version: 2.5
Name: astromansion
Version: 0.1.8
Summary: Official Python client for the AstroMansion astrology API.
Project-URL: Homepage, https://astromansion.com
Project-URL: Documentation, https://astromansion.com/en/docs
Project-URL: API, https://api.astromansion.com/docs
Author-email: Mustafa Yavuz Ak <founder@mustafayavuzak.com>
License: MIT
License-File: LICENSE
Keywords: api,astrology,chart,client,ephemeris,natal
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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: Topic :: Scientific/Engineering :: Astronomy
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx[socks]<1,>=0.24
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# astromansion

Official Python client for the [AstroMansion](https://astromansion.com) astrology API.

Nothing is computed locally. Every call reaches `https://api.astromansion.com`,
which owns the ephemeris, your plan, your quota and your rate limit. The
package never opens a feature the server did not grant.

## Install

```bash
pip install astromansion
```

Python 3.10 or newer. The only dependency is `httpx`, installed with its SOCKS
support so the client still works behind a corporate proxy, a local SOCKS
proxy or Tor without anything further to install.

## Get an API key

Create an account at [astromansion.com](https://astromansion.com), open your
account page and generate a key. Requests made with it count against that
account, under the plan it already has.

## First chart

Put the key in the environment rather than in your source:

```bash
export ASTROMANSION_API_KEY="your key"
```

```python
from astromansion import AstroMansion

client = AstroMansion()

chart = client.natal(
    date="1990-07-19",
    time="14:30",
    lat=41.0082,
    lon=28.9784,
    timezone=3,
)

print(chart.summary.Sun.sign)  # Cancer
print(chart.planets[0].house)  # 9
```

Birth data is passed flat. The API nests it under `birth`; the client does
that for you.

Fields: `date` as `YYYY-MM-DD`, `time` as `HH:MM` (omit if unknown), `lat` and
`lon` in decimal degrees, `timezone` as an hour offset or an IANA zone name,
`houses` for a house system.

You can pass a mapping instead of keywords, but not both at once:

```python
chart = client.natal({"date": "1990-07-19", "lat": 41.0082, "lon": 28.9784})
```

## Where the key comes from

In order: the `api_key` argument, then `astromansion.set_api_key(...)`, then
`ASTROMANSION_API_KEY`. With none of them the constructor raises
`AuthenticationError`, before a connection is opened and before the proxy
environment is read, so a forgotten key is reported as a forgotten key.

```python
client = AstroMansion(api_key="your key")
```

`astromansion.reset()` drops the module key and the shared client, which is
what a test wants between cases.

## Quick use

For a notebook or a one-file script:

```python
import astromansion as am

am.set_api_key("your key")  # or rely on the environment
chart = am.natal(date="1990-07-19", lat=41.0082, lon=28.9784)
```

Applications should build a client instead: it holds a connection pool, and
two of them can carry two different keys.

## Async

```python
from astromansion import AsyncAstroMansion

async with AsyncAstroMansion() as client:
    chart = await client.natal(
        date="1990-07-19",
        time="14:30",
        lat=41.0082,
        lon=28.9784,
        timezone=3,
    )
```

Same method names, same arguments, same exceptions. Python cannot make one
class serve both, so the bare name is synchronous and `Async` marks the other,
as in `httpx`, `openai` and `anthropic`.

## Reading a response

The response is the server's own JSON, readable either way:

```python
chart.summary.Sun.sign
chart["summary"]["Sun"]["sign"]
chart.to_dict()
```

Nothing is remodelled, so a field the API adds reaches you instead of being
dropped, and no field it did not send is invented.

Some endpoints answer with the data itself and some wrap it as
`{"technique": ..., "result": ...}`. The client opens that wrapper, so every
response reads the same way and you never have to remember which kind you
called:

```python
chart = client.vedic_chart(date="1990-07-19", lat=41.0082, lon=28.9784)

chart.data          # the payload, wrapper removed
chart.technique     # what the server called it, when it said
chart.raw           # the untouched body, wrapper included
```

A body that carries a real `result` field of its own is left alone; the
wrapper is recognised by its exact shape rather than by the presence of a
name that data is allowed to use.

## Arabic lots, fixed stars and the rest of the catalog

`natal` answers with the chart proper. Anything beyond it is named on `chart`,
through `options.categories`:

```python
lots = client.chart(
    date="1990-07-19", time="14:30",
    lat=41.0082, lon=28.9784, timezone=3,
    options={"categories": ["arabic_lots"]},
)

for lot in lots.data["bodies"]["arabic_lots"]:
    print(lot["name"], lot["sign"], lot["dms"], lot["house"])
```

Bodies come back grouped under the category that produced them, so read the
group you asked for. The catalog publishes 34 Arabic lots and 890 fixed stars,
along with `planets`, `dwarfs`, `asteroids`, `centaurs`, `comets`,
`hypotheticals`, `points`, `advanced_points`, `lilith`, `planetary_nodes`,
`exoplanets`, `moons` and `eclipses`.

`chart` answers one page at a time, and a large category is more than one
page. Use `bodies` to read the whole of it and let the client follow the
paging:

```python
found = client.bodies(
    "fixed_stars", "arabic_lots",
    date="1990-07-19", time="14:30",
    lat=41.0082, lon=28.9784, timezone=3,
)

len(found["fixed_stars"])  # 890, in six requests
len(found["arabic_lots"])  # 34
```

Naming several categories reads them in one walk. The return is always a
mapping keyed by the categories you asked for, one or several, so the shape
never depends on how many.

Each page costs a network round trip and the calculation inside it is a
rounding error beside that, so `bodies` asks for the largest page the API
serves. `page_size` lowers it, and lowering it buys nothing: 890 stars are
six requests at 160 and a hundred and eighty at five.

Reach for `chart` directly when you want one page rather than the category,
and read the group with `.get`. The chart's own bodies are calculated
alongside the categories you name and the page is a window over that whole
selection, so a small `catalog_limit` can fill the first page with planets
and angles before a single star appears, leaving no `fixed_stars` key at all.
`catalog_page.total` counts the same way, the whole selection rather than the
category.

There is no ceiling on how many you may read this way. `options.all_bodies`
walks every category at once and needs the full-catalog scope that comes with
Pro and Enterprise; naming the categories yourself does not.

## Satellites

Six planets, twenty-five moons, and three answers for each one rather than a
single longitude. Seen from Earth a moon sits within arcminutes of its planet,
so its observed position just repeats the planet's; `planetary_system` returns
the three layers apart:

```python
system = client.planetary_system(
    birth={"date": "2000-01-01", "time": "12:00",
           "lat": 51.4779, "lon": 0.0, "timezone": 0},
    parent="jupiter",
)

for moon in system.data["satellites"]:
    orbit = moon["parentcentric"]              # where it is in its own orbit
    seen = moon["geocentric"]                  # how far the light travelled
    projected = moon["natal_parent_projected"]
    print(moon["name"], orbit["sector"], projected["sign"], projected["house"])
```

| `parent` | Moons |
|---|---|
| `mars` | Phobos, Deimos |
| `jupiter` | Io, Europa, Ganymede, Callisto |
| `saturn` | Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Hyperion, Iapetus |
| `uranus` | Ariel, Umbriel, Titania, Oberon, Miranda |
| `neptune` | Triton |
| `pluto` | Charon, Nix, Hydra, Kerberos, Styx |

### The three layers

`parentcentric` is measured on the planet's own equator, in a frame that does
not turn with the planet. It carries a `sector` from one to twelve rather than
a house: houses need a horizon, and nobody was born on Jupiter.

`geocentric` carries the light time and the epoch the light left. Every moon
is solved on its own, because solving once for the planet and reusing it puts
an outer moon several seconds into the wrong place.

`natal_parent_projected` takes the moon's orbital phase, counted from the
direction of Earth as measured at the planet, and lays it on the natal circle
starting at the planet's own degree. A phase of zero returns the planet
exactly. It is labelled `astromansion_derived`, because it is this engine's
derivation and not a rule anyone inherited.

Three circles, so there is no shared `longitude` field and no aspect between
them. `options.correction` takes `NONE` for geometric or `LT` for
astrometric; `LT+S` names stellar aberration, which is not computed, and is
refused rather than quietly answered with `LT`.

### Which ephemeris answered

Every response names the kernel it ran on:

```python
system.data["ephemeris"]
# {'kernel': 'jupiter-galilean-1900-2100.bsp', 'source': 'jup365',
#  'matches_horizons': True, 'horizons_delta_km': None,
#  'coverage_jd': [[2415020.5, 2488069.5]]}
```

Mars, Jupiter, Saturn and Pluto agree with JPL Horizons to machine precision.
Uranus and Neptune do not, and say so: their kernels carry an older orbit
solution, and `horizons_delta_km` gives the measured difference. It is an
along-track phase offset rather than a wrong orbit, up to 218 km on Titania,
which is 0.016 arcseconds seen from Earth. The field is there so a reader
comparing against Horizons learns why before wondering.

Satellites are tabulated from 1900 to 2100. A date outside that is refused
rather than extrapolated, because an extrapolated moon is a different orbit
and not a rougher answer.

## Every endpoint

Every published operation, 67 of them, has a method on both clients and a
module-level shortcut, all generated from the schema: `natal`, `transits`, `synastry`, `composite`,
`solar_return`, `progression`, `harmonics`, `astrocartography`, `vedic_chart`,
`zodiacal_releasing`, `firdaria`, `horary`, `electional` and the rest.

Anything new is reachable before this client names it:

```python
result = client.request("POST", "/v1/harmonics", json={"birth": {...}})
```

Authentication, timeouts, retries and error handling behave identically there.

## Errors

```python
from astromansion import QuotaExceededError, RateLimitError

try:
    chart = client.natal(date="1990-07-19", lat=41.0, lon=29.0)
except RateLimitError as error:
    print("wait", error.retry_after, "seconds")
except QuotaExceededError:
    print("this period's allowance is spent")
```

| Exception | Meaning |
|---|---|
| `AuthenticationError` | Key missing, malformed or unknown |
| `PermissionDeniedError` | Valid key, feature not in the plan |
| `QuotaExceededError` | Allowance for the period is spent |
| `RateLimitError` | Too many requests just now; `retry_after` says how long |
| `ValidationError` | Request rejected; `details` names the field |
| `NotFoundError`, `ConflictError` | Missing resource, conflicting state |
| `ServerError` | The API failed to answer |
| `AstroMansionConnectionError` | The request never completed |

All descend from `AstroMansionError`. Each carries `status_code`,
`error_code`, `details`, `request_id` and `retry_after` when the API supplies
them.

## Rate limits and quota

A rate limit clears on its own after `retry_after`. A spent quota does not:
it needs a new period or a larger plan. They are separate exceptions for that
reason.

The client retries only failures that carry no result: connection errors, 429
and 5xx, twice by default, honouring `Retry-After`. A refusal you must fix is
never retried.

```python
client = AstroMansion(timeout=60.0, max_retries=0)
```

## Documents

```python
pdf = client.export_pdf(date="1990-07-19", lat=41.0082, lon=28.9784)

with open("chart.pdf", "wb") as file:
    file.write(pdf)
```

Or name a path and let the client write it:

```python
client.export_pdf(date="1990-07-19", lat=41.0082, lon=28.9784, output="chart.pdf")
```

Every endpoint that answers with a document takes `output` the same way:
`export_pdf`, `export_csv`, `render_svg`, `render_png`, `render_biwheel` and
`render_sharecard`, on both clients.

```python
client.render_svg(date="1990-07-19", lat=41.0082, lon=28.9784, output="wheel.svg")
```

Nothing is written to disk unless you name a path, so a call cannot overwrite
a file you did not choose. Without one you get the bytes and decide yourself.

## Security

The key travels in the `X-API-Key` header, never in a URL. It is masked in
`repr(client)` and appears in no exception or log line the package writes.
Keep it in the environment or a secret store, not in source control. Rotate it
from your account page if it leaks.

## Staging

```python
client = AstroMansion(base_url="http://localhost:8000")
```

Also readable from `ASTROMANSION_BASE_URL`.

## Links

- [API reference](https://api.astromansion.com/docs)
- [Documentation](https://astromansion.com/en/docs)
