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
396 changes: 112 additions & 284 deletions README.md

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions examples/http2_prior_knowledge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Run SmallServer's cleartext prior-knowledge HTTP/2 demo."""

from smallserver import HTTP2Config, Response, SmallServer


app = SmallServer()


@app.get("/health")
async def health(request):
return Response.json({"status": "ok", "protocol": request.version})


@app.post("/echo")
async def echo(request):
return Response(body=request.body, headers={"Content-Type": "application/octet-stream"})


if __name__ == "__main__":
print("HTTP/2 prior-knowledge server: http://127.0.0.1:8000")
print("Try: curl --http2-prior-knowledge http://127.0.0.1:8000/health")
app.listen(
host="127.0.0.1",
port=8000,
protocol="http2",
http2_config=HTTP2Config(max_concurrent_streams=32),
)
49 changes: 49 additions & 0 deletions guide/adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Third-party adapters

SmallServer handlers run on the SmallOS scheduler and must not call blocking
functions or drive a second event loop directly. SmallOS supplies explicit
escape hatches:

- `ThreadAdapter` for blocking or thread-affine callables;
- `AsyncioAdapter` for coroutine-based libraries on a persistent asyncio loop.

The application creates, bounds, and shuts down these adapters. SmallServer
does not create adapter workers as a side effect of `listen()`.

```python
from SmallPackage.adapters.errors import AdapterError
from SmallPackage.adapters.threads import ThreadAdapter
from smallserver import AdapterRegistry, Response, http_error_from_adapter

services = AdapterRegistry(blocking=ThreadAdapter(max_workers=2, max_pending=8))


async def handler(request):
try:
result = await services.call("blocking", str.upper, "smallserver")
except AdapterError as exc:
raise http_error_from_adapter(exc)
return Response.text(result)


services.shutdown()
```

In a real application, keep the registry alive around the complete runtime
lifecycle; do not shut it down immediately after defining a handler. The
context manager calls `shutdown()` automatically and cancels pending adapter
work when its body exits with an exception.

`AdapterRegistry` accepts named, user-created adapters that provide `call()`
and `shutdown()`. It rejects duplicate names and duplicate adapter objects,
delegates calls, exposes stable registration order through `names()` and
`items()`, and shuts adapters down in reverse registration order.

`http_error_from_adapter()` intentionally sanitizes adapter failures:
capacity, unavailable, closed, and cancelled conditions become generic 503
responses; other adapter failures become a generic 500. Log internal causes in
application-controlled telemetry if needed, but do not expose them to clients.

See [`examples/adapters_demo.py`](../examples/adapters_demo.py) for a runnable
SQLite and asyncio example and [`examples/manual_runtime.py`](../examples/manual_runtime.py)
for the surrounding runtime lifecycle.
111 changes: 111 additions & 0 deletions guide/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# API reference

This page summarizes the public names exported by `smallserver`. Signatures
omit overload detail where prose is clearer.

## Application

### `SmallServer(regex_config=None, *, route_error_observer=None, websocket_config=None)`

- `get(path)`, `post(path)`, `put(path)`, `patch(path)`, `delete(path)` — route
decorators for one supported method.
- `route(path, methods)` — atomic multi-method route decorator.
- `get_regex`, `post_regex`, `put_regex`, `patch_regex`, `delete_regex`, and
`route_regex(pattern, methods)` — optional timeout-bounded regex decorators.
- `websocket(path, *, origins=None, subprotocols=())` — static HTTP/1.1
WebSocket Upgrade decorator.
- `async dispatch(request)` — dispatch an existing `Request`.
- `listen(host="127.0.0.1", port=8000, config=None, *, protocol="http1",`
`http2_config=None, runtime=None, start=None)`
— managed blocking lifecycle or caller-owned scheduling/startup.
- `serve(runtime, host="127.0.0.1", port=8000, config=None, *,`
`protocol="http1", http2_config=None)` — schedule against a caller-owned
runtime and return immediately.

## HTTP values

### `Headers(values=None)`

Immutable, case-insensitive mapping with `items()` and `get()`.

### `Request(method, path, headers, body=b"", version="HTTP/1.1")`

Frozen request value with validated method, path, headers, and byte body, plus
`raw_target`, `query_string`, immutable `path_params`, and `route_pattern`.

### `Response(status=200, body=b"", headers=Headers())`

Frozen response value. `Response.text()`, `Response.json()`, and `to_http1()`
provide common construction and serialization paths.

## Server lifecycle

### `ServerConfig(...)`

Frozen finite-limit configuration. See [Configuration](configuration.md).

### `ManagedRuntimeConfig(...)`

Frozen SmallOS settings used only when `listen()` creates the runtime. A
caller-supplied runtime retains its own configuration.

### `HTTP2Config(...)`

Optional cleartext HTTP/2 stream, buffer, frame-batch, and timeout limits. See
[Cleartext HTTP/2](http2.md). Constructing a listener with `protocol="http2"`
requires the `smallserver[http2]` extra.

### `ServerHandle`

Read-only properties: `address`, `port`, `closed`, `failure`, `finished`,
`cleanup_errors`, `owned_connection_count`, `dropped_route_error_events`, and
`route_observer_failures`.

Operations: `close()`, `async close_from_task(task)`, and `finalize()`.

### `RegexRouteConfig(...)`

Finite optional-regex limits. `RouteErrorEvent`, `RouteMatchTimeout`,
`RoutePathTooLarge`, and `RegexRoutesUnavailable` describe its bounded error
surface. Runtime regex matching requires `smallserver[regex-routes]`.

## WebSockets

- `WebSocketConfig` — finite connection, frame, message, mailbox, and deadline
limits.
- `WebSocket` — `accept`, `reject`, receive/send methods, `ping`, `close`,
iteration, `request`, and negotiated `subprotocol`.
- `WebSocketMessage` — complete typed text or binary message.
- `WebSocketDisconnect`, `WebSocketStateError`, and `WebSocketCapacityError` —
application-visible lifecycle and capacity outcomes.
- `WebSocketUnavailable` — the optional `wsproto` engine is missing.

See [WebSockets](websockets.md) for handshake and completion semantics.

## Adapters

### `AdapterRegistry(**adapters)`

Methods: `register`, `get`, `call`, `names`, `items`, and `shutdown`. It also
implements a context manager and exposes `closed`.

### `http_error_from_adapter(exc)`

Convert a SmallOS `AdapterError` to a sanitized `HTTPError`.

### `AdapterShutdownError`

Raised after registry shutdown attempts every adapter but one or more fail.
Its `failures` tuple contains `(name, exception)` entries.

## Errors

- `HTTPError(status, detail="")`
- `ServerConfigurationError`
- `ServerStartupError`
- `ServerFinalizationError`

Cleanup errors expose `cleanup_errors`, `cleanup_complete`, `retry_cleanup()`,
and `finalize()`. `ServerStartupError` additionally exposes `primary_error`.

Public typing information is shipped through `smallserver/py.typed`.
85 changes: 85 additions & 0 deletions guide/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Configuration

Pass a `ServerConfig` to `listen()` or `serve()` to tune finite listener,
parser, and scheduling limits.

```python
from smallserver import ManagedRuntimeConfig, ServerConfig, SmallServer

app = SmallServer()
config = ServerConfig(
max_connections=50,
max_header_bytes=16 * 1024,
max_header_count=64,
max_body_bytes=512 * 1024,
receive_chunk_bytes=8 * 1024,
listener_priority=1,
connection_priority=2,
accept_batch_size=16,
max_request_target_bytes=8 * 1024,
max_route_error_events=16,
managed_runtime=ManagedRuntimeConfig(task_capacity=256),
)
```

| Setting | Default | Purpose |
| --- | ---: | --- |
| `max_connections` | 100 | Maximum connection streams still owned by the server. |
| `max_header_bytes` | 16 KiB | Maximum HTTP/1.1 request-head bytes. |
| `max_header_count` | 100 | Maximum number of request header fields. |
| `max_body_bytes` | 1 MiB | Maximum `Content-Length` and buffered request body. |
| `receive_chunk_bytes` | 8 KiB | Bytes requested from the transport per read. |
| `listener_priority` | 1 | SmallOS listener and close-watcher task priority. |
| `connection_priority` | 2 | SmallOS connection-task priority. |
| `accept_batch_size` | 16 | Accepts before the listener explicitly yields. |
| `max_request_target_bytes` | 8 KiB | Maximum HTTP/1.1 origin-form request target. |
| `max_route_error_events` | 16 | Bounded sanitized regex-timeout observer queue. |
| `managed_runtime` | `None` | Optional SmallOS settings used only when `listen()` creates the runtime. |

Every numeric `ServerConfig` field must be a positive integer; booleans are
rejected. `managed_runtime` must be `None` or a `ManagedRuntimeConfig`. The
public port must be an integer from 0 through 65535. `port=0` delegates port
selection to the kernel.

At connection capacity, the listener waits on a scheduler signal instead of
accepting and discarding more streams. Connections whose close failed still
count against the limit because the server continues to own them. A close
failure is fatal to further acceptance and remains visible for cleanup retry.

Limits are per `ServerHandle`. They bound HTTP input and framework-owned
connections, but they do not limit memory allocated by your handlers, response
bodies, adapter queues, or downstream libraries; configure those separately.

## Managed runtime configuration

`ManagedRuntimeConfig` controls the SmallOS instance created by blocking
`app.listen()` when no runtime is supplied. It exposes `task_capacity`,
`priority_levels`, `io_buffer_length`, `eternal_watchers`, and immutable
per-client `client_defaults`. Caller-owned runtimes must be configured directly;
SmallServer rejects `ServerConfig(managed_runtime=...)` when `runtime=` is
provided.

The managed task capacity must cover `max_connections + 2` for HTTP/1.1's
listener and shutdown-control tasks. Configuring `route_error_observer` adds
one dedicated task, raising that floor to `max_connections + 3`. HTTP/2 also
creates bounded connection-control and stream-handler tasks, so configure
additional capacity from the selected `HTTP2Config` concurrency limits.

## WebSocket configuration

Pass `websocket_config=WebSocketConfig(...)` to `SmallServer`. Its finite
limits cover accepted WebSocket connections, frames, messages, inbound and
outbound queues, write chunks, handshake/write/close/idle deadlines, and
ping/pong liveness. See [WebSockets](websockets.md) for the full boundary.

## HTTP/2 configuration

Pass `protocol="http2"` and an optional `HTTP2Config` for cleartext
prior-knowledge HTTP/2. Its finite limits cover streams, decoded and compressed
headers, request and response buffers, generated control output, frame size,
reader frame batches, handshake time, and idle time. See
[Cleartext HTTP/2](http2.md) for the complete protocol boundary.

`RegexRouteConfig` separately bounds optional regex pattern length, route and
capture counts, path bytes, per-match time, and total matching time. All time
limits must be finite positive numbers.
68 changes: 68 additions & 0 deletions guide/development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Development

## Set up

Use Python 3.10 or newer and install the canonical SmallOS master checkout plus
SmallServer in editable mode:

```console
python3 -m pip install -r requirements.txt
python3 -m pip install -e '.[test]'
```

For reproducible validation, put the canonical SmallOS checkout at the front of
`PYTHONPATH` rather than relying on an unrelated installed package named
`SmallPackage`.

## Validate

```console
python3 -m unittest discover -s tests -v
python3 -m compileall -q smallserver demo.py examples tests
git diff --check
```

The suite covers routing, HTTP values and parsing, adapters, lifecycle failure
ownership, kernel transport behavior, and real loopback serving when the local
environment permits binds. Documentation tests verify the tracked guide set,
relative Markdown links, and Python code-block syntax.

In a separate clean environment, verify the lazy optional-dependency boundary
without installing the test or HTTP/2 extras:

```console
python3 -m pip install -r requirements.txt
python3 -m pip install -e .
python3 -m unittest tests.test_http2 -v
python3 -m unittest tests.test_regex_routing -v
python3 -m unittest tests.test_websocket -v
```

The dependency-contract tests run and optional interoperability cases skip
cleanly; importing and testing ordinary HTTP/1.1 must require neither
hyper-h2, regex, nor wsproto.

Run the examples when their platform requirements are available:

```console
python3 demo.py
python3 examples/adapters_demo.py
python3 examples/manual_runtime.py
python3 examples/websocket_echo.py
```

The two network examples block until shutdown. `adapters_demo.py` completes on
its own and demonstrates SQLite thread affinity and a persistent asyncio loop.

## Contribution boundaries

- Keep framework networking behind SmallOS kernel abstractions.
- Preserve finite parsing, connection, and adapter limits.
- Keep the HTTP core independent of `asyncio`.
- Add lifecycle tests for partial acquisition and cleanup failure paths.
- Update the README and focused guide page when a public API changes.
- Extend [Protocol roadmap](protocol-roadmap.md) docs on the feature branch that
implements a protocol; do not describe planned APIs as present.

The ignored `docs/` and `skills/` trees support local agent workflows. Public,
versioned user documentation belongs in `README.md` and `guide/`.
Loading
Loading