Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ Built for use with [Home Assistant](https://www.home-assistant.io/) but can be u

- **Fully async** — built on [aiohttp](https://docs.aiohttp.org/)
- **Typed models** — all API responses are frozen dataclasses with full type annotations
- **15 endpoints** — public (server, character, corporation, universe) and authenticated (wallet, skills, location, industry, market, mail, fatigue)
- **23 endpoints** — public (server, character, corporation, universe) and authenticated (wallet, skills, location, industry, market, mail, notifications, clones, fatigue, contacts, calendar, loyalty, killmails)
- **Abstract auth** — implement `AbstractAuth` to plug in any OAuth2 token source
- **Type-safe** — PEP 561 compatible (`py.typed`), strict mypy configuration
- **Tested** — 100% test coverage
- **Tested** — ≥98% test coverage

## Installation

Expand Down Expand Up @@ -44,7 +44,7 @@ asyncio.run(main())

- [**Quickstart**](docs/quickstart.md) — public and authenticated endpoint examples
- [**Authentication**](docs/authentication.md) — implementing `AbstractAuth`, required OAuth scopes
- [**Endpoints**](docs/endpoints.md) — full reference with field tables for all 15 methods
- [**Endpoints**](docs/endpoints.md) — full reference with field tables for all 23 methods
- [**Error Handling**](docs/error-handling.md) — exception hierarchy, rate limiting, ESI cache times

## License
Expand Down
13 changes: 10 additions & 3 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,22 @@ Each endpoint requires specific Eve SSO scopes. The full list of scopes used by
| `esi-location.read_online.v1` | `async_get_character_online()` |
| `esi-location.read_location.v1` | `async_get_character_location()` |
| `esi-location.read_ship_type.v1` | `async_get_character_ship()` |
| `esi-wallet.read_character_wallet.v1` | `async_get_wallet_balance()` |
| `esi-wallet.read_character_wallet.v1` | `async_get_wallet_balance()`, `async_get_wallet_journal()` |
| `esi-skills.read_skills.v1` | `async_get_skills()` |
| `esi-skills.read_skillqueue.v1` | `async_get_skill_queue()` |
| `esi-mail.read_mail.v1` | `async_get_mail_labels()` |
| `esi-industry.read_character_jobs.v1` | `async_get_industry_jobs()` |
| `esi-markets.read_character_orders.v1` | `async_get_market_orders()` |
| `esi-characters.read_fatigue.v1` | `async_get_jump_fatigue()` |

The `DEFAULT_SCOPES` constant exports the full set as a tuple:
| `esi-characters.read_notifications.v1` | `async_get_notifications()` |
| `esi-clones.read_clones.v1` | `async_get_clones()` |
| `esi-clones.read_implants.v1` | `async_get_implants()` |
| `esi-characters.read_contacts.v1` | `async_get_contacts()` |
| `esi-calendar.read_calendar_events.v1` | `async_get_calendar()` |
| `esi-characters.read_loyalty.v1` | `async_get_loyalty_points()` |
| `esi-killmails.read_killmails.v1` | `async_get_killmails()` |

The `DEFAULT_SCOPES` constant exports a recommended baseline set of scopes as a tuple:

```python
from eveonline.const import DEFAULT_SCOPES
Expand Down
68 changes: 66 additions & 2 deletions docs/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,23 @@ All authenticated endpoints require `EveOnlineClient(auth=...)`. See [Authentica

### Pagination

Some ESI endpoints return data across multiple pages. The client handles this automatically using the `X-Pages` response header: it fetches page 1, reads the total page count, then sequentially fetches any remaining pages and returns a single combined list. No extra code is needed — just call the method as usual.
Some ESI endpoints return data across multiple pages. The client handles this automatically:

Endpoints that use automatic pagination are marked with *all pages fetched automatically* in their return type description.
1. It fetches page 1 and reads the `X-Pages` response header to determine the total page count.
2. It sequentially fetches any remaining pages.
3. All pages are merged into a single list before returning.

No extra code is needed — just call the method as usual.

Endpoints with automatic pagination:

| Method | Max items per page |
|---|---|
| `async_get_wallet_journal()` | 50 |
| `async_get_contacts()` | 500 |
| `async_get_killmails()` | 50 |

Endpoints that use automatic pagination are also marked with *all pages fetched automatically* in their return type description below.

---

Expand Down Expand Up @@ -514,3 +528,53 @@ for km in killmails:
|---|---|---|
| `killmail_id` | `int` | Unique killmail identifier |
| `killmail_hash` | `str` | Hash required to fetch the full killmail detail |

---

## Client utilities

### `clear_etag_cache()`

Discards all cached responses (both TTL and ETag layers) so the next request to each endpoint fetches fresh data from ESI.

```python
client.clear_etag_cache()
```

Useful when you need up-to-date data immediately, regardless of the remaining cache lifetime.

---

## Request caching

The client uses two automatic caching layers to reduce ESI traffic. Caching is active when a GET endpoint returns an `ETag` header — which is true for all endpoints documented here.

### Layer 1 — TTL (`Expires` header)

When ESI returns an `Expires` header, the client stores the expiry timestamp alongside the cached data. A repeat call before that time returns the cached result immediately **without making any HTTP request**.

```python
# First call — real HTTP request, Expires stored
status = await client.async_get_server_status()

# Second call within TTL window — no HTTP request, cache returned instantly
status = await client.async_get_server_status()
```

ESI cache durations vary by endpoint. Common values:

| Endpoint | Cache duration |
|---|---|
| `/status/` | 30 seconds |
| `/characters/{id}/online/` | 60 seconds |
| `/characters/{id}/wallet/` | 120 seconds |
| `/characters/{id}/skills/` | 120 seconds |
| `/characters/{id}/industry/jobs/` | 300 seconds |
| `/characters/{id}/orders/` | 1200 seconds |
| `/universe/names/` | 3600 seconds |

### Layer 2 — ETag (`If-None-Match` / 304)

Once the TTL has expired (or if no `Expires` header was present), the client sends the stored `ETag` value in an `If-None-Match` request header. If the data has not changed since it was last fetched, ESI responds with `304 Not Modified` and the client returns the previously cached data **without downloading a response body**.

Both layers are fully transparent — no configuration is needed. Use `clear_etag_cache()` to bypass them when fresh data is required.
21 changes: 1 addition & 20 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,4 @@ except EveOnlineRateLimitError as err:

## ESI caching headers

ESI endpoints each have a server-side cache duration. Fetching within the cache window returns the same data. Common cache times:

| Endpoint | Cache duration |
|---|---|
| `/status/` | 30 seconds |
| `/characters/{id}/online/` | 60 seconds |
| `/characters/{id}/wallet/` | 120 seconds |
| `/characters/{id}/skills/` | 120 seconds |
| `/characters/{id}/skillqueue/` | 120 seconds |
| `/characters/{id}/industry/jobs/` | 300 seconds |
| `/characters/{id}/orders/` | 1200 seconds |
| `/universe/names/` | 3600 seconds |

The client can use two layers of caching to minimise ESI traffic, but only when a cacheable response has been received (a GET response that includes an `ETag` header):

1. **TTL caching (`Expires` header)** — When a response is cached, the client stores its `Expires` value alongside the cached data if the header is present. If you call the same endpoint again before that time is reached, the client returns the cached result immediately without making any HTTP request. If no `Expires` value was stored for a cached entry, this layer is skipped and the request falls through to the ETag layer.

2. **ETag caching (`If-None-Match` / 304)** — Once the stored `Expires` time has passed, or when no `Expires` value was stored, the client sends the cached `ETag` in an `If-None-Match` header. If the data has not changed, ESI returns `304 Not Modified` and the client returns the previously cached data without downloading a response body.

Use `client.clear_etag_cache()` to discard both layers and force fresh responses on the next requests.
The client automatically caches ESI responses to reduce traffic and ESI error-budget consumption. See [Endpoints — Request caching](endpoints.md#request-caching) for the full reference including cache durations, TTL behaviour, ETag/304 handling, and `clear_etag_cache()`.
28 changes: 28 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,31 @@ location = await client.async_get_character_location(character_id)
print(location.solar_system_id) # always present
print(location.station_id) # int | None — None when in space
```

## Automatic pagination

Some endpoints return large datasets across multiple pages. The client fetches all pages automatically and returns a single combined list:

```python
# Fetches all pages automatically — no manual paging needed
journal = await client.async_get_wallet_journal(character_id)
print(f"{len(journal)} journal entries total")
```

Paginated endpoints: `async_get_wallet_journal()`, `async_get_contacts()`, `async_get_killmails()`.

## Request caching

The client caches ESI responses automatically using two layers:

1. **TTL** — If ESI returns an `Expires` header, repeated calls before that time skip the HTTP request entirely.
2. **ETag / 304** — Once the TTL expires, the client sends `If-None-Match`. If the data is unchanged, ESI returns `304 Not Modified` and no response body is downloaded.

Both layers are transparent and require no configuration. To force fresh data, call `clear_etag_cache()`:

```python
client.clear_etag_cache()
status = await client.async_get_server_status() # fresh request
```

See [Endpoints](endpoints.md#request-caching) for the full caching reference.
Loading