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

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.41.tar.gz (1.3 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.41-py3-none-any.whl (305.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: hunt_framework-0.2.41.tar.gz
  • Upload date:
  • Size: 1.3 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.41.tar.gz
Algorithm Hash digest
SHA256 c3528991842a97ccba98dd7d1b6b68ed93056c1a6d1be9ecca0834a3f9a4c7f5
MD5 a220975923f3a113960113a86794dff5
BLAKE2b-256 cd2744e732d05af3760cfc1f8877d2be20f42761a2c50260a4ef6e99815fd046

See more details on using hashes here.

Provenance

The following attestation bundles were made for hunt_framework-0.2.41.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.41-py3-none-any.whl.

File metadata

  • Download URL: hunt_framework-0.2.41-py3-none-any.whl
  • Upload date:
  • Size: 305.1 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.41-py3-none-any.whl
Algorithm Hash digest
SHA256 acc58af5292523d74f1d2ab8ec7d57ec373f9ff2d5179b7a74be3776d2c994b0
MD5 26a39b5a17d47c08bb04b685c8300e8f
BLAKE2b-256 94f016f3dfcfe34920ac2775311b959009ad0e2870013a86f39d5d366a671fcc

See more details on using hashes here.

Provenance

The following attestation bundles were made for hunt_framework-0.2.41-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