Skip to main content

Composable HTTP API client: pluggable transport, session, and presentation layers for maintainable wrappers.

Project description

wrapfast

wrapfast is a small Python library with a big opinion: API clients stay maintainable when you separate concerns instead of growing a single “do everything” class.

It exists to promote good practice, clear organisation, and real flexibility when you wrap REST (or HTTP-shaped) APIs in Python. You compose a pipeline from a few roles—each one easy to test, swap, or extend—instead of hard‑coding requests.get next to auth logic next to JSON parsing next to URL strings scattered across the codebase.


The idea in one glance

Piece Responsibility
Transport How a request leaves your process and bytes come back (requests, httpx, a mock, async later).
Presentation How domain objects become HttpRequest (encode), and HttpResponse becomes intermediate objects (decode), and intermediate objects become final domain models (narrow).
Session Cross-cutting behaviour on the intermediate types (e.g. JsonRequest, JsonResponse): tokens, headers, error-checking.
Endpoint A named operation: HTTP method, path, and the response type you expect.
HttpClient The orchestrator: Session.wrapPresentation.encodeTransport.sendPresentation.decodeSession.unwrapPresentation.narrow.

That split is the point: organisation (each type has one job), good practice (test transports and codecs without the network; test sessions without JSON details), and flexibility (change transport or codec without rewriting your endpoints).


Code that shows the shape

This is intentionally dense: it is the whole architecture on one screen, using Pydantic for request/response models and JSON.

import json
import requests
from dataclasses import dataclass
from typing import Any
from pydantic import BaseModel, ConfigDict

from wrapfast import (
    Endpoint,
    HttpClient,
    HttpRequest,
    HttpResponse,
    Presentation,
    Session,
    Transport,
)

@dataclass
class JsonRequest:
    headers: dict[str, str]
    body: dict[str, Any]

@dataclass
class JsonResponse:
    status: int
    body: Any

class User(BaseModel):
    model_config = ConfigDict(extra="ignore")
    id: int
    name: str

GET_USER = Endpoint("GET", "users/1", User)

class RequestsTransport(Transport):
    def send(self, request: HttpRequest) -> HttpResponse:
        r = requests.request(
            request.method,
            request.url,
            headers=request.headers,
            data=request.data or None,
            timeout=30,
        )
        return HttpResponse(
            status_code=r.status_code, 
            headers={k.lower(): v for k, v in r.headers.items()}, 
            data=r.content
        )

class BearerSession(Session[JsonRequest, JsonResponse]):
    def __init__(self, token: str) -> None:
        self._token = token

    def wrap(self, request: JsonRequest) -> JsonRequest:
        request.headers["Authorization"] = f"Bearer {self._token}"
        return request

    def unwrap(self, response: JsonResponse) -> JsonResponse:
        if response.status == 401:
            raise RuntimeError("Unauthorized")
        return response

class PydanticPresentation(Presentation[JsonRequest, JsonResponse]):
    def encode(self, request: JsonRequest, *, method: str, url: str) -> HttpRequest:
        return HttpRequest(
            method=method,
            url=url,
            headers={"Content-Type": "application/json", **request.headers},
            data=json.dumps(request.body).encode("utf-8") if request.body else b"",
        )

    def decode(self, response: HttpResponse) -> JsonResponse:
        body = json.loads(response.data) if response.data else {}
        return JsonResponse(status=response.status_code, body=body)

    def narrow[T](self, response: JsonResponse, target: type[T]) -> T:
        if issubclass(target, BaseModel):
            return target.model_validate(response.body)
        raise TypeError("target must be a BaseModel subclass")

client = HttpClient[JsonRequest, JsonResponse](
    base_url="https://api.example.com/",
    transport=RequestsTransport(),
    session=BearerSession("<access token>"),
    presentation=PydanticPresentation(),
)

user = client.send(GET_USER, JsonRequest(headers={}, body={}))  # User: validated model

Add pydantic and requests to your environment when using this pattern (also bundled as the optional examples extra in this repo).

HttpClient is the spine: it does not know which HTTP library you use, how you authenticate, or how bodies are serialised. Those are policies you inject. Your API surface becomes a set of Endpoint values plus Pydantic models (or other types you teach the codec)—easier to read, review, and reuse.

Presentation, Transport, and Session are abstract bases (abc.ABC). Codecs implement encode, decode, and narrow; transports and sessions implement the send / wrap / unwrap hooks shown above.


Why it matters

  • Tests: fake Transport returns canned HttpResponse; no sockets.
  • Auth: evolve Session (login, refresh, header rules) without touching codecs.
  • Formats: swap JSON for another presentation at the edge without renaming your domain models’ usage sites.
  • Readability: endpoints read like a table of operations; the “how we call HTTP” story lives in a few small classes.

Project layout & example

Path Path Role
src/wrapfast/ Installable package: HttpClient, protocols, Endpoint.
examples/dummyjson_requests.py End‑to‑end sample: requests, Pydantic, bearer session (login, /auth/me, refresh).

After pip install wrapfast, use import wrapfast. From a clone without installing, add the src directory to PYTHONPATH (see examples/dummyjson_requests.py). A fuller DummyJSON walkthrough lives in that example.


Requirements

Python 3.13+ (see pyproject.toml). The library itself has no required runtime dependencies; pair it with your transport and codec. The README snippet and examples extra use Pydantic and requests.


License

This project is released under the 0BSD license (see LICENSE): use it for anything, with no attribution requirement and minimal legal boilerplate. It is one of the most permissive widely used open-source terms for software.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wrapfast-0.1.1.tar.gz (19.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wrapfast-0.1.1-py3-none-any.whl (6.8 kB view details)

Uploaded Python 3

File details

Details for the file wrapfast-0.1.1.tar.gz.

File metadata

  • Download URL: wrapfast-0.1.1.tar.gz
  • Upload date:
  • Size: 19.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wrapfast-0.1.1.tar.gz
Algorithm Hash digest
SHA256 1f5603a14afc19c235ae4353b6f3e0568065f8cc38384a1c5e56988ce8ae1b79
MD5 e89ed0ff994d3b137339e7fc0c702d54
BLAKE2b-256 506dacd66c28a236568fb783cd6478d312f5ec100530cd3cdb9e12c01060902d

See more details on using hashes here.

File details

Details for the file wrapfast-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: wrapfast-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 6.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wrapfast-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 64fa64b5da1afb4e12891dd3829f5e2b52121a7e1e7be79357602e58397f3f7e
MD5 6f4f76acbf5070ad6c0b2cf916bff86d
BLAKE2b-256 e52ec2ff7b7f8901287fcc2cfaa5fe14908849551676568a25a80e2a28e16d9d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page