{"id":51455242,"url":"https://github.com/goduni/unihttp-openapi-generator","last_synced_at":"2026-07-06T00:01:11.034Z","repository":{"id":368489213,"uuid":"1254039748","full_name":"goduni/unihttp-openapi-generator","owner":"goduni","description":"Generate typed Python API clients from OpenAPI 3.0/3.1 specs, built on unihttp. adaptix/pydantic/msgspec models, sync \u0026 async clients, mypy --strict-clean output","archived":false,"fork":false,"pushed_at":"2026-06-30T19:19:44.000Z","size":243,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-30T21:10:24.816Z","etag":null,"topics":["adaptix","api-client","async","client-generator","code-generator","codegen","http-client","msgspec","openapi","openapi-generator","openapi3","pydantic","python","rest-api","sdk-generator","swagger","type-safe","unihttp"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/goduni.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-30T04:29:54.000Z","updated_at":"2026-06-30T20:49:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/goduni/unihttp-openapi-generator","commit_stats":null,"previous_names":["goduni/unihttp-openapi-generator"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/goduni/unihttp-openapi-generator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goduni%2Funihttp-openapi-generator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goduni%2Funihttp-openapi-generator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goduni%2Funihttp-openapi-generator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goduni%2Funihttp-openapi-generator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/goduni","download_url":"https://codeload.github.com/goduni/unihttp-openapi-generator/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goduni%2Funihttp-openapi-generator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35172975,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adaptix","api-client","async","client-generator","code-generator","codegen","http-client","msgspec","openapi","openapi-generator","openapi3","pydantic","python","rest-api","sdk-generator","swagger","type-safe","unihttp"],"created_at":"2026-07-06T00:01:10.280Z","updated_at":"2026-07-06T00:01:11.025Z","avatar_url":"https://github.com/goduni.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# unihttp-openapi-generator\n\n[![codecov](https://codecov.io/gh/goduni/unihttp-openapi-generator/branch/main/graph/badge.svg)](https://codecov.io/gh/goduni/unihttp-openapi-generator)\n[![PyPI version](https://img.shields.io/pypi/v/unihttp-openapi-generator.svg)](https://pypi.org/project/unihttp-openapi-generator)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/unihttp-openapi-generator)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/unihttp-openapi-generator)\n![GitHub License](https://img.shields.io/github/license/goduni/unihttp-openapi-generator)\n![GitHub Repo stars](https://img.shields.io/github/stars/goduni/unihttp-openapi-generator)\n[![Telegram](https://img.shields.io/badge/💬-Telegram-blue)](https://t.me/+OsmQESHc1xU1MGVi)\n\nTurn an OpenAPI spec into a typed Python API client built on\n[unihttp](https://github.com/goduni/unihttp).\n\nPoint it at a spec; get back an installable package with data models, request\nclasses, a sync and/or async client, an exception hierarchy, and authentication\nwiring. The output is formatted with `ruff` and type-checks clean under `mypy --strict`.\n\n## Why\n\n- **Actually typed.** Models, parameters, and return values carry real annotations;\n  the generated code passes `mypy --strict`. Your editor knows the shape of every\n  request and response.\n- **Three model backends.** Choose `adaptix` (default), `pydantic`, or `msgspec`\n  for the generated models — same client, your serializer.\n- **Sync, async, or both.** Backed by `httpx`, `aiohttp`, `requests`, `niquests`, or\n  `zapros`, chosen per client.\n- **Faithful to the spec.** `allOf`/`oneOf`/`anyOf`, discriminated unions, enums,\n  formats, nullable, defaults, multipart uploads, query array styles, security\n  schemes, and error responses are all carried through.\n- **Proven on large specs.** The Stripe, GitHub, OpenAI, and Kubernetes specs each\n  generate clean, importable code that passes `ruff` and `mypy --strict` on every\n  serializer.\n- **Readable, regenerable output.** Deterministic, `ruff`-formatted, organized by tag.\n\n## Install\n\n```bash\npip install unihttp-openapi-generator\n# or, with uv:\nuv tool install unihttp-openapi-generator\n```\n\n## Quick start\n\n```bash\nunihttp-openapi-generator generate openapi.yaml \\\n  --output-dir ./out --package-name acme_client\n```\n\nThe spec can be a local path or a URL, in JSON or YAML. Install the result and use it:\n\n```bash\npip install ./out\n```\n\n```python\nfrom acme_client import AcmeClient\n\nwith AcmeClient(base_url=\"https://api.example.com\", token=\"...\") as client:\n    pet = client.pets.get_pet(pet_id=1)   # -\u003e a typed model\n    print(pet.name)\n```\n\n## What you get\n\n```\nout/\n├── pyproject.toml          # installable; pins unihttp + your serializer + backend\n├── README.md\n└── acme_client/\n    ├── __init__.py         # exports the client(s), DEFAULT_BASE_URL, SERVERS\n    ├── py.typed\n    ├── models.py           # dataclass / BaseModel / msgspec.Struct\n    ├── _serialization.py    # request/response (de)serialization wiring\n    ├── exceptions.py       # ApiError hierarchy + status -\u003e exception map\n    ├── auth.py             # credential middlewares (when the spec defines security)\n    ├── methods/\u003ctag\u003e.py    # one request class per operation\n    └── client.py           # the client(s)\n```\n\nA request class and the client constructor (real output):\n\n```python\n@dataclass\nclass GetBooking(BaseMethod[GetBookingResponse]):\n    \"\"\"Get a booking\n\n    Returns the details of a specific booking.\n    \"\"\"\n    __url__ = \"/bookings/{bookingId}\"\n    __method__ = \"GET\"\n\n    booking_id: Path[UUID]\n\n\nclass TrainTravelAPIClient(RequestsSyncClient):\n    def __init__(self, base_url: str = DEFAULT_BASE_URL, *,\n                 session: Any = None, middleware: list[Any] | None = None,\n                 token: str | None = None) -\u003e None:\n        ...\n```\n\n## Using the client\n\nClients are context managers and close their transport on exit.\n\n```python\nfrom acme_client import AcmeClient\n\nwith AcmeClient(base_url=\"https://api.example.com\", token=\"secret\") as client:\n    booking = client.bookings.get_booking(booking_id=some_uuid)   # grouped layout\n    # client.get_booking(...)   # flat layout\n```\n\nAsync clients expose the same surface; their methods are awaitables:\n\n```python\nimport asyncio\nfrom acme_client import AsyncAcmeClient\n\nasync def main() -\u003e None:\n    async with AsyncAcmeClient(token=\"secret\") as client:\n        trips = await client.trips.get_trips(origin=a, destination=b, date=when)\n\nasyncio.run(main())\n```\n\n### Base URL and servers\n\nThe default base URL is taken from the spec's `servers` (preferring a production\nentry). Every server is also exported:\n\n```python\nfrom acme_client import DEFAULT_BASE_URL, SERVERS\n\nclient = AcmeClient(base_url=SERVERS[\"Production\"])\n```\n\n### Authentication\n\nEach security scheme becomes a constructor keyword that is injected via middleware:\n\n| Scheme | Keyword | Sent as |\n|---|---|---|\n| http bearer / oauth2 / openIdConnect | `token: str` | `Authorization: Bearer \u003ctoken\u003e` |\n| apiKey (header or query) | `\u003cscheme\u003e: str` | the named header or query parameter |\n| http basic | `\u003cscheme\u003e: tuple[str, str]` | `Authorization: Basic \u003cbase64\u003e` |\n\n### Custom headers, cookies, timeouts\n\nBuild the underlying HTTP client yourself and pass it as `session=` (its type matches\nthe chosen backend — `requests.Session` by default, `httpx.Client`, `aiohttp.ClientSession`, …):\n\n```python\nimport requests\n\nsession = requests.Session()\nsession.headers[\"User-Agent\"] = \"acme/1.0\"\nclient = AcmeClient(session=session)\n```\n\n### Errors\n\nNon-2xx responses raise. `\u003cpackage\u003e.exceptions` defines a base `ApiError` plus a\nsubclass per status code (`NotFoundError`, `UnprocessableEntityError`, …), with\n`4xx`/`5xx` falling back to unihttp's `ClientError`/`ServerError`.\n\n```python\nfrom acme_client.exceptions import ApiError, NotFoundError\n\ntry:\n    booking = client.bookings.get_booking(booking_id=bad_id)\nexcept NotFoundError as exc:\n    print(exc.status_code, exc.response.data)\nexcept ApiError:\n    ...\n```\n\n### Middleware\n\nPass any unihttp middleware; auth and error mapping are composed around it.\n\n```python\nfrom unihttp.middlewares.retry import RetryMiddleware\nclient = AcmeClient(middleware=[RetryMiddleware(retries=3)])\n```\n\n## CLI options\n\n```\nunihttp-openapi-generator generate SPEC [options]\n```\n\n| Option | Values (default) |\n|---|---|\n| `-o`, `--output-dir` | path (required) |\n| `--package-name` | identifier (required) |\n| `--serializer` | `adaptix` · `pydantic` · `msgspec` (`adaptix`) |\n| `--client` | `both` · `sync` · `async` (`both`) |\n| `--sync-backend` | `httpx` · `requests` · `niquests` · `zapros` (`requests`) |\n| `--async-backend` | `httpx` · `aiohttp` · `niquests` · `zapros` (`aiohttp`) |\n| `--layout` | `auto` · `flat` · `grouped` (`auto`) |\n| `--file-layout` | `single` · `per-object` (`single`) |\n| `--style` | `declarative` · `imperative` (`declarative`) |\n| `--optional` | `none` · `omitted` (`none`) — `omitted` distinguishes absent from null (adaptix) |\n| `--strip-prefix` | `auto` or a dotted prefix to drop from schema names (e.g. `io.k8s.api.core.v1.Pod` → `CoreV1Pod`) |\n| `--check` | run `ruff` and `mypy --strict` on the output |\n| `--config` | TOML config file |\n\n### Config file\n\nKeep your generation settings in a TOML file so a regenerate is a single command and\nthe configuration lives in version control.\n\n**Precedence.** For every setting: an explicit CLI flag wins, otherwise the config\nfile, otherwise the built-in default. So you can pin a project's settings in the file\nand still override one of them ad hoc on the command line:\n\n```bash\nunihttp-openapi-generator generate                       # use the discovered config\nunihttp-openapi-generator generate --serializer msgspec  # override just this one\n```\n\n**Discovery order** (the first that exists is used):\n\n1. the file passed to `--config FILE`,\n2. `unihttp-openapi-generator.toml` in the current directory,\n3. a `[tool.unihttp-openapi-generator]` table in `pyproject.toml`.\n\n**Keys** mirror the CLI options exactly. `spec`, `output_dir`, and `package_name` are\nrequired (from the file or the command line); everything else is optional and falls\nback to the default shown in the [CLI options](#cli-options) table. Unknown keys are\nrejected so typos surface immediately.\n\nA fully annotated `unihttp-openapi-generator.toml`:\n\n```toml\nspec = \"https://api.example.com/openapi.json\"  # path or URL; JSON or YAML\noutput_dir = \"out\"                             # where the package is written\npackage_name = \"acme_client\"                   # importable package name\n\nserializer = \"adaptix\"        # adaptix | pydantic | msgspec\nclient = \"both\"               # both | sync | async\nsync_backend = \"requests\"     # httpx | requests | niquests | zapros\nasync_backend = \"aiohttp\"     # httpx | aiohttp | niquests | zapros\nlayout = \"auto\"               # auto | flat | grouped     (client shape)\nfile_layout = \"single\"        # single | per-object       (files on disk)\nstyle = \"declarative\"         # declarative | imperative  (method style)\noptional = \"none\"             # none | omitted            (optional model fields)\nstrip_prefix = \"auto\"         # \"auto\" or a dotted prefix to drop from schema names\ncheck = true                  # run ruff + mypy --strict on the output\n```\n\nOr, to keep it inside an existing project, drop the same keys under a table in\n`pyproject.toml`:\n\n```toml\n[tool.unihttp-openapi-generator]\nspec = \"openapi.yaml\"\noutput_dir = \"out\"\npackage_name = \"acme_client\"\nserializer = \"pydantic\"\nclient = \"async\"\n```\n\n## Serializers\n\n| | adaptix (default) | pydantic | msgspec |\n|---|---|---|---|\n| Model type | `@dataclass` | `BaseModel` | `msgspec.Struct` |\n| Field aliasing | full (retort name mapping) | `Field(alias=…)` | `field(name=…)` |\n| Query array styles | full | explode only | explode only |\n| Runtime validation | — | yes | yes |\n\n`adaptix` gives the highest fidelity (parameter aliases and all query array styles).\n`pydantic` adds runtime validation; `msgspec` is the fastest.\n\n## Generation options\n\nThese shape the surface and style of the generated code. All have sensible defaults;\nreach for them to match an existing codebase or taste.\n\n### Client layout — `--layout`\n\nHow methods are exposed on the client.\n\n- `flat` — every operation is a method on one client class:\n  ```python\n  client.get_booking(booking_id=...)\n  client.create_booking(body=...)\n  ```\n- `grouped` — operations are grouped into sub-clients by their OpenAPI tag (nicer for\n  large APIs):\n  ```python\n  client.bookings.get_booking(booking_id=...)\n  client.payments.create_payment(...)\n  ```\n- `auto` (default) — `flat` when the spec has at most one tag, `grouped` otherwise.\n\n### File layout — `--file-layout`\n\nHow the package is split on disk. The import surface is identical either way.\n\n- `single` (default) — one `models.py` and one `methods/\u003ctag\u003e.py` per tag. Fewer, larger\n  files.\n- `per-object` — one file per model/enum and per request method\n  (`models/\u003cname\u003e.py`, `methods/\u003ctag\u003e/\u003cmethod\u003e.py`). Easier to navigate and gives small,\n  focused diffs on regeneration, at the cost of many files. Cross-references between\n  modules are resolved automatically without circular imports.\n\n### Method style — `--style`\n\nHow client methods are written.\n\n- `declarative` (default) — methods are bound from the request classes. Compact; the\n  call signature comes from the request dataclass:\n  ```python\n  class BookingsClient:\n      get_booking = bind_method(GetBooking)\n  ```\n- `imperative` — an explicit, fully-typed wrapper per operation. More generated code,\n  but the signature is spelled out for the best editor experience:\n  ```python\n  def get_trips(self, *, origin: UUID, destination: UUID, date: datetime,\n                page: int = 1, limit: int = 10) -\u003e GetTripsResponse:\n      return self.call_method(GetTrips(origin=origin, destination=destination,\n                                       date=date, page=page, limit=limit))\n  ```\n\n### Optional fields — `--optional`\n\nHow optional model fields are represented (adaptix only).\n\n- `none` (default) — `T | None = None`. Simple, but \"field absent\" and \"field is null\"\n  both read as `None`.\n  ```python\n  middle_name: str | None = None\n  ```\n- `omitted` — `Omittable[T] = Omitted()`. Distinguishes a field you never set from one\n  set to `null`; unset fields are dropped from the request body entirely. Useful for\n  PATCH-style APIs where sending `null` clears a value:\n  ```python\n  middle_name: Omittable[str | None] = Omitted()\n  ```\n\n## OpenAPI coverage\n\n- 3.0 and 3.1; JSON or YAML; file or URL; internal and external `$ref`.\n- Schemas: objects, `allOf` merge, `oneOf`/`anyOf`, discriminator (including\n  polymorphic bases), enums and `const`, formats, nullable, `additionalProperties`,\n  constraints, recursion, and `readOnly` (excluded from request bodies).\n- Operations: path/query/header parameters with defaults, JSON/form/multipart bodies,\n  file uploads, typed responses, and `deprecated`.\n- Security: apiKey, http bearer/basic, oauth2, openIdConnect.\n\n## Limitations\n\n- Response headers are not exposed; methods return the response body.\n- `deepObject` query parameters and full parameter aliasing work on `adaptix`; on\n  `pydantic` and `msgspec` they are limited.\n- Swagger / OpenAPI 2.0 is not supported (use the OpenAPI 3 description if a service\n  publishes both, as Kubernetes does).\n\n## Development\n\n```bash\nuv sync\nuv run pytest\nuv run ruff check src tests\nuv run mypy\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoduni%2Funihttp-openapi-generator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgoduni%2Funihttp-openapi-generator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoduni%2Funihttp-openapi-generator/lists"}