Skip to main content

Opinionated FastAPI toolkit with CLI-driven architecture

Project description

PaperDraft

Opinionated Python REST API framework with CLI-driven architecture.

PaperDraft scaffolds projects and services with an enforced layered structure, and ships a production-ready core library covering auth, database, encryption, email, middleware, and more.

Status: pre-release · v0.1 in progress · Paper Plane Consulting LLC


Table of Contents

  1. Models & Entities
  2. Getting Started
  3. Setup
  4. CLI Reference
  5. Building with PaperDraft
  6. Project Structure
  7. Architecture

Packages

PaperDraft is split into two packages:

Package Purpose
paper-core Runtime library — auth, DB, security, middleware, email, errors, audit
paper-draft CLI tooling — project scaffolding and service generation

Models & Entities

Where should SQLAlchemy entities and Pydantic models live?

Recommended: a dedicated models package (separate repo)

The cleanest architecture puts all entities and models in their own repository, installed as a package into every service that needs them. No code is duplicated. Your backend services share a single source of truth. Your TypeScript frontend generates its types from the same OpenAPI schema those models produce.

Why a separate repo?

  • A single entity change is made once and versioned once.
  • Services can pin to a specific version and upgrade independently.
  • Models are never coupled to any one service's dependencies.
  • The package can be installed directly from a private Bitbucket (or GitHub/GitLab) repository — no PyPI account or private registry required.

Install from a private Bitbucket repo (no publishing needed):

# Using HTTPS + app password
pip install "git+https://your-user:your-app-password@bitbucket.org/your-org/myapp-models.git@main"

# Using SSH (recommended for CI/CD — add the deploy key to Bitbucket)
pip install "git+ssh://git@bitbucket.org/your-org/myapp-models.git@main"

# Pin to a tag for reproducible installs
pip install "git+ssh://git@bitbucket.org/your-org/myapp-models.git@v1.2.0"

In requirements.txt:

# Pinned to a release tag — change the tag when you want to upgrade
myapp-models @ git+ssh://git@bitbucket.org/your-org/myapp-models.git@v1.2.0

Minimal models package layout:

myapp-models/
├── pyproject.toml
└── myapp_models/
    ├── __init__.py
    ├── entities/
    │   ├── base.py      # SQLAlchemy DeclarativeBase + Base (id, timestamps)
    │   ├── user.py      # User entity
    │   └── account.py   # Account entity
    └── schemas/
        ├── user.py      # UserModel (Pydantic)
        └── account.py   # AccountModel (Pydantic)

Then in any service:

from myapp_models.user import UserEntity, UserModel

Alternative: models/ package inside this project

If a separate repo isn't practical yet (early stage, solo project, strict monorepo), put a models/ package at the project root. Services import from it directly. The structure mirrors the separate-repo layout exactly, so extracting it later is a rename (from models.xfrom myapp_models.x) and nothing else changes.

my-api/
├── models/
│   ├── __init__.py
│   ├── entities/
│   │   ├── base.py      # SQLAlchemy DeclarativeBase + Base (id, timestamps)
│   │   ├── user.py      # User entity
│   │   └── account.py   # Account entity
│   └── schemas/
│       ├── user.py      # UserModel (Pydantic)
│       └── account.py   # AccountModel (Pydantic)
├── services/
│   └── users/
│       └── service.py   # from models.entities.user import User
│                        # from models.schemas.user  import UserModel

Sharing across repos without a dedicated models package is not clean. The practical options — installing the whole app as a package (brings all its dependencies), copying files, or running a private PyPI registry — all have meaningful downsides. At that point, extracting models/ into its own repo is the right call.


Getting Started

Requirements: Python 3.11+

1. Open your workspace in VS Code

Open VS Code, select your workspace folder, and open an integrated terminal (Terminal → New Terminal).

2. Create a folder for your API service

mkdir my-api
cd my-api

3. Create a virtual environment

macOS / Linux:

python -m venv .venv

Windows:

python -m venv .venv

4. Activate the virtual environment

macOS / Linux:

source .venv/bin/activate

Windows:

source .venv/Scripts/activate

Windows:

.venv\Scripts\activate

Your terminal prompt will show (.venv) once active.

5. Install PaperDraft

pip install paper-draft

paper-core is installed automatically as a dependency.

6. Verify the install

draft --help

You should see the full list of available draft commands. You're ready to build.


Setup

Environment Files

PaperDraft uses layered .env files. The base .env sets the active environment, and the environment-specific file is loaded on top.

.env               ← sets ENV=dev (or staging, production)
.env.dev           ← development values (auto-created by draft init)
.env.staging       ← staging values (create manually)
.env.production    ← production values (create manually)

.env (minimal — sets the active environment):

ENV=dev

.env.dev (created automatically by draft init):

ENV=dev
APP_URL=http://localhost:8000
APP_PORT=8000

POSTGRES_CONN_STRING=postgresql+asyncpg://user:password@localhost:5432/dbname
POSTGRES_ADMIN_CONN_STRING=postgresql+asyncpg://user:password@localhost:5432/postgres

ENCRYPTION_PUBLIC_KEY=<base64-encoded PEM public key>
ENCRYPTION_PRIVATE_KEY=<base64-encoded PEM private key>

EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USERNAME=your@email.com
EMAIL_SMTP_PASSWORD=your-smtp-password
EMAIL_SENDER_NAME=Your App
EMAIL_SENDER_ADDRESS=no-reply@example.com

Generating Encryption Keys

PaperDraft uses RSA key pairs (RS256) for JWT signing and field-level encryption.

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
base64 -w 0 private.pem   # → ENCRYPTION_PRIVATE_KEY
base64 -w 0 public.pem    # → ENCRYPTION_PUBLIC_KEY

CLI Reference

draft init

Initialises a PaperDraft project in the current directory.

draft init
draft init --port 9000
draft init --port 9000 --description "My API"

Creates:

my-api/
├── main.py            # FastAPI app entry point
├── dependencies.py    # DB + auth dependency injection
├── config.py          # Pydantic Settings configuration
├── requirements.txt   # App dependencies
├── .gitignore
├── .env.dev           # Pre-filled environment stub (RSA keys auto-generated)
├── services/          # Your services live here
└── .venv/             # Python virtual environment (auto-created)

Then:

# Fill in .env.dev with your DB connection string
# Activate: source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate (Windows)

draft service new <name>

Scaffolds a new service and registers it in main.py.

draft service new users                              # prefix defaults to /users
draft service new authenticate --prefix /auth        # explicit prefix
draft service new order -p /v1/orders
draft service new users --model account              # wire Account entity + AccountModel
draft service new users -m AccountSubscription       # case-insensitive

Creates:

services/users/
├── __init__.py
├── router.py       # FastAPI APIRouter — HTTP layer only, routes + auth
├── controller.py   # UsersController — input validation + orchestration
└── service.py      # UsersService — DB access and business logic

Patches main.py with the import and app.include_router() call automatically.

--model NAME — when provided, all three files use the named model instead of Dict[str, Any] stubs:

  • Imports Account from models/entities/account.py and AccountModel from models/schemas/account.py
  • Route signatures and return types use AccountModel
  • Service DB calls (create, retrieve, single, update, delete, filter) are wired and active — no raise NotImplementedError

Accepts any casing — account, Account, account_subscription, AccountSubscription all work.


draft model init

Initialises the models/ package inside an existing project.

draft model init

Creates:

models/
├── __init__.py          # package root
├── entities/
│   ├── base.py          # SQLAlchemy Base + full column/relationship reference
│   └── __init__.py      # entity exports accumulate here
└── schemas/
    ├── base.py          # BaseSchema (from_attributes=True) + Pydantic field reference
    └── __init__.py      # schema exports accumulate here

Run once per project. Then use draft model entity and draft model schema to add files.


draft model new <name>

Scaffolds both an entity and a schema in one step. Equivalent to running draft model entity and draft model schema with the same arguments.

draft model new account
draft model new account_subscription --section "Master DB — Billing"

Creates:

  • models/entities/account.pyclass Account(Base)
  • models/schemas/account.pyclass AccountModel(BaseSchema)
  • Registers both in their respective __init__.py under the same section

draft model entity <name>

Scaffolds a SQLAlchemy entity in models/entities/.

draft model entity account
draft model entity account_subscription --section "Master DB — Billing"

Creates models/entities/account.py with:

  • class Account(Base) inheriting id, created_at, updated_at
  • Inline commented examples for every column category (strings, numerics, booleans, timestamps, FKs, relationships, constraints)
  • Full reference documentation lives in models/entities/base.py — read once, applies to every entity

Adds from models.entities.account import Account to models/entities/__init__.py.

--section sets the comment header label (e.g. "Master DB — Core", "Tenant DB — Billing"). Use the same label for the matching schema so paired files are visually obvious.


draft model schema <name>

Scaffolds a Pydantic schema in models/schemas/.

draft model schema account
draft model schema account_subscription --section "Master DB — Billing"

Creates models/schemas/account.py with:

  • class AccountModel(BaseSchema) — inherits from_attributes=True, no repeated config
  • Inline commented examples for every field category (required, optional, FK, nested, sensitive)
  • Full reference lives in models/schemas/base.pyField(), validators, split patterns, model_config options

Adds from models.schemas.account import AccountModel to models/schemas/__init__.py.


draft service validate <service>

Validates a service's structure and main.py registration.

draft service validate users
draft service validate users --quiet   # suppress output on success

Checks:

  • services/users/ directory exists
  • router.py, controller.py, service.py, __init__.py all present
  • router.py references UsersController
  • controller.py defines class UsersController
  • service.py defines class UsersService
  • main.py imports and registers the router

Exits 0 on success, 1 on validation errors, 2 if not in a project root.


draft run

Runs the app with uvicorn. Port is read from APP_PORT in your .env.{ENV} file.

draft run               # dev mode — auto-reload enabled
draft run --prod        # production — 4 workers, no reload
draft run --host 0.0.0.0 --prod

Building with PaperDraft

Workflow

# 1. Create a new project
draft init --port 8000
cd my-api

# 2. Fill in .env.dev with your DB connection string

# 3. Initialise the models package
draft model init

# 4. Add an entity and its matching schema (both at once)
draft model new account --section "Master DB — Core"
# Define columns in models/entities/account.py
# Mirror them as fields in models/schemas/account.py

# 5. Add a service
draft service new users

# 6. Define routes in services/users/router.py
# 7. Validate + orchestrate in services/users/controller.py
# 8. Implement DB logic in services/users/service.py
#    Import from models: from models.entities.account import Account
#                        from models.schemas.account  import AccountModel

# 9. Validate the service
draft service validate users

# 10. Run
draft run

Layer Responsibilities

Each scaffolded service has three layers with strict responsibilities:

router.py       ← HTTP only. Receives request, injects dependencies, returns response.
                  No logic. No direct DB access.

controller.py   ← Validates the request. Orchestrates service calls.
                  No business logic. No direct DB access.

service.py      ← All business logic lives here.
                  Calls the DB via injected Postgres instance.

Dependency Injection

dependencies.py wires up DB and config — inject them directly in route signatures:

from dependencies import MasterDb, TenantDb, Configuration

@router.get("")
async def retrieve(
    db:     MasterDb,
    config: Configuration,
    claims: Dict[str, Any] = Depends(Authenticate())
):
    return await UsersController.retrieve(db, config, claims)

Auth

Use Authenticate for any valid JWT, Authorize for role enforcement:

from paper.core.auth import Authenticate, Authorize

# Any authenticated user
claims = Depends(Authenticate())

# Role-restricted
claims = Depends(Authorize(["admin", "manager"]))

Project Structure

A full PaperDraft project looks like this:

my-api/
├── main.py                   # FastAPI app, middleware, router registration
├── dependencies.py           # DB + auth dependency wiring
├── config.py                 # Pydantic Settings
├── requirements.txt
├── .env                      # Sets ENV=dev|staging|production
├── .env.dev                  # Dev config (never commit)
├── models/                   # Entities + Pydantic schemas (see Models & Entities above)
│   ├── __init__.py           # or install myapp-models as a package instead
│   ├── entities/             # SQLAlchemy ORM classes
│   │   ├── base.py           # DeclarativeBase + Base (id, created_at, updated_at)
│   │   └── account.py        # Account entity
│   └── schemas/              # Pydantic schemas
│       ├── base.py           # BaseSchema + Pydantic field/validator reference
│       └── account.py        # AccountModel schema
├── services/
│   ├── users/
│   │   ├── __init__.py
│   │   ├── router.py
│   │   ├── controller.py
│   │   └── service.py
│   └── auth/
│       ├── __init__.py
│       ├── router.py
│       ├── controller.py
│       └── service.py
└── .venv/

Architecture

HTTP Request
    ↓
[ Router ]            receives request, injects dependencies, returns response
    ↓
[ Auth Middleware ]    JWT validation + RBAC — always first, never skipped
    ↓
[ Custom Middleware ]  CORS, HIPAA headers, rate limiting, audit logging
    ↓
[ Controller ]        validates request, orchestrates service calls
    ↓
[ Service ]           all business logic lives here
    ↓
[ Postgres / DB ]     async SQLAlchemy — injected, never instantiated directly

Core modules available via paper.core:

Module Contents
paper.core.auth Authenticate, Authorize, LoginAttemptLimit, Password, JWT utils
paper.core.db Postgres, Repository, FilterType, MultiTenantDbDependency
paper.core.security RSACrypto, BaseCrypto, Crypto, Hasher, Pem
paper.core.email SMTPEmailService, BaseEmailService, Subject, EmailTheme
paper.core.errors ErrorHandler, ErrorMessage
paper.core.middleware HipaaResponseHeaders, RequestLoggingMiddleware, RequestIdMiddleware
paper.core.audit Audit

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

paper_draft-0.2.0.tar.gz (27.3 kB view details)

Uploaded Source

Built Distribution

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

paper_draft-0.2.0-py3-none-any.whl (25.1 kB view details)

Uploaded Python 3

File details

Details for the file paper_draft-0.2.0.tar.gz.

File metadata

  • Download URL: paper_draft-0.2.0.tar.gz
  • Upload date:
  • Size: 27.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.6

File hashes

Hashes for paper_draft-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e32b1d36c012eedf6fb7031294f421b8815ab7617995b7f8bb9bb56603c13087
MD5 6810edf9b5aa968c2f1acf6cac9bbd15
BLAKE2b-256 36287b5c1c2b618eeea9a635265ced714a1461760ece0d9af8ce426daca44a35

See more details on using hashes here.

File details

Details for the file paper_draft-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: paper_draft-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 25.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.6

File hashes

Hashes for paper_draft-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a43df81973076d1df8ed12172f6bff8521951202a479c515848249cdc0e2153c
MD5 ba1bb2c3d59cf41ad196115de61bedc7
BLAKE2b-256 36f38cb981a76541de42a625fff7d6bcc712a3625851f208a08d26cd761696f3

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