Architecture¶
How Mapilli is designed and implemented.
Overview¶
Mapilli follows the architecture patterns established in Nauyaca, a modern Gemini protocol implementation. It uses asyncio's Protocol/Transport pattern for efficient networking.
Package Structure¶
mapilli/
├── __init__.py # Public API exports
├── __main__.py # CLI entry point
├── protocol/ # Protocol types
│ ├── constants.py # Protocol constants
│ ├── request.py # FingerRequest
│ └── response.py # FingerResponse
├── client/ # Client implementation
│ ├── protocol.py # FingerClientProtocol
│ └── session.py # FingerClient
└── utils/ # Utilities
└── logging.py # Structured logging
Protocol/Transport Pattern¶
Mapilli uses asyncio's low-level Protocol/Transport pattern instead of the higher-level StreamReader/StreamWriter:
flowchart LR
A[FingerClient] -->|creates| B[Future]
A -->|creates| C[FingerClientProtocol]
C -->|writes| D[Transport]
D -->|data_received| C
C -->|set_result| B
B -->|await| A
Why Protocol/Transport?¶
- Lower overhead: No async/await context switching for each read/write
- Better for simple protocols: Finger is request-response, no streaming needed
- Efficient buffering: We control exactly how data is accumulated
- Natural flow: Connection lifecycle maps cleanly to protocol methods
Key Components¶
FingerClientProtocol¶
The low-level protocol handler:
class FingerClientProtocol(asyncio.Protocol):
def connection_made(self, transport):
# Send query immediately
transport.write(f"{self.query}\r\n".encode("ascii"))
def data_received(self, data):
# Buffer incoming data
self.buffer += data
def connection_lost(self, exc):
# Create response and resolve Future
response = FingerResponse(body=self.buffer.decode(), ...)
self.response_future.set_result(response)
FingerClient¶
The high-level async API:
class FingerClient:
async def finger(self, host, query):
loop = asyncio.get_running_loop()
response_future = loop.create_future()
# Create connection with protocol
transport, protocol = await loop.create_connection(
lambda: FingerClientProtocol(query, host, port, response_future),
host=host,
port=port,
)
# Wait for response via Future
return await response_future
Bridging Callbacks to Async/Await¶
The key insight is using asyncio.Future to bridge:
- Create Future: Before connecting
- Pass to Protocol: Protocol stores reference to Future
- Resolve in callback:
connection_lost()sets the result - Await in caller: High-level code awaits the Future
sequenceDiagram
participant Client as FingerClient
participant Proto as Protocol
participant Future as Future
participant Server as Server
Client->>Future: create_future()
Client->>Proto: create with future
Proto->>Server: connect + send query
Server->>Proto: data_received()
Proto->>Proto: buffer data
Server->>Proto: connection_lost()
Proto->>Future: set_result(response)
Future->>Client: await returns
Query Parsing¶
The FingerRequest class handles RFC 1288 query parsing:
request = FingerRequest.parse("alice@example.com")
# request.username = "alice"
# request.hostname = "example.com"
# request.query_type = QueryType.USER_REMOTE
request = FingerRequest.parse("/W alice")
# request.username = "alice"
# request.verbose = True
Error Handling¶
Errors are propagated through the Future:
- Connection errors: Caught in
create_connection(), raised asConnectionError - Timeouts:
asyncio.wait_for()raisesTimeoutError - Protocol errors: Set as exception on Future via
set_exception()
Type Safety¶
Mapilli uses full type annotations:
- All public APIs have type hints
py.typedmarker for PEP 561- Verified with ty type checker
See Also¶
- Finger Protocol - Protocol specification
- API Reference - Complete API docs