Skip to content

hunt-core/hunt

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+
  • 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")

Altering tables:

Schema.table("posts", lambda bp: [
    bp.string("subtitle").nullable(),        # add column
    bp.drop_column("legacy_field"),          # drop column (raises if absent)
    bp.drop_column_if_exists("old_field"),   # safe drop — idempotent
    bp.rename_column("body", "content"),     # rename column
])

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)

Two-factor authentication

Add TOTP-based 2FA to any application with one command:

hunt make:2fa-controllers

This scaffolds routes, controllers, Tailwind-styled templates, and a migration that adds two_factor_secret, two_factor_enabled, and two_factor_recovery_codes columns to the users table. After running hunt migrate, protect any route group with the included middleware:

from hunt.http.middleware.two_factor import EnsureTwoFactorAuthenticated

with router.group(middleware=[Authenticate, EnsureTwoFactorAuthenticated]):
    router.get("/dashboard", DashboardController().index)

Users who have 2FA enabled are redirected to /two-factor/challenge after login. Recovery codes are generated automatically during setup.

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 import SelectFilter, BooleanFilter, DateRangeFilter

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

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

class CreatedAtFilter(DateRangeFilter):
    name = "Created At"
    attribute = "created_at"

Actions:

from hunt.admin import Action, ActionResponse

class ActivateUsers(Action):
    name = "Activate"

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

Built-in actions: BulkDeleteAction, RestoreAction (soft deletes), ExportCsvAction (CSV download).

ActionResponse types: .success(text), .error(text), .redirect(url), .download(content, filename).

Customizing templates:

hunt admin:publish          # copy all admin templates to resources/views/admin/
hunt admin:publish --force  # overwrite existing

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

# Application
hunt new <name>                      # scaffold a new application
hunt upgrade                         # pull in new scaffold files to existing app
hunt serve                           # start the dev server (auto-reload)
hunt tinker                          # interactive REPL with app bootstrapped
hunt key:generate                    # generate and write a new APP_KEY

# Routes
hunt route:list                      # print all registered routes

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

# Database
hunt db:seed                         # run database seeders
hunt db:seed --class PostSeeder      # run a specific seeder

# Cache
hunt cache:clear                     # clear all cached values
hunt cache:forget <key>              # remove a single cache key

# Queue
hunt queue:work                      # start the queue worker
hunt queue:work --queue high --tries 3
hunt queue:failed                    # list failed jobs
hunt queue:retry <id>                # re-queue a failed job
hunt queue:flush                     # delete all failed jobs
hunt queue:table                     # create the jobs migration

# Jobs
hunt job:list                        # list all discovered Job classes
hunt job:run <name>                  # run a job synchronously
hunt job:run <name> --data key=value

# Scheduler
hunt schedule:run                    # run due scheduled tasks (call from cron)
hunt schedule:list                   # list all scheduled tasks

# Code generation
hunt make:model <Name>               # app/models/name.py
hunt make:model <Name> -m            # model + migration
hunt make:controller <Name>          # app/controllers/name_controller.py
hunt make:controller <Name> --resource
hunt make:migration <name>           # database/migrations/TIMESTAMP_name.py
hunt make:middleware <Name>          # app/middleware/name.py
hunt make:request <Name>             # app/requests/name_request.py
hunt make:event <Name>               # app/events/name.py
hunt make:listener <Name>            # app/listeners/name.py
hunt make:mail <Name>                # app/mail/name.py
hunt make:notification <Name>        # app/notifications/name.py
hunt make:seeder <Name>              # database/seeders/NameSeeder.py
hunt make:factory <Name>             # database/factories/NameFactory.py
hunt make:job <Name>                 # app/jobs/name.py
hunt make:command <Name>             # app/console/commands/name.py
hunt make:admin-resource <Model>     # app/admin/model_resource.py
hunt make:2fa-controllers            # 2FA routes, controllers, templates, migration

# Admin
hunt admin:publish                   # copy admin templates to resources/views/admin/

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

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors