From vovk
Generates typed Python SDKs from Vovk APIs using vovk-python, with TypedDict shapes, JSON Lines streaming, client validation, and PyPI publishing workflows.
npx claudepluginhub finom/vovkThis skill uses the workspace's default tool permissions.
`vovk-python` generates typed Python package from same `.vovk-schema/` artifacts driving TS client. One source of truth, multi-language.
Creates distributable Python packages with src/flat layouts, pyproject.toml/setup.py, build backends like setuptools/poetry/hatchling, wheels/sdists, and PyPI publishing for libraries and CLI tools.
Generates type-safe client SDKs in TypeScript, Python, Go, Java from OpenAPI specs with auth, retries, pagination, and tests.
Share bugs, ideas, or general feedback.
vovk-python generates typed Python package from same .vovk-schema/ artifacts driving TS client. One source of truth, multi-language.
Experimental — generated API may shift between Vovk versions. Pin version on consume.
Covers:
vovk-python + generate Python package.py (standalone) vs pySrc (source files to embed).vovk.config.mjs.TypedDict per [MethodName][Input|Output|Body|Query|Params]).Out of scope:
procedure skill.vovk bundle general (TS, Rust) → bundle skill.jsonlines skill.openapi skill.npm i -D vovk-python
Emits complete Python package (PyPI-ready). Canonical output dir ./dist_python (hello-world convention):
npx vovk generate --from py --out ./dist_python
Output:
dist_python/
src/<package_name>/__init__.py # RPC functions + TypedDict definitions
src/<package_name>/api_client.py # HTTP client
src/<package_name>/py.typed # Type-hint marker
src/<package_name>/schema.json # Generated schema
pyproject.toml # hatchling build backend
setup.cfg
README.md
Emits .py files only — drop into existing Python project:
npx vovk generate --from pySrc --out ./python_src
Wire Python into vovk generate → runs alongside TS:
// vovk.config.mjs
const config = {
composedClient: {
fromTemplates: ['js', 'py'],
},
};
export default config;
Bake prod API URL via clientTemplateDefs.py.outputConfig.origin — generated client uses this by default. Pattern from hello-world:
// vovk.config.js
const PROD_ORIGIN = 'https://hello-world.vovk.dev';
const config = {
composedClient: { fromTemplates: ['js', 'py'] },
clientTemplateDefs: {
py: {
extends: 'py',
outputConfig: { origin: PROD_ORIGIN },
// composedClient: { outDir: './dist_python' }, // override output dir if needed
},
},
};
After this, every npx vovk generate (+ every vovk dev regen) refreshes Python client — PROD_ORIGIN baked in for consumers, overridable per-call via api_root.
Types follow [PascalCaseMethodName][Body|Query|Params|Output] as TypedDict. Methods static, snake_case. Positional arg order from generator (packages/vovk-python/client-templates/pySrc/__init__.py.ejs:39-49): only emits slots where validation declared, in this order: body, files (multipart only, sits between body and query), query, params, then always-present trailing kwargs headers, api_root, disable_client_validation.
from my_api_client import UserRPC # whatever package the generator wrote
body: UserRPC.UpdateUserBody = {
"email": "john@example.com",
"profile": {"name": "John Doe", "age": 25},
}
query: UserRPC.UpdateUserQuery = {"notify": "email"}
params: UserRPC.UpdateUserParams = {
"id": "123e4567-e89b-12d3-a456-426614174000",
}
# Keyword args work for clarity at call sites:
response = UserRPC.update_user(body=body, query=query, params=params)
Method names snake_case. Type names PascalCase.
filesProcedures with multipart/form-data accept files kwarg — pass Dict[str, Any] matching requests upload format:
with open("avatar.png", "rb") as f:
response = UserRPC.upload_avatar(
params={"id": user_id},
files={"avatar": ("avatar.png", f, "image/png")},
)
Procedures with iteration schemas (server-side async function* handlers — see jsonlines skill) generate sync Python generators client-side. Return type Generator[<MethodName>Iteration, None, None]. Generator only emits slots for validation declared on procedure — for an iteration-only endpoint with no body / query / params, signature is just trailing kwargs:
@staticmethod
def stream_tokens(
headers: Optional[Dict[str, str]] = None,
api_root: Optional[str] = None,
disable_client_validation: bool = False,
) -> Generator[StreamTokensIteration, None, None]:
...
Add body: StreamTokensBody, query: StreamTokensQuery, params: StreamTokensParams to the signature only when the procedure declares validation for them (same rule as JSON endpoints).
Yielded items typed against procedure's iteration schema — StreamTokensIteration is TypedDict → editors autocomplete fields.
from my_api_client import StreamRPC
for item in StreamRPC.stream_tokens():
print(item["message"], end="", flush=True)
Sync generators support usual idioms:
# Manual pull
stream = StreamRPC.stream_tokens()
first = next(stream)
second = next(stream)
# Collect into a list (drains the stream)
all_items = list(StreamRPC.stream_tokens())
# Comprehensions
messages = [item["message"] for item in StreamRPC.stream_tokens()]
# Early termination — break out of the for-loop
for item in StreamRPC.stream_tokens():
if item["message"] == "STOP":
break
handle(item)
# The underlying HTTP connection closes when the generator goes out of scope.
Streaming endpoints accept same input shape as JSON:
from my_api_client import ChatRPC
for token in ChatRPC.complete(
body={"messages": [{"role": "user", "content": "hello"}]},
query={"model": "gpt-4"},
):
print(token["text"], end="", flush=True)
HTTP fail or iteration validation fail → generator raises on next next(). Wrap in try/except to recover:
try:
for item in StreamRPC.stream_tokens():
handle(item)
except Exception as exc:
log.error("stream failed mid-iteration: %s", exc)
Generator sync — backed by requests, no async API. For asyncio, run iteration in thread pool (asyncio.to_thread or loop.run_in_executor) or wrap each next() call. Native async streaming not in current Python client.
See jsonlines skill for server-side handler.
Validation runs against schema.json by default — same Zod/Valibot shapes server enforces, cross-compiled to JSON Schema.
Skip for hot paths or server-only enforcement:
response = UserRPC.update_user(
params=params, body=body, query=query,
disable_client_validation=True,
)
Generated pyproject.toml declares (per hello-world):
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
requires-python = ">=3.8"
dependencies = ["requests", "jsonschema", "rfc3987", "urllib3==1.26.15"]
requests — HTTP client.jsonschema — client-side validation.rfc3987 — URI/IRI validation (used by jsonschema format checkers).urllib3==1.26.15 — pinned to dodge requests/urllib3 v2 compat issues.Build backend hatchling, no runtime overhead past deps above.
Generated client targets URL baked at generate time. Override per call via api_root kwarg — fully replaces baked-in value for that call. No module-level setter exists; the client = ApiClient(...) inside generated __init__.py is frozen at generate time. For dynamic auth, pass headers per call.
Don't bake secrets into generated package. Consumers supply API keys at runtime.
Canonical script from hello-world (build wheel + sdist, upload via twine):
python3 -m build ./dist_python --wheel --sdist && python3 -m twine upload ./dist_python/dist/*
Both python3 -m build and python3 -m twine invoke modules directly — works with any Python env that has them installed (pip install build twine). Run twine with creds via ~/.pypirc or TWINE_USERNAME / TWINE_PASSWORD env vars.
Wire into release flow alongside npm bundle + Rust crate (hello-world chains all three under postversion):
"scripts": {
"publish:node": "npm publish ./dist",
"publish:rust": "cargo publish --manifest-path dist_rust/Cargo.toml --allow-dirty",
"publish:python": "python3 -m build ./dist_python --wheel --sdist && python3 -m twine upload ./dist_python/dist/*",
"postversion": "vovk generate && vovk bundle && npm run publish:node && npm run publish:rust && npm run publish:python"
}
Version / package name flow from root package.json (generator copies whitelisted fields — name, version, description, license, author, contributors, repository, homepage, bugs, keywords — into generated pyproject.toml). Set version in root package.json, npm version patch propagates to all three targets.
Name transform — pin before publishing. Python package name = root name with hyphens → underscores (my-api → my_api). Same name used for filesystem path (src/<name>/), pyproject.toml name, pip install command in README. Two cases need explicit override:
@org/foo) — @ and / not transformed → default yields invalid Python name. Set override below.my-api, PyPI acme-client).Override via clientTemplateDefs.py.outputConfig.package.name (same field shape bundle skill documents):
// vovk.config.mjs
const config = {
composedClient: { fromTemplates: ['js', 'py'] },
clientTemplateDefs: {
py: {
extends: 'py',
outputConfig: {
origin: 'https://api.example.com',
package: { name: 'acme_client' }, // PyPI name + filesystem path
},
},
},
};
npm i -D vovk-python.'py' to composedClient.fromTemplates in vovk.config.mjs.npx vovk generate.pip install -e ./dist_python (or wherever generator wrote it).from vovk_hello_world import UserRPC and call (editable install exposes the package directly — no dist_python.src. prefix).npx vovk generate --from py --out ./python_package.info.title + info.version in vovk.config.mjs before generating.python3 -m build && twine upload dist/* from python_package/.Server defines procedure({ iteration }) with async function* handler (see jsonlines). Client iterates:
for item in StreamRPC.stream_tokens():
handle(item)
Use pySrc:
npx vovk generate --from pySrc --out ./my_project/vovk_client
Then import from my_project.vovk_client....
vovk-python version, expect breaks on upgrade. Integration-test after each bump.TypedDict, not classes. Generated shapes dict-based for JSON interop; treat as plain dicts with type hints, not Pydantic models.updateUser server → update_user(...) Python, UpdateUserBody etc.jsonschema — small import + runtime cost. Disable per-call for speed or server-only enforcement.api_root kwarg overrides the URL baked at generate time. No module-level setter; the ApiClient instance is frozen.vovk generate before Python client reflects. CI should regen as part of build.text/plain and application/octet-stream request bodies not fully supported yet (roadmap). For binary uploads, prefer multipart via files arg.$refs may fail to generate — Python generator can't yet resolve cycles in third-party OpenAPI specs. Roadmap item.components/schemas don't yet produce shared importable types. Each call site gets own scoped TypedDict instead of single UserSchema reused across methods. Roadmap item — until fixed, expect duplicated type definitions.