A Python web framework
Project description
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
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 hunt_framework-0.2.42.tar.gz.
File metadata
- Download URL: hunt_framework-0.2.42.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83d406ff7611642ba4fba7a2864e5afa6ceee0230705495afd0f95765e92c60b
|
|
| MD5 |
5c8e8a1981ff813a13911eca92447809
|
|
| BLAKE2b-256 |
f0585208f5664ab9caab57fd340ab2e2297c4d6746d3f32e57c4b9700042b405
|
Provenance
The following attestation bundles were made for hunt_framework-0.2.42.tar.gz:
Publisher:
publish.yml on hunt-core/hunt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hunt_framework-0.2.42.tar.gz -
Subject digest:
83d406ff7611642ba4fba7a2864e5afa6ceee0230705495afd0f95765e92c60b - Sigstore transparency entry: 1607418563
- Sigstore integration time:
-
Permalink:
hunt-core/hunt@d80e42e8e52e4482d9874131aeb52a3a2ec24f76 -
Branch / Tag:
refs/tags/v0.2.42 - Owner: https://github.com/hunt-core
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d80e42e8e52e4482d9874131aeb52a3a2ec24f76 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hunt_framework-0.2.42-py3-none-any.whl.
File metadata
- Download URL: hunt_framework-0.2.42-py3-none-any.whl
- Upload date:
- Size: 302.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d02d49a0510e64b0a49607651723c49299cb2b200e1eecf351f0bbb8fbe2b71
|
|
| MD5 |
2743594699d3662f1ea1f41485aebc24
|
|
| BLAKE2b-256 |
3ccab3e76705ea092d91f197c008d4aa18a923701e669f46b59db043e73a080b
|
Provenance
The following attestation bundles were made for hunt_framework-0.2.42-py3-none-any.whl:
Publisher:
publish.yml on hunt-core/hunt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hunt_framework-0.2.42-py3-none-any.whl -
Subject digest:
8d02d49a0510e64b0a49607651723c49299cb2b200e1eecf351f0bbb8fbe2b71 - Sigstore transparency entry: 1607418653
- Sigstore integration time:
-
Permalink:
hunt-core/hunt@d80e42e8e52e4482d9874131aeb52a3a2ec24f76 -
Branch / Tag:
refs/tags/v0.2.42 - Owner: https://github.com/hunt-core
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d80e42e8e52e4482d9874131aeb52a3a2ec24f76 -
Trigger Event:
push
-
Statement type: