Core & CLI
The openviper.core package contains internal machinery for application
bootstrapping, request-context variables, and the flexible app resolver.
The viperctl CLI command provides management operations (schema
synchronization, console, worker, etc.) for projects with non-standard
directory layouts.
Overview
openviper.core is not typically used directly in application code. It
exposes the following building blocks:
AppResolver - discovers app modules from
INSTALLED_APPSregardless of whether they live as sub-packages or flat modules onsys.path.Context variables -
current_user,ignore_permissions_ctx,current_request,request_perms_cache, andcurrent_routerContextVars that flow through the async task tree for the duration of a single request.FlexibleAdapter - bootstraps
OPENVIPER_SETTINGS_MODULEand runs management commands; used by theviperctlCLI entry point.Email subsystem - async email delivery with template rendering, attachment resolution (files, URLs, bytes), SSRF protection, MIME validation, and pluggable backends (console, SMTP).
The viperctl sub-command is exposed through the openviper CLI (see
Command-Line Interface for the full openviper command reference) and supports the
following management commands:
Command |
Description |
|---|---|
|
Generate or update JSON schema files for changed models. |
|
Apply pending schema changes and run data patches. |
|
Interactively create an admin superuser. |
|
Change a user’s password interactively. |
|
Open a Python REPL with models and settings pre-loaded. |
|
Start the ASGI development server. |
|
Start the background task worker and scheduler. |
|
Collect static assets into |
|
Scaffold a new app package with models, views, and routes. |
|
Scaffold a new AI provider package with tests and README. |
|
Scaffold a new management command inside an app. |
|
Run the project test suite via pytest. |
|
Create a compressed database backup archive. |
|
Restore a database from a |
Key Classes & Functions
- class openviper.core.app_resolver.AppResolver
Resolves physical filesystem paths for each entry in
INSTALLED_APPS. Handles both package-style apps (myproject.blog) and flat modules.
- openviper.core.context.current_user
contextvars.ContextVarholding the authenticated user for the current async task. Set byAuthenticationMiddleware.
- openviper.core.context.ignore_permissions_ctx
contextvars.ContextVar(bool) used by the ORM permission layer. WhenTrue, all model-level permission checks are bypassed for the current async task.
- openviper.core.context.current_request
contextvars.ContextVarholding the current HTTP request object for the active async task.
- openviper.core.context.request_perms_cache
contextvars.ContextVar(dict | None) caching per-request permission lookups. Defaults toNone(not a mutable dict) to prevent cross-request cache leakage.
- openviper.core.context.current_router
contextvars.ContextVarholding the activeRouterinstance so that response helpers (e.g.RedirectResponse) can resolve named routes.
- openviper.core.email.sender.send_email(recipients, subject, ...) bool
Primary entry point for sending email. Supports immediate or background delivery, Jinja2 templates, Markdown rendering, and attachments.
- class openviper.core.email.attachments.AttachmentData
Normalized attachment payload with
filename,content(bytes), andmimetypefields.
- openviper.core.email.attachments.resolve_attachments(attachments) list[AttachmentData]
Resolve a mixed list of attachment inputs (bytes, dicts, tuples, file paths, URLs) into a list of
AttachmentDatainstances.
- openviper.core.email.templates.render_template_content(template, context=None, template_dir='templates') tuple[str | None, str | None]
Render a Jinja2 template and return
(text, html). Markdown templates (.md) produce both text and HTML;.htmltemplates produce HTML only;.txttemplates produce text only.
- class openviper.core.email.backends.EmailBackend
Protocol that backends must implement. Provides
send(message_data).
- class openviper.core.email.backends.ConsoleBackend
Prints the email message to stdout. Useful for development.
- class openviper.core.email.backends.SMTPBackend
Sends email via SMTP using
EMAIL_*settings.
Example Usage
Running Management Commands
There are two ways to run viperctl commands:
Using ``openviper viperctl`` (works from any project directory):
# Auto-detect project from CWD (use '.' for current directory)
openviper viperctl makemigrations .
openviper viperctl migrate .
# Specify a module name explicitly
openviper viperctl --settings myproject.settings makemigrations myapp
# Interactive console
openviper viperctl console .
# Start a background task worker
openviper viperctl start-worker .
# Collect static files
openviper viperctl collectstatic .
# Start the development server
openviper viperctl start-server .
Using ``python viperctl.py`` (from a project with a viperctl.py script):
# Generated by 'openviper create-project'
cd myproject
python viperctl.py makemigrations
python viperctl.py migrate
python viperctl.py console
python viperctl.py start-server
python viperctl.py start-worker
Both approaches are equivalent. openviper viperctl auto-discovers
the project layout from the current working directory, while
python viperctl.py uses the settings module configured in the script.
Target resolution for ``openviper viperctl``:
.- auto-detect the project from the current working directory. Works for both root-layout projects (e.g.examples/fx) and module-organized projects (e.g.examples/ai_moderation_platform).myproject- a dotted module path resolved relative to the CWD.
Command Reference
makemigrations
Generate or update JSON schema files for model changes.
python viperctl.py makemigrations
python viperctl.py makemigrations blog users
python viperctl.py makemigrations --check
python viperctl.py makemigrations --force
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, varargs |
all apps |
App labels to generate schemas for |
|
flag |
|
Exit non-zero if schema changes are needed (no files written) |
|
flag |
|
Force type changes that would normally be rejected |
migrate
Apply pending schema changes to the database.
python viperctl.py migrate
python viperctl.py migrate blog
python viperctl.py migrate -v
python viperctl.py migrate --database default
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, optional |
all apps |
Only sync this app |
|
string |
|
Database alias to sync |
|
flag |
|
Show detailed operation output |
createsuperuser
Create an admin superuser account interactively.
Note
Passwords entered interactively must be at least 8 characters long
(enforced by MIN_CLI_PASSWORD_LENGTH). Blank passwords and
mismatched confirmations are rejected.
python viperctl.py createsuperuser
python viperctl.py createsuperuser --username admin --email admin@example.com
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
string |
prompted |
Username for the superuser |
|
string |
prompted |
Email address |
|
flag |
|
Skip interactive prompts |
changepassword
Change a user’s password.
python viperctl.py changepassword alice
python viperctl.py changepassword alice --password newsecret
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, optional |
prompted |
Username to change password for |
|
string |
prompted |
New password (skips interactive prompt) |
console
Start an IPython REPL with models and settings pre-loaded.
python viperctl.py console
python viperctl.py console --no-models
python viperctl.py console -c "print(await Post.objects.count())"
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
flag |
|
Don’t auto-import models from INSTALLED_APPS |
|
string |
none |
Execute the given command string and exit |
start-server
Start the ASGI development server using uvicorn.
python viperctl.py start-server
python viperctl.py start-server --host 0.0.0.0 --port 8080
python viperctl.py start-server --no-reload --workers 4
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
string |
|
Bind address |
|
int |
|
Bind port |
|
flag |
|
Auto-reload on file changes (forced off with |
|
flag |
Disable auto-reload |
|
|
int |
|
Number of worker processes |
|
positional, optional |
auto |
ASGI application module path |
start-worker
Start the background task worker and scheduler.
python viperctl.py start-worker
python viperctl.py start-worker --processes 2 --threads 16
python viperctl.py start-worker --queues emails default
python viperctl.py start-worker --no-scheduler
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
list |
auto-discovered |
Additional task modules to import |
|
list |
all |
Specific queues to consume |
|
int |
|
Threads per process |
|
int |
|
Number of worker processes |
|
flag |
|
Disable the periodic scheduler thread |
collectstatic
Copy static files from all STATICFILES_DIRS into STATIC_ROOT.
python viperctl.py collectstatic
python viperctl.py collectstatic --clear
python viperctl.py collectstatic --dry-run
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
flag |
|
Skip confirmation prompt |
|
flag |
|
Clear STATIC_ROOT before collecting |
|
flag |
|
Show what would be collected without copying |
create-app
Scaffold a new application directory with models, views, routes, and tests.
python viperctl.py create-app blog
python viperctl.py create-app blog --directory /projects/myproject
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, required |
Name of the new app (snake_case recommended) |
|
|
string |
CWD |
Base directory for the new app |
create-provider
Scaffold a new custom AI provider package with tests and README.
python viperctl.py create-provider my_provider
python viperctl.py create-provider my_provider --output-dir ./packages
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, required |
Provider name in snake_case (e.g. |
|
|
string |
CWD |
Output directory for the provider package |
create-command
Scaffold a new management command file inside an app.
python viperctl.py create-command cleanup blog
python viperctl.py create-command cleanup blog --directory /projects/myproject
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, required |
Name of the new command (snake_case) |
|
|
positional, required |
App that will own this command |
|
|
string |
CWD |
Base directory |
test
Run the project test suite using pytest.
python viperctl.py test
python viperctl.py test tests/test_models.py -v
python viperctl.py test --failfast --keepdb
Argument |
Type |
Default |
Description |
|---|---|---|---|
|
positional, varargs |
all |
Specific test paths/labels passed to pytest |
|
count |
|
Increase verbosity (repeatable) |
|
flag |
|
Stop on first failure |
|
flag |
|
Preserve test database (alias for |
|
flag |
|
Force creation of the test database |
|
flag |
|
Reuse existing test database instead of recreating |
backup-db
Create a compressed database backup archive. See the Database Backup & Restore section below for full details.
python viperctl.py backup-db
python viperctl.py backup-db --path /var/backups --name myapp_prod
restore-db
Restore a database from a .tar.gz or .sql backup archive. See the
Database Backup & Restore section below for full details.
python viperctl.py restore-db backup.tar.gz --force
Accessing the Current User in Async Code
from openviper.core.context import current_user
async def my_service_function() -> None:
user = current_user.get()
if user and user.is_authenticated:
print(f"Called by: {user.username}")
Bypassing Permissions for Internal Operations
from openviper.db.executor import bypass_permissions
from myapp.models import SensitiveRecord
async def migrate_records() -> None:
with bypass_permissions():
records = await SensitiveRecord.objects.all()
for record in records:
await record.save()
Email Subsystem
The openviper.core.email package provides async email delivery with
template rendering, attachment resolution, pluggable backends, and background
queue support.
Sending Email
The primary entry point is send_email():
from openviper.core.email import send_email
# Plain-text email
await send_email(
recipients=["user@example.com"],
subject="Welcome",
text="Hello from OpenViper!",
)
# HTML + text with a Jinja2 template
await send_email(
recipients=["user@example.com"],
subject="Order Confirmation",
template="order_confirmation.html",
context={"order_id": "ABC-123"},
)
# Background delivery (enqueued to a worker)
await send_email(
recipients=["admin@example.com"],
subject="Report Ready",
text="Your report is available.",
background=True,
)
Key parameters:
Parameter |
Type |
Description |
|---|---|---|
|
|
Required. One or more email addresses. |
|
|
Required. Email subject line. |
|
|
Plain-text body. Required unless |
|
|
HTML body. If omitted and |
|
|
Jinja2 template name (resolved against |
|
|
Template context variables. |
|
|
List of attachments (see below). |
|
|
|
|
|
Suppress exceptions on delivery failure. |
|
|
Override the |
Template Rendering
render_template_content() resolves
Jinja2 templates and optionally renders Markdown to HTML:
from openviper.core.email.templates import render_template_content
# Markdown template → (text, html)
text, html = render_template_content("welcome.md", context={"name": "Ada"})
# HTML-only template → (None, html)
text, html = render_template_content("invoice.html", context={"total": 99.0})
# Plain-text template → (text, None)
text, html = render_template_content("receipt.txt", context={"id": "TX-42"})
Security: Template names are validated to prevent path traversal. Null
bytes, .. sequences, absolute paths, and double-encoded sequences
(%252f) are all rejected with ValueError.
Attachments
resolve_attachments() accepts a
flexible mix of attachment inputs:
from openviper.core.email import send_email, AttachmentData
# Bytes payload
await send_email(
recipients=["user@example.com"],
subject="Report",
text="See attached.",
attachments=[
AttachmentData(filename="report.csv", content=b"a,b\n1,2"),
],
)
# File path (must be inside ATTACHMENT_ALLOWED_DIRS)
await send_email(
recipients=["user@example.com"],
subject="Log",
text="Attached.",
attachments=["/var/app/uploads/debug.log"],
)
# URL attachment (fetched over HTTP/HTTPS)
await send_email(
recipients=["user@example.com"],
subject="Data",
text="Fetched from remote.",
attachments=["https://example.com/data.csv"],
)
# Dict with explicit mimetype
await send_email(
recipients=["user@example.com"],
subject="Chart",
text="See chart.",
attachments=[{"content": b"...", "filename": "chart.png", "mimetype": "image/png"}],
)
Attachment Security
The attachment subsystem enforces multiple security layers:
File path restrictions. ATTACHMENT_ALLOWED_DIRS is a module-level list
that gates which directories are safe for file attachments. When empty
(the default), file attachments are disabled entirely - calling
resolve_file_attachment() raises
ValueError. Populate it with trusted directories:
from openviper.core.email import attachments
attachments.ATTACHMENT_ALLOWED_DIRS = ["/var/app/uploads", "/tmp/attachments"]
All paths are resolved (Path.resolve()) and checked with
is_relative_to() to prevent .. traversal and symlink attacks.
SSRF protection. URL attachments are validated before any network request:
Only
httpandhttpsschemes are allowed.is_private_hostname()resolves the hostname and blocks private/loopback/link-local/reserved IP ranges.NoRedirectHandler()rejects HTTP redirects so every target is validated before access.validate_public_ip_address()checks the connected peer address to block DNS-rebinding attacks.Responses larger than
MAX_ATTACHMENT_BYTES(25 MiB by default) are rejected.
MIME type validation. Explicit MIME types passed via the mimetype
key or tuple third element are validated against the type/subtype format
required by RFC 2045 §5.1. Invalid values (e.g. "image" or
"/png") raise ValueError.
CRLF injection. Filenames are stripped of \\r and \\n characters
to prevent header injection in the generated MIME messages.
Email address validation. Control characters (\\x00–\\x1f,
\\x7f) in recipient addresses are rejected by
normalize_addresses().
Delivery Backends
Two built-in backends are provided:
ConsoleBackend- prints the message to stdout (useful for development).SMTPBackend- sends via SMTP using theEMAIL_*settings.
The backend is selected via the EMAIL_BACKEND setting key (defaults to
"console"). Custom backends can implement the
EmailBackend protocol.
Background Queue
When background=True is passed to send_email(),
the message is serialized and enqueued for a worker process:
from openviper.core.email.queue import enqueue_email_job, worker_available
if worker_available():
await send_email(..., background=True) # enqueued
else:
await send_email(..., background=False) # immediate fallback
enqueue_email_job() serializes the
EmailMessageData payload and dispatches
it to the configured task broker.
Context Variables
The email subsystem uses the following module-level configuration:
- openviper.core.email.attachments.ATTACHMENT_ALLOWED_DIRS
A
list[str]of directory paths that are safe for file attachment resolution. When empty (the default), file attachments are disabled entirely. Populate with trusted directories before using file-path attachments.
- openviper.core.email.attachments.MAX_ATTACHMENT_BYTES
Maximum size (in bytes) for a single attachment. Defaults to 25 MiB. Attachments exceeding this limit raise
ValueError.
- openviper.core.email.attachments.ALLOWED_URL_SCHEMES
A
setof permitted URL schemes for remote attachments. Defaults to{"http", "https"}.
Database Backup & Restore
The backup-db and restore-db commands are built-in management commands
available in every project via viperctl.py.
System Requirements
Each database engine requires the corresponding native CLI tools to be
installed and available on PATH:
Engine |
Required tools |
Install hint |
|---|---|---|
PostgreSQL |
|
|
MariaDB / MySQL |
|
|
Oracle |
|
Included with Oracle Instant Client |
SQL Server |
|
|
SQLite |
(none) |
SQLite is backed up by file copy - no extra tools needed |
Usage
Backup a database:
python viperctl.py backup-db
Back up to a custom directory:
python viperctl.py backup-db --path /var/backups/myapp
Custom filename (without extension):
python viperctl.py backup-db --name myapp_prod
Specify a database URL explicitly:
python viperctl.py backup-db --db postgresql://user:pass@host/mydb
Skip compression (produce a plain .sql file):
python viperctl.py backup-db --no-compress
Restore from an archive:
python viperctl.py restore-db postgres_20260404-121212.tar.gz
Force overwrite of the existing database:
python viperctl.py restore-db postgres_20260404-121212.tar.gz --force
Restore to an explicit database URL:
python viperctl.py restore-db backup.tar.gz --db postgresql://user:pass@host/mydb
Restore from a plain SQL file:
python viperctl.py restore-db backup.sql --db sqlite:///db.sqlite3
Supported Databases
Engine |
Backup Method |
Restore Method |
|---|---|---|
SQLite |
File copy (async) |
File copy |
PostgreSQL |
|
|
MariaDB / MySQL |
|
|
Oracle |
|
|
SQL Server |
|
|
Archive Format
Each compressed backup produces a .tar.gz file containing:
postgres_20260404-121212.tar.gz
└── backup.sql # database dump or raw file copy
A sidecar metadata file is written alongside the archive:
postgres_20260404-121212.tar.gz.meta.json
Field |
Description |
|---|---|
|
Logical name derived from the database URL |
|
Engine identifier ( |
|
UTC ISO-8601 timestamp when the backup was taken |
|
Archive filename |
|
Version of OpenViper used to create the backup |
|
SHA-256 hex digest of the archive |
Backup files are named automatically using UTC datetime:
{database_name}_{YYYYMMDD-HHMMSS}.tar.gz
Example filenames:
postgres_20260404-121212.tar.gz
sqlite_20260404-121212.tar.gz
Workflow Examples
Backup before migrations:
python viperctl.py backup-db --path ./backups
python viperctl.py migrate
# Roll back if needed:
python viperctl.py restore-db ./backups/sqlite_20260404-121212.tar.gz --force
Production PostgreSQL backup:
python viperctl.py backup-db \\
--db postgresql://appuser:password@db.prod.internal/appdb \\
--path /backups/daily \\
--name appdb_daily
Restore workflow:
python viperctl.py restore-db \\
/backups/daily/appdb_daily_20260404-020000.tar.gz \\
--db postgresql://appuser:password@db.prod.internal/appdb \\
--force
Scheduled nightly backup (cron):
# crontab -e
0 2 * * * cd /srv/myapp && /srv/venv/bin/python viperctl.py backup-db \\
--path /backups/nightly \\
--name myapp_nightly \\
>> /var/log/openviper_backup.log 2>&1
Error Handling
Both commands exit with code 1 on failure and print an error message
to stderr.
Error |
Cause / fix |
|---|---|
|
No |
|
URL prefix not in the engine registry. Check the URL format. |
|
The path supplied to |
|
Archive is corrupt or was not created by |
|
PostgreSQL client tools not on |
|
MariaDB/MySQL client tools not on |
|
|
Configuration Reference
backup-db arguments:
restore-db arguments: