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+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
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.6.tar.gz.
File metadata
- Download URL: hunt_framework-0.2.6.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c455882e4f63ebb6b71d67d0ab855260190ad4a94e0ea1369c16a1a73abbc77d
|
|
| MD5 |
c432fbefd9f5b580dea440949b7eef9f
|
|
| BLAKE2b-256 |
e2df671d9ae70656bff5a12ce75bc2befdab7a9cb704630f4a5675ee5aeb70e8
|
Provenance
The following attestation bundles were made for hunt_framework-0.2.6.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.6.tar.gz -
Subject digest:
c455882e4f63ebb6b71d67d0ab855260190ad4a94e0ea1369c16a1a73abbc77d - Sigstore transparency entry: 1573430319
- Sigstore integration time:
-
Permalink:
hunt-core/hunt@07abd48942b787373ad1ba02763645b3c6cca0c4 -
Branch / Tag:
refs/tags/v0.2.6 - Owner: https://github.com/hunt-core
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@07abd48942b787373ad1ba02763645b3c6cca0c4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hunt_framework-0.2.6-py3-none-any.whl.
File metadata
- Download URL: hunt_framework-0.2.6-py3-none-any.whl
- Upload date:
- Size: 218.4 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 |
9ab82fa4f041cc656f15c90a3b77edb2efe729998ef3fe8fd62f7679ea7b7fb8
|
|
| MD5 |
c4999d7f7bea9cb440d79ab34afec7ad
|
|
| BLAKE2b-256 |
4b5d4b674906c7470b8807f3a6f285488ff61aa63160caf8feb2438772b28ec1
|
Provenance
The following attestation bundles were made for hunt_framework-0.2.6-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.6-py3-none-any.whl -
Subject digest:
9ab82fa4f041cc656f15c90a3b77edb2efe729998ef3fe8fd62f7679ea7b7fb8 - Sigstore transparency entry: 1573430355
- Sigstore integration time:
-
Permalink:
hunt-core/hunt@07abd48942b787373ad1ba02763645b3c6cca0c4 -
Branch / Tag:
refs/tags/v0.2.6 - Owner: https://github.com/hunt-core
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@07abd48942b787373ad1ba02763645b3c6cca0c4 -
Trigger Event:
push
-
Statement type: