Non-TypeScript Codegen: Go and Python Clients
Shows how the Go SDK's ogen client and the Python SDK's openapi-python-client client are both generated from the same normalized OpenAPI spec, and what post-processing each language applies before the output ships.
The Go SDK and Python SDK don't hand-write their API clients. Both generate from the same normalized OpenAPI document that OpenAPI Spec Normalization and Codegen Artifacts produces, using an off-the-shelf generator for their language, then run a small fixup pass that repairs generator quirks the platform's wire contract doesn't tolerate.
Reach for this page when you're regenerating the Go or Python client after an openapi/primitive-api.yaml change, or debugging why generated code in sdk-go/api or sdk-python/src/primitive/api looks different from what you expected.
What each language generates with#
Go generates its client with ogen into sdk-go/api, and Python generates with openapi-python-client into sdk-python/src/primitive/api. Both generators consume openapi/primitive-api.codegen.json, the OpenAPI 3.0.3 build artifact normalized from the hand-written 3.1 spec (never edit that JSON file directly; see OpenAPI Spec Normalization and Codegen Artifacts).
| SDK | Generator | Output location | Client type |
|---|---|---|---|
| Go | ogen | sdk-go/api | Generated Go package, imported as primitiveapi |
| Python | openapi-python-client | sdk-python/src/primitive/api | Generated Python package, imported as primitive.api |
The Node SDK's counterpart is the workspace-internal @primitivedotdev/api-core package (see What is API Core?); Go and Python have no equivalent shared package, so each generates and post-processes its client directly inside its own SDK directory.
For day-to-day app code, use the high-level client.send/reply/forward surface, not the generated client directly. Reach for the generated client only for operations the high-level client doesn't cover, see Generated API Client for the Python surface.
Regenerating the Go client#
Run make go-generate from the repo root, then make go-check and commit the regenerated sdk-go/api files with your spec change.
- 1
Run the Go generate target#
From the repo root:
make go-generateThis regenerates
sdk-go/apifromopenapi/primitive-api.codegen.jsonusing ogen. - 2
Run the Go SDK's checks#
make go-checkThe Go SDK's own checks are
go test ./..., the shared-fixture rungo test -run TestSharedCompatibilityFixtures ./..., andgofmt -w .. See Go SDK Testing and Development for the full local workflow. - 3
Commit the regenerated output#
Commit the changed files under
sdk-go/apialongside your spec change. ogen's output is deterministic for a given spec, so a diff here should map directly to your OpenAPI change.
Regenerating the Python client#
Run make python-generate from the repo root; it invokes sdk-python/scripts/generate_api_client.py, which regenerates sdk-python/src/primitive/api and applies three fixups.
Python's generation script, sdk-python/scripts/generate_api_client.py, wraps the openapi-python-client CLI and then applies three fixups the raw generator output needs before it's usable.
- 1
Run the Python generate target#
From the repo root:
make python-generateUnder the hood this invokes
generate_api_client.py, which:- Runs
openapi-python-client generate --meta none --config openapi-python-client-config.yml --path openapi/primitive-api.codegen.json --output-path <temp dir>from thesdk-pythondirectory. - Deletes the previous generated
api/,client.py,errors.py,models/, andtypes.pyundersdk-python/src/primitive/api. - Copies the freshly generated versions into their place.
- Applies the three post-processing fixups described below.
- Runs
- 2
Run the Python SDK's checks#
make python-checkThe Python SDK's own checks are
uv run pytest,uv run ruff check ., anduv run basedpyright. - 3
Commit the regenerated output#
Commit the changed files under
sdk-python/src/primitive/apialongside your spec change.
Python's three fixups#
The three fixups deduplicate repeated imports, guard the Content-Type header on optional-body operations, and switch file responses to raw bytes. generate_api_client.py runs each of these over every generated .py file after copying, in this order.
Deduplicate imports#
openapi-python-client 0.28.3 occasionally emits the same from X import ... line twice, for example from ..types import UNSET, Unset inside a 201-response model. dedupe_imports strips repeated module-level import lines (matched by a regex that only touches unindented from ... import ... lines, so imports inside TYPE_CHECKING blocks or function bodies are left alone). The duplicates are semantically harmless but trip strict linters downstream.
Guard the Content-Type header on optional-body operations#
openapi-python-client 0.28.3 unconditionally emits:
headers["Content-Type"] = "application/json"
even for operations whose request body is optional. When a caller omits the body, the generated code still sends Content-Type: application/json with no body, which trips middleware expecting either both or neither. guard_optional_body_content_type re-indents that assignment so it fires only inside the existing if not isinstance(body, Unset): block, matching this generator pattern exactly:
if not isinstance(body, Unset):
_kwargs["json"] = body.to_dict()
headers["Content-Type"] = "application/json"
becomes:
if not isinstance(body, Unset):
_kwargs["json"] = body.to_dict()
headers["Content-Type"] = "application/json"
This is the Python-side counterpart to the same class of bug the Node SDK fixes in generated TypeScript, see Generated TypeScript Client Fixups, and it's also the failure mode covered in Codegen Troubleshooting for mismatched Content-Type headers on optional-body requests.
Use raw bytes for file/download responses#
File payloads wrap BytesIO, which requires bytes, not str. use_bytes_for_file_responses replaces every occurrence of BytesIO(response.text) with BytesIO(response.content) across the generated tree, so binary downloads (raw email .eml bytes, attachment bundles) decode correctly instead of being mangled through text decoding first.
Never hand-edit files under sdk-python/src/primitive/api or sdk-go/api. They're regenerated wholesale on every make python-generate / make go-generate run, and manual edits are silently discarded. Fix the spec, the generator config, or the fixup script instead.
Verifying the regeneration worked#
Build and test each regenerated SDK, and confirm both languages show a matching diff for the same spec change. After regenerating either client, confirm:
- Go:
go build ./...succeeds fromsdk-go/, andgo test -run TestSharedCompatibilityFixtures ./...passes. - Python:
uv run basedpyrightreports no new type errors, anduv run pytestpasses.
A spec change that adds or removes an operation should show up as a corresponding diff in both sdk-go/api and sdk-python/src/primitive/api. If only one language's output changed, the other generator run was likely skipped.
Next steps#
See how the source 3.1 YAML becomes the 3.0.3 JSON both generators consume.
Regenerating SDK Code from the OpenAPI SpecRun the full end-to-end pipeline across all three SDK languages in one pass.
Codegen TroubleshootingDiagnose schema drift, stale imports, and Content-Type mismatches after a regeneration.
Go SDK Testing and DevelopmentRun the Go SDK's test suite and shared compatibility fixtures locally.
Was this page helpful?