Opinionated FastAPI toolkit with CLI-driven architecture
Reason this release was yanked:
still in test mode
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
- Installation
- Setup
- CLI Reference
- Building with paper-draft
- Project Structure
- Extending paper-draft
- 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 |
Installation
Requirements: Python 3.11+
pip install paper-draft
paper-core is installed automatically as a dependency.
Local Development Setup
Prerequisites: Python 3.11+
# 1. Clone the CLI repo
git clone https://flypaperplane@bitbucket.org/ppc-llc/paper-draft.git
cd paper-draft
# 2. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
# 3. Install paper-core
pip install paper-core
# 4. Install paper-draft CLI in editable mode with dev dependencies
pip install -e ".[dev]"
Once installed, the draft CLI is available in your environment:
draft --help
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.
# Generate a 2048-bit RSA private key
openssl genrsa -out private.pem 2048
# Derive the public key
openssl rsa -in private.pem -pubout -out public.pem
# Base64-encode each for .env storage
base64 -w 0 private.pem # → ENCRYPTION_PRIVATE_KEY
base64 -w 0 public.pem # → ENCRYPTION_PUBLIC_KEY
CLI Reference
draft init <name> --port <port>
Creates a new project in a <name>/ directory.
draft init my-api --port 8000
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
├── services/ # Your services go here
└── .venv/ # Python virtual environment (auto-created)
Then:
cd my-api
# Fill in .env.dev with your DB connection string and keys
# Activate the venv: source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate (Windows)
draft new service <name>
Scaffolds a new service inside an existing project.
draft new service users
draft new service users --prefix api/v1/users # custom URL prefix
Creates:
services/users/
├── __init__.py
├── router.py # FastAPI APIRouter — define your routes here
├── controller.py # UsersController — validate + orchestrate
└── service.py # UsersService — business logic
Also patches main.py to import and register the router automatically.
draft validate <service>
Validates a service's structure and main.py registration.
draft validate users
draft 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 my-api --port 8000
cd my-api
# 2. Fill in .env.dev
# 3. Add a service
draft new service users
# 4. Define your routes in services/users/router.py
# 5. Implement controller logic in services/users/controller.py
# 6. Implement business logic in services/users/service.py
# 7. Validate
draft validate users
# 8. 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)
├── services/
│ ├── users/
│ │ ├── router.py
│ │ ├── controller.py
│ │ └── service.py
│ └── auth/
│ ├── router.py
│ ├── controller.py
│ └── service.py
└── .venv/
Extending PaperDraft
PaperDraft's core components are built on abstract base classes. Extend them to add new database engines, encryption algorithms, or email providers while keeping the same interface everywhere.
Custom Database Engine
Extend Repository to add a new DB engine (MySQL, MongoDB, etc.).
from paper.core.db.base import Repository, FilterType
from typing import Any, Dict, List, Optional
class MySQLRepository(Repository[T, M]):
def __init__(self, connection_string: str) -> None:
# set up your engine here
...
async def create(self, entity, model, data):
...
async def retrieve(self, entity, model, filter=None):
...
async def single(self, entity, model, id):
...
async def update(self, entity, model, id, data):
...
async def delete(self, entity, id):
...
Inject it the same way as Postgres via dependencies.py.
Custom Encryption Algorithm
Extend BaseCrypto to add a new cipher (AES, ChaCha20, etc.).
from paper.core.security.base import BaseCrypto
class AESCrypto(BaseCrypto):
def __init__(self, key: str) -> None:
self._key = key
def encrypt(self, value: str) -> str:
...
def decrypt(self, cipher: str) -> str:
...
def encrypt_urlsafe(self, value: str) -> str:
...
def decrypt_urlsafe(self, cipher: str) -> str:
...
def encrypt_raw(self, value: str) -> bytes:
...
def decrypt_raw(self, cipher_bytes: bytes) -> str:
...
Pass your implementation to MultiTenantDbDependency or anywhere BaseCrypto is accepted:
crypto = AESCrypto(key=config.ENCRYPTION.KEY.SYMMETRIC)
Custom Email Provider
Extend BaseEmailService to add a new provider (SendGrid, SES, Postmark, etc.).
from paper.core.email.base import BaseEmailService
from typing import Dict
class SendGridEmailService(BaseEmailService):
def __init__(self, api_key: str, sender_name: str, sender_email: str) -> None:
self._api_key = api_key
self._sender_name = sender_name
self._sender_email = sender_email
def send(
self,
subject: str,
recipient_name: str,
recipient_email: str,
data: Dict[str, str],
) -> bool:
# implement SendGrid API call here
...
Instantiate in dependencies.py and inject wherever email is needed.
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.1.0.tar.gz.
File metadata
- Download URL: paper_draft-0.1.0.tar.gz
- Upload date:
- Size: 16.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ba4564d87e89033b7adee4d387dd0d3556f0361bfc8fd9788af84d75d434cfc
|
|
| MD5 |
522db998ebdbd7acd0cfc7ba924a9f61
|
|
| BLAKE2b-256 |
2ee6850cb87794b79660c0390765d9a970b5e46529d093b3b62c7da61e4d98d4
|
File details
Details for the file paper_draft-0.1.0-py3-none-any.whl.
File metadata
- Download URL: paper_draft-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.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 |
f573190c03f0509e7e2d6e275f373ab58903ac9afec394e7a8466ac1933bb60c
|
|
| MD5 |
3742201261f04755ba13bfd5b3abee87
|
|
| BLAKE2b-256 |
ddc51c54b94e1f4e8845e2b2d826097dc91de548791c8f29e08e4aa21f379ac5
|