Unihttp client
Generate a typed, packaged unihttp API client library (SDK) from an OpenAPI 3.x spec or a plain API description. Produces typed DTOs, BaseMethod definitions, a client class, ruff + mypy (strict) config, sensible contract tests, and packaging. Use when the user wants to scaffold, bootstrap, or generate an API client / SDK on top of unihttp.From its SKILL.md
npx -y skills add goduni/unihttp --skill unihttp-clientAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
8.9 KB, as published. Nobody here has run it
unihttp client generator
Generate a runnable, packaged API client on top of unihttp from either an
OpenAPI 3.x specification or a written API description. The output is an
installable Python package with typed models, declarative methods, a client
class, ruff + mypy (strict) config, and a small set of common-sense tests.
Always write idiomatic unihttp code — load the companion unihttp skill (or
../unihttp/SKILL.md) for the Wrong/Correct rules. The most
important one: import from submodules (unihttp.method, unihttp.markers,
unihttp.bind_method), never from the empty top-level unihttp package.
Defaults (override only when the user or spec asks):
- Serializer: adaptix + stdlib dataclasses.
- Backend:
aiohttpasync (AiohttpAsyncClient); userequests(RequestsSyncClient) when the user wants a synchronous client. - Layout: one file per method, model, and enum (see references/package-layout.md).
- Tests: contract tests at the unihttp seam (
build_http_request/make_response) — onlypytestneeded, no HTTP-mock library.
Map OpenAPI constructs with references/openapi-to-unihttp.md.
Workflow
1. Establish the input mode
- OpenAPI spec (file path, URL, or pasted document): confirm
openapiis3.0.xor3.1.x. If it is older, ambiguous, or incomplete, say so before generating. Extract: tags / path groups,operationIds, parameters (path/query/header), request bodies, response schemas + status codes, reusablecomponents.schemas, and security schemes. Flag hard constructs early:oneOf,anyOf,allOf, discriminators, multiple media types, file uploads. - API description (no spec): gather, per endpoint, the method, URL template, path/query/header params, request body shape, and the response shape. Ask for the missing pieces rather than inventing them.
2. Confirm the essentials with the user (only what you cannot infer)
- Package/distribution name and import name.
- Sync or async (default async/aiohttp).
- Auth scheme (header token, bearer, API key, …) if the spec declares one.
- Whether to scaffold a fresh project or add the client into an existing repo.
Keep this short — infer sensible defaults and state them rather than interrogating.
3. Plan the package layout
Mirror unihttp's own conventions: src/<import_name>/ layout, hatchling
build, ruff.toml + mypy.ini, py.typed. Use one file per symbol:
models/<model>.py, enums/<enum>.py, methods/<operation>.py, and a single
client.py. Re-export the public surface from __init__.py. For a very small
API (a handful of endpoints), it is fine to collapse models into one
models.py — use judgement, do not over-fragment a 3-endpoint client.
The full tree and file templates are in references/package-layout.md.
4. Generate DTOs (models and enums)
- One
@dataclassper schema; oneenum.StrEnum/IntEnumper enumerated type. - Keep idiomatic snake_case Python fields. Preserve wire names via adaptix
name_mappingin the client'sRetort, not by renaming attributes. - Split request and response models when the response adds server-owned fields (id, timestamps, computed values).
- Reuse shared schemas across operations instead of duplicating classes.
- Map
nullable/oneOf nulltoT | None;oneOf/anyOftoUnion;allOfto composition or a merged dataclass.
5. Generate methods
One BaseMethod[ResponseType] per operation, in methods/<operation>.py:
- Class name from
operationId(PascalCase); else<Verb><Resource>(GetUser,CreateUser,ListUsers). __url__= the path template,__method__= the HTTP verb (uppercase).- Map parameters to markers (
Path/Query/Header), request body toBody, form toForm, file uploads toFile+UploadFile. See the mapping table. - For paginated list endpoints, model the response envelope as a DTO (not a bare
list) or expose page/offset as optionalQueryfields — see "Pagination" in the mapping reference.datetime/date/UUID/Decimal/Enumfields need no special handling (adaptix loads them from their standard string forms). - The generic argument is the response model. Use
NoneONLY for a genuinely empty body (a real204with no content); if the endpoint returns an empty object{}(common forDELETE), useBaseMethod[dict[str, Any]]—NoneraisesTypeLoadErrorbecausemake_responsealways deserializes the body.
6. Generate the client
A single *Client subclass in client.py:
- Inherit
AiohttpAsyncClient(async, default) orRequestsSyncClient(sync). - Bind each operation with
bind_method. - Build a
Retort(extendDEFAULT_RETORT) carrying anyname_mapping, and passAdaptixDumper(retort)/AdaptixLoader(retort). The mapping must cover the method classes (Body/Queryfields), not only response models — for an all-camelCase API use one globalname_mapping(map={...})with no class predicate, which renames bodies, query filters, and models at once. - Accept
base_urland auth inputs in__init__; defaultbase_urlfrom the spec'sservers[0].urlso callers can construct the client with no args. Inject auth via aHeaderfield on methods or an auth middleware (AsyncMiddlewarefor async clients; typenext_handlerasAsyncHandler). - For endpoints needing pre/post logic, write an explicit method using
call_methodinstead ofbind_method.
7. Handle auth and errors
- Simple token/api-key auth: add it as a header (middleware or per-method
Header). Leave aTODOfor OAuth/refresh flows rather than inventing them. - Map documented error status codes to exceptions with
AsyncErrorMapperMiddleware/SyncErrorMapperMiddleware, or overridehandle_error. Keep a small exceptions module if the API has a real error taxonomy.
8. Generate packaging
Produce pyproject.toml (hatchling, src layout), ruff.toml, mypy.ini,
py.typed, .gitignore, and a README.md with install + quickstart. Runtime
dependency is unihttp with the chosen extras, e.g.
unihttp[aiohttp,adaptix]>=0.2.9. Dev dependencies for the default tests are just
ruff, mypy, pytest. In ruff.toml use extend-exclude (not exclude) so
ruff keeps ignoring .venv. Templates in
references/package-layout.md.
9. Generate common-sense tests
Default to contract tests at the unihttp seam — they cover exactly what generated code introduces and avoid flaky HTTP-mock dependencies:
- Per operation, assert
Method(...).build_http_request(dumper)produces the righturl/method/query/body(markers wired correctly). - Per response model, assert
Method(...).make_response(HTTPResponse(...), response_loader=loader)parses into the right DTO. Call it by keyword (response_loader=) — that is howcall_methodinvokes it, so a method overridingmake_responsewith a wrong parameter name is caught instead of silently passing a positional test. - These are synchronous and backend-agnostic — only
pytestis needed, the same for async (aiohttp) and sync (requests) clients. See references/package-layout.md.
Optionally add one full-pipeline test through a mocked transport, but treat it
as an extra. Note that aioresponses lags new aiohttp releases (it breaks on
aiohttp 3.14), so pin a compatible pair, prefer respx for httpx clients, or use
a local pytest-aiohttp server. Skip property-based / contract fuzzing unless the
user asks.
10. Verify before claiming done
Run, from the generated package, and report real output:
ruff check .
ruff format --check .
mypy
pytest
Freshly generated code routinely needs one ruff check --fix + ruff format
pass before it is clean (import grouping, long __all__ lines). Run those first,
then the gate. Fix what remains. Do not claim the package is correct until these
pass (or explicitly report what does not and why).
Output checklist
pyproject.toml(+ruff.toml,mypy.ini,py.typed) when scaffolding a package.- A DTO per request/response schema that matters; an enum per enumerated type.
- A
BaseMethodper operation; a single client binding them. - A
Retortcarrying wire-name mappings when the API is not snake_case. - Minimal contract tests (seam-level), or a clear note on why tests were skipped.
README.mdwith install and a runnable quickstart (usingasync withfor async clients).- A short report of
ruff/mypy/pytestresults.