Skip to main content

Bottler

A Bottle subclass that bundles REST resources, JWT auth, SQLite/MySQL access and CORS into one app, plus a CLI to scaffold and run projects and an optional admin dashboard.

The REST, JWT, SQL and CORS plugins are built into the package (bottler.plugins). Runtime dependencies are only bottle, PyJWT, pymysql and toml. The plugin classes (API, Resource, JWTPlugin, sqlitePlugin, sqlPlugin, CorsPlugin) can also be imported from bottler.plugins and installed on a plain Bottle app.

Installation

python3 -m pip install bottler

Examples

Complete example projects live in examples/:

  • hello_world - hand-written resources, no database
  • task_tracker - sqlite auto-CRUD, roles, custom joins, the dashboard
  • auth_hooks - auth.py, hooks.py, api_prefix, default_roles, CORS
  • mounts - several packages and databases in one app
  • plain_bottle - the plugins on a plain bottle.Bottle

Usage

CLI

Create Project

$ bottler-create
Enter project name. <bottler-project>:
Create a SQLite database? [y/N]:
Attach an SQL database? [y/N]:
Created project bottler-project
    cd bottler-project
    bottler -d -r

Run

$ cd bottler-project
$ bottler -d -r
View dashboard @ http://localhost:8000/dashboard
Bottle v0.13-dev server starting up (using WSGIRefServer(dashboard=True))...
Listening on http://localhost:8000/
Hit Ctrl-C to quit.

Options

usage: bottler [-h] [--port PORT] [--host HOST] [--dir PATH] [--jwt [JWT_KEY]]
               [--sqlite [PATH]] [--dbhost DBHOST] [--dbname DBNAME]
               [--dbuser DBUSER] [--dbpass DBPASS] [--cors CORS] [-r] [-d]

options:
  -h, --help       show this help message and exit
  --port PORT      Port to listen on
  --host HOST      Host to listen on
  --dir PATH       Change to this directory before starting, so bottler.toml
                   and resources/ are resolved relative to it
  --jwt [JWT_KEY]  JWT key
  --sqlite [PATH]  Path to SQLite database (default: <package>/cli/tmp.db)
  --dbhost DBHOST  SQL database host
  --dbname DBNAME  SQL database name
  --dbuser DBUSER  SQL database username
  --dbpass DBPASS  SQL database password
  --cors CORS      Enable CORS
  -r               Automatic reloading
  -d               Enable dashboard

--jwt with no value generates a random key. --sqlite with no path uses a temporary database that is deleted on exit.

Import

app.py

from bottler import Bottler

app = Bottler()
app.run(reloader=True)

Resources

Resource Folder

Bottler will attempt to automatically find Resources objects in a resource folder in the working directory and create endpoints for them. By default it will look for a folder named "resources". All ".py" files in the folder will be scanned for Resource objects.

Example Project Structure

├── src
│   ├── resources
│   │   ├── __init__.py
│   │   ├── resource_a.py
|   |   ├── resource_b.py
│   ├── app.py

Resource Objects

A resource is a subclass of bottler.Resource. Each method it defines (options, get, post, put, patch, delete) becomes a route for that HTTP method on every path the resource is added at.

Method arguments are filled from URL parameters and the JSON request body. If a required argument (one with no default) is missing from both, the request fails with 400 {"<name>": "This field is required"}. Query-string values are available through self.params.

When a SQLite or SQL database is configured, an argument named db receives a database cursor (SQLite uses ? placeholders, MySQL uses %s):

from bottler import Resource

class ProjectTasks(Resource):
    def get(self, db, project_id):
        sql = "SELECT * FROM tasks WHERE project_id = ?"
        return {"tasks": db.execute(sql, (project_id,)).fetchall()}

Per-method route options such as roles are set as class attributes named after the HTTP method:

class Reports(Resource):
    GET = {"roles": ["admin"]}

    def get(self):
        return {}

resource_a.py

from bottler import Resource

class ResourceA(Resource):

    def options(self):
        pass

    def get(self):
        return {}
    
    def post(self):
        pass
    
    def put(self):
        pass

    def patch(self):
        pass

    def delete(self):
        pass

Adding Endpoints for Resources

Bottler has a Bottle REST object. Resources can be added by accessing the Bottle REST object and calling addResource().

app.py

from resources.resource_a import ResourceA
from bottler import Bottler

app = Bottler()
app.rest.addResource(ResourceA, "/resource_a")
app.run(reloader=True)

Auto-Generated Database Resources

When a SQLite or SQL database is configured (sqlite=... / sql=..., via the constructor or bottler.toml), Bottler introspects every table on startup and auto-generates a full CRUD Resource for each table that doesn't already have a route (gen_db=True by default - pass gen_db=False to disable). Only tables with a detected primary key are eligible.

For a table widgets with primary key id, this registers:

  • GET/POST /widgets - list (filterable by any ?column=value, LIKE-matched) / create
  • GET/PUT/PATCH/DELETE /widgets/<id> - fetch/replace/patch/delete a single row
  • GET /<ref_table>/<ref_id>/widgets - fetch rows referencing another table's row via a detected foreign key

Any auto-CRUD GET also accepts ?levels=N to inline foreign-key references N levels deep - e.g. GET /widgets/1?levels=2 replaces a category_id column with a nested category object.

Per-table role gating and custom paths are configured under [resources.<table>] in bottler.toml. See examples/task_tracker/ for a full working demo.

API Prefix

Set api_prefix (constructor kwarg or top-level bottler.toml key, e.g. api_prefix = "/api") to prepend a fixed prefix to every route Bottler registers as part of your project's own API — hand-authored resources, auto-generated database resources, and the JWT /token/ /users/current endpoints. This is useful when something else (a static-site resource, a frontend build) needs to own the unprefixed paths. Bottler's own dashboard and meta/admin routes (/dashboard, /openapi.json, /_resources, etc.) are never affected by api_prefix.

An individual Resource subclass can opt out of the global prefix (or use a different one) by setting its own api_prefix class attribute:

from bottler import Resource

class ResourceA(Resource):
    api_prefix = ""  # always unprefixed, even if the app sets api_prefix

    def get(self):
        return {}

A resource with an api_prefix override — e.g. a root-mounted catch-all like /<path:path> serving a static SPA build — is always registered after every default-prefixed route (hand-authored, built-in, and auto-generated DB resources), so it can never shadow a real API route no matter what order its module happens to load in.

Default Roles

Hand-authored and auto-generated database resources are open unless they set roles. Set default_roles (constructor kwarg or top-level bottler.toml key) to protect all of them at once:

default_roles = true                  # any valid token
# default_roles = ["admin", "finance"]  # token must hold at least one role

A list can also be given as a comma-separated string ("admin,finance"). The default applies only to methods that declare no roles of their own. Roles set on the resource class, or under [resources.<name>.roles], win. An explicit false opts a method out, for example a public static-site resource:

[resources.spa.roles]
get = false

OPTIONS never gets the default, so CORS preflight still works. The built-in meta routes (/_resources, /_roles, ...) keep their own roles = true and ignore default_roles. The JWT /token route is unaffected.

Mounting Packages

One app can serve several installed packages, each under its own URL prefix and each with its own database. List them under mounts in bottler.toml:

jwt = "..."
api_prefix = "api"

[mounts]
billing = "billing_api"        # short form: config folder = "billing"

[mounts.inventory]
package = "inventory_api"      # importable package with a resources/ subpackage
base_dir = "services/inventory"  # its config folder (default: the mount name)

Each mount is a child Bottler built from <package>.resources (not ./resources), served at <api_prefix>/<name>/. Its own <base_dir>/bottler.toml sets its sql/sqlite, resources.* paths and roles, and default_roles; DB auto-CRUD works per mount. The child always takes jwt, cors, rest, dashboard, openapi and api_prefix from the parent (its own values for those are ignored), so one token works everywhere and only the parent's /token issues one. A mount without its own default_roles inherits the parent's.

The package's __init__.py may define configure(app). It runs last, like hooks.py, and app.base_dir is the mount's config folder. Packages never get an auth.py or hooks.py from the working directory.

A package can also run on its own (e.g. in its own repo for development): Bottler(package="inventory_api", base_dir="config").

Custom Setup Hooks

If a project defines a top-level hooks.py with a callable configure(app), Bottler calls it automatically at the very end of __init__ — after every resource, DB table, and built-in route (including /openapi.json) is already registered. This is the place to add anything Bottler doesn't already cover: extra routes, additional Bottle plugins, middleware, or overriding a route Bottler itself just set up.

hooks.py

def configure(app):
    @app.route("/healthz")
    def healthz():
        return {"status": "ok"}

Disable auto-discovery with gen_hooks=False (constructor-only, not bottler.toml-configurable). This works the same way whether the app is started via the bottler CLI, a hand-rolled app.py, or a WSGI deploy — hooks.py just needs to sit at the process's working directory when Bottler() is constructed.

Metadata

Release files for bottler 0.0.20

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bottler 0.0.20
File Size Uploaded Provenance
bottler-0.0.20.tar.gz 1.9 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for bottler 0.0.20
File Interpreter ABI Platform
bottler-0.0.20-py3-none-any.whl Python 3 none any Details

Total release size: 3.8 MB

Release files / bottler-0.0.20.tar.gz

Download URL bottler-0.0.20.tar.gz
Size 1.9 MB
Tags Source
SHA-256 checksum
How to use checksums
94ee7cc18bf92493e92bafba9c4547a2a544038daaf7ef1774edffd41879704c
BLAKE2b-256 checksum
How to use checksums
b003e660fae1236fe62d26b13e3424e8087aa323c2f497ec163bc16cb581b1f7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / bottler-0.0.20-py3-none-any.whl

Download URL bottler-0.0.20-py3-none-any.whl
Size 1.9 MB
Tags Python 3
SHA-256 checksum
How to use checksums
a806fb93e41a3de5b15225062844d228f7d08b635a68c8d14f0c7bc5574e84d7
BLAKE2b-256 checksum
How to use checksums
ccb632295fb0c1bd96277281031c1b6b89bb49f03eb477fb161c9954aaca6cc0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for bottler 0.0.20

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page