1. Expoce the specification
Spin up any HTTP service that hosts a valid openapi.json (or swagger.json) at a reachable URL, e.g. http://localhost:8000/openapi.json.
2. Launch the generator service
docker run -d -p 8090:8080 \
--name openapi_generator \
openapitools/openapi-generator-online:latest-release
3. Generate the Python client
#!/usr/bin/env python3
"""
Minimal helper that downloads a freshly generated Python client.
"""
import logging
import os
import shutil
import tempfile
import zipfile
from pathlib import Path
import requests
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("sdk_builder")
def build_sdk(
spec_url: str,
package: str,
dest: str | Path,
generator: str = "http://localhost:8090",
) -> None:
"""
Build a Python SDK from an OpenAPI spec and copy the resulting
package into *dest*.
"""
payload = {
"openAPIUrl": spec_url,
"options": {
"packageName": package,
"projectName": package,
"generateSourceCodeOnly": "true",
},
"spec": {},
}
log.info("Triggering client generation …")
r = requests.post(f"{generator}/api/gen/clients/python", json=payload)
r.raise_for_status()
zip_url = r.json()["link"]
log.info("Downloading %s", zip_url)
r = requests.get(zip_url)
r.raise_for_status()
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
archive = tmp / "sdk.zip"
archive.write_bytes(r.content)
extract_to = tmp / "out"
with zipfile.ZipFile(archive) as zf:
zf.extractall(extract_to)
root = package.split(".")[0]
src = extract_to / "python-client" / root
dst = Path(dest) / root
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(src, dst, dirs_exist_ok=True)
log.info("SDK copied to %s", dst)
if __name__ == "__main__":
build_sdk(
spec_url=os.getenv("OPENAPI_SPEC_URL", "http://host.docker.internal:8000/openapi.json"),
package=os.getenv("PACKAGE_NAME", "my_client"),
dest=os.getenv("PACKAGE_PATH", "."),
)
4. Use the generated SDK
The generator produces a package containing a DefaultApi clas that exposes every enpdoint defined in the spec.
from enum import Enum
import pytest
from my_client import Configuration
from my_client.api.default_api import DefaultApi
from my_client.api_client import ApiClient
from my_client.exceptions import ServiceException
from my_client.models.operation import Operation
from my_client.models.operation_response import OperationResponse
from my_client.models.operation_type import OperationType
@pytest.fixture(scope="module")
def api():
cfg = Configuration(host="http://localhost:8000")
with ApiClient(cfg) as client:
yield DefaultApi(client)
def test_root(api: DefaultApi):
assert api.root_get() == {"message": "Hello World"}
def test_greet(api: DefaultApi):
assert api.say_hello_get(name="pytest") == {"message": "Hello pytest"}
def test_add(api: DefaultApi):
op = Operation(a=1, b=2, type=OperationType.ADD)
assert api.operation_post(op) == OperationResponse(result=3)
def test_sub(api: DefaultApi):
op = Operation(a=1, b=2, type=OperationType.SUB)
assert api.operation_post(op) == OperationResponse(result=-1)
def test_mul(api: DefaultApi):
op = Operation(a=1, b=2, type=OperationType.MUL)
assert api.operation_post(op) == OperationResponse(result=2)
def test_div(api: DefaultApi):
op = Operation(a=1, b=2, type=OperationType.DV)
assert api.operation_post(op) == OperationResponse(result=0.5)
def test_div_by_zero(api: DefaultApi):
with pytest.raises(ServiceException):
op = Operation(a=1, b=0, type=OperationType.DIV)
api.operation_post(op)
def test_operation_type_enum():
assert OperationType.ADD == "add"
assert OperationType.SUB == "sub"
assert OperationType.MUL == "mul"
assert OperationType.DIV == "div"
assert issubclass(OperationType, (str, Enum))