App¶
App
¶
App(
title: str = "Aether",
version: str = "0.1.0",
description: str = "",
openapi_url: str | None = "/openapi.json",
docs_url: str | None = "/docs",
mcp_url: str | None = "/mcp",
debug: bool = False,
access_log: bool = False,
redis_url: str | None = None,
)
openapi_url and docs_url can each be set to None to disable them.
debug returns handler exception text in the 500 response. Leave it off
outside development: exception messages routinely carry connection
strings, file paths and user data.
access_log adds one log line per request. Opt-in: it costs something
per request, and many deployments already log at the proxy.
redis_url enables durable topics. Nothing connects until the first
durable topic is used.
mcp_url is where agents reach the service over the Model Context
Protocol. It exposes only routes marked tool=True, plus topics as
readable resources. Set it to None to turn the endpoint off entirely.
Source code in python/aether/_app.py
route
¶
Register a route.
tool=True also exposes it to agents over MCP. Opt-in on purpose:
every route being agent-callable by default would mean an
administrative delete endpoint is agent-callable by default.
Source code in python/aether/_app.py
get
¶
post
¶
put
¶
delete
¶
middleware
¶
Register middleware, which runs around every HTTP handler.
@app.middleware
async def require_key(request, call_next):
if request.header("x-api-key") != SECRET:
return Reply({"error": "unauthorized"}, status=401)
return await call_next(request)
Runs outermost-first in registration order. WebSocket routes are not wrapped: their handshake completes before the handler runs, so there is nothing useful to intercept yet.
Source code in python/aether/_app.py
websocket
¶
Register a WebSocket endpoint.
authorize runs before the handshake and can refuse the upgrade,
which the handler cannot: by the time it runs, the 101 has been sent
and the client believes it is connected. Return None or True to
accept, or a Response/Reply to refuse.
async def members_only(request):
if not valid(request.header("authorization")):
return Response(b"nope", status=401, content_type="text/plain")
@app.websocket("/ws", authorize=members_only)
async def feed(request, ws): ...
The handler takes the request and the socket. Aether performs the handshake, so the socket is already open when the handler runs, and the connection closes when it returns.
@app.websocket("/ws")
async def echo(request, ws):
async for message in ws:
await ws.send(message)
Source code in python/aether/_app.py
backend
¶
The shared Redis backend, connected lazily on first use.
Source code in python/aether/_app.py
topic
¶
topic(
name: str,
maxsize: int | None = None,
policy: str | None = None,
durable: bool = False,
) -> Topic
Get or create a named topic.
Shared across every worker loop in the process, so a message emitted by
one handler reaches subscribers running on all of them. durable=True
additionally shares it across processes and records it in Redis, which
needs App(redis_url=...).
maxsize, policy and durable apply only when the topic is first
created.
Source code in python/aether/_app.py
capabilities
¶
The capabilities this service exposes to agents.
Built from the routes marked tool=True, without starting a server, so
it can be inspected or checked into a test.
Source code in python/aether/_app.py
openapi
¶
The OpenAPI 3.1 document for the routes registered so far.
Built from the same metadata the router uses, so it cannot describe an endpoint the server would not accept. Callable without running the server, which makes it usable for client generation in CI.
Source code in python/aether/_app.py
run
¶
run(
host: str = "127.0.0.1",
port: int = 8000,
workers: int | None = None,
max_concurrency: int = DEFAULT_MAX_CONCURRENCY,
max_body: int = DEFAULT_MAX_BODY,
request_timeout: float = DEFAULT_REQUEST_TIMEOUT,
shutdown_grace: float = DEFAULT_SHUTDOWN_GRACE,
max_connections: int = DEFAULT_MAX_CONNECTIONS,
) -> None
Serve until interrupted.
max_concurrency bounds the requests a single worker loop will accept
at once, counting both those queued and those already running. When
every worker is at its limit the server answers 503 rather than growing
without bound. Lower it for slow handlers, where a deep backlog only
adds latency before an inevitable client timeout; raise it to absorb
larger bursts of fast requests.
max_body caps a request body; anything larger is answered 413 without
being buffered.
request_timeout is how long to wait for a handler's first response
before answering 504. It does not cut short a stream that has already
started, so SSE and WebSocket are unaffected. Zero disables it.
shutdown_grace is how long Ctrl-C waits for in-flight requests to
finish before stopping anyway.
max_connections caps sockets held open. At the limit the server stops
accepting rather than refusing, so the wait lands in the OS backlog.
Source code in python/aether/_app.py
build_server
¶
build_server(
host: str = "127.0.0.1",
port: int = 8000,
workers: int | None = None,
max_concurrency: int = DEFAULT_MAX_CONCURRENCY,
max_body: int = DEFAULT_MAX_BODY,
request_timeout: float = DEFAULT_REQUEST_TIMEOUT,
shutdown_grace: float = DEFAULT_SHUTDOWN_GRACE,
max_connections: int = DEFAULT_MAX_CONNECTIONS,
announce: bool = False,
)
Prepare a server without starting it.
run uses this; so does the test client, which needs to start the
server on one thread and stop it from another.