Vovk.ts Python client
SkillDev toolsThe Vovk.ts Python client — generating a typed Python package from a Vovk API with `vovk-python`, the `py` / `pySrc` templates, `TypedDict` shapes, JSON + JSON Lines consumption, client-side validation, and PyPI publishing. Use whenever the user wants a typed Python SDK, ongoing Python integration, or PyPI publishing — phrasings like "generate a Python client / SDK", "ship a Python package for my API", "use vovk-python", "pySrc template", "python client streaming", "publish to PyPI". Experimental package — shape may shift. Does NOT cover controller / procedure authoring → hand off to `procedure` skill. Does NOT cover `vovk bundle` in general → hand off to `bundle` skill. Does NOT cover JSON Lines server-side → hand off to `jsonlines` skill. **Does NOT cover one-off HTTP calls** — Vovk endpoints are plain REST, so a `curl`, `httpx`, or `requests` call against `/api/...` is the simpler answer for "I just want to hit my API from a Python script / CLI / shell" use cases. Reach for this skill only when the user actually wants generated typed client code.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Vovk.ts Python client skill
What this skill tells your AI
The instructions your AI receives, as published by finom/vovk in skills/python/SKILL.md and read by ahel’s review.
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.
Scope
Covers:
- Install
vovk-python+ generate Python package. - Templates:
py(standalone) vspySrc(source files to embed). - Auto-gen config in
vovk.config.mjs. - Call shape (
TypedDictper[MethodName][Input|Output|Body|Query|Params]). - JSON Lines streaming via Python generators.
- Client-side validation toggle.
- PyPI publishing flow.
Out of scope:
- Procedure / controller authoring →
procedureskill. vovk bundlegeneral (TS, Rust) →bundleskill.- Server-side JSON Lines →
jsonlinesskill. - OpenAPI spec tuning →
openapiskill.
Install + generate
npm i -D vovk-python
Standalone package
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
Embedded source
Emits .py files only — drop into existing Python project:
npx vovk generate --from pySrc --out ./python_src
Auto-generate with composed client
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.
Generated call shape
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.
Multipart uploads via files
Procedures 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")},
)
JSON Lines streaming
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.
Basic consumption
from my_api_client import StreamRPC
for item in StreamRPC.stream_tokens():
print(item["message"], end="", flush=True)
Standard generator patterns
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.
With body / query / params
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)
Error handling
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)
Sync, not async
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.
Client-side validation
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,
)
Dependencies pulled in
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 byjsonschemaformat checkers).urllib3==1.26.15— pinned to dodgerequests/urllib3v2 compat issues.
Build backend hatchling, no runtime overhead past deps above.
Auth + base URL
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.
PyPI publishing
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:
- Scoped packages (
@org/foo) —@and/not transformed → default yields invalid Python name. Set override below. - Different distribution name than npm (e.g., npm
my-api, PyPIacme-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
},
},
},
};
Flows
"Call my Vovk API from a Python script"
npm i -D vovk-python.- Add
'py'tocomposedClient.fromTemplatesinvovk.config.mjs. npx vovk generate.pip install -e ./dist_python(or wherever generator wrote it).from vovk_hello_world import UserRPCand call (editable install exposes the package directly — nodist_python.src.prefix).
"Ship a Python SDK to PyPI"
npx vovk generate --from py --out ./python_package.- Set
info.title+info.versioninvovk.config.mjsbefore generating. python3 -m build && twine upload dist/*frompython_package/.
"Consume a streaming endpoint from Python"
Server defines procedure({ iteration }) with async function* handler (see jsonlines). Client iterates:
for item in StreamRPC.stream_tokens():
handle(item)
"Embed the client into an existing Python project"
Use pySrc:
npx vovk generate --from pySrc --out ./my_project/vovk_client
Then import from my_project.vovk_client....
Gotchas
- Experimental. API shape not frozen — pin
vovk-pythonversion, 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.- Method names snake_case, type names PascalCase.
updateUserserver →update_user(...)Python,UpdateUserBodyetc. - Client-side validation uses
jsonschema— small import + runtime cost. Disable per-call for speed or server-only enforcement. - Base URL override is per-call only.
api_rootkwarg overrides the URL baked at generate time. No module-level setter; theApiClientinstance is frozen. - Regen on schema changes. Any procedure / schema change server-side needs
vovk generatebefore Python client reflects. CI should regen as part of build. - Don't hand-edit generated files. Put shared Python utilities alongside, not inside, generated package.
- Body content types past JSON + multipart limited.
text/plainandapplication/octet-streamrequest bodies not fully supported yet (roadmap). For binary uploads, prefer multipart viafilesarg. - Mixins with circular
$refsmay fail to generate — Python generator can't yet resolve cycles in third-party OpenAPI specs. Roadmap item. - Named schemas in
components/schemasdon't yet produce shared importable types. Each call site gets own scopedTypedDictinstead of singleUserSchemareused across methods. Roadmap item — until fixed, expect duplicated type definitions.
Signals
- GitHub stars
- 52
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
python-finom- Source
- github.com/finom/vovk