RahiFrame — A Python web framework built from scratch.
Project description
RahiFrame
A Python web framework engineered from scratch — built to understand exactly how modern web frameworks work under the hood.
Most developers use Flask or Django without knowing what happens beneath
@app.route. This project tears that abstraction apart and rebuilds it piece by piece.
Installation
pip install rahiframe
Project Structure
python-framework-from-scratch/
│
├── rahiframe/
│ ├── __init__.py
│ ├── api.py # Core: routing, views, middleware, templates
│ ├── middleware.py # Middleware base class and pipeline
│ └── response.py # Response class with automatic content-type handling
│
├── static/
│ └── main.css # Styles served via WhiteNoise
├── templates/
│ └── index.html # Jinja2 template
├── app.py # Example application demonstrating all features
├── conftest.py # Pytest fixtures
└── test_rahiframe.py # Full test suite
What This Is
RahiFrame is a fully functional WSGI-based Python web framework built without relying on any existing framework. It implements the core primitives every web framework needs:
- Routing engine — URL pattern matching with dynamic parameters
- Function-based & class-based views — both handler styles supported
- Middleware pipeline — composable layers that wrap every request
- Request/Response cycle — automatic content-type handling for JSON, HTML, and plain text
- Template rendering — Jinja2-powered HTML templating
- Static file serving — WhiteNoise integration for CSS and assets
- HTTP method control — restrict routes to specific methods
- Exception handling — custom error handlers
- Test client — built-in session for integration tests without a running server
Setup
Install via PyPI:
pip install rahiframe
Or install dependencies manually:
pip install gunicorn webob parse requests requests-wsgi-adapter jinja2 whitenoise
Run the example app:
python -m gunicorn app:app
Run tests:
python -m pytest test_rahiframe.py -v
Features & Usage
Function-Based Routes
from rahiframe.api import API
app = API()
@app.route("/home")
def home(request, response):
response.text = "Hello from the HOME page"
Dynamic URL Parameters
@app.route("/hello/{name}")
def greeting(request, response, name):
response.text = f"Hello, {name}!"
Visit /hello/Rahi → Hello, Rahi!
Class-Based Views
Group related HTTP methods under one class:
@app.route("/books")
class BooksResource:
def get(self, req, resp):
resp.json = {"books": ["Book 1", "Book 2", "Book 3"]}
def post(self, req, resp):
resp.json = {"message": "Book created successfully"}
With typed URL parameters:
@app.route("/users/{id:d}")
class UserResource:
def get(self, req, resp, id):
resp.text = f"Get user {id}"
def put(self, req, resp, id):
resp.text = f"Update user {id}"
def delete(self, req, resp, id):
resp.text = f"Delete user {id}"
Django-Style Route Registration
Register routes without decorators:
def sample_handler(req, resp):
resp.text = "Django-style route registration"
app.add_route("/sample", sample_handler)
Response Types
RahiFrame automatically sets the correct Content-Type header based on which property you set:
# Plain text → Content-Type: text/plain
@app.route("/text")
def text_handler(req, resp):
resp.text = "This is plain text"
# JSON → Content-Type: application/json
@app.route("/json")
def json_handler(req, resp):
resp.json = {"name": "data", "type": "JSON"}
# HTML → Content-Type: text/html
@app.route("/template")
def template_handler(req, resp):
resp.html = app.template("index.html", context={"title": "RahiFrame", "name": "RahiFrame"})
Template Rendering
Templates use Jinja2 and live in the templates/ folder.
templates/index.html:
<html>
<header>
<title>{{ title }}</title>
<link rel="stylesheet" type="text/css" href="static/main.css">
</header>
<body>
<h1>The name of the framework is {{ name }}</h1>
</body>
</html>
Render it from a handler:
app = API(templates_dir="templates")
@app.route("/template")
def template_handler(req, resp):
resp.html = app.template(
"index.html",
context={"title": "RahiFrame", "name": "RahiFrame"}
)
Static Files
Static files live in static/ and are served automatically via WhiteNoise:
app = API(static_dir="static")
static/main.css:
body {
background-color: teal;
font-family: Arial, sans-serif;
margin: 20px;
}
h1 {
color: white;
text-align: center;
}
Access at: http://localhost:8000/static/main.css
Referenced directly from templates:
<link rel="stylesheet" type="text/css" href="static/main.css">
HTTP Method Control
@app.route("/api/products", allowed_methods=["GET", "POST"])
def products_api(request, response):
if request.method == "GET":
response.text = "List products"
elif request.method == "POST":
response.text = "Create product"
# Also works with add_route
app.add_route("/api/admin", admin_handler, allowed_methods=["PATCH"])
Middleware
Subclass Middleware and implement process_request and/or process_response:
from rahiframe.middleware import Middleware
class SimpleCustomMiddleware(Middleware):
def process_request(self, req):
print("Processing request", req.url)
def process_response(self, req, resp):
print("Processing response", req.url)
app.add_middleware(SimpleCustomMiddleware)
Stack multiple middleware layers — they execute in order:
import time
class RequestTimingMiddleware(Middleware):
def process_request(self, req):
req.start_time = time.time()
def process_response(self, req, resp):
if hasattr(req, "start_time"):
duration = time.time() - req.start_time
resp.headers["X-Response-Time"] = f"{duration:.4f}s"
app.add_middleware(RequestTimingMiddleware)
Exception Handling
def custom_exception_handler(request, response, exception_cls):
response.text = f"Error occurred: {str(exception_cls)}"
app.add_exception_handler(custom_exception_handler)
Testing
RahiFrame ships with a built-in test client — no running server needed.
import pytest
from rahiframe.api import API
@pytest.fixture
def app():
return API()
@pytest.fixture
def client(app):
return app.test_session()
def test_homepage(app, client):
@app.route("/home")
def home(request, response):
response.text = "Hello!"
res = client.get("http://testserver/home")
assert res.status_code == 200
assert res.text == "Hello!"
Static file tests use temporary directories via tmpdir_factory — your real static/ folder is never touched during testing. Tests are fully isolated:
def test_assets_are_served(tmpdir_factory):
static_dir = tmpdir_factory.mktemp("static") # temporary, deleted after test
...
Run the full suite:
python -m pytest test_rahiframe.py -v
How It Works
WSGI (Web Server Gateway Interface) is the contract between a Python web app and a server like Gunicorn. At its core, every WSGI app is just a callable:
def app(environ, start_response):
start_response("200 OK", [("Content-Type", "text/plain")])
return [b"Hello World"]
RahiFrame wraps this contract into a clean, usable API. Here's the full request lifecycle:
HTTP Request
│
▼
Gunicorn (WSGI Server)
│ calls app(environ, start_response)
▼
Middleware Stack
│ process_request() on each layer (outermost first)
▼
API.__call__()
│ parses environ → WebOb Request object
▼
Router
│ matches URL pattern → resolves handler
▼
Handler (function-based or class-based view)
│ populates Response object
▼
Middleware Stack
│ process_response() on each layer (innermost first)
▼
Response.__call__()
│ sets Content-Type, status code, body
▼
HTTP Response
Why Build a Framework?
Using Flask or Django without understanding their internals is like driving a car without knowing how an engine works — fine until something breaks. Building RahiFrame taught me:
- How WSGI works and why it exists
- How URL routing is implemented with pattern matching
- How middleware pipelines compose behavior without touching core logic
- How HTTP responses are constructed and content-typed automatically
- How Jinja2 templating integrates into a request/response cycle
- How static files are served via WhiteNoise
- How test clients simulate HTTP without a network
This isn't meant to replace Flask. It's meant to make you understand why Flask exists.
Author
Mottakin Kamal Rahi
📧 mottakin.chy@gmail.com
🐙 github.com/Mottakinrahi
License
MIT — free to use, learn from, and build on.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rahiframe-0.0.2.tar.gz.
File metadata
- Download URL: rahiframe-0.0.2.tar.gz
- Upload date:
- Size: 8.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a63397b5b2870f170af3d17cb036ae2194fb3873ebc5149a51cfef26b7764f1b
|
|
| MD5 |
a7de3e537862acacff6c82a5b306adeb
|
|
| BLAKE2b-256 |
91926d10ec34ceeac212687ea0af5ce91c64c55677d42dc8e21918dd2bc1ea01
|
File details
Details for the file rahiframe-0.0.2-py3-none-any.whl.
File metadata
- Download URL: rahiframe-0.0.2-py3-none-any.whl
- Upload date:
- Size: 7.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45cbdc94e75794a8a1cf6d31c5152402f6ff158cee70537068c34ccd8259d984
|
|
| MD5 |
c98078da87494c18ec96f49b685ea69c
|
|
| BLAKE2b-256 |
eb2cc951d1d5c880a90c24bbe331d4a45b2dd9f702b208be18b2597f420c7c27
|