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_APPS regardless of whether they live as sub-packages or flat modules on sys.path.

  • Context variables - current_user, ignore_permissions_ctx, current_request, request_perms_cache, and current_router ContextVars that flow through the async task tree for the duration of a single request.

  • FlexibleAdapter - bootstraps OPENVIPER_SETTINGS_MODULE and runs management commands; used by the viperctl CLI 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

makemigrations

Generate or update JSON schema files for changed models.

migrate

Apply pending schema changes and run data patches.

createsuperuser

Interactively create an admin superuser.

changepassword

Change a user’s password interactively.

console

Open a Python REPL with models and settings pre-loaded.

start-server

Start the ASGI development server.

start-worker

Start the background task worker and scheduler.

collectstatic

Collect static assets into STATIC_ROOT.

create-app

Scaffold a new app package with models, views, and routes.

create-provider

Scaffold a new AI provider package with tests and README.

create-command

Scaffold a new management command inside an app.

test

Run the project test suite via pytest.

backup-db

Create a compressed database backup archive.

restore-db

Restore a database from a .tar.gz or .sql backup archive.

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.

get_app_dirs() list[Path]

Return a list of resolved directories for all installed apps.

openviper.core.context.current_user

contextvars.ContextVar holding the authenticated user for the current async task. Set by AuthenticationMiddleware.

openviper.core.context.ignore_permissions_ctx

contextvars.ContextVar (bool) used by the ORM permission layer. When True, all model-level permission checks are bypassed for the current async task.

openviper.core.context.current_request

contextvars.ContextVar holding 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 to None (not a mutable dict) to prevent cross-request cache leakage.

openviper.core.context.current_router

contextvars.ContextVar holding the active Router instance 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), and mimetype fields.

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 AttachmentData instances.

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; .html templates produce HTML only; .txt templates 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.

openviper.core.email.attachments.is_private_hostname(hostname) bool

Return True if hostname resolves to a private, loopback, link-local, or reserved IP address. Used to block SSRF attacks on URL attachments.

openviper.core.email.attachments.detect_mimetype(filename, content, explicit) str

Determine MIME type preferring explicit hint, then magic bytes, then file extension. Validates explicit types against the type/subtype format required by RFC 2045 §5.1.

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

app_labels

positional, varargs

all apps

App labels to generate schemas for

--check

flag

False

Exit non-zero if schema changes are needed (no files written)

--force

flag

False

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

app_label

positional, optional

all apps

Only sync this app

--database

string

default

Database alias to sync

-v / --verbose

flag

False

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

--username

string

prompted

Username for the superuser

--email

string

prompted

Email address

--no-input

flag

False

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

username

positional, optional

prompted

Username to change password for

--password

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

--no-models

flag

False

Don’t auto-import models from INSTALLED_APPS

-c / --command

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

--host

string

127.0.0.1

Bind address

--port

int

8000

Bind port

--reload

flag

True

Auto-reload on file changes (forced off with --no-reload)

--no-reload

flag

Disable auto-reload

--workers

int

1

Number of worker processes

app

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

--modules

list

auto-discovered

Additional task modules to import

--queues

list

all

Specific queues to consume

--threads

int

8

Threads per process

--processes

int

1

Number of worker processes

--no-scheduler

flag

False

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

--no-input

flag

False

Skip confirmation prompt

--clear

flag

False

Clear STATIC_ROOT before collecting

--dry-run

flag

False

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

name

positional, required

Name of the new app (snake_case recommended)

--directory / -d

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

name

positional, required

Provider name in snake_case (e.g. my_provider)

--output-dir / -o

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

command_name

positional, required

Name of the new command (snake_case)

app_name

positional, required

App that will own this command

--directory / -d

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

test_labels

positional, varargs

all

Specific test paths/labels passed to pytest

-v / --verbose

count

0

Increase verbosity (repeatable)

--failfast / -x

flag

False

Stop on first failure

--keepdb

flag

False

Preserve test database (alias for --reuse-db)

--create-db

flag

False

Force creation of the test database

--reuse-db

flag

False

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

recipients

list[str] | str

Required. One or more email addresses.

subject

str

Required. Email subject line.

text

str | None

Plain-text body. Required unless template is provided.

html

str | None

HTML body. If omitted and template ends in .md, HTML is generated.

template

str | None

Jinja2 template name (resolved against TEMPLATES_DIR).

context

dict | None

Template context variables.

attachments

list | None

List of attachments (see below).

background

bool | None

True to enqueue, False to send immediately, None for auto.

fail_silently

bool | None

Suppress exceptions on delivery failure.

sender

str | None

Override the DEFAULT_FROM_EMAIL sender address.

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 http and https schemes 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 the EMAIL_* 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 set of 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

pg_dump, psql

apt install postgresql-client / brew install libpq

MariaDB / MySQL

mysqldump, mysql

apt install mariadb-client / brew install mariadb

Oracle

expdp, impdp

Included with Oracle Instant Client

SQL Server

sqlcmd

apt install mssql-tools / ODBC driver package

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

pg_dump --format=plain

psql

MariaDB / MySQL

mysqldump

mysql

Oracle

expdp (Data Pump)

impdp

SQL Server

sqlcmd BACKUP DATABASE

sqlcmd RESTORE DATABASE

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
Metadata fields

Field

Description

database_name

Logical name derived from the database URL

db_engine

Engine identifier (sqlite, postgres, …)

timestamp

UTC ISO-8601 timestamp when the backup was taken

filename

Archive filename

openviper_version

Version of OpenViper used to create the backup

checksum

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.

Common errors

Error

Cause / fix

No database URL configured

No DATABASES['default']['OPTIONS']['URL'] in settings and --db not supplied.

Unsupported database scheme '<scheme>'

URL prefix not in the engine registry. Check the URL format.

Backup file not found

The path supplied to restore-db does not exist.

No 'backup.sql' member found in archive

Archive is corrupt or was not created by backup-db.

pg_dump / psql exits non-zero

PostgreSQL client tools not on PATH, or wrong credentials.

mysqldump / mysql exits non-zero

MariaDB/MySQL client tools not on PATH, or wrong credentials.

Path traversal detected

--path or file argument contains .. sequences.

Configuration Reference

backup-db arguments:

restore-db arguments: