Python SDK for Verstka API v2: open editor sessions, verify callbacks, download and process material content (media + fonts).
Project description
verstka-sdk
Features:
- Sync (
VerstkaClient) and async (AsyncVerstkaClient) clients, both onhttpx. - Framework-agnostic core. Pluggable integrations for FastAPI, Flask, Django (async views), and Django REST Framework.
- HMAC-SHA256 signature verification for incoming callbacks.
- Streaming ZIP download with a configurable size cap and path-traversal protection.
- Automatic extraction of
vms_media/*,vms_json.json,vms_html.html, and font bundles (vms_fonts/*,vms_fonts.json,vms_fonts.css). - Automatic
dummy-*replacement in HTML/CSS andclientUrlupdates insidevms_json.assetsand the fonts tree. - Storage-adapter contract (
StorageAdapter/AsyncStorageAdapter) with a reference filesystem implementation; bring your own S3/GCS/CDN backend. - Typed
on_finalizecallback with a full context object (saved URLs, rewrittenvms_json/vms_html, metadata, etc.). - Optional PreSave hooks (
on_pre_save): run extra checks before any ZIP download or storage write — e.g. allow/deny by editor-suppliedmetadata["user_email"]/metadata["user_ip"](reserved keys, refreshed on every save) or by any custom keys you set inmetadatawhen opening the editor session. Python SDK for Verstka API v2. Handles signatures,session/open, callback processing, ZIP download, and media/font persistence through a storage adapter.
Installation
pip install verstka-sdk # core only
pip install 'verstka-sdk[fastapi]' # + FastAPI integration
pip install 'verstka-sdk[flask]' # + Flask integration
pip install 'verstka-sdk[django]' # + Django integration
pip install 'verstka-sdk[drf]' # + Django REST Framework integration
pip install 'verstka-sdk[all]' # all integrations
Minimum Python version: 3.10.
Configuration
from verstka_sdk import VerstkaConfig
config = VerstkaConfig(
api_key="verstka-api-key",
api_secret="verstka-api-secret",
callback_url="https://site.example/verstka/callback",
api_url="https://api.r2.verstka.org/integration",
max_content_size=200 * 1024 * 1024,
request_timeout=60.0,
download_timeout=120.0,
debug=False,
)
api_url defaults to https://api.r2.verstka.org/integration.
max_content_size defaults to 200 MiB when omitted.
Main methods
| Method | Where | Purpose |
|---|---|---|
VerstkaClient.get_editor_url(...) |
sync | Opens a session via POST /session/open and returns the editor URL. |
AsyncVerstkaClient.get_editor_url(...) |
async | Same for async applications. |
VerstkaClient.process_material_callback(...) |
sync | Handles article_saved: signature, ZIP, media, on_finalize. |
AsyncVerstkaClient.process_material_callback(...) |
async | Async article_saved handler. |
VerstkaClient.process_fonts_callback(...) |
sync | Handles site_fonts_updated: signature, fonts ZIP, font files, manifests. |
AsyncVerstkaClient.process_fonts_callback(...) |
async | Async site_fonts_updated handler. |
LocalStorageAdapter |
sync | Filesystem reference storage adapter. |
LocalAsyncStorageAdapter |
async | Async reference storage adapter. |
sign_material(...) |
helper | Builds HMAC for material_id:url. |
verify_signature(...) |
helper | Verifies HMAC safely. |
build_authorized_content_url(...) |
helper | Adds api_key and material_id to content_url for ZIP download. |
Open editor
from verstka_sdk import AsyncVerstkaClient, VerstkaConfig
async def open_editor(material_id: str, vms_json: dict | None) -> str:
async with AsyncVerstkaClient(VerstkaConfig(...)) as client:
return await client.get_editor_url(
material_id=material_id,
vms_json=vms_json,
metadata={
# optional: "anySiteAdditionalKey": "anySiteAdditionalValue",
# optional: "timeLimitedAuthToken": "cms-scope-token",
# optional: "customContainers": {},
# optional: "webhook_auth_user": "callback-user", # (see Callback Authorization)
# optional: "webhook_auth_password": "callback-password",
},
)
Both vms_json and metadata accept a dict or a JSON string. The SDK sends
metadata as a JSON object and automatically adds
version: "python_<sdk-version>". For Basic Auth or a Bearer token, pass
webhook_auth_user and optionally webhook_auth_password in metadata when
calling get_editor_url (see
Callback Authorization).
Sites usually pass timeLimitedAuthToken and customContainers, plus any site-specific keys (e.g. anySiteAdditionalKey: anySiteAdditionalValue).
Other custom keys are allowed — see
metadata in the API
docs. Values you pass are echoed in callbacks and available in StorageAdapter,
on_pre_save, and on_finalize.
Service keys: version_id, version_cdate, user_email, user_ip — the
Verstka backend adds or updates them in the callback after save;
webhook_auth_user and webhook_auth_password authorize the outgoing callback
(see
Callback Authorization).
Your callback handler usually ignores webhook_auth_*.
Open the editor in a separate tab:
<a href="/getEditorUrlScript" target="_blank" rel="noopener noreferrer">
Edit in Verstka
</a>
StorageAdapter
The SDK does not know where your site stores files — the adapter does.
from pathlib import Path
from collections.abc import Mapping
from typing import Any
class StorageAdapter:
def save_media(
self,
filename: str,
temp_path: Path,
material_id: str,
metadata: Mapping[str, Any],
) -> str: ...
def save_font_file(
self,
filename: str,
temp_path: Path,
material_id: str,
metadata: Mapping[str, Any],
) -> str: ...
def save_fonts_manifest(
self,
filename: str,
temp_path: Path,
material_id: str,
metadata: Mapping[str, Any],
) -> str: ...
AsyncStorageAdapter is the async twin — each method returns an awaitable.
Every call receives the trailing (material_id, metadata) pair so multi-tenant
adapters can route writes by metadata["AnyOtherKey"], metadata["tenant"],
environment, etc.
The SDK ships with filesystem-backed reference implementations:
from verstka_sdk import LocalStorageAdapter, LocalAsyncStorageAdapter
storage = LocalStorageAdapter(
root="/var/www/example.com/public/static/verstka-media",
base_url="https://cdn.example.com",
)
# → media: /var/www/example.com/public/static/verstka-media/materials/<material_id>/<filename>
# → fonts: /var/www/example.com/public/static/verstka-media/fonts/<filename>
# → URL: https://cdn.example.com/materials/<material_id>/<filename>
Writing a custom adapter (e.g. S3):
class S3Storage:
def __init__(self, bucket: str, cdn_url: str) -> None:
self.bucket = bucket
self.cdn_url = cdn_url.rstrip("/")
def save_media(self, filename, temp_path, material_id, metadata):
key = f"materials/{material_id}/{filename}"
s3_client.upload_file(str(temp_path), self.bucket, key)
return f"{self.cdn_url}/{key}"
def save_font_file(self, filename, temp_path, material_id, metadata):
key = f"fonts/{filename}"
s3_client.upload_file(str(temp_path), self.bucket, key)
return f"{self.cdn_url}/{key}"
def save_fonts_manifest(self, filename, temp_path, material_id, metadata):
return self.save_font_file(filename, temp_path, material_id, metadata)
Handling the material callback
The JSON body that Verstka POSTs to your callback_url looks like:
{
"material_id": "42",
"content_url": "https://api.r2.verstka.org/integration/download/<token>",
"metadata": {
"userId": 11,
"AnyOtherKey": "AnyOtherVal",
"user_email": "author@example.com",
"user_ip": "203.0.113.10"
}
}
Each method must persist the file and return a public URL. The SDK substitutes
these URLs into vms_html, vms_json.assets[*].clientUrl, vms_fonts.css, and
the fonts tree.
You provide:
- A
StorageAdapter/AsyncStorageAdapter— the SDK callssave_mediafor every file found undervms_media/. - An
on_finalizecallback — invoked once, after all IO, with a typedContentFinalizeContext. - Optionally an
on_pre_savecallback — see Access control viaon_pre_savehooks. Use it for extra validation onctx.metadata(your session keys merged with editor-reserveduser_email/user_ip, which are refreshed on every save) before the content ZIP is downloaded.
Material callback
from verstka_sdk import (
AsyncVerstkaClient,
ContentFinalizeContext,
ContentFinalizeResult,
)
async def on_content_finalize(ctx: ContentFinalizeContext) -> ContentFinalizeResult:
await db.save_article(
material_id=ctx.material_id,
html=ctx.vms_html,
vms_json=ctx.vms_json,
metadata=dict(ctx.metadata),
)
return ContentFinalizeResult(success=True, vms_json=ctx.vms_json)
result = await client.process_material_callback(
callback_data,
signature=request.headers.get("X-Verstka-Signature", ""),
storage=storage,
on_finalize=on_content_finalize,
)
return result.to_response()
ContentFinalizeContext:
| Field | Purpose |
|---|---|
material_id |
Material ID from your CMS. |
metadata |
Metadata from the callback. |
vms_json |
Article JSON with updated clientUrl values. |
vms_html |
Article HTML with dummy-* URLs replaced. |
saved_media_urls |
{filename: public_url} map. |
Fonts callback
from verstka_sdk import FontsFinalizeContext, FontsFinalizeResult
async def on_fonts_finalize(ctx: FontsFinalizeContext) -> FontsFinalizeResult:
await db.save_site_fonts(
fonts=ctx.fonts,
css_url=ctx.css_url,
json_url=ctx.json_url,
)
return FontsFinalizeResult(success=True, fonts=ctx.fonts)
result = await client.process_fonts_callback(
callback_data,
signature=request.headers.get("X-Verstka-Signature", ""),
storage=storage,
on_finalize=on_fonts_finalize,
)
on_finalize for fonts is optional. Without it, the SDK still saves files via
storage and returns the fonts tree with clientUrl to Verstka.
PreSave hooks
Both callback methods accept on_pre_save. The hook runs after signature
verification but before ZIP download — use it for permissions, locks, quotas,
and tenant policy.
from verstka_sdk import ContentPreSaveContext, PreSaveDecision
def can_save(ctx: ContentPreSaveContext) -> PreSaveDecision:
if not user_can_edit(ctx.metadata.get("timeLimitedAuthToken"), ctx.material_id):
return PreSaveDecision(allow=False, reason="Access denied")
return PreSaveDecision(allow=True)
If allow=False, the SDK skips the ZIP download, writes no files, and responds
to Verstka with rc: 0.
Framework integrations
| Framework | Tooling |
|---|---|
| FastAPI | install_exception_handlers(app), build_callback_router(...) |
| Flask | register_error_handlers(app), build_blueprint(...) |
| Django | build_callback_views(...), VerstkaExceptionMiddleware |
| DRF | build_callback_views(...), verstka_exception_handler |
Framework adapters register a single callback endpoint and dispatch to
process_material_callback or process_fonts_callback based on event.
Documentation
Full integration guide (Russian): frontend/docs/ru/dev/sdk-python.md
Related: API integration, site integration.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file verstka_sdk-0.1.8.tar.gz.
File metadata
- Download URL: verstka_sdk-0.1.8.tar.gz
- Upload date:
- Size: 33.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab63ff429730c6f660aba36c070dcb89b20ed6237fea80e7e42f1f8dae3317c6
|
|
| MD5 |
c8e61762a2d4d332e03cbe5c3f89229f
|
|
| BLAKE2b-256 |
36d7e829d5cd60817b0a53ea4938ab0e94f14b4bb7c0e31582c09bdf2953fe20
|
File details
Details for the file verstka_sdk-0.1.8-py3-none-any.whl.
File metadata
- Download URL: verstka_sdk-0.1.8-py3-none-any.whl
- Upload date:
- Size: 35.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d33e399997777a36e0d2b265fd6d5426c3e94572e4dd929ae5ac44ed5d5d166b
|
|
| MD5 |
f005aba39ded7e667300d9662d32223f
|
|
| BLAKE2b-256 |
6b866ab714177823021caea8522323efd01d0f807600a731ef544e62abb854b5
|