quillet 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quillet-0.1.0/.cursor/plans/flask_newsletter_package_e8a6fd1a.plan.md +298 -0
- quillet-0.1.0/.cursor/rules/project-rules.mdc +41 -0
- quillet-0.1.0/.cursor/rules/python.mdc +12 -0
- quillet-0.1.0/.github/workflows/release.yml +74 -0
- quillet-0.1.0/.github/workflows/test.yml +28 -0
- quillet-0.1.0/.gitignore +14 -0
- quillet-0.1.0/Dockerfile +22 -0
- quillet-0.1.0/LICENSE +21 -0
- quillet-0.1.0/PKG-INFO +322 -0
- quillet-0.1.0/README.md +286 -0
- quillet-0.1.0/docker-compose.yml +30 -0
- quillet-0.1.0/llm_runbook.md +506 -0
- quillet-0.1.0/pyproject.toml +48 -0
- quillet-0.1.0/quillet/__init__.py +22 -0
- quillet-0.1.0/quillet/_app.py +5 -0
- quillet-0.1.0/quillet/admin.py +176 -0
- quillet-0.1.0/quillet/auth.py +36 -0
- quillet-0.1.0/quillet/cli.py +126 -0
- quillet-0.1.0/quillet/db/__init__.py +33 -0
- quillet-0.1.0/quillet/db/sqlalchemy.py +246 -0
- quillet-0.1.0/quillet/db/supabase_rest.py +228 -0
- quillet-0.1.0/quillet/email/__init__.py +20 -0
- quillet-0.1.0/quillet/email/mailgun.py +91 -0
- quillet-0.1.0/quillet/email/noop.py +39 -0
- quillet-0.1.0/quillet/email/smtp.py +108 -0
- quillet-0.1.0/quillet/factory.py +153 -0
- quillet-0.1.0/quillet/models.py +29 -0
- quillet-0.1.0/quillet/routes.py +254 -0
- quillet-0.1.0/quillet/templates/quillet/admin/dashboard.html +76 -0
- quillet-0.1.0/quillet/templates/quillet/admin/post_form.html +99 -0
- quillet-0.1.0/quillet/templates/quillet/admin/subscribers.html +55 -0
- quillet-0.1.0/quillet/templates/quillet/base.html +34 -0
- quillet-0.1.0/quillet/templates/quillet/post_detail.html +20 -0
- quillet-0.1.0/quillet/templates/quillet/post_list.html +33 -0
- quillet-0.1.0/quillet/templates/quillet/subscribe_confirm.html +29 -0
- quillet-0.1.0/scripts/docker_entrypoint_test.sh +67 -0
- quillet-0.1.0/scripts/docker_test.sh +41 -0
- quillet-0.1.0/scripts/http_smoke_test.py +217 -0
- quillet-0.1.0/scripts/smoke_test.py +148 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Quillet
|
|
3
|
+
overview: Design and build QUILLET — a standalone, open-source Flask newsletter/microblog package that can be used as an importable Blueprint, a standalone Docker web app, or a headless API — with pluggable DB and email backends.
|
|
4
|
+
todos:
|
|
5
|
+
- id: repo-scaffold
|
|
6
|
+
content: "Create new repo: pyproject.toml, Dockerfile, docker-compose.yml, package skeleton"
|
|
7
|
+
status: pending
|
|
8
|
+
- id: models
|
|
9
|
+
content: "Implement models.py: Newsletter, Post, Subscriber NamedTuples"
|
|
10
|
+
status: pending
|
|
11
|
+
- id: protocols
|
|
12
|
+
content: Define NewsletterRepository and EmailSender Protocol classes in db/__init__.py and email/__init__.py
|
|
13
|
+
status: pending
|
|
14
|
+
- id: db-sqlalchemy
|
|
15
|
+
content: Implement SQLAlchemyRepository (SQLite + Postgres, schema via create_all)
|
|
16
|
+
status: pending
|
|
17
|
+
- id: db-supabase-rest
|
|
18
|
+
content: Implement SupabaseRestRepository using HTTP REST API
|
|
19
|
+
status: pending
|
|
20
|
+
- id: email-mailgun
|
|
21
|
+
content: Implement MailgunSender (send_confirmation, send_post with batch sending)
|
|
22
|
+
status: pending
|
|
23
|
+
- id: email-smtp
|
|
24
|
+
content: Implement SmtpSender using stdlib smtplib
|
|
25
|
+
status: pending
|
|
26
|
+
- id: auth
|
|
27
|
+
content: Implement HTTP Basic Auth decorator in auth.py
|
|
28
|
+
status: pending
|
|
29
|
+
- id: routes
|
|
30
|
+
content: Implement all routes in routes.py, mode-aware (web vs api)
|
|
31
|
+
status: pending
|
|
32
|
+
- id: templates
|
|
33
|
+
content: Create minimal default templates under templates/quillet/ (public + admin UI)
|
|
34
|
+
status: pending
|
|
35
|
+
- id: admin-ui
|
|
36
|
+
content: Implement admin UI routes in admin.py (post list, new/edit post form, subscriber list), protected by Basic Auth, disabled via config
|
|
37
|
+
status: pending
|
|
38
|
+
- id: cli
|
|
39
|
+
content: Implement Flask CLI commands in cli.py
|
|
40
|
+
status: pending
|
|
41
|
+
- id: create-app
|
|
42
|
+
content: Implement create_blueprint() and create_app() in __init__.py, env-var driven for standalone
|
|
43
|
+
status: pending
|
|
44
|
+
isProject: false
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
# QUILLET
|
|
48
|
+
|
|
49
|
+
Flask newsletter + microblog package. Importable Blueprint, standalone web app, or headless API.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Repo structure (new standalone repo)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
quillet/
|
|
57
|
+
├── quillet/
|
|
58
|
+
│ ├── __init__.py # public API: create_blueprint, create_app
|
|
59
|
+
│ ├── models.py # Newsletter, Post, Subscriber NamedTuples
|
|
60
|
+
│ ├── auth.py # HTTP Basic Auth decorator
|
|
61
|
+
│ ├── routes.py # public route handlers (mode-aware)
|
|
62
|
+
│ ├── admin.py # admin UI route handlers (Basic Auth, optional)
|
|
63
|
+
│ ├── cli.py # flask quillet ... CLI commands
|
|
64
|
+
│ ├── db/
|
|
65
|
+
│ │ ├── __init__.py # NewsletterRepository Protocol
|
|
66
|
+
│ │ ├── sqlalchemy.py # SQLAlchemy impl (SQLite + Postgres/Supabase)
|
|
67
|
+
│ │ └── supabase_rest.py # Supabase REST API impl (no connection string)
|
|
68
|
+
│ ├── email/
|
|
69
|
+
│ │ ├── __init__.py # EmailSender Protocol
|
|
70
|
+
│ │ ├── mailgun.py # Mailgun HTTP API
|
|
71
|
+
│ │ └── smtp.py # generic SMTP (stdlib smtplib)
|
|
72
|
+
│ └── templates/
|
|
73
|
+
│ └── quillet/ # namespaced to avoid host-app collisions
|
|
74
|
+
│ ├── base.html
|
|
75
|
+
│ ├── post_list.html
|
|
76
|
+
│ ├── post_detail.html
|
|
77
|
+
│ ├── subscribe_confirm.html
|
|
78
|
+
│ └── admin/
|
|
79
|
+
│ ├── dashboard.html # post list + subscriber count
|
|
80
|
+
│ ├── post_form.html # create / edit post
|
|
81
|
+
│ └── subscribers.html
|
|
82
|
+
├── Dockerfile
|
|
83
|
+
├── docker-compose.yml # with SQLite volume mount example
|
|
84
|
+
├── pyproject.toml
|
|
85
|
+
└── README.md
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Three modes
|
|
91
|
+
|
|
92
|
+
```mermaid
|
|
93
|
+
flowchart TD
|
|
94
|
+
user["User / Client"]
|
|
95
|
+
user -->|"import + register_blueprint"| modeA
|
|
96
|
+
user -->|"Docker + QUILLET_MODE=web"| modeB
|
|
97
|
+
user -->|"Docker + QUILLET_MODE=api"| modeC
|
|
98
|
+
|
|
99
|
+
subgraph modeA [Mode 1: Blueprint]
|
|
100
|
+
bp["create_blueprint(db=..., email=..., password=...)"]
|
|
101
|
+
bp --> hostapp["Host Flask app\n(tinthe.dev, etc.)"]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
subgraph modeB [Mode 2: Standalone Web]
|
|
105
|
+
webApp["create_app() reads env vars\nHTML + JSON routes"]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
subgraph modeC [Mode 3: Headless API]
|
|
109
|
+
apiApp["create_app() reads env vars\nJSON routes only"]
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
### Mode 1 — Blueprint (1-5 lines of integration)
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from quillet import create_blueprint
|
|
119
|
+
from quillet.db.sqlalchemy import SQLAlchemyRepository
|
|
120
|
+
from quillet.email.mailgun import MailgunSender
|
|
121
|
+
|
|
122
|
+
app.register_blueprint(
|
|
123
|
+
create_blueprint(
|
|
124
|
+
db=SQLAlchemyRepository("sqlite:///newsletter.db"),
|
|
125
|
+
email=MailgunSender(api_key="...", domain="..."),
|
|
126
|
+
admin_password="secret",
|
|
127
|
+
),
|
|
128
|
+
url_prefix="/newsletter",
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Mode 2 / 3 — Docker (env-var driven)
|
|
133
|
+
|
|
134
|
+
`create_app()` reads all config from env vars:
|
|
135
|
+
|
|
136
|
+
- `QUILLET_MODE` = `web` (default) | `api`
|
|
137
|
+
- `QUILLET_ADMIN_PASSWORD`
|
|
138
|
+
- `QUILLET_DB_BACKEND` = `sqlalchemy` | `supabase_rest`
|
|
139
|
+
- `QUILLET_DB_URL` (SQLAlchemy connection string)
|
|
140
|
+
- `QUILLET_SUPABASE_URL`, `QUILLET_SUPABASE_KEY`
|
|
141
|
+
- `QUILLET_EMAIL_BACKEND` = `mailgun` | `smtp`
|
|
142
|
+
- `QUILLET_MAILGUN_API_KEY`, `QUILLET_MAILGUN_DOMAIN`
|
|
143
|
+
- `QUILLET_SMTP_HOST`, `QUILLET_SMTP_PORT`, etc.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Template override mechanism (Mode 2)
|
|
148
|
+
|
|
149
|
+
Flask resolves templates: **app templates dir > blueprint templates dir**.
|
|
150
|
+
|
|
151
|
+
- **Blueprint mode**: host app places overrides at `templates/quillet/post_list.html` — Flask finds them first automatically. No config needed.
|
|
152
|
+
- **Standalone Docker**: mount a volume to `/app/templates/quillet/`. The Dockerfile exposes this path. Default templates are minimal/unstyled so they're easy to replace.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Data models
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# models.py — all NamedTuples, immutable
|
|
160
|
+
class Newsletter(NamedTuple):
|
|
161
|
+
id: int
|
|
162
|
+
slug: str
|
|
163
|
+
name: str
|
|
164
|
+
from_email: str
|
|
165
|
+
from_name: str
|
|
166
|
+
reply_to: str | None
|
|
167
|
+
|
|
168
|
+
class Post(NamedTuple):
|
|
169
|
+
id: int
|
|
170
|
+
newsletter_id: int
|
|
171
|
+
slug: str
|
|
172
|
+
title: str
|
|
173
|
+
body_md: str
|
|
174
|
+
published_at: datetime | None
|
|
175
|
+
sent_at: datetime | None # None = not yet emailed
|
|
176
|
+
|
|
177
|
+
class Subscriber(NamedTuple):
|
|
178
|
+
id: int
|
|
179
|
+
newsletter_id: int
|
|
180
|
+
email: str
|
|
181
|
+
token: str # used for confirm + unsubscribe URLs
|
|
182
|
+
confirmed_at: datetime | None
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Plugin protocols
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# db/__init__.py
|
|
191
|
+
class NewsletterRepository(Protocol):
|
|
192
|
+
def get_newsletter(self, slug: str) -> Newsletter | None: ...
|
|
193
|
+
def list_posts(self, newsletter_slug: str, published_only: bool = True) -> list[Post]: ...
|
|
194
|
+
def get_post(self, newsletter_slug: str, post_slug: str) -> Post | None: ...
|
|
195
|
+
def create_post(self, newsletter_slug: str, title: str, body_md: str) -> Post: ...
|
|
196
|
+
def publish_post(self, post_id: int) -> Post: ...
|
|
197
|
+
def mark_sent(self, post_id: int) -> None: ...
|
|
198
|
+
def add_subscriber(self, newsletter_slug: str, email: str) -> Subscriber: ...
|
|
199
|
+
def confirm_subscriber(self, token: str) -> Subscriber | None: ...
|
|
200
|
+
def list_confirmed_subscribers(self, newsletter_slug: str) -> list[Subscriber]: ...
|
|
201
|
+
def unsubscribe(self, token: str) -> None: ...
|
|
202
|
+
|
|
203
|
+
# email/__init__.py
|
|
204
|
+
class EmailSender(Protocol):
|
|
205
|
+
def send_confirmation(self, newsletter: Newsletter, subscriber: Subscriber, confirm_url: str) -> None: ...
|
|
206
|
+
def send_post(self, newsletter: Newsletter, post: Post, subscribers: list[Subscriber]) -> None: ...
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Anyone can implement these protocols to add new backends (Postmark, DynamoDB, etc.) without touching library internals.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Routes (multi-newsletter, all prefixed by `/<newsletter_slug>`)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
| Method | Path | Auth | Notes |
|
|
217
|
+
| ------ | ----------------------------------------- | ---------- | --------------------------------------------------- |
|
|
218
|
+
| GET | `/<slug>/` | — | post list |
|
|
219
|
+
| GET | `/<slug>/posts/<post_slug>` | — | post detail |
|
|
220
|
+
| POST | `/<slug>/subscribe` | — | creates unconfirmed subscriber, sends confirm email |
|
|
221
|
+
| GET | `/<slug>/confirm/<token>` | — | marks subscriber confirmed |
|
|
222
|
+
| GET | `/<slug>/unsubscribe/<token>` | — | removes subscriber |
|
|
223
|
+
| POST | `/<slug>/admin/posts` | Basic Auth | create post |
|
|
224
|
+
| POST | `/<slug>/admin/posts/<post_slug>/publish` | Basic Auth | set published_at |
|
|
225
|
+
| POST | `/<slug>/admin/posts/<post_slug>/send` | Basic Auth | email all confirmed subscribers |
|
|
226
|
+
| GET | `/<slug>/admin/subscribers` | Basic Auth | list subscribers |
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
In `api` mode: all responses are JSON. In `web` mode: HTML for public routes, JSON for admin routes (admin is always API-shaped).
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## CLI
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
flask quillet create "My Blog" --slug=blog
|
|
237
|
+
flask quillet list-newsletters
|
|
238
|
+
flask quillet send blog <post-slug> # manual send trigger
|
|
239
|
+
flask quillet subscribers blog
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## DB: SQLAlchemy vs Supabase REST
|
|
245
|
+
|
|
246
|
+
- `SQLAlchemyRepository` — uses SQLAlchemy 2.x Core (not ORM), handles SQLite and Postgres transparently. Runs `create_all()` on first use. This covers 95% of use cases including Supabase via its Postgres connection string.
|
|
247
|
+
- `SupabaseRestRepository` — uses Supabase's HTTP REST API (like the current tinthe `/book-call` pattern). For serverless / API-key-only deployments where a persistent DB connection isn't available.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## pyproject.toml optional extras
|
|
252
|
+
|
|
253
|
+
```toml
|
|
254
|
+
[project.optional-dependencies]
|
|
255
|
+
sqlalchemy = ["SQLAlchemy>=2.0"]
|
|
256
|
+
mailgun = ["requests>=2.31"]
|
|
257
|
+
smtp = [] # stdlib only
|
|
258
|
+
supabase = ["requests>=2.31"]
|
|
259
|
+
all = ["SQLAlchemy>=2.0", "requests>=2.31"]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Admin UI
|
|
265
|
+
|
|
266
|
+
A minimal, dependency-free HTML interface living at `/<slug>/admin/`. All routes require Basic Auth. Enabled by default, disabled via:
|
|
267
|
+
|
|
268
|
+
- Blueprint: `create_blueprint(..., admin_ui=False)`
|
|
269
|
+
- Standalone/Docker: `QUILLET_ADMIN_UI=false`
|
|
270
|
+
|
|
271
|
+
In `api` mode the admin UI is automatically disabled (API-only, no HTML).
|
|
272
|
+
|
|
273
|
+
### Admin routes
|
|
274
|
+
|
|
275
|
+
- `GET /<slug>/admin/` — dashboard: paginated post list with status badges (draft / published / sent), total subscriber count
|
|
276
|
+
- `GET /<slug>/admin/posts/new` — blank post form
|
|
277
|
+
- `POST /<slug>/admin/posts/new` — create post, redirect to edit view
|
|
278
|
+
- `GET /<slug>/admin/posts/<post_slug>/edit` — edit post (title, body, slug)
|
|
279
|
+
- `POST /<slug>/admin/posts/<post_slug>/edit` — save edits
|
|
280
|
+
- `POST /<slug>/admin/posts/<post_slug>/publish` — set `published_at`, redirect to dashboard
|
|
281
|
+
- `POST /<slug>/admin/posts/<post_slug>/send` — trigger email send, redirect to dashboard
|
|
282
|
+
- `GET /<slug>/admin/subscribers` — subscriber list with confirm status
|
|
283
|
+
|
|
284
|
+
### UI philosophy
|
|
285
|
+
|
|
286
|
+
- Plain HTML forms, zero JS, zero external CSS dependencies
|
|
287
|
+
- Inline `<style>` block, ~50 lines — functional, not pretty
|
|
288
|
+
- All actions are POST forms (no AJAX) so it works without JS and is trivially overridable
|
|
289
|
+
- Template override works the same way: drop `templates/quillet/admin/dashboard.html` in your host app to replace the default
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## What this is NOT
|
|
294
|
+
|
|
295
|
+
- No admin UI (CRUD is via API/CLI — keeps scope tight, though an optional built-in UI is available)
|
|
296
|
+
- No comment system (reply-to-email approach; Giscus can be added in templates)
|
|
297
|
+
- No auth beyond Basic Auth (host app can wrap routes with its own decorator)
|
|
298
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description:
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
Write code that is robust.
|
|
7
|
+
Write code that is pragmatic.
|
|
8
|
+
Do not overexplain.
|
|
9
|
+
Focus on performance and simplicity, use high level abstractions.
|
|
10
|
+
Always try to find a ready-made solution.
|
|
11
|
+
Always search for existing implementations and use existing project patterns.
|
|
12
|
+
Make sure you never reimplement an existing pattern without approval.
|
|
13
|
+
We are writing well behaved Python code with latest standards.
|
|
14
|
+
We apply typing to function arguments and returns.
|
|
15
|
+
We prefer to invert conditionals and exit, when possible, example:
|
|
16
|
+
```
|
|
17
|
+
# Bad:
|
|
18
|
+
if user:
|
|
19
|
+
# logic
|
|
20
|
+
else:
|
|
21
|
+
raise Exception("No user")
|
|
22
|
+
|
|
23
|
+
# Good:
|
|
24
|
+
if not user:
|
|
25
|
+
raise Exception("No user")
|
|
26
|
+
# logic
|
|
27
|
+
```
|
|
28
|
+
We prefer to de-indent code where it is possible.
|
|
29
|
+
We prefer to use list comprehension.
|
|
30
|
+
We prefer to define NamedTuple instead of using dictionaries for data structures.
|
|
31
|
+
We prefer immutable structures (NamedTuple, frozen dataclasses, etc.) where mutation is unnecessary.
|
|
32
|
+
We prefer to keep functions short, up to 15 semantic lines.
|
|
33
|
+
We use generous whitespace and blank lines to group code logically and enhance readability.
|
|
34
|
+
We prefer to be explicit and verbose instead of using short variable names.
|
|
35
|
+
We prefer specific exceptions and avoid catching broad Exception unless justified.
|
|
36
|
+
We only import at the top of the module, outside functions. If importing inside a function is absolutely necessary (e.g., for type-checking or avoiding circular imports), the exception must be documented clearly.
|
|
37
|
+
We use relative imports for our code.
|
|
38
|
+
We import smallest possible units: `from foo import bar` is good. `import foo; foo.bar()` is to be avoided.
|
|
39
|
+
We avoid defining classes ONLY TO encapsulate methods, we rely on Python's modules.
|
|
40
|
+
We avoid commenting self-explanatory code, our comments are focused to docstrings (in functions) and to large scope explanations.
|
|
41
|
+
We avoid deep nesting (e.g., >2 levels). Break into smaller functions when needed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
alwaysApply: true
|
|
3
|
+
---
|
|
4
|
+
# Python Edit Policy
|
|
5
|
+
|
|
6
|
+
After every edit to a `.py` file, run `read_lints` on the `tinthe/` directory and fix any
|
|
7
|
+
errors before considering the task done. Never hand back to the user with outstanding lint errors.
|
|
8
|
+
|
|
9
|
+
Common mistakes to watch for in this codebase:
|
|
10
|
+
- Stale imports after feature removal (e.g. removing a route but leaving its `import`)
|
|
11
|
+
- Passing a `Path` object to a function that calls string methods (`.split`, `[0]`, etc.) — always check the type the callee expects
|
|
12
|
+
- Bare relative imports like `from foo import ...` instead of `from .foo import ...` inside the `tinthe` package
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: pip install -e ".[all,dev]"
|
|
17
|
+
- run: ruff check .
|
|
18
|
+
- run: black --check . -l 120
|
|
19
|
+
- run: python scripts/smoke_test.py
|
|
20
|
+
|
|
21
|
+
pypi:
|
|
22
|
+
needs: test
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment: pypi
|
|
25
|
+
permissions:
|
|
26
|
+
id-token: write # required for OIDC trusted publishing
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.12"
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: |
|
|
36
|
+
pip install build
|
|
37
|
+
python -m build
|
|
38
|
+
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
41
|
+
|
|
42
|
+
docker:
|
|
43
|
+
needs: test
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
permissions:
|
|
46
|
+
contents: read
|
|
47
|
+
packages: write # required to push to GHCR
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Log in to GHCR
|
|
52
|
+
uses: docker/login-action@v3
|
|
53
|
+
with:
|
|
54
|
+
registry: ghcr.io
|
|
55
|
+
username: ${{ github.actor }}
|
|
56
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
57
|
+
|
|
58
|
+
- name: Extract version from tag
|
|
59
|
+
id: meta
|
|
60
|
+
uses: docker/metadata-action@v5
|
|
61
|
+
with:
|
|
62
|
+
images: ghcr.io/${{ github.repository }}
|
|
63
|
+
tags: |
|
|
64
|
+
type=semver,pattern={{version}}
|
|
65
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
66
|
+
type=raw,value=latest
|
|
67
|
+
|
|
68
|
+
- name: Build and push
|
|
69
|
+
uses: docker/build-push-action@v6
|
|
70
|
+
with:
|
|
71
|
+
context: .
|
|
72
|
+
push: true
|
|
73
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
74
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
|
|
18
|
+
- name: Install
|
|
19
|
+
run: pip install -e ".[all,dev]"
|
|
20
|
+
|
|
21
|
+
- name: Lint (ruff)
|
|
22
|
+
run: ruff check .
|
|
23
|
+
|
|
24
|
+
- name: Format check (black)
|
|
25
|
+
run: black --check . -l 120
|
|
26
|
+
|
|
27
|
+
- name: Smoke test
|
|
28
|
+
run: python scripts/smoke_test.py
|
quillet-0.1.0/.gitignore
ADDED
quillet-0.1.0/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY pyproject.toml README.md ./
|
|
6
|
+
RUN pip install --no-cache-dir ".[all]" gunicorn
|
|
7
|
+
|
|
8
|
+
COPY quillet/ quillet/
|
|
9
|
+
COPY scripts/ scripts/
|
|
10
|
+
|
|
11
|
+
# Mountable custom templates directory — takes precedence over built-in templates.
|
|
12
|
+
# docker run -v ./my-templates:/app/templates/quillet ...
|
|
13
|
+
VOLUME ["/app/templates"]
|
|
14
|
+
|
|
15
|
+
ENV QUILLET_MODE=web
|
|
16
|
+
ENV QUILLET_DB_BACKEND=sqlalchemy
|
|
17
|
+
ENV QUILLET_DB_URL=sqlite:////data/quillet.db
|
|
18
|
+
ENV QUILLET_EMAIL_BACKEND=smtp
|
|
19
|
+
|
|
20
|
+
EXPOSE 8000
|
|
21
|
+
|
|
22
|
+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "quillet._app:application"]
|
quillet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tin Marković
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|