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

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.5.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.5-py3-none-any.whl (215.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: hunt_framework-0.2.5.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.5.tar.gz
Algorithm Hash digest
SHA256 3e6b5dc3b7d71177f0541311b20e4c4f0f2fc2cf5fab88707c8015150fde7fff
MD5 e8a101a4a563c2a9cda3256237f4c0d8
BLAKE2b-256 4d2806dc61b237b09877e1a663b3a44bf3aff02364603206288df78688ff7eee

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: hunt_framework-0.2.5-py3-none-any.whl
  • Upload date:
  • Size: 215.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.5-py3-none-any.whl
Algorithm Hash digest
SHA256 fc9ded1254e4a56fe3f0f51976195e0a1c455dfbc069e13300aa1332712a3cf3
MD5 0384ef76462ed44ed50226a92b5aea72
BLAKE2b-256 87df4d09666cab32d8caa95045ded18108b00a6b84a8c513218d43932be50378

See more details on using hashes here.

Provenance

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