Metadata-Version: 2.4
Name: rahiframe
Version: 0.0.2
Summary: RahiFrame — A Python web framework built from scratch.
Author: Mottakin Kamal Rahi
Author-email: mottakin.chy@gmail.com
License: MIT
Requires-Python: >=3.6.0
Description-Content-Type: text/markdown
Requires-Dist: Jinja2
Requires-Dist: parse
Requires-Dist: requests
Requires-Dist: requests-wsgi-adapter
Requires-Dist: WebOb
Requires-Dist: whitenoise
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary


# RahiFrame

![Python](https://img.shields.io/badge/python-3.6%2B-blue.svg)
![PyPI](https://img.shields.io/pypi/v/rahiframe.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)
![Status](https://img.shields.io/badge/status-learning%20project-orange.svg)

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

```bash
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:**

```bash
pip install rahiframe
```

**Or install dependencies manually:**

```bash
pip install gunicorn webob parse requests requests-wsgi-adapter jinja2 whitenoise
```

**Run the example app:**

```bash
python -m gunicorn app:app
```

**Run tests:**

```bash
python -m pytest test_rahiframe.py -v
```

---

## Features & Usage

### Function-Based Routes

```python
from rahiframe.api import API

app = API()

@app.route("/home")
def home(request, response):
    response.text = "Hello from the HOME page"
```

### Dynamic URL Parameters

```python
@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:

```python
@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:

```python
@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:

```python
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:

```python
# 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
<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:

```python
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**:

```python
app = API(static_dir="static")
```

`static/main.css`:

```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:

```html
<link rel="stylesheet" type="text/css" href="static/main.css">
```

### HTTP Method Control

```python
@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`:

```python
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:

```python
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

```python
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.

```python
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:

```python
def test_assets_are_served(tmpdir_factory):
    static_dir = tmpdir_factory.mktemp("static")  # temporary, deleted after test
    ...
```

Run the full suite:

```bash
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:

```python
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](https://github.com/Mottakinrahi)

---

## License

MIT — free to use, learn from, and build on.
