Mirrors and a custom HTTP client¶
By default the SDK talks to https://aniliberty.top/api/v1 through an httpx.AsyncClient it
creates and closes itself. Both can be changed: base_url picks the host, http_client hands
the SDK your own httpx.AsyncClient.
examples/15_custom_http_client_and_mirrors.py
is a runnable version of this page.
Mirrors¶
The API is served on two hosts, with the same data and the same responses:
| Host | base_url |
|---|---|
| main server (the default) | https://aniliberty.top/api/v1 |
| mirror | https://anilibria.top/api/v1 |
async with AniLiberty(base_url="https://anilibria.top/api/v1") as client:
release = await client.releases.get("sousou-no-frieren")
release.poster.preview_url
# 'https://anilibria.top/storage/releases/posters/9542/....jpg'
base_url includes the /api/v1 prefix. Image paths in responses are relative, and the SDK
resolves them against the host of base_url, so image URLs point at the host you use.
Failing over¶
Try hosts in order and keep the first one that answers. Catch the errors another host can fix
— no response, a block page, server errors — and keep the health check's timeout short: a dead
host costs the SDK's retries first, about 4 × timeout + 7 s.
from aniliberty import AccessBlockedError, AniLiberty, ServerError, TransportError
HOSTS = ["https://aniliberty.top/api/v1", "https://anilibria.top/api/v1"]
async def first_working(base_urls: list[str], *, timeout: float = 2.0) -> str:
for base_url in base_urls:
try:
async with AniLiberty(base_url=base_url, timeout=timeout) as client:
if (await client.app.status()).is_alive:
return base_url
except (TransportError, AccessBlockedError, ServerError):
continue
raise RuntimeError("No API host is reachable")
async with AniLiberty(base_url=await first_working(HOSTS)) as client:
...
With one unreachable host ahead of the main one, this took 14 s in the example run.
Your own httpx.AsyncClient¶
Pass http_client= for anything httpx can do — a proxy, detailed timeouts, connection
limits, event hooks, a custom transport:
import httpx
http_client = httpx.AsyncClient(
proxy="http://user:pass@proxy.example:8080",
timeout=httpx.Timeout(10.0, connect=5.0),
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
)
async with http_client:
async with AniLiberty(http_client=http_client) as client:
...
What comes from where:
With http_client= |
|
|---|---|
| Timeout | your client's; AniLiberty(timeout=...) is ignored |
| Proxy, TLS, connection pool, transport | your client's |
| Concurrency limit, pacing, retries | the SDK's, on top of your client |
| Headers | your client's, plus the SDK's Accept, User-Agent and headers= on every request |
| Error mapping | the SDK's |
base_url set on the httpx client |
ignored — the SDK sends absolute URLs built from its own base_url |
| Closing | yours — AniLiberty doesn't close a client it didn't create |
One httpx.AsyncClient can serve several AniLiberty instances, e.g. the main host and the
mirror sharing one connection pool:
async with httpx.AsyncClient() as http_client:
main = AniLiberty(http_client=http_client)
mirror = AniLiberty(http_client=http_client, base_url="https://anilibria.top/api/v1")
async with main, mirror:
...
Each AniLiberty still has its own concurrency limit and pacing.
Event hooks¶
httpx event hooks see every attempt the SDK makes, retries included — handy for metrics or
debugging:
async def log_request(request: httpx.Request) -> None:
print("->", request.method, request.url)
async def log_response(response: httpx.Response) -> None:
print("<-", response.status_code, response.request.url)
http_client = httpx.AsyncClient(event_hooks={"request": [log_request], "response": [log_response]})
A response hook runs as soon as the headers arrive, before the SDK reads the body and maps
the status to an exception. Don't call response.raise_for_status() in it: let the SDK raise
its own exceptions.
Transport-level retries¶
httpx.AsyncHTTPTransport(retries=...) retries failed connection attempts inside httpx,
before the SDK sees an error. That adds to the SDK's own retries instead of replacing them, so
it's rarely needed.
Headers¶
headers= adds headers to every request over the SDK's defaults; a key you pass replaces the
default one, matched case-insensitively. The usual reason is to identify your application:
AniLiberty(headers={"User-Agent": "my-app/1.0 (+https://example.com)"})
The defaults are Accept: application/json and
User-Agent: py-aniliberty/<version> (+https://github.com/LLIJIYAPNIK/py-aniliberty).
Keep Accept: application/json
Without it the API renders errors as HTML pages, which the SDK reports as
AccessBlockedError instead of NotFoundError and the
rest. Override Accept only if you know why.