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
- Models & Entities
- Getting Started
- Setup
- CLI Reference
- Building with PaperDraft
- Project Structure
- 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.x → from 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
Accountfrommodels/entities/account.pyandAccountModelfrommodels/schemas/account.py - Route signatures and return types use
AccountModel - Service DB calls (
create,retrieve,single,update,delete,filter) are wired and active — noraise 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.py—class Account(Base)models/schemas/account.py—class AccountModel(BaseSchema)- Registers both in their respective
__init__.pyunder 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)inheritingid,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)— inheritsfrom_attributes=True, no repeated config- Inline commented examples for every field category (required, optional, FK, nested, sensitive)
- Full reference lives in
models/schemas/base.py—Field(), validators, split patterns,model_configoptions
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 existsrouter.py,controller.py,service.py,__init__.pyall presentrouter.pyreferencesUsersControllercontroller.pydefinesclass UsersControllerservice.pydefinesclass UsersServicemain.pyimports 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e32b1d36c012eedf6fb7031294f421b8815ab7617995b7f8bb9bb56603c13087
|
|
| MD5 |
6810edf9b5aa968c2f1acf6cac9bbd15
|
|
| BLAKE2b-256 |
36287b5c1c2b618eeea9a635265ced714a1461760ece0d9af8ce426daca44a35
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a43df81973076d1df8ed12172f6bff8521951202a479c515848249cdc0e2153c
|
|
| MD5 |
ba1bb2c3d59cf41ad196115de61bedc7
|
|
| BLAKE2b-256 |
36f38cb981a76541de42a625fff7d6bcc712a3625851f208a08d26cd761696f3
|