Skip to main content

A Python web framework

Project description

hunt framework

CI Lint PyPI Python License

A Python web framework. Routing, ORM, templates, migrations, validation, authentication, admin panel, and a CLI — all in one package.


Requirements

  • Python 3.11+how
  • uv (recommended) or pip

Installation

uv pip install hunt-framework

Or with pip:

pip install hunt-framework

To install the hunt CLI as a global tool:

uv tool install hunt-framework

Creating a new project

hunt new myapp
cd myapp
uv venv && uv pip install -e .
hunt migrate
hunt serve

Visit http://localhost:8000 — you should see the welcome page.


Project structure

myapp/
├── app/
│   ├── admin/             # Admin resources
│   ├── controllers/       # HTTP controllers
│   │   └── auth/          # Login, register, password controllers
│   ├── middleware/        # Middleware classes
│   ├── models/            # Database models
│   └── providers/         # Service providers
├── bootstrap/
│   └── app.py             # Application bootstrap (ASGI entry point)
├── config/
│   ├── app.py             # Application settings
│   ├── auth.py            # Auth feature flags
│   ├── database.py        # Database connections
│   ├── filesystems.py     # Storage disks
│   ├── mail.py            # Mail transport
│   └── view.py            # View/template settings
├── database/
│   └── migrations/        # Migration files
├── resources/
│   └── views/             # hunt-style HTML templates
├── routes/
│   ├── admin.py           # Admin panel routes
│   ├── api.py             # API routes
│   ├── auth.py            # Auth routes
│   └── web.py             # Web routes
├── storage/               # Logs, compiled views, cache
├── tests/                 # Test suite
├── .env                   # Environment variables (not committed)
├── .env.example           # Environment template
└── pyproject.toml         # Project dependencies

Routing

Define routes in routes/web.py or routes/api.py:

from hunt.http.router import Router

def register(router: Router) -> None:
    router.get("/", HomeController().index).named("home")
    router.post("/users", UserController().store).named("users.store")
    router.get("/users/{id}", UserController().show).named("users.show")

HTTP methods: get, post, put, patch, delete, any

Route groups with shared prefix and middleware:

with router.group(prefix="/api", middleware=[AuthMiddleware]):
    router.get("/users", UserController().index)
    router.post("/users", UserController().store)

Controllers

hunt make:controller UserController
hunt make:controller UserController --resource   # CRUD methods
hunt make:controller UserController --api        # API resource (no view methods)
from hunt.http.controller import Controller
from hunt.http.request import Request
from hunt.http.response import Response

class UserController(Controller):
    def index(self, request: Request) -> Response:
        users = User.all()
        return self.view("users.index", {"users": users})

    def store(self, request: Request) -> Response:
        data = self.validate(request, {
            "name": "required|string|max:255",
            "email": "required|email",
        })
        user = User.create(data)
        return self.json(user.to_dict(), 201)

    def show(self, request: Request, id: str) -> Response:
        user = User.find_or_fail(id)
        return self.json(user.to_dict())

Response helpers:

Method Description
self.view("template", data) Render a hunt template
self.json(data, status) JSON response
self.redirect("/url") Redirect response
self.response("html", status) Plain text/HTML response

Models

hunt make:model Post
hunt make:model Post -m   # also creates a migration
from hunt.database.model import Model

class Post(Model):
    table = "posts"
    fillable = ["title", "body", "user_id"]
    hidden = ["deleted_at"]
    casts = {"published": "bool"}
    timestamps = True

Querying:

Post.all()
Post.find(1)
Post.find_or_fail(1)
Post.where("published", True).order_by("created_at", "desc").get()
Post.where("views", ">", 100).limit(10).paginate(per_page=10, page=1)
Post.first_or_create({"slug": "hello-world"}, {"title": "Hello World"})

Creating and saving:

post = Post.create({"title": "Hello", "body": "World"})

post = Post({"title": "Hello"})
post.body = "World"
post.save()

Post.where("published", False).update({"published": True})

Relationships:

class Post(Model):
    def author(self):
        return self.belongs_to(User)

    def comments(self):
        return self.has_many(Comment)

Migrations

hunt make:migration create_posts_table
hunt make:migration add_published_to_posts --table=posts
from hunt.database.schema.builder import Schema
from hunt.database.schema.migration import Migration

class CreatePostsTable(Migration):
    def up(self) -> None:
        Schema.create("posts", lambda bp: [
            bp.id(),
            bp.string("title"),
            bp.text("body").nullable(),
            bp.boolean("published").default(False),
            bp.foreign_id("user_id"),
            bp.timestamps(),
        ])

    def down(self) -> None:
        Schema.drop_if_exists("posts")

Running migrations:

hunt migrate          # run pending migrations
hunt migrate:status   # show migration status
hunt migrate:rollback # rollback last batch
hunt migrate:fresh    # drop all tables and re-run

Templates

Templates live in resources/views/ and use hunt-style syntax. Files use the .html extension.

resources/views/layout.html

<!DOCTYPE html>
<html>
<head><title>@yield('title', 'My App')</title></head>
<body>@yield('content')</body>
</html>

resources/views/posts/index.html

@extends('layout')

@section('content')
<h1>Posts</h1>

@foreach($posts as $post)
    <article>
        <h2>{{ $post.title }}</h2>
        <p>{{ $post.body }}</p>
    </article>
@endforeach
@endsection

Supported directives:

Directive Description
@extends('layout') Inherit a parent layout
@section('name') / @endsection Define a content block
@yield('name') Output a block
@include('partial') Include a sub-template
@foreach($items as $item) / @endforeach Loop
@if($condition) / @elseif / @else / @endif Conditionals
@unless($condition) / @endunless Negated conditional
{{ $variable }} Escaped output
{!! $html !!} Raw (unescaped) output
@csrf CSRF hidden input
@error('field') / @enderror Show validation errors
@auth / @endauth Authenticated user block
@guest / @endguest Guest user block
{{-- comment --}} Template comment (not rendered)

Authentication

hunt new scaffolds a complete auth system: login, registration, and password reset — all wired up and ready to use.

from hunt.auth.manager import Auth

Auth.attempt({"email": "...", "password": "..."})  # login + session
Auth.login(user)     # log in without credential check
Auth.logout()        # clear session
Auth.check()         # True if authenticated
Auth.user()          # current User instance or None
Auth.id()            # current user's primary key or None

Protect routes with the included Authenticate middleware:

from hunt.http.middleware.authenticate import Authenticate

router.get("/dashboard", DashboardController().index).middleware(Authenticate)

Feature flags

config/auth.py controls which auth features are active. Set any flag to False to remove those routes entirely (returns 404) and hide the corresponding links in the built-in auth views:

config = {
    "features": {
        "registration": True,
        "login": True,
        "forgot_password": True,
    }
}

Admin panel

The admin panel is available at /hunt-admin after registration. Define resources in app/admin/:

from hunt.admin import AdminResource
from hunt.admin.fields import Text, Email, Boolean, DateTime

class UserResource(AdminResource):
    model = User
    label = "User"
    search_columns = ["name", "email"]

    def fields(self):
        return [
            Text("Name", attribute="name").rules("required", "max:255").sortable(),
            Email("Email", attribute="email").rules("required", "email"),
            Boolean("Admin", attribute="is_admin"),
            DateTime("Created At", attribute="created_at").hide_from_forms(),
        ]

Register in routes/admin.py:

from hunt.admin import Admin
from app.admin.user_resource import UserResource

Admin.resource(UserResource)
Admin.register_to(router)

Available field types: Text, Email, Password, Textarea, RichText, Number, Boolean, Select, Image, DateTime, BelongsTo, HasMany, Badge

Filters:

from hunt.admin.filter import SelectFilter, BooleanFilter

class StatusFilter(SelectFilter):
    name = "Status"
    attribute = "status"

    def options(self):
        return [("active", "Active"), ("inactive", "Inactive")]

Actions:

from hunt.admin.action import Action, ActionResponse

class ActivateUsers(Action):
    name = "Activate"

    def handle(self, request, models):
        for user in models:
            user._attributes["status"] = "active"
            user.save()
        return ActionResponse.success(f"{len(models)} user(s) activated.")

Validation

data = self.validate(request, {
    "name":     "required|string|max:255",
    "email":    "required|email|unique:users,email",
    "password": "required|min:8|confirmed",
    "role":     "required|in:admin,editor,viewer",
})

Available rules:

required · string · integer · numeric · boolean · email · url · min:n · max:n · size:n · in:a,b,c · not_in:a,b,c · confirmed · regex:pattern · unique:table,column · array


Middleware

hunt make:middleware AuthMiddleware
from hunt.http.middleware import Middleware, Next
from hunt.http.request import Request
from hunt.http.response import Response

class AuthMiddleware(Middleware):
    async def handle(self, request: Request, next: Next) -> Response:
        token = request.bearer_token()
        if not token:
            from hunt.http.response import JsonResponse
            return JsonResponse({"error": "Unauthenticated"}, 401)
        return await next(request)

CLI reference

hunt new <name>               # scaffold a new application
hunt upgrade                  # add missing scaffold files to an existing app
hunt serve                    # start the development server
hunt serve --port 3000        # custom port
hunt serve --host 0.0.0.0     # bind to all interfaces
hunt tinker                   # interactive REPL

hunt make:model <Name>        # create a model
hunt make:model <Name> -m     # model + migration
hunt make:controller <Name>   # create a controller
hunt make:controller <Name> --resource  # CRUD controller
hunt make:controller <Name> --api       # API controller
hunt make:migration <name>    # create a migration
hunt make:middleware <Name>   # create a middleware

hunt migrate                  # run pending migrations
hunt migrate:rollback         # rollback last batch
hunt migrate:fresh            # drop all + re-run
hunt migrate:status           # show migration status

hunt route:list               # list all registered routes
hunt key:generate             # generate a new APP_KEY

Environment variables

Variable Default Description
APP_NAME hunt Application name
APP_ENV production Environment (local, production)
APP_DEBUG false Enable debug mode
APP_URL http://localhost:8000 Application URL
APP_KEY Encryption key (auto-generated by hunt new)
DB_CONNECTION sqlite Driver (sqlite, mysql, postgresql)
DB_HOST 127.0.0.1 Database host
DB_PORT 3306 / 5432 Database port
DB_DATABASE Database name / SQLite file path
DB_USERNAME Database username
DB_PASSWORD Database password

Testing

import pytest
from hunt.testing.test_case import HuntTestCase
from hunt.http.router import Router
from hunt.http.kernel import HttpKernel

class TestMyApp(HuntTestCase):
    def setup_method(self):
        router = Router()
        router.get("/users/{id}", lambda req, id: {"id": id})
        self.kernel = HttpKernel(router)

    @pytest.mark.asyncio
    async def test_get_user(self):
        resp = await self.get("/users/42")
        resp.assert_ok().assert_json("id", "42")
pytest

Documentation

Full documentation at hunt-framework.com

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hunt_framework-0.2.8.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hunt_framework-0.2.8-py3-none-any.whl (219.2 kB view details)

Uploaded Python 3

File details

Details for the file hunt_framework-0.2.8.tar.gz.

File metadata

  • Download URL: hunt_framework-0.2.8.tar.gz
  • Upload date:
  • Size: 1.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hunt_framework-0.2.8.tar.gz
Algorithm Hash digest
SHA256 2d6ccab409fe93b6c89feed7d7df6d6b204c049b5aea084b2f71e62a47589ef3
MD5 7228c7b0ac4ec705b048d7b95b68daaf
BLAKE2b-256 b163110d45496df074b3cbea59a8ba99561dd7318fe340ce441ed63f1a3e5646

See more details on using hashes here.

Provenance

The following attestation bundles were made for hunt_framework-0.2.8.tar.gz:

Publisher: publish.yml on hunt-core/hunt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hunt_framework-0.2.8-py3-none-any.whl.

File metadata

  • Download URL: hunt_framework-0.2.8-py3-none-any.whl
  • Upload date:
  • Size: 219.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hunt_framework-0.2.8-py3-none-any.whl
Algorithm Hash digest
SHA256 a7a86e42324db5ba07a6e719e5fdca1263bd4e18b9d031ff6713da37caddeaad
MD5 fe1074deccabf7f580bc6cd194958f7b
BLAKE2b-256 a061957af272959a7230c9fa07f67c1790a3041bbeaba33a95bb9ac3ca2777f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for hunt_framework-0.2.8-py3-none-any.whl:

Publisher: publish.yml on hunt-core/hunt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page