Metadata-Version: 2.5
Name: airanks
Version: 1.0.1
Summary: Python client for the AIR API (airanks.net) — AI optimization rankings: look up a domain's AIR score, search domains/brands/phrases, and check who's logged in.
Project-URL: Homepage, https://airanks.net
Project-URL: Toolbar, https://airanks.net/toolbar
Project-URL: Repository, https://git.shoemoney.ai/shoemoney/airanks-oss
Author: Jeremy Schoemaker
License: MIT
License-File: LICENSE
Keywords: ai optimization,ai search,air,airanks,api-client,llm seo
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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 :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# 🐍 airanks — the AIR Python SDK

![PyPI-ready](https://img.shields.io/badge/PyPI-not%20yet%20published-orange)
![python](https://img.shields.io/badge/python-3.9%2B-blue)
![license](https://img.shields.io/badge/license-MIT-green)
![typed](https://img.shields.io/badge/typing-PEP%20561%20(py.typed)-blueviolet)
![deps](https://img.shields.io/badge/dependencies-requests-lightgrey)

**What is AIR?** AIR (**A**rtificial **I**ntelligence **R**anking) by [airanks](https://airanks.net)
makes **AI optimization** visible — a 0–10 score for how often, and how well, an AI assistant like
ChatGPT cites a given domain when answering real questions. Look up any site's AIR score at
**[airanks.net](https://airanks.net)**, or install the [browser toolbar](https://airanks.net/toolbar)
to see it while you browse.

`airanks` is the Python door into that data: **one client class, three methods, one dependency.**
No CLI, no scaffolding — `pip install`, import, go. 🚀

---

## 📚 Table of contents

- [What is AIR?](#-airanks--the-air-python-sdk)
- [Install](#-install)
- [Quickstart](#-quickstart)
- [API reference](#-api-reference)
- [Shared authentication](#-shared-authentication)
- [How auth resolution works](#-how-auth-resolution-works)
- [Error handling](#-error-handling)
- [Testing](#-testing)
- [The AIR ecosystem](#-the-air-ecosystem)
- [License](#-license)

---

## 📦 Install

```bash
pip install airanks
```

> Requires **Python 3.9+**. One runtime dependency: [`requests`](https://pypi.org/project/requests/).
> Ships a `py.typed` marker, so your type checker sees real annotations, not `Any` soup.

<details>
<summary>📥 Installing from source (until the PyPI release lands)</summary>

```bash
git clone https://git.shoemoney.ai/shoemoney/airanks-oss.git
cd airanks-oss/python-sdk
pip install -e .
```

</details>

---

## ⚡ Quickstart

> 🔑 Every method now requires a token — a free account gets you one at
> [airanks.net/tokens](https://airanks.net/tokens). Set it via `AIR_API_KEY` (or pass
> `api_key=` below) before calling anything; see [Shared authentication](#-shared-authentication).

```python
import os
from airanks import AirClient

os.environ["AIR_API_KEY"] = "your-token-here"  # or export it in your shell
client = AirClient()

domain = client.domain("stripe.com")
print(domain["data"]["air_score"])  # 0-10

results = client.search("payment processing")
who = client.user()  # raises ApiError(401) if the token is missing, invalid, or revoked
```

A fuller example with error handling lives in [`examples/lookup.py`](examples/lookup.py) — run it
with `python examples/lookup.py stripe.com`.

---

## 🔌 API reference

`AirClient` is the whole surface area. Three methods, all `GET`, all JSON in and out:

| Method | Returns | Notes |
|---|---|---|
| `client.domain(host: str) -> dict` | `{"data": {...domain}, "meta": {"dataset_version": ...}}` | AIR score, percentile, and AI-file posture (`llms.txt`, `ai.txt`, `robots.txt` AI-agent rules, JSON-LD) for a hostname. Always 200s for a valid host — a never-before-seen domain triggers server-side hydration, so `data["ai_files"]["status"] == "pending"` means **"check again shortly,"** not an error. |
| `client.search(query: str) -> dict` | `{"data": {"domains": [], "brands": [], "phrases": []}, "meta": {...}}` | Matches across everything AIR tracks. |
| `client.user() -> dict` | The authenticated user (`name`, `email`) for whichever token was resolved. | Raises `ApiError` with `status_code == 401` if the token is missing, invalid, or revoked. |

**Constructor:**

```python
AirClient(api_key: str | None = None, api_base: str | None = None)
```

| Arg | Default | Effect |
|---|---|---|
| `api_key` | `None` → shared resolution order | An explicit key takes priority over everything and **always attaches**, same as an env-sourced token. |
| `api_base` | `AIR_API_BASE` env, else `https://airanks.net/api/v1` | Point at staging, a mirror, or a local dev server. |

---

## 🔐 Shared authentication

**A free account is required.** Anonymous requests now get a `401 authentication_required` —
grab a token at [airanks.net/tokens](https://airanks.net/tokens), then set it via `AIR_API_KEY`
or pass `api_key=` to `AirClient()`. **One login works across every AIR client**: run `air login`
once from the `air` CLI or the browser toolbar, and this SDK picks up the same token — no
separate config, no re-auth.

Resolution order (first hit wins), identical across every AIR client — the `air` CLI, the browser
toolbar, and every other language SDK in the ecosystem:

| Priority | Source | Behavior |
|---|---|---|
| 1️⃣ | **`AIR_API_KEY` env var** | Explicit intent — always attaches, to any host. |
| 2️⃣ | **`~/.config/air/auth.json`** | The file `air login` writes. **Host-scoped**: only attaches to requests aimed at the host it was saved for, so a repointed `AIR_API_BASE` can't accidentally leak a token elsewhere. |
| 3️⃣ | **Anonymous** | No token attached — the API now rejects these with `401 authentication_required`. Every method requires a token from one of the two sources above (or an explicit `api_key=`). |

Skip all of that and use a key unconditionally:

```python
client = AirClient(api_key="your-token-here")
```

### 🧭 How auth resolution works

```mermaid
flowchart TD
    Start(["AirClient() constructed"]) --> Explicit{"api_key passed\nto constructor?"}
    Explicit -- yes --> UseExplicit["source = explicit\nalways attaches"]
    Explicit -- no --> Env{"AIR_API_KEY\nenv var set?"}
    Env -- yes --> UseEnv["source = env\nalways attaches"]
    Env -- no --> File{"~/.config/air/auth.json\nreadable + has token?"}
    File -- yes --> UseFile["source = file\nattaches ONLY if\nrequest host == saved host"]
    File -- no --> Anon["source = anonymous\nno Authorization header\n→ 401 authentication_required"]

    UseExplicit --> Request["client._get(url)"]
    UseEnv --> Request
    UseFile --> HostCheck{"urlparse(url).hostname\n== saved host?"}
    Anon --> Request
    HostCheck -- yes --> Attach["Authorization: Bearer <token>"]
    HostCheck -- no --> NoAttach["request sent unauthenticated\n→ 401 authentication_required"]
    Attach --> Request
    NoAttach --> Request
```

---

## 🚨 Error handling

Non-2xx responses (and transport failures — timeouts, DNS errors) raise `airanks.ApiError`:

```python
from airanks import AirClient, ApiError

client = AirClient()
try:
    domain = client.domain("example.com")
except ApiError as e:
    if e.status_code == 429:
        time.sleep(e.retry_after or 30)
```

| Attribute | Type | Meaning |
|---|---|---|
| `status_code` | `int \| None` | HTTP status. `None` means a transport-level failure, not an HTTP response. |
| `retry_after` | `int \| None` | Present on a `429` when the server sends a `Retry-After` header — seconds to wait. |

The error message is pulled from the response body (`{"error": {"message": ...}}` or the `422`
shape `{"message": ...}`), falling back to a generic `"API returned {status_code}"`.

---

## 🧪 Testing

`tests/test_auth.py` covers token-resolution order and the host-scoped attach rule — **no network
calls, fully offline:**

```bash
pip install -e ".[dev]"   # or: pip install -e . pytest
pytest
```

---

## 🌐 The AIR ecosystem

`airanks` (this package) is one door into AIR. Same API, same shared auth, different language:

| Client | What it is |
|---|---|
| 🖥️ [go-cli](https://github.com/airanks-net/go-cli) | Zero-dependency Go CLI |
| 🖥️ [node-cli](https://github.com/airanks-net/node-cli) | Node.js CLI |
| 🦀 [rust-cli](https://github.com/airanks-net/rust-cli) | Rust CLI |
| 🐘 [composer-package](https://github.com/airanks-net/composer-package) | PHP client |
| 📜 [js-sdk](https://github.com/airanks-net/js-sdk) | JS/TS SDK for Node & browser |
| 🧰 [chrome-extension](https://github.com/airanks-net/chrome-extension) | The [AIR browser toolbar](https://airanks.net/toolbar) |
| 🔌 [mcp-server](https://github.com/airanks-net/mcp-server) | MCP server — `air_rank` / `air_files` / `air_search` tools for any agent |
| 🤖 [agent-toolkit](https://github.com/airanks-net/agent-toolkit) | Universal AI-agent toolkit (Claude, Codex, Cursor, Cline, …) |

Why AIR: search used to be the whole game — now the traffic that matters is an AI assistant
deciding whether to cite you at all, and that's a different **AI optimization** problem than
classic SEO. AIR exists to make that measurable, with a 0–10 score backed by real observed
citations, not a self-reported checklist. This client returns the same numbers
[airanks.net](https://airanks.net) and the [browser toolbar](https://airanks.net/toolbar) show —
available from Python.

---

## 📄 License

**MIT** — see [LICENSE](LICENSE).

<div align="center">

Made with 🐍 + ☕ for anyone who wants their **AI Rank** without leaving Python.

</div>
