Walkthrough =========== Build a small app end to end: package layout, configuration, a template, a form and tests. The code below runs as written with ``muffin`` and ``muffin-jinja2`` installed. Layout ------ :: todo/ __init__.py application object, imports the rest settings.py configuration module storage.py the "database" views.py routes manage.py CLI commands templates/ index.html tests/ conftest.py test_app.py ``__init__.py`` creates the application and pulls in the modules that register routes and commands: .. code-block:: python # todo/__init__.py from muffin import Application app = Application("todo.settings") app.import_submodules("views", "manage") ``import_submodules`` imports the named submodules of the calling package, in order, so registration order is explicit instead of alphabetical. Called without arguments it imports every submodule, which is fine while the project is small. Configuration ------------- .. code-block:: python # todo/settings.py NAME = "todo" DEBUG = False JINJA2_TEMPLATE_FOLDERS = ["todo/templates"] Set ``NAME`` first: it prefixes environment variables. With the module above, every option is overridable as ``TODO_*``: .. code-block:: console $ TODO_DEBUG=true uvicorn todo:app Template folders are resolved against the process working directory, so they are written relative to the project root. Precedence rules are in :doc:`configuration`. Storage ------- .. code-block:: python # todo/storage.py ITEMS = [] async def save(title): ITEMS.append(title) async def load_items(): return list(ITEMS) A real project would talk to a database here, through one of the plugins. Routes and templates -------------------- ``views.py`` registers routes on the application object imported from the package: .. code-block:: python # todo/views.py import muffin_jinja2 from . import app from .storage import load_items, save jinja2 = muffin_jinja2.Plugin(app) @app.route("/", methods=["GET", "POST"]) async def index(request): if request.method == "POST": form = await request.form() title = form.get("title", "") if title: app.run_after_response(save(title)) return 303, {"Location": "/"}, "" items = await load_items() return await jinja2.render("index.html", items=items) The handler reads the form, schedules storage work that does not need to delay the response, and renders a template. ``303`` with ``Location`` is a redirect the browser replays as a ``GET``, so refreshing the page does not resubmit the form. Returning a tuple of ``(status, headers, content)`` is what makes the redirect possible; a two-element tuple is read as ``(status, content)``. .. code-block:: html