Architecture¶
This document explains how Teyaotlani is structured internally, helping you understand the codebase for contributions or customization.
Module organization¶
Teyaotlani is organized into focused modules:
teyaotlani/
├── __init__.py # Public API exports
├── __main__.py # CLI entry point
├── client/ # Client implementation
│ ├── protocol.py # Client protocol handler
│ └── session.py # SpartanClient class
├── server/ # Server implementation
│ ├── config.py # ServerConfig class
│ ├── handler.py # Request handlers
│ ├── middleware.py # Rate limiting, access control
│ ├── protocol.py # Server protocol handler
│ └── server.py # Server runner
├── protocol/ # Shared protocol components
│ ├── constants.py # Protocol constants
│ ├── request.py # SpartanRequest class
│ ├── response.py # SpartanResponse class
│ └── status.py # StatusCode enum
├── content/ # Content handling
│ └── gemtext.py # Gemtext utilities
└── utils/ # Shared utilities
└── url.py # URL parsing
Design principles¶
Async-first¶
Teyaotlani is built entirely on Python's asyncio. This provides:
- High concurrency - Handle many connections efficiently
- Non-blocking I/O - No threads needed for I/O operations
- Native coroutines - Clean, readable async code
# All public APIs are async
async with SpartanClient() as client:
response = await client.get("spartan://example.com/")
Protocol-oriented¶
Both client and server use asyncio.Protocol for network communication:
class SpartanClientProtocol(asyncio.Protocol):
def connection_made(self, transport):
# Send request
...
def data_received(self, data):
# Parse response
...
Benefits:
- Direct control over the connection lifecycle
- Efficient buffer management
- Clean separation of concerns
Context managers¶
Resources that need cleanup use context managers:
async with SpartanClient() as client:
# Client is properly initialized
response = await client.get(url)
# Client is properly closed
This ensures connections are always cleaned up, even on errors.
Client architecture¶
SpartanClient¶
The main client class manages connections:
SpartanClient
├── get(url) -> SpartanResponse
├── upload(url, data) -> SpartanResponse
└── Internal:
├── _connect(host, port)
└── _send_request(request)
Request flow¶
- Parse URL to extract host, port, path
- Create
SpartanRequestobject - Open connection to server
- Send request bytes
- Receive and parse response
- Return
SpartanResponse
# Simplified flow
request = SpartanRequest(host, path, content_length=0)
transport, protocol = await loop.create_connection(...)
transport.write(request.to_bytes())
response = await protocol.response_future
Server architecture¶
Components¶
Server
├── ServerConfig # Configuration container
├── SpartanServer # Main server class
├── SpartanProtocol # Per-connection handler
├── RequestHandler # Static file serving
└── Middleware
├── RateLimiter
└── AccessControl
Request handling flow¶
- Accept connection
- Check access control (if enabled)
- Parse request
- Check rate limits (if enabled)
- Route to handler
- Send response
Handler chain¶
Handlers process requests in order:
handlers = [
StaticFileHandler(config),
UploadHandler(config),
]
for handler in handlers:
response = await handler.handle(request)
if response:
return response
Middleware¶
Middleware runs before handlers:
# Rate limiting
if rate_limiter.is_limited(client_ip):
return Response(5, "Rate limited")
# Access control
if not access_control.is_allowed(client_ip):
return Response(4, "Access denied")
Protocol layer¶
SpartanRequest¶
Represents an incoming request:
SpartanResponse¶
Represents a server response:
StatusCode¶
Enum for protocol status codes:
Configuration system¶
ServerConfig uses a dataclass with TOML loading:
@dataclass
class ServerConfig:
host: str = "localhost"
port: int = 300
document_root: Path = Path("./capsule")
# ... more fields
@classmethod
def from_toml(cls, path: str) -> "ServerConfig":
data = tomllib.load(open(path, "rb"))
return cls(**data)
Content handling¶
Gemtext utilities¶
The content.gemtext module provides Gemtext parsing:
Extension points¶
Custom handlers¶
Create handlers by implementing the handler interface:
class MyHandler:
async def handle(self, request: SpartanRequest) -> SpartanResponse | None:
if request.path.startswith("/api/"):
return SpartanResponse(2, "application/json", '{"ok": true}')
return None # Pass to next handler
Middleware¶
Add middleware for cross-cutting concerns:
class LoggingMiddleware:
async def process(self, request: SpartanRequest) -> SpartanRequest:
logger.info(f"Request: {request.path}")
return request
Testing¶
Tests are organized by module:
tests/
├── test_client/ # Client tests
├── test_server/ # Server tests
├── test_protocol/ # Protocol tests
├── test_integration/ # End-to-end tests
└── conftest.py # Shared fixtures
Run tests with:
Contributing¶
When contributing:
- Follow existing patterns
- Add tests for new functionality
- Update documentation
- Run
uv run ruff checkanduv run ruff format