FastTcpAPI
FastAPI-style TCP command routing with user-defined request/response frames.
Start a server
python examples/server.py
The built-in JsonLengthPrefixFrame uses a four-byte unsigned big-endian JSON
payload length, followed by the UTF-8 JSON payload. Requests look like:
{"command":"echo","args":["FastTcpAPI"],"kwargs":{"prefix":"Hello, "}}
Default responses are also command frames. A normal return becomes:
{"command":"response.ok","args":[{"message":"Hello, FastTcpAPI"}],"kwargs":{}}
An exception becomes response.error; its kwargs contain code, message,
and solution.
The default protocol reserves __fasttcpapi__.schema for the dynamic client.
There is no extra end frame: each command declares its exact response frame
count, and the client receives exactly that many frames.
Define a command
from fasttcpapi import Server
server = Server()
@server.command("user.get")
async def get_user(user_id: int, verbose: bool = False):
return {"id": user_id, "verbose": verbose}
@server.command("clock.watch", response_frames=2)
async def watch_clock():
for tick in range(2):
yield {"tick": tick}
return sends one response. A yielding command must declare response_frames
equal to its exact number of yielded responses; each yield sends one frame.
Any Exception
is passed to frame.decode_from_exception, allowing the frame to choose its
error response. CommandError optionally provides code and solution
attributes.
@server.command("sensor.read", response_frames=2)
def read_sensor():
yield {"channel": 1, "value": 12.5}
yield {"channel": 2, "value": 13.0}
The server sends exactly two frames for this command, with no completion frame. If a handler produces fewer results, the server sends one error frame. Results after the declared count are discarded to preserve the request/response frame boundary.
Active pushes
Use push for a handler that starts once per TCP connection. It may return one
value or continuously yield values; every value is sent immediately as an
unsolicited frame:
@server.push("clock.push")
async def push_clock():
while True:
yield {"unix_time": time.time()}
await asyncio.sleep(1)
Push tasks run concurrently with normal command handling and are cancelled when
the connection closes. A client for a custom protocol must distinguish push
frames from command responses using the command field or its equivalent. The
complete default-frame example is python examples/push_server.py.
Custom frames
Implement Frame, then pass the class to Server. A frame instance is
created for each incoming request. Its lifecycle is:
decode_from_reader(reader) -> select @app.command(frame.command) -> parse_args(params)
-> handler -> decode_from_result(value) or decode_from_exception(error) -> encode()
decode_from_reader must assign self.command to the value used by
@app.command(...); it can also retain session IDs, raw parameters, or any
other request metadata.
parse_args receives a list of Param(name, type) created from the selected
function signature, and must assign self.args and/or self.kwargs. encode
encodes the current self.command, self.args, and self.kwargs as bytes.
decode_from_result and decode_from_exception convert application outcomes
into those same three fields for the response frame. result() is used by a
client after decoding a response frame: it returns the value for a successful
response or raises the represented exception for an error response.
class MyFrame(Frame):
async def decode_from_reader(self, reader):
self.command = await reader.readexactly(1)
self.payload = await reader.readexactly(4)
def parse_args(self, param_list):
self.args = (int.from_bytes(self.payload, "little"),)
self.kwargs = {}
def decode_from_result(self, result):
self.command = b"\x81"
self.args = (result,)
self.kwargs = {}
def decode_from_exception(self, exception):
self.command = b"\x82"
self.args = (str(exception),)
self.kwargs = {}
def result(self):
if self.command == b"\x81":
return self.args[0]
raise RuntimeError(self.args[0])
def encode(self):
if self.command == b"\x82":
return self.command + str(self.args[0]).encode("utf-8")
return self.command + int(self.args[0]).to_bytes(4, "little")
app = Server(MyFrame)
@app.command(b"\x01")
def command(value: int):
return value * 2
For untagged binary parameter data, decode_typed_arguments(payload, param_list,
byteorder="little") handles int as signed int32, float as IEEE-754
float32, bool as 0/1 byte, str as NUL-terminated text, and ctypes types as
ctypes.sizeof(type) bytes.
Runnable custom examples:
python examples/custom_default_json_frame.py
python examples/custom_device_binary_codec.py
The second example implements:
55 AA | length:uint32-little | session:uint8 | device:uint8 |
function:uint8 | parameters
Its length counts all bytes after the length field. It reuses the decoded
session/device IDs in ACK responses, and selects ACK function codes 0x81 and
0x82 in encode. Change those choices in the frame class to match another
protocol.
Client
Client supports the built-in JsonLengthPrefixFrame protocol. Start the
server, then run the included client example in another terminal:
python examples/server.py
python examples/client.py
from fasttcpapi import Client, RemoteError
client = Client("127.0.0.1", 9000, sync=False)
message = await client.echo("FastTcpAPI", prefix="Hello, ")
ticks = await client.call("clock.watch") # response_frames=2: returns a two-item list.
Before sending a command, the client validates positional argument count,
keyword names, required arguments, and built-in bool, bytes, float,
int, and str annotations from the fetched definition. A mismatch raises
TypeError locally; it does not open a command request to the server.
The client keeps one TCP connection open and routes response frames by the
session_id supplied by each request Frame. Responses with an unknown
session ID are discarded. Set timeout on @server.command to a positive number, or provide a
list with one value per response frame. A missing matching response raises
TimeoutError in the client.
The service definition marks push commands with push: true. The client uses
that command list to route matching frames to its push queue; all other frames
are routed by session_id to the waiting call. Custom frame protocols only
need to preserve the command and session fields they define; no extra frame
type field is required. Push frames are available through
await client.next_push().
Client(..., push_queue_size=100) bounds the local push queue. When full, the
oldest unread push is discarded so a fast producer cannot grow client memory
without limit.
For diagnostics, both Client and Server provide
add_on_request_callback, add_on_response_callback, and
add_on_push_callback. Callbacks may be synchronous or asynchronous; callback
errors are ignored so logging cannot break request processing.
The first connection writes fasttcpapi/client.pyi alongside the client module.
It describes the command proxy's __call__(...) method.
Commands containing dots or
other non-identifier characters remain callable with:
result = await client.call("user.get", 42)
The default frame's result() raises RemoteError for response.error,
exposing code and solution. For built-in Python exceptions whose original
args are JSON-serializable, it instead reconstructs and raises the same
built-in exception type with the same arguments. Custom exceptions, including
CommandError, and built-in exceptions with non-serializable arguments fall
back to RemoteError. Custom frame protocols require their own client because
their wire format and response completion rules are application-specific.
Set client.sync = True for blocking calls, or use the explicit methods:
One Client instance owns a dedicated background event loop and TCP connection.
It can therefore be used from multiple caller event loops and threads;
async_call() bridges the result back to the caller's loop, while submit()
returns a standard concurrent.futures.Future.
from fasttcpapi import Client
client = Client("127.0.0.1", 9000, sync=True)
value = client.echo("FastTCP")
future = client.submit("clock.watch")
ticks = future.result()
Run the complete blocking example with python examples/sync_client.py.
FastTcpAPI remains an alias for Server for backwards compatibility.
Release files for fasttcpapi 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fasttcpapi-0.1.0.tar.gz | 23.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fasttcpapi-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.8 kB
Release files / fasttcpapi-0.1.0.tar.gz
| Download URL | fasttcpapi-0.1.0.tar.gz |
|---|---|
| Size | 23.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e95c31f6e549d357c77ba9798b3cb14f2e378a9c014f5ff558952cbd06d0fd93
|
|
BLAKE2b-256 checksum How to use checksums |
52c798aa9c0723ad9f713e911d3d05448c29915f6def6a8b4086fd8679444a2d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / fasttcpapi-0.1.0-py3-none-any.whl
| Download URL | fasttcpapi-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d211bb3b84cc13e105ddd276f71c010abc450e300a56246c5a4a5315a7595815
|
|
BLAKE2b-256 checksum How to use checksums |
4bb60f24e1e7a06408b475690d3f205d14d90af4d9c50f266a266fc6b765a58e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|