Python RPC Client (experimental)
Python client can be generated with vovk generate
command using py template or pySrc template.
Generate Python package with the CLI:
npx vovk generate --from py --out ./python_package
This will generate a Python package with the following structure:
- __init__.py
- api_client.py
- py.typed
- schema.json
- pyproject.toml
- setup.cfg
- README.md
The package is ready to be published to PyPI with the following command.
python3 -m build ./python_package --wheel --sdist && python3 -m twine upload ./python_package/dist/*
In case if you want to generate Python source only, to be used as part of another Python project, you can use pySrc
template:
npx vovk generate --from pySrc --out ./python_src
This will generate a Python source with the following structure:
- __init__.py
- api_client.py
- py.typed
- schema.json
Configuring the Python Client
The generation can be configured, so the client will be generated automatically with generate
command with no flags but also when vovk dev is run, that performs “hot generation” whenever the schema is changed. In order to do that, you need to add py
template to the composed client config:
/** @type {import('vovk').VovkConfig} */
const config = {
composedClient: {
fromTemplates: ['cjs', 'mjs', 'py'], // keep the default "cjs" and "mjs" templates
},
};
export default config;
The py template as well as some other templates has default outDir
(by default equals to ./dist_python
) for the composed client configuration, that can be changed by defining a template definition in the config:
/** @type {import('vovk').VovkConfig} */
const config = {
// ...
clientTemplateDefs: {
py: {
extends: 'py', // extends the built-in "py" template
composedClient: {
outDir: './my_dist_python', // updates the out directory for the composed client
},
},
},
};
export default config;
Generated Python Client Example
JSON endpoints
All the code below was copied with slight modifications from the real example, explained at Hello World page.
A controller like this:
… will emit Vovk.ts Schema, that in its turn will be used to generate the Python client, following the language conventions where possible, adding comments defined at schema description
, and generating the most suitable types. For example, the age
is generated as int
.
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://vovk-hello-world.vercel.app/api')
class UserRPC:
# UserRPC.update_user POST `https://vovk-hello-world.vercel.app/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 the __init__.py
file, that contains all the RPC methods as functions but also types that are used in the RPC methods. The nested data structures are generated as TypedDict
s.
The types of body
, query
and params
can be obtained from the RPC module using pattern [PascalCaseMethodName][InputType]
. For example, a controller method originaly named updateUser
will have types UpdateUserBody
, UpdateUserQuery
and UpdateUserParams
generated.
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}")
Behind the scenes it uses requests for HTTP requests, jsonschema for client-side validation and some other libraries.
JSONLines endpoints
To implement continious streaming with JSONLines endpoints, the client uses Generator
to that can be used to iterate over the streamed data.
A controller like this:
Will be compiled to the following Python code:
class StreamRPC:
# StreamRPC.stream_tokens GET `https://vovk-hello-world.vercel.app/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
)
That can be used like this:
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}")