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