Python RPC Client (Experimental)
Generate the Python client with vovk generate using the py or pySrc template.
Create a Python package with the CLI:
npx vovk generate --from py --out ./python_packageThis produces:
- __init__.py
- api_client.py
- py.typed
- schema.json
- pyproject.toml
- setup.cfg
- README.md
Publish to PyPI with:
python3 -m build ./python_package --wheel --sdist && python3 -m twine upload ./python_package/dist/*If you prefer generating source files to embed in another Python project, use the pySrc template:
npx vovk generate --from pySrc --out ./python_srcThis generates:
- __init__.py
- api_client.py
- py.typed
- schema.json
Configuring the Python Client
You can configure generation so the client is produced automatically by the default generate command (no flags) and during vovk dev, which performs “hot generation” on schema changes. Add the py template to the composed client config:
/** @type {import('vovk').VovkConfig} */
const config = {
composedClient: {
fromTemplates: ['cjs', 'mjs', 'py'], // keeps the default "cjs" and "mjs" templates
},
};
export default config;The py template (and others) has a default outDir (./dist_python) for composed clients. Override it via template definitions:
/** @type {import('vovk').VovkConfig} */
const config = {
// ...
clientTemplateDefs: {
py: {
extends: 'py', // extends the built-in "py" template
composedClient: {
outDir: './my_dist_python', // custom output directory for the composed client
},
},
},
};
export default config;Generated Python Client Example
JSON Endpoints
The snippets below are adapted from a real example described on the Hello World page.
A controller like this:
…emits a Vovk.ts schema, which is then used to generate the Python client, following Python conventions, adding comments from description, and selecting appropriate number types. For example, age is generated as int, matching the controller definition.
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional, Set, TypedDict, Union, Tuple, Generator # type: ignore
from .api_client import ApiClient, HttpException
HttpException = HttpException
client = ApiClient('https://hello-world.vovk.dev/api')
class UserRPC:
# UserRPC.update_user POST `https://hello-world.vovk.dev/api/users/{id}`
class __UpdateUserBody_profile(TypedDict):
"""
User profile object
"""
name: str
age: int
class UpdateUserBody(TypedDict):
"""
User data object
"""
email: str
profile: UserRPC.__UpdateUserBody_profile
class UpdateUserQuery(TypedDict):
"""
Query parameters
"""
notify: Literal["email", "push", "none"]
class UpdateUserParams(TypedDict):
"""
Path parameters
"""
id: str
class UpdateUserOutput(TypedDict):
"""
Response object
"""
success: bool
@staticmethod
def update_user(
body: UpdateUserBody,
query: UpdateUserQuery,
params: UpdateUserParams,
headers: Optional[Dict[str, str]] = None,
files: Optional[Dict[str, Any]] = None,
api_root: Optional[str] = None,
disable_client_validation: bool = False
) -> UpdateUserOutput:
"""
Update user
Description: Update user by ID
Body: User data object
Query: Query parameters
Returns: Response object
"""
return client.request( # type: ignore
segment_name='',
rpc_name='UserRPC',
handler_name='updateUser',
body=body,
query=query,
params=params,
headers=headers,
files=files,
api_root=api_root,
disable_client_validation=disable_client_validation
)All RPC modules are generated in __init__.py, which contains the RPC functions and the associated types. Types for nested structures are emitted as TypedDicts.
Types for body, query, and params follow the pattern [PascalCaseMethodName][InputType]. For a method updateUser, you get UpdateUserBody, UpdateUserQuery, and UpdateUserParams.
from dist_python.src.vovk_hello_world import UserRPC
import vovk_hello_world
def main() -> None:
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"}
# Update user using local module
update_user_response = UserRPC.update_user(
params=params,
body=body,
query=query
)
print('UserRPC.update_user:', update_user_response)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"Error: {e}")Under the hood, it uses requests for HTTP, jsonschema for client-side validation, and other common libraries.
JSON Lines Endpoints
For continuous streaming with JSON Lines endpoints, the Python client returns a Generator you can iterate over.
A controller like this:
Compiles to:
class StreamRPC:
# StreamRPC.stream_tokens GET `https://hello-world.vovk.dev/api/streams/tokens`
class StreamTokensIteration(TypedDict):
"""
Streamed token object
"""
message: str
@staticmethod
def stream_tokens(
headers: Optional[Dict[str, str]] = None,
files: Optional[Dict[str, Any]] = None,
api_root: Optional[str] = None,
disable_client_validation: bool = False
) -> Generator[StreamTokensIteration, None, None]:
"""
Stream tokens
Description: Stream tokens to the client
"""
return client.request( # type: ignore
segment_name='',
rpc_name='StreamRPC',
handler_name='streamTokens',
headers=headers,
files=files,
api_root=api_root,
disable_client_validation=disable_client_validation
)Usage:
from dist_python.src.vovk_hello_world import StreamRPC # local module
import vovk_hello_world
def main() -> None:
stream_response = StreamRPC.stream_tokens()
print("streamTokens:")
for item in stream_response:
print(item['message'], end='', flush=True)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"Error: {e}")Roadmap
- 🐞 Allow circular $refs with OpenAPI Mixins.
- ✨ Generate importable types for named schemas defined in
components/schemas.