Welcome to Mel

Your application is running. Install a plugin to add more features.

Developer Quick Start

This page is rendered by the route welcome in web/app.py, which calls bootstrap.render("pages/welcome.html"). The template extends base.html and its content replaces the {{ child_block }} placeholder.

Project structure
web/app.py            # Entry point — routes are defined here or in web/routers/
web/bootstrap.py      # Composition root — DI, render(), services, MelApp instance
domain/model.py       # Pydantic models → auto-mapped to SQL tables
services/             # Business logic (one file per service)
repository/repo.py    # Raw SQL queries (legacy — prefer DAL)
web/views/pages/      # Jinja2 templates for full pages
web/views/components/ # Reusable v-* components (e.g. v-button.html)
config/config.py      # DB, session, locale, SMTP settings
plugins/              # Drop-in plugins (routes, models, templates)
tests/                # 100% coverage required
Creating a new route

Add routes directly in web/app.py or create a router module in web/routers/:

# web/routers/items.py
from fastapi import APIRouter, Request
from web import bootstrap

router = APIRouter()
render = bootstrap.render

@router.get("/items")
async def list_items(request: Request):
    items = await bootstrap.setting_svc.find()  # example service call
    return render("pages/items.html", items=items)

# Then in web/app.py:
# from web.routers import items
# app.include_router(items.router)

Use @app.defer.get() for catch-all routes like /{slug} so plugins register first.

Rendering templates

render("pages/my_page.html", foo=bar) renders a Jinja2 template and auto-injects request-scoped context: ACTOR (logged-in user), theme, T() (i18n), PERMISSIONS, plus anything set via set_context().

Templates extend a base layout. Public pages extend base.html; admin pages extend base_admin.html. Page content replaces {{ child_block }}.

{% extends 'base.html' %}

<section>
  <h1>My Page</h1>
  <p>Hello, {{ ACTOR.name }}</p>
</section>
Models

Models live in domain/model.py as Pydantic classes. They auto-generate SQL tables on startup:

from pydantic import BaseModel
from mel.domain.model import Field

class Article(BaseModel):
    __audit_table__ = "actor"  # adds created_by, updated_by, timestamps

    title: str = Field(max_length=200)
    slug: str = Field(unique=True)
    body: str
    published: bool = Field(default=False)
    author_id: int = Field(reference="actor")

Input models for request validation are auto-generated via generate_input_models() at the bottom of model.py.

Services & DAL (querying data)

Business logic goes in services/. Services extend mel.services.base.Service and get CRUD proxy methods: save, find, find_one, update, delete.

# services/article.py
from mel.services.base import Service

class Article(Service):
    def __init__(self, db, **kwargs):
        super().__init__(db, **kwargs)

    async def get_published(self):
        return await self.find({"published": True})

Register the service in bootstrap.py:

article_svc = article.Article(db)

The DAL supports direct queries with Python expressions:

# Select all
items = await db(db.article).select()

# Select with filters (Python expressions, not dicts)
article = await db(db.article.slug == "hello").select_one()
published = await db(db.article.published == True).select()

# Order, limit, offset
recent = await db(db.article).select(
    orderby=~db.article.id,  # ~ = DESC
    limit=10, offset=0,
)

# String matching: like, startswith, endswith
await db(db.article.title.like("%python%")).select()
await db(db.article.title.startswith("Hello")).select()

# IN / NOT IN
await db(db.article.id.contains([1, 2, 3])).select()

# AND (&) / OR (|)
await db(
    (db.article.published == True) & (db.article.type == "post")
).select()

# Insert, update, delete
await db(db.article).insert({"title": "Hello", "slug": "hello"})
await db(db.article.slug == "hello").update({"title": "Updated"})
await db(db.article.slug == "hello").delete()

# Count
total = await db(db.article).count()
Authentication

Protect routes with decorators from bootstrap.auth_svc:

@router.get("/dashboard")
@bootstrap.auth_svc.auth              # require login
async def dashboard(request: Request):
    actor = request.state.actor        # logged-in user dict
    ...

@router.get("/admin/settings")
@bootstrap.auth_svc.require_internal  # staff only (profile.is_internal)
async def admin_settings(request: Request): ...

@router.get("/admin/posts")
@bootstrap.auth_svc.has_permissions("manage_content")  # RBAC check
async def admin_posts(request: Request): ...

Super Admin bypasses all permission checks.

Testing

100% coverage is enforced by the pre-commit hook. Run tests with:

mel test                        # run all tests
mel coverage --output=cli       # with coverage report

Service test — use the repo fixture for a fresh database:

import pytest
from services import actor as actor_service
from tests.fixtures import profiles

@pytest.mark.asyncio
async def test_save_actor(repo):
    p = await profiles.create_profile(repo, "Member")
    svc = actor_service.Actor(repo)
    actor = await svc.save({
        "name": "Jane", "email": "jane@test.com",
        "password": "secret", "profile_id": p["id"],
    })
    assert actor["email"] == "jane@test.com"

Route test — use TestClient and login_web:

import pytest
from web import app
from async_asgi_testclient import TestClient
from tests.fixtures import actors

HEADERS = {"X-Requested-With": "vz"}

@pytest.mark.asyncio
async def test_admin_welcome(repo, login_web):
    async with TestClient(app.app) as client:
        repo = app.bootstrap.db
        await actors.create_actors(repo)
        await login_web(client)
        resp = await client.get("/admin")
        assert resp.status_code == 200
Useful commands
mel run web                     # start dev server (localhost:8000)
mel setup                       # initialize framework/database
mel script seed                 # seed database
mel test                        # run tests
mel shell                       # interactive shell with a db object instantiated
mel coverage --output=cli       # coverage report
mel lint                        # lint