req

This module is designed to handle common HTTP operations in a clean and reusable manner, with dynamic endpoint resolution and robust response management.

Let’s run a micro server:

# test.py
from usrv import route

# allow req.Endpoint.connect
@route.bind("/", methods=["HEAD"])
def base():
    return 200,

# public key endoint for encryption
@route.bind("/puk", methods=["GET"])
def puk():
    return 200, route.PUBLIC_KEY

@route.bind("/index")
def index(*args):
    return (200, ) + args

@route.bind("/api/endpoint", methods=["GET", "POST"])
def endpoit(a, b, **kwargs):
    method = kwargs["method"]
    if method == "POST":
        return 202, kwargs["data"]
    elif method == "GET":
        return 200, a, b
    else:
        return 404,

route.run(host='127.0.0.1', port=5000)
$ python path/to/test.py
INFO:usrv:listening on http://127.0.0.1:5000
CTRL+C to stop...

Connect to a peer

Remote path / allows HEAD request:

>>> from usrv import req
>>> req.Endpoint.connect("http://127.0.0.1:5000")
200

Else, maunally set peer to req.Endpoint or use _peer keyword on each request :

>>> from usrv import req
>>> req.ENDPOINT.peer = "http://127.0.0.1:5000"
>>> # or
>>> req.GET.api.endpoint(_peer="http://127.0.0.1:5000")
>>> req.POST.api.endpoint(_peer="http://127.0.0.1:5000")
>>> # ...

Endpoints

>>> # GET http://127.0.0.1:5000/puk
>>> req.GET.puk()
pP15aGDcFoqGTHTReiIfEvUcQ2c3AQjYcgCeLgKhpa38Rsub69i6RifuYPGtOOyld7j6y0LP6i0aqBuFYcSmTQ==
>>> # GET http://127.0.0.1:5000/api/endpoints?a=12&b=test
>>> req.GET.api.endpoint(a=12, b="test")
["12", "test"]
>>> # POST data to http://127.0.0.1:5000/api/endpoints
>>> req.POST.api.endpoint(value1=1, value2=2)
'{"value1": 1, "value2": 2}'

Encrypt HTTP body

>>> # encrypt only server body response
>>> req.POST.api.endpoint(
...   value1=1, value2=2, _headers={"Sender-Public-Key:req.PUBLIC_KEY}
... )
'{"value1": 1, "value2": 2}'
>>> # encrypt request and response bodies
>>> puk = req.GET.puk()
>>> puk
pP15aGDcFoqGTHTReiIfEvUcQ2c3AQjYcgCeLgKhpa38Rsub69i6RifuYPGtOOyld7j6y0LP6i0aqBuFYcSmTQ==
>>> req.POST.api.endpoint(value1=1, value2=2, _puk=puk)
'{"value1": 1, "value2": 2}'
>>> from usrv import secp256k1
>>> # generate a random keypair
>>> prk, puk = secp256k1.generate_keypair()
>>> # target public key is not server public key
>>> puk == req.GET.puk()
False
>>> print(req.POST.api.endpoints(value1=1, value2=2, _puk=puk))
<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>
    </head>
    <body>
    <h1>Error response</h1>
        <p>Error code: 500</p>
        <p>Message: Encryption error.</p>
        <p>Error code explanation: 5e234e6f68f30a056c2bd53e97b785e49895b1ae85d3cf323a95ot encrypted for public key pP15aGDcFoqGTHTReiIfEvUcQ2c3AQjYcgCeLgKhpa38Rsub69i6RifuYPGtOOyld7j6y0LP6i0aqBuFYcSmTQ==.</p>
    </body>
</html>
>>> # target public key is not the client public key
>>> req.POST.api.endpoint(
...   value1=1, value2=2, _headers={"Sender-Public-Key":puk}
... )
ERROR:usrv:Encryption error:
cc72124506d28ccb7bda70d6649f3f007bca1b8f1d829b047267ea543aa34c96ab39 not encrypted for public key pP15aGDcFoqGTHTReiIfEvUcQ2c3AQjYcgCeLgKhpa38Rsub69i6RifuYPGtOOyld7j6y0LP6i0aqBuFYcSmTQ==
'cc72124506d28ccb7bda70d6649f3f007bca1b8f1d829b047267ea543aa34c96ab39'
>>>

RequestCache Objects

class RequestCache()

Cache manager for HTTP Request objects.

RequestCache.__init__

def __init__(max_size: int = 100, ttl: int = 300)

Initialize the cache.

Arguments:

  • max_size int - Maximum number of entries in the cache.
  • ttl int - Time-to-live for cache entries in seconds.

RequestCache.generate_key

@staticmethod
def generate_key(method: str, path: str, **kwargs) -> str

Generate a unique cache key for a request.

RequestCache.get

def get(key: str) -> typing.Union[None, typing.Tuple[int, Request]]

Retrieve an entry from the cache.

RequestCache.set

def set(key: str, request: Request) -> None

Add an entry to the cache.

FormData Objects

class FormData(list)

Implementation of multipart/form-data encoder.

This class provides methods to construct, encode, and decode multipart/form-data content, as described in RFC 7578.

FormData.append_json

def append_json(name: str, value: dict = {}, **kwval) -> None

Add a JSON object to the multipart body.

Arguments:

  • name str - The name of the form field.
  • value dict, optional - A dictionary representing the JSON object. Defaults to None.
  • kwval - Additional key-value pairs to include in the JSON object.

Returns:

  • typing.Any - The updated FormData instance.

FormData.append_value

def append_value(name: str, value: typing.Union[str, bytes],
                 **headers) -> None

Add a text or binary value to the multipart body.

Arguments:

  • name str - The name of the form field.
  • value Union[str, bytes] - The value to add. Can be a string or bytes.
  • headers - Additional headers to include for this field.

FormData.append_file

def append_file(name: str, path: str) -> typing.Any

Add a file to the multipart body.

Arguments:

  • name str - The name of the form field.
  • path str - The path to the file to be added.

Raises:

  • IOError - If the file does not exist.

FormData.dumps

def dumps() -> str

Encode the FormData instance as a multipart/form-data body.

Returns:

  • str - The encoded body and the corresponding Content-Type header.

FormData.dump

def dump(folder: str = None) -> None

Save the FormData instance to files in a directory.

Each field in the FormData is written to a separate file. Additional metadata is saved as JSON.

Returns:

None

FormData.encode

@staticmethod
def encode(data: dict) -> str

Encode a dictionary as a multipart/form-data string.

Arguments:

  • data dict - The data to encode. Can include filepath, strings, or FormData instances.

Returns:

  • str - The encoded multipart/form-data string.

FormData.decode

@staticmethod
def decode(data: str) -> typing.Any

Decode a multipart/form-data string into a FormData instance.

Arguments:

  • data str - The multipart/form-data string to decode.

Returns:

  • FormData - The decoded FormData instance.

Endpoint Objects

class Endpoint()

Represents an HTTP endpoint with dynamic attribute handling.

Attributes:

  • startswith_ re.Pattern - Pattern to match internal attributes.
  • timeout int - Default timeout for requests.
  • opener OpenerDirector - Opener to handle HTTP requests.
  • peer str - Base URL for the endpoint.

Endpoint.__init__

def __init__(master: typing.Any = None,
             name: str = "",
             method: Callable = None) -> None

Initializes an Endpoint instance.

Arguments:

  • master typing.Any - Parent endpoint.
  • name str - Name of the current endpoint.
  • method Callable - Request-building method.

Endpoint.__getattr__

def __getattr__(attr: str) -> typing.Any

Dynamically resolves sub-endpoints.

Arguments:

  • attr str - Attribute name.

Returns:

  • Endpoint - New sub-endpoint instance.

Endpoint.__call__

def __call__(*args, **kwargs) -> typing.Any

Executes the endpoint’s method with provided arguments.

Arguments:

  • **kwargs - Parameters for the HTTP request.

Returns:

  • typing.Any - value returned by method attribute.

Endpoint.connect

@staticmethod
def connect(peer: str) -> typing.Union[int, bool]

Tests connection to a peer endpoint and store it if success.

Arguments:

  • peer str - Peer URL to test.

Returns:

typing.Union[int, bool]: HTTP status code or False on failure.

build_request

def build_request(method: str = "GET", path: str = "/", **kwargs) -> Request

Builds an HTTP request object.

Arguments:

  • method str - HTTP method (e.g., ‘GET’, ‘POST’). Defaults to ‘GET’.
  • path str - URL path for the request. Defaults to ‘/’.
  • **kwargs - Additional keyword arguments for query parameters, headers, and data.

Returns:

  • Request - Configured HTTP request object.

manage_response

def manage_response(resp: HTTPResponse) -> typing.Union[dict, str]

Parses the HTTP response.

Arguments:

  • resp HTTPResponse - HTTP response object.

Returns:

typing.Union[dict, str]: Decoded response content.

build_endpoint

def build_endpoint(http_req: str = "GET",
                   encoder: Callable = json.dumps,
                   timeout: int = Endpoint.timeout) -> Endpoint

Creates a root endpoint.

Arguments:

  • http_req str - Name of HTTP method (i.e. HEAD, GET, POST etc…).
  • encoder Callable - Data encoder function to use. (defaults to json)
  • timeout int - Request timeout in seconds.

Returns:

  • Endpoint - Root endpoint.