Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bitpoint

One file per endpoint, dependencies included.

# routes/repo/get.py

# /// script
# dependencies = ["httpx"]
# ///
import httpx

def handle(request):
    return httpx.get("https://api.github.com/repos/tanrax/bitpoint").json()

Drop that file into routes/, start the server, and GET /repo is live. The line at the top is PEP 723 inline metadata: Bitpoint installs httpx into an environment that belongs to this endpoint alone. No shared virtualenv, no requirements.txt, no app object. Two endpoints can even depend on incompatible versions of the same library without conflict.

  • Per-endpoint dependencies: each file declares what it needs and runs in its own isolated, uv-managed environment. No other framework does this.
  • File-based routing: the directory structure defines the routes, the file name defines the HTTP method. Add or remove a file and the routes change, no restart needed.

Bitpoint is for when the API is not the product, just plumbing: a webhook, a mock for the frontend, a small internal service, a few endpoints on a personal server. You write one file and you get one endpoint, dependencies included.

There is nothing to register and nothing to configure: no app object, no decorators, no config files, just a handle function per file. Dependency isolation is delegated to uv, and responses follow the type you return: a string becomes plain text, a dict becomes JSON.

Some use cases:

  • Webhooks
  • Internal tools
  • API mocks
  • Glue between services
  • Personal server
  • Easy deployment

Installation

pip install bitpoint

Or, if you use uv:

uv tool install bitpoint

Python 3.9 or later. If you want per-endpoint dependencies, uv must be available on the server.

Quickstart

Create a directory called routes and, inside it, a subdirectory called hello (this will be the route). Inside hello, create a file called get.py with the following content:

# routes/hello/get.py

def handle(request):
    return "world"

Start the server with:

bitpoint

python -m bitpoint works too. And with uv you can even skip the installation step and run it directly:

uvx bitpoint

You can now try your endpoint:

curl 127.0.0.1:8000/hello

It will return:

world

Routing

The URL path maps to the directory path inside routes, and the HTTP method maps to the file name:

routes/
├── webhooks/
│   └── github/
│       └── post.py     → POST /webhooks/github (a webhook receiver)
├── invoices/
│   ├── get.py          → GET  /invoices (an internal service, or a mock)
│   ├── post.py         → POST /invoices
│   └── [id]/
│       ├── get.py      → GET  /invoices/42
│       └── delete.py   → DELETE /invoices/42
└── lib/                → shared code, does not generate routes
    └── db.py

Valid file names are the HTTP methods in lowercase: get.py, post.py, put.py, patch.py, delete.py, head.py and options.py. Any other file is ignored and does not generate routes, so you can keep helper modules next to your endpoints.

Disabling endpoints

Prefix a name with an underscore to disable it without deleting anything. Renaming get.py to _get.py stops serving that method, and renaming a directory to _invoices/ takes its whole subtree offline: requests respond with 404 (or 405 if the route still serves other methods). Rename it back and the endpoint is live again, no restart needed.

One detail: a [param] directory still captures URL segments that happen to start with an underscore, disabling only affects how your files are routed, not which URLs are valid.

Dynamic routes

A directory whose name is wrapped in brackets captures a URL segment:

# routes/invoices/[id]/get.py

def handle(request):
    return {"invoice_id": request.params["id"]}
curl 127.0.0.1:8000/invoices/42
# {"invoice_id": "42"}

Captured values always arrive as str. Type conversion is the endpoint's responsibility.

The handle contract

Each endpoint exposes a handle(request) function. It is the only symbol Bitpoint looks for in the file, the rest of the module is yours.

The request object

Attribute Type Description
request.method str HTTP method in uppercase, for example "GET"
request.path str Request path, for example /users/42
request.params dict[str, str] Dynamic path segments
request.args dict[str, str] Query string parameters
request.headers dict[str, str] Headers, case-insensitive keys
request.body bytes Raw request body
request.json() Any Body parsed as JSON, raises a 400 error if invalid

Example with a query string:

# routes/greet/get.py

def handle(request):
    name = request.args.get("name", "world")
    return f"Hello, {name}!"
curl "127.0.0.1:8000/greet?name=Bob"
# Hello, Bob!

Return values

The return type determines the response:

Return Response
str 200, text/plain; charset=utf-8
dict or list 200, application/json
bytes 200, application/octet-stream
(body, status) As above, with the given status code
(body, status, headers) Additionally with custom headers
None 204 No Content
# routes/users/post.py

def handle(request):
    data = request.json()
    return {"created": data["name"]}, 201

Errors

  • If handle raises an exception, Bitpoint responds with 500.
  • In development mode (the default) the body includes the traceback. In production (--production) the body is a generic message and the traceback goes to the log.
  • Path with no matching directory: 404. Directory exists but there is no file for that method: 405 with the Allow header listing the available methods.

Dependencies

Declare each endpoint's dependencies with a PEP 723 block at the top of the file, the same format understood by uv and pipx:

# routes/repo/get.py

# /// script
# dependencies = ["httpx"]
# ///
import httpx

def handle(request):
    return httpx.get("https://api.github.com/repos/tanrax/bitpoint").json()

Bitpoint delegates resolution and installation to uv: each endpoint runs with its dependencies in an isolated, cached environment. Two endpoints can use incompatible versions of the same library without conflict.

  • An endpoint without a PEP 723 block runs in the base environment, with no extra cost.
  • Installation happens at startup and on reload, not on every request.

Security note: installing dependencies automatically after a git pull means running third-party code at deploy time. If you prefer to control that step, start with --no-install and Bitpoint will fail with a clear error on endpoints whose dependencies are not already installed.

Shared code

The routes/lib/ directory is reserved: it does not generate routes and is importable from any endpoint:

# routes/lib/db.py

def get_connection():
    ...
# routes/users/get.py

from lib.db import get_connection

def handle(request):
    conn = get_connection()
    ...

Configuration

Everything is controlled from the command line, there is no configuration file. The same options work with every launcher: bitpoint, python -m bitpoint or uvx bitpoint.

bitpoint [options]
Option Default Description
--port 8000 Listening port
--host 127.0.0.1 Listening address
--dir ./routes Root directory for endpoints
--production disabled Hides error tracebacks in responses and writes a bitpoint.pid file
--no-install disabled Does not install dependencies automatically

Deployment

Bitpoint loads each endpoint once and caches it for the life of the process, so editing a file does not change a running server. Routing is the exception: it resolves against the filesystem on every request, so adding or removing an endpoint changes the available routes without a restart.

To pick up a change to existing code, reload. Started with --production, Bitpoint writes a bitpoint.pid file and reloads every endpoint on SIGHUP, re-executing modules, respawning workers and re-syncing dependencies, which makes a git pull deploy a one-liner:

git pull && kill -HUP $(cat bitpoint.pid)

There is no shared state across a reload: if an endpoint keeps in-memory state (caches, connections), it is lost. For persistent state use external resources (a database, Redis, files).

Running with Gunicorn

The built-in server is enough for development and small deployments. For production you can serve Bitpoint with Gunicorn or any other WSGI server, since create_app is a standard WSGI application factory:

pip install gunicorn
gunicorn "bitpoint.app:create_app('./routes', production=True)" --bind 127.0.0.1:8000 --workers 4 --pid gunicorn.pid

The factory takes the same options as the command line: create_app(routes_dir="./routes", production=False, install=True). Pass production=True to hide tracebacks, and install=False if you want the --no-install behavior:

gunicorn "bitpoint.app:create_app('./routes', production=True, install=False)" --bind 127.0.0.1:8000

Two things to keep in mind. Each Gunicorn worker is a separate process with its own pool of endpoint workers, so with --workers 4 and three PEP 723 endpoints you can end up with twelve worker processes: keep the worker count modest. And Gunicorn manages its own reload, so the deploy flow becomes a git pull followed by a signal to the Gunicorn master:

git pull && kill -HUP $(cat gunicorn.pid)

A real example

A GitHub webhook that sends a Telegram message on every push:

# routes/webhooks/github/post.py

# /// script
# dependencies = ["httpx"]
# ///
import os
import httpx

def handle(request):
    if request.headers.get("x-github-event") != "push":
        return None
    payload = request.json()
    pusher = payload["pusher"]["name"]
    repo = payload["repository"]["name"]
    commits = len(payload["commits"])
    httpx.post(
        f"https://api.telegram.org/bot{os.environ['TELEGRAM_TOKEN']}/sendMessage",
        json={
            "chat_id": os.environ["TELEGRAM_CHAT_ID"],
            "text": f"{pusher} pushed {commits} commit(s) to {repo}",
        },
    )
    return None

One file, deployed with a git pull. No project, no virtualenv, no route registration, no restart.

Small core

Some areas are explicitly not covered in order to keep the core small:

  • Middleware and global hooks
  • Authentication
  • WebSockets and streaming
  • async endpoints
  • Static files

If you need any of this today, Bitpoint is not your tool, you will have to use external tools.

About

File-based HTTP endpoints for Python, where each file carries its own isolated dependencies.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages