patx/micropie
add more docs
Commit 621f928 · patx · 2025-10-20T00:40:45-04:00
Comments
No comments yet.
Diff
diff --git a/docs/apidocs/conf.py b/docs/apidocs/conf.py
index c2bfb74..3114105 100644
--- a/docs/apidocs/conf.py
+++ b/docs/apidocs/conf.py
@@ -6,9 +6,25 @@ release = "0.20"
extensions = [
"sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.doctest",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.todo",
"sphinx.ext.viewcode",
]
+autosummary_generate = True
+autodoc_typehints = "description"
+napoleon_google_docstring = True
+napoleon_numpy_docstring = False
+todo_include_todos = True
+
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3", {}),
+ "asgiref": ("https://asgiref.readthedocs.io/en/latest", {}),
+}
+
templates_path = ["_templates"]
exclude_patterns = []
@@ -19,6 +35,11 @@ html_theme_options = {
"github_user": "patx",
"github_repo": "micropie",
"fixed_sidebar": True,
+ "extra_nav_links": {
+ "Project README": "https://github.com/patx/micropie",
+ },
}
+
+pygments_style = "friendly"
html_static_path = ["_static"]
diff --git a/docs/apidocs/explanation/architecture.rst b/docs/apidocs/explanation/architecture.rst
index a55c720..349b725 100644
--- a/docs/apidocs/explanation/architecture.rst
+++ b/docs/apidocs/explanation/architecture.rst
@@ -73,6 +73,27 @@ responses in a small loop that listens for disconnect events and
cancels the generator accordingly. Remember to include a
``Content‑Type: text/event-stream`` header when sending SSE.
+Lifespan hooks
+--------------
+
+ASGI defines a lifespan protocol for startup and shutdown events. MicroPie
+exposes ``startup_handlers`` and ``shutdown_handlers`` lists on the
+:class:`~micropie.App` instance. Handlers are executed sequentially and
+may be synchronous or asynchronous. Use them to open database connections,
+prime caches or register background tasks. Lifespan functions run before
+any request or WebSocket traffic is accepted, ensuring your dependencies
+are ready.
+
+Templating and JSON helpers
+---------------------------
+
+If :mod:`jinja2` is installed, MicroPie enables the
+:func:`~micropie.App.render_template` helper to render templates from a
+``templates`` directory, returning HTML responses with the correct
+``Content-Type``. For JSON, the framework prefers :mod:`orjson` when
+available and gracefully falls back to :mod:`json`. This keeps the core
+lean while letting you opt into performance boosts.
+
Error handling
--------------
@@ -92,4 +113,14 @@ protocols like Socket.IO, or implement your own session storage. The
framework imposes few constraints so that you remain in control of
your stack.
-.. _ASGI: https://asgi.readthedocs.io/
\ No newline at end of file
+WebSocket pipeline
+------------------
+
+WebSocket connections follow a parallel flow to HTTP requests. The
+``ws_`` method naming convention resolves handlers, middleware gates the
+connection before :meth:`~micropie.WebSocket.accept` is called, and the
+:class:`~micropie.WebSocket` helper manages receive/send coroutines. Session
+data is shared with HTTP handlers so users can authenticate once and reuse
+the same session across protocols.
+
+.. _ASGI: https://asgi.readthedocs.io/
diff --git a/docs/apidocs/explanation/design_philosophy.rst b/docs/apidocs/explanation/design_philosophy.rst
new file mode 100644
index 0000000..03d1db3
--- /dev/null
+++ b/docs/apidocs/explanation/design_philosophy.rst
@@ -0,0 +1,80 @@
+Design philosophy
+=================
+
+Understanding MicroPie's guiding principles makes it easier to decide
+whether the framework fits your project and how to extend it without
+fighting the grain. This page complements the
+:doc:`architecture <architecture>` overview by focusing on the trade-offs
+behind key decisions.
+
+Keep the core tiny
+------------------
+
+MicroPie prefers explicit, readable Python over layers of abstraction.
+The entire framework fits in a single module so you can audit its
+behaviour quickly. Features graduate into the core only if they pull
+their weight for the majority of applications. Everything else—ORMs,
+background workers, dependency injection—remains a userland concern.
+
+Route by convention, customise with middleware
+----------------------------------------------
+
+Automatic route discovery lowers the barrier to entry: write a method,
+get an endpoint. The flip side is that large applications sometimes need
+more deliberate routing. Instead of complicating the core dispatcher,
+MicroPie encourages you to plug in :class:`~micropie.HttpMiddleware`
+that rewrites the request target or delegates to sub-apps. This keeps the
+framework flexible without sacrificing the quick-start experience.
+
+Treat async as the default
+--------------------------
+
+ASGI enables concurrency, so MicroPie leans into ``async``/``await`` at
+every layer. Synchronous handlers are supported for convenience, but the
+internal plumbing always assumes asynchronous execution. Middleware and
+session backends follow the same rule. When you need to integrate a
+blocking library, run it in a thread pool explicitly so the framework
+stays responsive.
+
+Design for graceful degradation
+-------------------------------
+
+Optional dependencies such as ``jinja2``, ``multipart`` and ``orjson``
+are detected at runtime. If they are missing, MicroPie falls back to
+standard-library implementations. This approach keeps installation
+friction low for small services while allowing power users to opt in to
+richer features.
+
+Favour composition over special cases
+-------------------------------------
+
+Rather than adding bespoke APIs for every scenario, MicroPie exposes a
+few flexible hooks:
+
+* Context variables keep the current request accessible without global
+ state.
+* Middleware runs before and after handlers for both HTTP and WebSocket
+ flows.
+* Session backends are swappable classes with a small, well-documented
+ interface.
+
+By composing these building blocks you can implement authentication,
+logging, rate limiting and other behaviours without touching the core.
+
+Balance ergonomics with transparency
+------------------------------------
+
+MicroPie does not hide the ASGI protocol. Request and WebSocket objects
+mirror the underlying scope so you can drop to the metal when required.
+At the same time, helper methods normalise headers, cookies and body
+parsing. The goal is to keep magic to a minimum while smoothing common
+operations.
+
+Where to go next
+----------------
+
+* Revisit the :doc:`tutorials <../tutorial/index>` with the design goals
+ in mind to see how they shape the developer experience.
+* Browse ``tests.py`` to understand the expected behaviour of each hook.
+* If you plan to extend MicroPie, read the
+ :doc:`reference/index` to learn the public APIs you can rely on.
diff --git a/docs/apidocs/explanation/index.rst b/docs/apidocs/explanation/index.rst
index 0d3ac07..d6f97aa 100644
--- a/docs/apidocs/explanation/index.rst
+++ b/docs/apidocs/explanation/index.rst
@@ -10,4 +10,5 @@ precise definitions consult the reference.
:maxdepth: 1
:caption: Explanations
- architecture
\ No newline at end of file
+ architecture
+ design_philosophy
diff --git a/docs/apidocs/glossary.rst b/docs/apidocs/glossary.rst
index c02a776..677ed15 100644
--- a/docs/apidocs/glossary.rst
+++ b/docs/apidocs/glossary.rst
@@ -41,4 +41,21 @@ This glossary defines terms used throughout the MicroPie documentation.
local to the current asynchronous context. MicroPie stores the
current request in a context variable so it is accessible via
:meth:`~micropie.App.request` without passing it through your
- function calls.
\ No newline at end of file
+ function calls.
+
+ lifespan
+ The ASGI lifecycle protocol that allows applications to run
+ startup and shutdown hooks. MicroPie exposes
+ ``startup_handlers`` and ``shutdown_handlers`` lists so you can
+ register functions that prepare resources before serving traffic.
+
+ session backend
+ The implementation responsible for persisting session data.
+ MicroPie ships with an in-memory backend but allows you to
+ implement :class:`~micropie.SessionBackend` to store sessions in
+ Redis, databases or other stores.
+
+ ASGI server
+ A web server capable of speaking the ASGI protocol and invoking
+ your application callable. Examples include ``uvicorn``,
+ ``hypercorn`` and ``daphne``.
diff --git a/docs/apidocs/howto/index.rst b/docs/apidocs/howto/index.rst
index 6d3e1a2..df073d4 100644
--- a/docs/apidocs/howto/index.rst
+++ b/docs/apidocs/howto/index.rst
@@ -16,4 +16,5 @@ introduction.
templates
streaming
static_files
- socketio
\ No newline at end of file
+ socketio
+ testing
diff --git a/docs/apidocs/howto/testing.rst b/docs/apidocs/howto/testing.rst
new file mode 100644
index 0000000..4c95191
--- /dev/null
+++ b/docs/apidocs/howto/testing.rst
@@ -0,0 +1,126 @@
+Testing MicroPie applications
+=============================
+
+This guide shows practical approaches for testing MicroPie applications.
+MicroPie does not ship with a bespoke test client, but because it is a
+regular ASGI application you can exercise it using familiar Python
+libraries such as :mod:`unittest`, :mod:`pytest` and
+:mod:`httpx`'s ASGI tools.
+
+Choosing a test framework
+-------------------------
+
+MicroPie itself uses :class:`unittest.IsolatedAsyncioTestCase` in
+``tests.py`` to run asynchronous tests. ``pytest`` with the
+``pytest-asyncio`` plugin offers a similar developer experience. Pick the
+library that best matches your project's conventions—the examples below
+work with either.
+
+Unit testing handlers directly
+------------------------------
+
+Because handlers are regular functions, you can instantiate your
+:class:`~micropie.App` subclass and call methods directly. Use the
+:func:`micropie.current_request` context variable to set up any request
+state that your handler expects.
+
+.. code-block:: python
+
+ from contextvars import Token
+
+ from micropie import App, Request, current_request
+
+ class MyApp(App):
+ async def greet(self, name="World"):
+ return f"Hello {name}!"
+
+ async def test_greet_uses_default():
+ app = MyApp()
+ scope = {"type": "http", "method": "GET", "path": "/"}
+ request = Request(scope)
+ token: Token = current_request.set(request)
+ try:
+ response = await app.greet()
+ finally:
+ current_request.reset(token)
+ assert response == "Hello World!"
+
+Testing through the ASGI interface
+----------------------------------
+
+For higher confidence, drive the full ASGI stack. ``httpx`` provides an
+``ASGITransport`` class that can mount a MicroPie app. Install ``httpx``
+with ``pip install httpx``. The example below uses ``pytest`` style
+asserts, but the structure works in ``unittest`` with
+``self.assertEqual``.
+
+.. code-block:: python
+
+ import pytest
+ import httpx
+
+ from micropie import App
+
+ class MyApp(App):
+ async def index(self):
+ return {"status": "ok"}
+
+ @pytest.mark.asyncio
+ async def test_index_returns_json():
+ app = MyApp()
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
+ response = await client.get("http://test/" )
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+Simulating sessions and middleware
+----------------------------------
+
+To assert session behaviour, populate ``scope["headers"]`` with a
+``cookie`` header and inspect the response headers for the updated
+``Set-Cookie`` value. Middleware can be tested by attaching it to your
+app instance before issuing requests.
+
+.. code-block:: python
+
+ from micropie import App, HttpMiddleware
+
+ class AddHeader(HttpMiddleware):
+ async def after_request(self, request, response):
+ response.setdefault("headers", []).append((b"x-test", b"1"))
+ return response
+
+ class MyApp(App):
+ async def index(self):
+ return "hi"
+
+ async def test_middleware_header():
+ app = MyApp()
+ app.middleware.append(AddHeader())
+ transport = httpx.ASGITransport(app=app)
+ async with httpx.AsyncClient(transport=transport) as client:
+ response = await client.get("http://test/")
+ assert response.headers["x-test"] == "1"
+
+Handling lifespan events
+------------------------
+
+If your application registers startup or shutdown handlers, wrap your
+ASGI client in a lifespan manager. ``httpx`` exposes
+:class:`httpx.ASGITransport` with a ``lifespan="auto"`` mode that will
+run lifespan events before the first request and after the client exits.
+
+.. code-block:: python
+
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=app, lifespan="auto")
+ ) as client:
+ ...
+
+Further reading
+---------------
+
+* Browse ``tests.py`` in the MicroPie source tree for additional
+ patterns, including WebSocket testing helpers.
+* The `httpx documentation <https://www.python-httpx.org/advanced/#calling-into-python-web-apps>`_
+ has more on driving ASGI apps from tests.
diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst
index 3ab55be..9d3d3c0 100644
--- a/docs/apidocs/index.rst
+++ b/docs/apidocs/index.rst
@@ -11,6 +11,39 @@ session back‑ends, middleware hooks, WebSocket support and optional
template rendering. Its focus on minimalism makes it a good choice for
lightweight services, APIs and educational projects.
+Core features
+-------------
+
+* **Convention over configuration.** Public methods on your
+ :class:`~micropie.App` subclass automatically become routes so you can
+ ship a prototype with only a handful of lines of code.
+* **Async‑first request handling.** MicroPie speaks ASGI fluently and
+ embraces ``async``/``await`` for HTTP and WebSocket handlers while
+ keeping synchronous handlers ergonomic.
+* **Built‑in sessions and middleware.** A pluggable session backend and
+ request/response middleware hooks make it easy to add authentication,
+ analytics and other cross‑cutting concerns.
+* **Optional batteries included.** Extras for templating, fast JSON and
+ multipart parsing allow you to scale capabilities without bloating the
+ core package.
+
+Learning path
+-------------
+
+New to MicroPie? Follow this recommended progression:
+
+1. :doc:`tutorial/quickstart` – install the framework and serve your
+ first response.
+2. :doc:`tutorial/routing` – understand how method names map to URL
+ paths and how handler arguments are populated.
+3. :doc:`tutorial/websockets` – build a live, bidirectional endpoint.
+4. :doc:`howto/index` – explore recipes for common tasks like sessions,
+ templating and streaming.
+5. :doc:`reference/index` – deep dive into class and function
+ definitions when you need the authoritative contract.
+6. :doc:`explanation/index` – read about the philosophy behind the
+ design to better understand trade‑offs and extension points.
+
This documentation is organized according to the
Diátaxis documentation framework. That framework separates documentation into
four distinct types:
@@ -26,12 +59,13 @@ four distinct types:
* **Explanation** – discussions that explore the rationale behind
design choices and deeper concepts in MicroPie.
-In addition to these four types, a small glossary collects
-terminology used throughout the framework. If you are new to
-MicroPie, start with the tutorial. If you want to accomplish a
-specific task, head to the how‑to guides. If you need to look up a
-particular method or class, consult the reference manual. For
-background and design considerations, see the explanation section.
+In addition to these four types, a small glossary collects terminology
+used throughout the framework. Quick links to other helpful resources:
+
+* :doc:`glossary` – definitions of common MicroPie and ASGI terms.
+* :doc:`whats_new` – highlights from recent releases and upgrade tips.
+* `Project README <https://github.com/patx/micropie#readme>`_ – the
+ high-level project overview from the source repository.
.. _Diátaxis documentation framework: https://diataxis.fr/
@@ -46,6 +80,7 @@ Contents
howto/index
reference/index
explanation/index
+ whats_new
glossary
Indices and tables
diff --git a/docs/apidocs/whats_new.rst b/docs/apidocs/whats_new.rst
new file mode 100644
index 0000000..7001783
--- /dev/null
+++ b/docs/apidocs/whats_new.rst
@@ -0,0 +1,59 @@
+.. _release-notes:
+
+What's new in MicroPie
+======================
+
+This page summarises noteworthy changes in recent MicroPie releases. It
+is not an exhaustive changelog, but it highlights the features and bug
+fixes most likely to affect application developers. For the full list of
+releases, consult the `GitHub releases page <https://github.com/patx/micropie/releases>`_.
+
+Version highlights
+------------------
+
+* **0.23** – Ensures background multipart parsing stops immediately when a
+ request is terminated by middleware, preventing unnecessary resource
+ usage.
+* **0.22** – Fixes body parsing in mounted sub-applications by sharing
+ ``body_params`` and ``body_parsed`` state with the parent request.
+* **0.21** – Allows the implicit ``index`` route handler to receive path
+ parameters, aligning it with other handlers.
+* **0.20** – Introduces concurrent multipart parsing with bounded queues
+ so that large uploads do not block other requests.
+* **0.19** – Improves debugging via richer tracebacks and adds the
+ ``_sub_app`` attribute for mounting other ASGI applications.
+* **0.18** – Cancels asynchronous generator handlers when the client
+ disconnects to avoid leaking resources during streaming responses.
+* **0.17** – Updates the lifespan hook API to match middleware APIs,
+ enabling ``app.startup_handlers.append(handler)`` style usage.
+* **0.16** – Adds first-class lifespan event support with
+ ``on_startup`` and ``on_shutdown`` helpers.
+* **0.15** – Hardens optional dependency imports, improving behaviour
+ when extras such as ``orjson`` or ``jinja2`` are unavailable.
+* **0.14** – Renames the package import to ``micropie`` (breaking change)
+ to align module naming with Python conventions.
+* **0.13** – Introduces built-in WebSocket support, opening the door to
+ real-time applications without additional middleware.
+
+Upgrade tips
+------------
+
+* **Check optional extras.** When upgrading, confirm that any optional
+ dependencies you rely on (``jinja2``, ``multipart``, ``orjson``) are
+ still installed, especially if you pin minimal environments.
+* **Review lifespan handlers.** Releases 0.16 and 0.17 reshape the
+ startup/shutdown API. Adjust custom startup hooks to use the new
+ ``app.startup_handlers`` and ``app.shutdown_handlers`` lists.
+* **Audit long-lived streams.** If you emit server-sent events or
+ streaming responses, ensure your handlers handle cancellation so the
+ 0.18 change does not mask cleanup bugs.
+* **Evaluate mounted applications.** If you mount other ASGI apps using
+ middleware, upgrade to at least 0.19 to benefit from the ``_sub_app``
+ attribute and the body parsing fixes introduced in 0.22.
+
+Looking for more?
+-----------------
+
+The :mod:`micropie` source distribution ships a ``docs/release_notes.md``
+file with the raw changelog. You can also browse historical discussions
+and pull requests on the project's `GitHub repository <https://github.com/patx/micropie>`_.
diff --git a/examples/middleware/csrf.py b/examples/middleware/csrf.py
index 38eca86..e76763f 100644
--- a/examples/middleware/csrf.py
+++ b/examples/middleware/csrf.py
@@ -1,51 +1,150 @@
from typing import Optional, Dict, List, Tuple, Any
-from html import escape
+from urllib.parse import urlparse
import uuid
-from itsdangerous import URLSafeTimedSerializer, BadSignature
+from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from micropie import App, HttpMiddleware, Request
class CSRFMiddleware(HttpMiddleware):
- """Middleware for CSRF protection using itsdangerous-signed tokens."""
- def __init__(self, app: App, secret_key: str, max_age: int = 8 * 3600):
- self.app = app # Store the App instance
+ """
+ MicroPie-ready CSRF middleware using itsdangerous + session binding.
+
+ - Verifies on POST/PUT/PATCH/DELETE
+ - Accepts token from body (form or JSON) or 'X-CSRF-Token' header
+ - For multipart/form-data, strongly prefer header (parser may still be streaming)
+ - Token payload includes the session_id (when present) to bind token to that session
+ - Emits 'X-CSRF-Token' **only** when we create/rotate one during this request
+ - Supports exempt_paths (e.g. webhook endpoints like /sms_order)
+ """
+
+ def __init__(
+ self,
+ app: App,
+ secret_key: str,
+ *,
+ max_age: int = 8 * 3600,
+ trusted_origins: Optional[List[str]] = None,
+ body_field: str = "csrf_token",
+ header_name: str = "x-csrf-token",
+ require_header_for_multipart: bool = True,
+ exempt_paths: Optional[List[str]] = None,
+ ):
+ self.app = app
self.serializer = URLSafeTimedSerializer(secret_key, salt="csrf-token")
self.max_age = max_age
+ self.trusted = set(trusted_origins or []) # e.g. ["https://gardenfresh.vegy.app"]
+ self.body_field = body_field
+ self.header_name = header_name.lower()
+ self.require_header_for_multipart = require_header_for_multipart
+ self.exempt_paths = set(exempt_paths or [])
- async def before_request(self, request: Request) -> Optional[Dict]:
- """Verify CSRF token for POST/PUT/PATCH requests and generate a new token if needed."""
- # Extract session ID from cookies or generate a new one
- session_id = request.headers.get("cookie", "").split("session_id=")[-1].split(";")[0] if "session_id=" in request.headers.get("cookie", "") else str(uuid.uuid4())
-
- if request.method in ("POST", "PUT", "PATCH"):
- print(f"Request body_params: {request.body_params}") # Debugging
- submitted_token = request.body_params.get("csrf_token", [""])[0]
- print(f"Submitted CSRF token: {submitted_token}") # Debugging
- if not submitted_token:
- return {"status_code": 403, "body": "Missing CSRF token"}
+ # ---------- helpers ----------
+
+ def _is_mutating(self, method: str) -> bool:
+ return method in ("POST", "PUT", "PATCH", "DELETE")
+
+ def _is_multipart(self, ct: str) -> bool:
+ return "multipart/form-data" in (ct or "")
+
+ def _origin_ok(self, headers: Dict[str, str]) -> bool:
+ if not self.trusted:
+ return True
+ origin = headers.get("origin")
+ referer = headers.get("referer")
+ for hdr in (origin, referer):
+ if not hdr:
+ continue
try:
- # Verify the submitted token's signature
- self.serializer.loads(submitted_token, max_age=self.max_age)
- except BadSignature:
- return {"status_code": 403, "body": "Invalid or expired CSRF token"}
+ p = urlparse(hdr)
+ base = f"{p.scheme}://{p.netloc}"
+ if base in self.trusted:
+ return True
+ except Exception:
+ pass
+ return False
+
+ def _get_session_id(self, request: Request) -> Optional[str]:
+ cookie = request.headers.get("cookie", "")
+ if "session_id=" in cookie:
+ return cookie.split("session_id=", 1)[1].split(";", 1)[0].strip() or None
+ return None
+
+ def _issue_token(self, session_id: Optional[str]) -> str:
+ payload = {"nonce": str(uuid.uuid4())}
+ if session_id:
+ payload["sid"] = session_id
+ return self.serializer.dumps(payload)
+
+ def _extract_submitted_token(self, request: Request) -> Optional[str]:
+ ct = request.headers.get("content-type", "")
+ if self._is_multipart(ct) and self.require_header_for_multipart:
+ token = request.headers.get(self.header_name)
+ if token:
+ return token
+
+ lst = request.body_params.get(self.body_field)
+ if lst and isinstance(lst, list) and lst:
+ return lst[0]
+
+ j = getattr(request, "get_json", None)
+ if isinstance(j, dict):
+ tok = j.get(self.body_field)
+ if isinstance(tok, str):
+ return tok
+
+ return request.headers.get(self.header_name)
+
+ # ---------- middleware hooks ----------
+
+ async def before_request(self, request: Request) -> Optional[Dict]:
+ path = request.scope.get("path", "")
+
+ # Exempt specific paths (e.g. /sms_order webhook)
+ if path in self.exempt_paths:
+ return None
- # Generate a new CSRF token if one doesn't exist in the session
if "csrf_token" not in request.session:
- csrf_token = str(uuid.uuid4())
- signed_token = self.serializer.dumps(csrf_token)
- request.session["csrf_token"] = signed_token
- # Save the session
- print(f"Saving session with CSRF token: {signed_token}") # Debugging
- await self.app.session_backend.save(session_id, request.session, self.max_age)
+ sid = self._get_session_id(request)
+ request.session["csrf_token"] = self._issue_token(sid)
+ setattr(request, "_csrf_emit", request.session["csrf_token"])
+
+ if not self._is_mutating(request.method):
+ return None
+
+ if not self._origin_ok(request.headers):
+ return {"status_code": 403, "body": "Forbidden: invalid origin/referer"}
+
+ submitted = self._extract_submitted_token(request)
+ if not submitted:
+ return {"status_code": 403, "body": "Missing CSRF token"}
+
+ try:
+ data = self.serializer.loads(submitted, max_age=self.max_age)
+ except (BadSignature, SignatureExpired):
+ return {"status_code": 403, "body": "Invalid or expired CSRF token"}
+
+ sid = self._get_session_id(request)
+ token_sid = data.get("sid")
+ if token_sid is not None and (sid != token_sid):
+ return {"status_code": 403, "body": "Invalid CSRF token for this session"}
+
+ sid_now = self._get_session_id(request)
+ new_token = self._issue_token(sid_now)
+ request.session["csrf_token"] = new_token
+ setattr(request, "_csrf_emit", new_token)
return None
async def after_request(
- self, request: Request, status_code: int, response_body: Any, extra_headers: List[Tuple[str, str]]
+ self,
+ request: Request,
+ status_code: int,
+ response_body: Any,
+ extra_headers: List[Tuple[str, str]],
) -> Optional[Dict]:
- """Include CSRF token in response headers for client-side use."""
- if request.session.get("csrf_token"):
- extra_headers.append(("X-CSRF-Token", request.session["csrf_token"]))
+ to_emit = getattr(request, "_csrf_emit", None)
+ if to_emit:
+ extra_headers.append(("X-CSRF-Token", to_emit))
return None
@@ -53,7 +152,6 @@ class Root(App):
async def index(self):
csrf_token = self.request.session.get("csrf_token", "")
- print(f"Rendering form with CSRF token: {csrf_token}")
return f"""<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{csrf_token}">
<input type="text" name="name">
@@ -67,4 +165,11 @@ class Root(App):
app = Root()
-app.middlewares.append(CSRFMiddleware(app=app, secret_key="my-secret-key"))
+app.middlewares.append(
+ CSRFMiddleware(
+ app=app,
+ secret_key="my-secret-key",
+ exempt_paths=["/sms_order"],
+ )
+)
+
diff --git a/examples/pastebin/app.py b/examples/pastebin/app.py
index e0946de..2127e41 100644
--- a/examples/pastebin/app.py
+++ b/examples/pastebin/app.py
@@ -9,9 +9,9 @@ db = AsyncPickleDB('pastes.json')
class Root(App):
- async def index(self):
+ async def index(self, paste_content=None):
if self.request.method == "POST":
- paste_content = self.request.body_params.get('paste_content', [''])[0]
+ #paste_content = self.request.body_params.get('paste_content', [''])[0]
pid = str(uuid4())
await db.aset(pid, escape(paste_content))
await db.asave()
diff --git a/examples/pastebin/pastes.json b/examples/pastebin/pastes.json
new file mode 100644
index 0000000..e77b949
--- /dev/null
+++ b/examples/pastebin/pastes.json
@@ -0,0 +1 @@
+{"11d967c7-5a5d-4441-8fb8-035c5c6e6df0":"{% extends "base.html" %}\r\n\r\n{% block title %}{{ client_name }} - Home{% endblock %}\r\n\r\n{% block content %}\r\n<div class="jumbotron glass">\r\n <h1 class="display-4">\r\n <img src="https://raw.githubusercontent.com/patx/vegy-static/refs/heads/main/gardenfresh/gardenfresh_logo_clear.png"\r\n style="max-width: 90%;" alt="Garden Fresh Logo">\r\n </h1>\r\n {% if request.session.logged_in %}\r\n <p class="lead">Welcome {{ request.session.email }}!</p>\r\n {% else %}\r\n <p class="lead">Order fresh produce easily!</p>\r\n {% endif %}\r\n {% if message == "registered" %}\r\n <div class="alert alert-warning">\r\n Your account has been successfully registered! A team member will reach out shortly after we approve your account. \r\n You will not be able to log in until we approve your new account. \r\n <strong>\r\n You can install our app now if you are on your mobile device. \r\n <a href="https://github.com/patx/vegy-static/raw/refs/heads/main/gardenfresh/install.MP4?raw=True" target="_blank">\r\n Watch the installation video here\r\n </a>!\r\n </strong> \r\n If your account has already been approved you may login below.\r\n </div>\r\n {% endif %}\r\n {% if request.session.logged_in and request.session.admin %}\r\n <a class="btn btn-primary btn-lg" href="/admin" role="button">Orders</a>\r\n <a class="btn btn-primary btn-lg" href="/items" role="button">Items</a>\r\n <a class="btn btn-primary btn-lg" href="/invites" role="button">Accounts</a>\r\n <br><br><br>\r\n <a href="/logout" class="btn btn-sm btn-outline-warning">Logout</a>\r\n <button type="button" class="btn btn-sm btn-outline-warning" data-bs-toggle="modal" data-bs-target="#changePasswordModal">\r\n Change Password\r\n </button>\r\n {% elif request.session.logged_in %}\r\n <a class="btn btn-primary btn-lg" href="/new_order" role="button">Place a New Order</a><br><br>\r\n <a class="btn btn-primary btn-lg" href="/orders" role="button">View Your Orders</a>\r\n <br><br><br>\r\n <a href="/logout" class="btn btn-sm btn-outline-warning">Logout</a>\r\n <button type="button" class="btn btn-sm btn-outline-warning" data-bs-toggle="modal" data-bs-target="#changePasswordModal">\r\n Change Password\r\n </button>\r\n {% else %}\r\n <a class="btn btn-primary btn-lg" href="/login" role="button">Login</a>\r\n {% endif %}\r\n\r\n <!-- Change Password Modal -->\r\n {% if request.session.logged_in %}\r\n <!-- keep your modal + script code here -->\r\n {% endif %}\r\n</div>\r\n\r\n<style>\r\n/* === Full-page background === */\r\nhtml, body {\r\n height: 100%;\r\n margin: 0;\r\n background: url("https://raw.githubusercontent.com/patx/vegy-static/main/screenshots/bg.jpg")\r\n no-repeat center center fixed;\r\n background-size: cover;\r\n}\r\n\r\n/* Buttons & text spacing */\r\na, button { margin-top: 10px; }\r\n\r\n/* Logo heading */\r\nh1.display-4 {\r\n font-family: "Poppins", sans-serif;\r\n font-weight: 800;\r\n}\r\n\r\n/* Center hero */\r\n.container {\r\n max-width: 1200px;\r\n flex: 1;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n}\r\n\r\n/* Frosted glass effect for content */\r\n.jumbotron.glass {\r\n text-align: center;\r\n padding: 2rem;\r\n width: 100%;\r\n max-width: 600px;\r\n margin: 2rem auto;\r\n background: rgba(255, 255, 255, 0.5); /* semi-transparent white */\r\n border-radius: 16px;\r\n box-shadow: 0 8px 24px rgba(0,0,0,0.2);\r\n backdrop-filter: blur(12px); /* frosted blur */\r\n -webkit-backdrop-filter: blur(12px); /* Safari support */\r\n margin-top: 200px;\r\n}\r\nh1 { margin-top: 0; }\r\n</style>\r\n{% endblock %}\r\n"}
\ No newline at end of file