Skip to main content

Automated OpenAPI 3.1.0 documentation for Bottle

Project description

Bottle AutoDocs

BottleAutoDocs simplifies OpenAPI 3.1.0 documentation for Bottle applications by allowing you to define API details directly in route definitions eliminating the need to write YAML manually. Most of the complexity is handled internally, providing a clean and intuitive way to generate accurate API specs. It supports JWT and Basic authentication, file uploads, parameterized routes, and subapp mounting, making it easy to document and test complex APIs.

Installation

pip install bottle-autodocs

Usage

Define route details directly in the route decorator using OpenAPI specification information:

from bottle import Bottle , run
from bottle_autodocs import bottle_autodocs

app = Bottle()
auto_docs = bottle_autodocs.BottleAutoDocs()
app.install(auto_docs)

@app.route('/home', method='POST', summary="This is summary for home API", description="This is description for home API")
def home():
    return {'message': 'Welcome home'}



if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Now run the app and go to http://localhost:8080
here the swagger ui is served

Metadata

You can set some of the metadata fields that are used in the OpenAPI specification and the automatic API docs UIs

from bottle import Bottle, run
from bottle_autodocs import bottle_autodocs

app = Bottle()

auto_docs = bottle_autodocs.BottleAutoDocs( title="My API",
version="1.0.0",
description="My API Docs",
terms_of_service="http://example.com/terms/",
    contact={
        "name": "name of author",
        "url": "http://url.example.com/contact/",
        "email": "author.example.com",
    },
    license_info={
        "name": "Apache 2.0",
        "url": "https://license_link.com",
    },summary="This is the main summary" )

app.install(auto_docs)





if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Tags

Use OpenAPI tags in the route decorator to organize endpoints into logical groups within the Swagger UI

from bottle import Bottle, run
from bottle_autodocs import bottle_autodocs

tags_metadata = [
    {
        "name": "users",
        "description": " users API docs",
    },
    {
        "name": "Products",
        "description": "Products API docs.",
        "externalDocs": {
            "description": "Products external docs",
            "url": "https://bottlepy.org/docs/dev/",
        },
    },
]

app = Bottle()

# Pass the tags parameter 
auto_docs = bottle_autodocs.BottleAutoDocs(openapi_tags=tags_metadata,title="My API",version="1.0.0",description=" API Docs tags")
app.install(auto_docs)

@app.route('/products', summary="Add product", description="Add new product",tags=['Products'])
def add_product():
    return {"category": 1, "id": 1}

@app.route('/users/<id:int>', method='GET', summary="Get user", description="Get user by ID",tags=['users'])
def get_user(id):
    return {"id": id, "name": "John Doe"}



if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Parameter passing

Parameters defined in route paths (e.g., /user/) are automatically converted into input fields in Swagger UI, allowing users to test the endpoints directly from the documentation

from bottle import Bottle, run
from bottle_autodocs import bottle_autodocs

app = Bottle()


# Install the plugin
auto_docs = bottle_autodocs.BottleAutoDocs(title="My API", version="1.0.0", description=" API Docs for params")
app.install(auto_docs)

@app.route('/user/<param>', method='GET', summary="Get user", description="Get users")
def param_passing(param):
    return f"This is the parameter passed {param}"

@app.route('/users/<param:int>', summary="Create user", description="Create user")
def type_param_passing(param):
    return f"This is the parameter passed with type specified {param}"

@app.route('/users/<param1>/<param2:int>', summary="Override user", description="Override user")
def nesting_param_passing(param1,param2):
    return f"This is nesting of params {param1} , {param2}"



if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Subapps

Routes defined in different subapps are automatically separated into distinct sections based on the subapp name if no tags are provided

from bottle import Bottle, run
from bottle_autodocs import bottle_autodocs


app = Bottle()
subapp1 = Bottle()
subapp2 = Bottle()



# app ---> subapp1 ---> subapp2

auto_docs = bottle_autodocs.BottleAutoDocs(title="My API", version="1.0.0", description="API subapps")
app.install(auto_docs)


@subapp1.route('/',summary='This is the subapp1 ',description='This is under app')
def fun1():
    return 'from subapp1 under app'


@subapp2.route('/',summary='This is the subapp2 ',description='This is under subapp1')
def fun2():
    return 'from subapp2 under subapp1'

@app.route('/',summary='This is the app ',description='This is the main app')
def fun3():
    return 'from the main app'

app.mount('/sub1',subapp1)
subapp1.mount('/sub2',subapp2)


if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Files

BottleAutoDocs detects file uploads (both single and multiple) and generates the correct OpenAPI spec. Files can be uploaded directly in Swagger UI for the defined endpoint

from bottle import Bottle, run , request
from bottle_autodocs import bottle_autodocs

app = Bottle()


UPLOAD_PATH = r'\downloads' # Make sure this folder exist or change name to a folder that exists

# Install the plugin
auto_docs = bottle_autodocs.BottleAutoDocs(title="My API", version="1.0.0", description="API Docs File Uploads ")
app.install(auto_docs)


@app.route('/multiupload', method='POST', summary="This is summary for multi file upload", description="This is desc for multi file upload")
def upload_files():
    # Get the list of files
    uploads = request.files.getall('files')  # Use getall() to handle multiple files
    metadata = request.forms.get('metadata')
    meta = request.forms.get('meta')

    if not uploads:
        return {"error": "No files provided"}

    saved_files = []
    for upload in uploads:
        file_path = f"{UPLOAD_PATH}/{upload.filename}"
        upload.save(file_path)  # Save file to UPLOAD_PATH
        saved_files.append(upload.filename)

    return {
        "message": f"{len(saved_files)} files uploaded successfully",
        "uploaded_files": saved_files,
        "metadata": metadata,
        'meta': meta
    }


@app.route('/upload', method='POST', summary="This is summary for single file upload", description="This is desc for single file upload")
def upload_file():
    # Get the list of files
    upload = request.files.get('file')  # Use getall() to handle multiple files
    metadata = request.forms.get('metadata')

    if not upload:
        return {"error": "No files provided"}

    file_path = f"{UPLOAD_PATH}/{upload.filename}"
    upload.save(file_path) 


    return {
        "message": f" files uploaded successfully",
        "metadata": metadata,
    }


if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Basic Auth

BottleAutoDocs supports Basic Authentication (username and password). After logging in once using the "Authorize" button in Swagger UI, the credentials are automatically included in subsequent requests
Note
Bottle Autodocs internally checks for request.auth and determines if it is a protected route

from bottle import Bottle, request, HTTPError, run
import functools
from bottle_autodocs import bottle_autodocs
app = Bottle()
app.install(bottle_autodocs.BottleAutoDocs(title="My API", version="1.0.0", description=" API Docs basic auth"))
USERS = {
    'admin': 'secret',
    'user': 'password'
}


def is_authenticated_user(user, password):
    return USERS.get(user) == password


def auth_basic(check, realm="private", text="Access denied"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*a, **ka):
            user, password = request.auth or (None, None)
            if user is None or not check(user, password):
                err = HTTPError(401, text)
                err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
                return err
            return func(*a, **ka)
        return wrapper
    return decorator


@app.route('/protected',summary='This is protected route',description='cannot access without logging in ')
@auth_basic(is_authenticated_user)
def protected():
    return {'status': 'success', 'message': f'Hello, {request.auth[0]}! You are authenticated.'}


# run the app and click on authorize and enter the following credentials 
# username = admin
# password = secret
# subsequent request will be automatically called with the credentials 
if __name__ == '__main__':
    run(app, host='localhost', port=8080)

Token Auth

BottleAutoDocs supports JWT-based authentication where a unique token is generated upon login and passed in the Authorization header. After logging in, pass the token through the "Authorize" button in Swagger UI, and the token will be automatically included in subsequent requests
Note
BottleAutoDocs internally checks for request.get_header('Authorization') and request.get_header('Authorization').startswith('Bearer ') to determine if it is a token-based authentication route.

import jwt
import datetime
import os
from bottle import request, run, HTTPResponse, Bottle
from bottle_autodocs import bottle_autodocs


SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')

app = Bottle()
app.install(bottle_autodocs.BottleAutoDocs(title="My API", version="1.0.0", description=" API Docs auth bearer"))


def generate_token(user):
    payload = {
        'user': user,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)  
    }
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token


def decode_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return {'error': 'Token expired'}
    except jwt.InvalidTokenError:
        return {'error': 'Invalid token'}


def auth_bearer(func):
    def wrapper(*args, **kwargs):
        auth_header = request.get_header('Authorization')
        if auth_header and auth_header.startswith('Bearer '):
            token = auth_header.split(' ')[1]
            payload = decode_token(token)
            if isinstance(payload, dict) and 'user' in payload:
                request.user = payload['user']
                return func(*args, **kwargs)
            else:
                return HTTPResponse(
                    status=401, 
                    body={'status': 'error', 'message': payload.get('error', 'Unauthorized')}
                )
        else:
            return HTTPResponse(
                status=401, 
                body={'status': 'error', 'message': 'Missing token'}
            )
    return wrapper


@app.route('/login', method='POST',summary='login route',description='used for loggin user in credentails are username=admin, password=secret')
def login():
    username = request.forms.get('username')
    password = request.forms.get('password')

   
    if username == 'admin' and password == 'secret':
        token = generate_token(username)
        return {'status': 'success', 'token': token}
    else:
        return HTTPResponse(
            status=401, 
            body={'status': 'error', 'message': 'Invalid credentials'}
        )


@app.route('/protected',summary='This is protected route',description='cannot access without logging in ')
@auth_bearer
def protected():
    return {'status': 'success', 'message': f'Hello, {request.user}! You are authorized.'}
@app.route('/protected/<id:int>',summary='This is protected route',description='cannot access without logging in ')
@auth_bearer
def protected(id):
    return {'status': 'success', 'message': f'Hello, {request.user}! You are authorized. and id is {id}'}

@app.route('/public',summary='this is public route',description='no need to log in to access this route')
def public():
    return 'no auth required for this route'


# run the app and go to login and enter the following credentials 
# username = admin
# password = secret
# copy the bearer token and paste it in the bearer field of authorize modal 
# subsequent request will be automatically with the bearer token
if __name__ == '__main__':
    run(app, host='localhost', port=8080, debug=True)

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

bottle_autodocs-0.1.23.tar.gz (12.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bottle_autodocs-0.1.23-py3-none-any.whl (9.7 kB view details)

Uploaded Python 3

File details

Details for the file bottle_autodocs-0.1.23.tar.gz.

File metadata

  • Download URL: bottle_autodocs-0.1.23.tar.gz
  • Upload date:
  • Size: 12.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for bottle_autodocs-0.1.23.tar.gz
Algorithm Hash digest
SHA256 9a4296a9826590c7c594a84b8b56c7953b2f21acc7da3c32f3444220e825aa23
MD5 8ea4a08a4f346c778e00e3bb20b8b511
BLAKE2b-256 899b3210e11fde77c442f1e2eb079ef8924401269d93b17ddfc95089e7f729be

See more details on using hashes here.

File details

Details for the file bottle_autodocs-0.1.23-py3-none-any.whl.

File metadata

File hashes

Hashes for bottle_autodocs-0.1.23-py3-none-any.whl
Algorithm Hash digest
SHA256 ed90fdabd9ac1795591562029e132080d919ef9ed4eb11b3cb6259bd0f569b89
MD5 aa52f9e61211b99969acaaa0e733abb2
BLAKE2b-256 8b0dce2ee9e56c9f8ad0a77462be865f7c38ccdcc706ab2063dab0b7d2a513f3

See more details on using hashes here.

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