Flask newsletter + microblog package. Importable Blueprint, standalone web app, or headless API.
Project description
QUILLET
Flask newsletter + microblog package. Three deployment modes, pluggable backends, minimal deps.
pip install "quillet[sqlalchemy,mailgun]"
Three modes
| Mode | When to use |
|---|---|
| Blueprint | Add newsletters to an existing Flask app |
| Standalone web | Run as a separate Docker service, proxy via nginx |
| Headless API | JSON-only backend; bring your own frontend |
Mode 1 — Flask Blueprint
from quillet import create_blueprint
from quillet.db.sqlalchemy import SQLAlchemyRepository
from quillet.email.mailgun import MailgunSender
app.register_blueprint(
create_blueprint(
db=SQLAlchemyRepository("sqlite:///newsletter.db"),
email=MailgunSender(api_key="...", domain="..."),
admin_password="secret",
),
url_prefix="/newsletter",
)
All routes are now available at /newsletter/<newsletter_slug>/. Create your first newsletter:
export FLASK_APP=your_app
flask quillet create "My Blog" --slug=blog --from-email=hi@example.com
Then visit http://localhost:5000/newsletter/blog/.
Blueprint options
create_blueprint(
db=...,
email=...,
admin_password="secret",
mode="web", # "web" (default) or "api" — disables HTML routes
admin_ui=True, # set False to disable the browser admin UI
base_url="", # override for email links (confirm, unsubscribe)
name="quillet", # blueprint name; change if you register multiple instances
)
Mode 2 — Standalone Docker (web)
docker compose up
Visit http://localhost:8000. Configure via environment variables:
| Variable | Default | Description |
|---|---|---|
QUILLET_MODE |
web |
web or api |
QUILLET_ADMIN_PASSWORD |
(required) | Basic Auth password; username is always admin |
QUILLET_ADMIN_UI |
true |
Set false to disable the browser admin panel |
QUILLET_BASE_URL |
(host URL) | Public URL used in email links — set this in production |
QUILLET_DB_BACKEND |
sqlalchemy |
sqlalchemy or supabase_rest |
QUILLET_DB_URL |
sqlite:////data/quillet.db |
SQLAlchemy connection string |
QUILLET_SUPABASE_URL |
— | Supabase project URL |
QUILLET_SUPABASE_KEY |
— | Supabase anon key |
QUILLET_EMAIL_BACKEND |
smtp |
mailgun, smtp, or noop |
QUILLET_MAILGUN_API_KEY |
— | Mailgun private API key |
QUILLET_MAILGUN_DOMAIN |
— | Mailgun sending domain |
QUILLET_SMTP_HOST |
localhost |
SMTP host |
QUILLET_SMTP_PORT |
587 |
SMTP port |
QUILLET_SMTP_USE_TLS |
true |
Set false for local dev (e.g. Mailhog) |
QUILLET_SMTP_USERNAME |
— | SMTP username (optional) |
QUILLET_SMTP_PASSWORD |
— | SMTP password (optional) |
QUILLET_SMTP_FROM_EMAIL |
(required) | Sender email address |
QUILLET_SMTP_FROM_NAME |
— | Sender display name |
Create a newsletter after starting:
docker compose exec quillet flask quillet create "My Blog" --slug=blog --from-email=hi@example.com
Mode 3 — Headless API
Set QUILLET_MODE=api (and QUILLET_ADMIN_UI=false). All endpoints return JSON only; no HTML templates are rendered.
Routes
All routes are prefixed with /<newsletter_slug>/.
Public
| Method | Path | Description |
|---|---|---|
GET |
/<slug>/ |
Post archive. Returns HTML or JSON (Accept: application/json). |
GET |
/<slug>/posts/<post_slug> |
Single post. Returns HTML or JSON. |
POST |
/<slug>/subscribe |
Subscribe. Accepts form email or JSON {"email": "..."}. Sends double opt-in email. |
GET |
/<slug>/confirm/<token> |
Confirm subscription via emailed link. |
GET |
/<slug>/unsubscribe/<token> |
Unsubscribe via emailed link. |
Admin browser UI (Basic Auth)
| Method | Path | Description |
|---|---|---|
GET |
/<slug>/admin/ |
Dashboard — post list with status badges, subscriber count. |
GET/POST |
/<slug>/admin/posts/new |
Create post form. |
GET/POST |
/<slug>/admin/posts/<post_slug>/edit |
Edit post. Publish and Send buttons are inline. |
POST |
/<slug>/admin/posts/<post_slug>/publish |
Publish (sets published_at). |
POST |
/<slug>/admin/posts/<post_slug>/send |
Send to all confirmed subscribers. Idempotent — a sent post cannot be re-sent. |
GET |
/<slug>/admin/subscribers |
Subscriber list with confirmation status. |
JSON API (Basic Auth)
| Method | Path | Body / Response |
|---|---|---|
POST |
/<slug>/api/posts |
{"title", "slug", "body_md"} → {"post": {...}} |
POST |
/<slug>/api/posts/<post_slug>/publish |
→ {"post": {...}} |
POST |
/<slug>/api/posts/<post_slug>/send |
→ {"ok": true, "recipients": N} |
GET |
/<slug>/api/subscribers |
→ {"subscribers": [...]} |
CLI
# Create a newsletter
flask quillet create "My Blog" --slug=blog --from-email=hi@example.com --from-name="My Blog"
# List all newsletters
flask quillet list
# Send a published post
flask quillet send blog my-post-slug
# Force re-send (already sent posts are blocked by default)
flask quillet send blog my-post-slug --force
# List subscribers
flask quillet subscribers blog
Custom backends
Implement the NewsletterRepository or EmailSender protocol — no base classes, just matching method signatures.
Custom DB backend
from quillet.db import NewsletterRepository
from quillet.models import Newsletter, Post, Subscriber
class MyRepository:
def get_newsletter(self, slug: str) -> Newsletter | None: ...
def list_posts(self, newsletter_slug: str, published_only: bool = True) -> list[Post]: ...
def get_post(self, newsletter_slug: str, post_slug: str) -> Post | None: ...
def create_post(self, newsletter_slug: str, title: str, slug: str, body_md: str) -> Post: ...
def update_post(self, post_id: int, title: str, slug: str, body_md: str) -> Post: ...
def publish_post(self, post_id: int) -> Post: ...
def mark_sent(self, post_id: int) -> None: ...
def add_subscriber(self, newsletter_slug: str, email: str, token: str) -> Subscriber: ...
def confirm_subscriber(self, token: str) -> Subscriber | None: ...
def list_confirmed_subscribers(self, newsletter_slug: str) -> list[Subscriber]: ...
def list_all_subscribers(self, newsletter_slug: str) -> list[Subscriber]: ...
def unsubscribe(self, token: str) -> None: ...
def create_newsletter(self, slug: str, name: str, from_email: str, from_name: str, reply_to: str | None) -> Newsletter: ...
Custom email backend
from quillet.email import EmailSender
from quillet.models import Newsletter, Post, Subscriber
class MyEmailSender:
def send_confirmation(
self,
newsletter: Newsletter,
subscriber: Subscriber,
confirm_url: str,
) -> None: ...
def send_post(
self,
newsletter: Newsletter,
post: Post,
subscribers: list[Subscriber],
unsubscribe_url_template: str, # contains {token} placeholder
) -> None: ...
Template overrides
All built-in templates live at quillet/templates/quillet/. Flask resolves templates in this order: app templates → blueprint templates. Drop an override anywhere Flask finds templates first.
Blueprint mode — add to your app's templates/ directory:
templates/
└── quillet/
├── base.html
├── post_list.html
├── post_detail.html
├── subscribe_confirm.html
└── admin/
├── dashboard.html
├── post_form.html
└── subscribers.html
Standalone Docker — mount a volume:
volumes:
- ./my-templates:/app/templates/quillet
Template context variables
| Template | Variables |
|---|---|
post_list.html |
newsletter, posts, subscribe_error (optional) |
post_detail.html |
newsletter, post, post_html (rendered HTML string) |
subscribe_confirm.html |
newsletter, state (pending/confirmed/unsubscribed/invalid) |
admin/dashboard.html |
newsletter, posts, subscriber_count, confirmed_count |
admin/post_form.html |
newsletter, post (None if new), error (optional) |
admin/subscribers.html |
newsletter, subscribers |
All model fields are accessible as attributes (e.g. newsletter.name, post.title, post.published_at).
Development email backend
Use noop to skip all email sending — confirmation links are printed to logs instead:
from quillet.email.noop import NoopSender
create_blueprint(db=..., email=NoopSender(), admin_password="...")
Or via env var: QUILLET_EMAIL_BACKEND=noop
Testing
# Fast: Flask test client, no server required
python scripts/smoke_test.py
# Full: build Docker image + one-shot container test
./scripts/docker_test.sh
# Skip rebuild
./scripts/docker_test.sh --no-build
# HTTP tests against any running instance
QUILLET_TEST_URL=https://your-server.com \
QUILLET_TEST_SLUG=blog \
QUILLET_ADMIN_PASSWORD=secret \
python scripts/http_smoke_test.py
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 quillet-0.2.0.tar.gz.
File metadata
- Download URL: quillet-0.2.0.tar.gz
- Upload date:
- Size: 32.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b01cddb0f49976a79bbfee814aa80a0fd87332144cf4e58042140ccf3638191
|
|
| MD5 |
c5ef5f446a7888974557ee25b32f3b48
|
|
| BLAKE2b-256 |
282ed088b8b96482166c43fc07ff155bcfde5a803f7122c29979cfe5e8bf8980
|
Provenance
The following attestation bundles were made for quillet-0.2.0.tar.gz:
Publisher:
release.yml on TinMarkovic/quillet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillet-0.2.0.tar.gz -
Subject digest:
9b01cddb0f49976a79bbfee814aa80a0fd87332144cf4e58042140ccf3638191 - Sigstore transparency entry: 1294430729
- Sigstore integration time:
-
Permalink:
TinMarkovic/quillet@d9fe810bdc7c827fb2c2250e8bf87b166be6aea1 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/TinMarkovic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d9fe810bdc7c827fb2c2250e8bf87b166be6aea1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file quillet-0.2.0-py3-none-any.whl.
File metadata
- Download URL: quillet-0.2.0-py3-none-any.whl
- Upload date:
- Size: 26.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7812d8f24fcebb69285ef19d2d35e242b411147af1d9cba43af27ae0b2785f9
|
|
| MD5 |
68ae1e87e97045936135e89c20f0bbe0
|
|
| BLAKE2b-256 |
2d196c3c98b61631d751a8eaf0cabbec08b87e4583ac2d2de28fb6cbb5914d3e
|
Provenance
The following attestation bundles were made for quillet-0.2.0-py3-none-any.whl:
Publisher:
release.yml on TinMarkovic/quillet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillet-0.2.0-py3-none-any.whl -
Subject digest:
e7812d8f24fcebb69285ef19d2d35e242b411147af1d9cba43af27ae0b2785f9 - Sigstore transparency entry: 1294430793
- Sigstore integration time:
-
Permalink:
TinMarkovic/quillet@d9fe810bdc7c827fb2c2250e8bf87b166be6aea1 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/TinMarkovic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d9fe810bdc7c827fb2c2250e8bf87b166be6aea1 -
Trigger Event:
push
-
Statement type: