Skip to main content

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 on httpx.
  • 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 and clientUrl updates inside vms_json.assets and the fonts tree.
  • Storage-adapter contract (StorageAdapter / AsyncStorageAdapter) with a reference filesystem implementation; bring your own S3/GCS/CDN backend.
  • Typed on_finalize callback with a full context object (saved URLs, rewritten vms_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-supplied metadata["user_email"] / metadata["user_ip"] (reserved keys, refreshed on every save) or by any custom keys you set in metadata when 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 calls save_media for every file found under vms_media/.
  • An on_finalize callback — invoked once, after all IO, with a typed ContentFinalizeContext.
  • Optionally an on_pre_save callback — see Access control via on_pre_save hooks. Use it for extra validation on ctx.metadata (your session keys merged with editor-reserved user_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

verstka_sdk-0.1.8.tar.gz (33.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

verstka_sdk-0.1.8-py3-none-any.whl (35.2 kB view details)

Uploaded Python 3

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

Hashes for verstka_sdk-0.1.8.tar.gz
Algorithm Hash digest
SHA256 ab63ff429730c6f660aba36c070dcb89b20ed6237fea80e7e42f1f8dae3317c6
MD5 c8e61762a2d4d332e03cbe5c3f89229f
BLAKE2b-256 36d7e829d5cd60817b0a53ea4938ab0e94f14b4bb7c0e31582c09bdf2953fe20

See more details on using hashes here.

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

Hashes for verstka_sdk-0.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 d33e399997777a36e0d2b265fd6d5426c3e94572e4dd929ae5ac44ed5d5d166b
MD5 f005aba39ded7e667300d9662d32223f
BLAKE2b-256 6b866ab714177823021caea8522323efd01d0f807600a731ef544e62abb854b5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page