skrift 0.1.0a14__py3-none-any.whl → 0.1.0a16__py3-none-any.whl
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.
- skrift/admin/controller.py +114 -26
- skrift/alembic/versions/20260202_add_content_scheduling.py +35 -0
- skrift/alembic/versions/20260202_add_page_ordering.py +32 -0
- skrift/alembic/versions/20260202_add_page_revisions.py +46 -0
- skrift/alembic/versions/20260202_add_seo_fields.py +42 -0
- skrift/claude_skill/SKILL.md +205 -0
- skrift/claude_skill/__init__.py +0 -0
- skrift/claude_skill/architecture.md +328 -0
- skrift/claude_skill/patterns.md +552 -0
- skrift/cli.py +47 -0
- skrift/controllers/sitemap.py +116 -0
- skrift/controllers/web.py +18 -1
- skrift/db/models/__init__.py +2 -1
- skrift/db/models/page.py +26 -1
- skrift/db/models/page_revision.py +45 -0
- skrift/db/services/page_service.py +113 -6
- skrift/db/services/revision_service.py +146 -0
- skrift/db/services/setting_service.py +7 -0
- skrift/lib/__init__.py +12 -1
- skrift/lib/flash.py +103 -0
- skrift/lib/hooks.py +292 -0
- skrift/lib/seo.py +103 -0
- skrift/static/css/style.css +92 -2
- skrift/templates/admin/pages/edit.html +44 -4
- skrift/templates/admin/pages/list.html +8 -1
- skrift/templates/admin/pages/revisions.html +59 -0
- skrift/templates/base.html +58 -4
- {skrift-0.1.0a14.dist-info → skrift-0.1.0a16.dist-info}/METADATA +14 -3
- {skrift-0.1.0a14.dist-info → skrift-0.1.0a16.dist-info}/RECORD +31 -16
- {skrift-0.1.0a14.dist-info → skrift-0.1.0a16.dist-info}/WHEEL +0 -0
- {skrift-0.1.0a14.dist-info → skrift-0.1.0a16.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Skrift Architecture
|
|
2
|
+
|
|
3
|
+
## Application Lifecycle
|
|
4
|
+
|
|
5
|
+
### AppDispatcher Pattern
|
|
6
|
+
|
|
7
|
+
Skrift uses a dispatcher architecture that routes between setup and main applications:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────┐
|
|
11
|
+
│ AppDispatcher │
|
|
12
|
+
│ (skrift/asgi.py) │
|
|
13
|
+
└─────────────┬───────────────┘
|
|
14
|
+
│
|
|
15
|
+
┌───────────────────┼───────────────────┐
|
|
16
|
+
│ │ │
|
|
17
|
+
▼ ▼ ▼
|
|
18
|
+
┌────────────────┐ ┌────────────┐ ┌────────────────┐
|
|
19
|
+
│ Setup App │ │ /static │ │ Main App │
|
|
20
|
+
│ (/setup/*) │ │ Files │ │ (everything) │
|
|
21
|
+
└────────────────┘ └────────────┘ └────────────────┘
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Key behaviors:**
|
|
25
|
+
- `setup_locked=False`: /setup/* routes active, checks DB for setup completion
|
|
26
|
+
- `setup_locked=True`: All traffic goes to main app, /setup/* returns 404
|
|
27
|
+
- Main app is lazily created after setup completes (no restart needed)
|
|
28
|
+
|
|
29
|
+
**Entry point:** `skrift.asgi:app` (created by `create_dispatcher()`)
|
|
30
|
+
|
|
31
|
+
### Startup Flow
|
|
32
|
+
|
|
33
|
+
1. `create_dispatcher()` called at module load
|
|
34
|
+
2. Checks if setup complete in database
|
|
35
|
+
3. If complete + config valid: returns `create_app()` directly
|
|
36
|
+
4. Otherwise: returns `AppDispatcher` with lazy main app creation
|
|
37
|
+
|
|
38
|
+
### Request Flow
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Request → AppDispatcher → Route Decision:
|
|
42
|
+
│
|
|
43
|
+
├─ /setup/* or /static/* → Setup App
|
|
44
|
+
│
|
|
45
|
+
├─ Setup not complete → Check DB:
|
|
46
|
+
│ ├─ Complete → Create main app, lock setup, route to main
|
|
47
|
+
│ └─ Not complete → Redirect to /setup
|
|
48
|
+
│
|
|
49
|
+
└─ Setup locked → Main App (handles everything including 404 for /setup/*)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration System
|
|
53
|
+
|
|
54
|
+
### Configuration Flow
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
.env (loaded early) → app.yaml (with $VAR interpolation) → Settings (Pydantic)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Environment-Specific Config
|
|
61
|
+
|
|
62
|
+
| Environment | Config File |
|
|
63
|
+
|-------------|-------------|
|
|
64
|
+
| production (default) | `app.yaml` |
|
|
65
|
+
| development | `app.dev.yaml` |
|
|
66
|
+
| testing | `app.test.yaml` |
|
|
67
|
+
|
|
68
|
+
Set via `SKRIFT_ENV` environment variable.
|
|
69
|
+
|
|
70
|
+
### app.yaml Structure
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# Database connection
|
|
74
|
+
db:
|
|
75
|
+
url: $DATABASE_URL # Interpolates from .env
|
|
76
|
+
pool_size: 5
|
|
77
|
+
pool_overflow: 10
|
|
78
|
+
pool_timeout: 30
|
|
79
|
+
echo: false # SQL logging
|
|
80
|
+
|
|
81
|
+
# Authentication
|
|
82
|
+
auth:
|
|
83
|
+
redirect_base_url: "https://example.com"
|
|
84
|
+
allowed_redirect_domains: []
|
|
85
|
+
providers:
|
|
86
|
+
google:
|
|
87
|
+
client_id: $GOOGLE_CLIENT_ID
|
|
88
|
+
client_secret: $GOOGLE_CLIENT_SECRET
|
|
89
|
+
scopes: ["openid", "email", "profile"]
|
|
90
|
+
|
|
91
|
+
# Session cookies
|
|
92
|
+
session:
|
|
93
|
+
cookie_domain: null # null = exact host only
|
|
94
|
+
|
|
95
|
+
# Controllers to load
|
|
96
|
+
controllers:
|
|
97
|
+
- skrift.controllers.auth:AuthController
|
|
98
|
+
- skrift.controllers.web:WebController
|
|
99
|
+
- myapp.controllers:CustomController
|
|
100
|
+
|
|
101
|
+
# Middleware (optional)
|
|
102
|
+
middleware:
|
|
103
|
+
- myapp.middleware:create_logging_middleware
|
|
104
|
+
- factory: myapp.middleware:create_rate_limit
|
|
105
|
+
kwargs:
|
|
106
|
+
requests_per_minute: 100
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Settings Class (`skrift/config.py`)
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
class Settings(BaseSettings):
|
|
113
|
+
debug: bool = False
|
|
114
|
+
secret_key: str # Required - from .env
|
|
115
|
+
|
|
116
|
+
db: DatabaseConfig
|
|
117
|
+
auth: AuthConfig
|
|
118
|
+
session: SessionConfig
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Access settings: `from skrift.config import get_settings`
|
|
122
|
+
|
|
123
|
+
## Database Layer
|
|
124
|
+
|
|
125
|
+
### Base Model
|
|
126
|
+
|
|
127
|
+
All models inherit from `skrift.db.base.Base`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from advanced_alchemy.base import UUIDAuditBase
|
|
131
|
+
|
|
132
|
+
class Base(UUIDAuditBase):
|
|
133
|
+
__abstract__ = True
|
|
134
|
+
# Provides: id (UUID), created_at, updated_at
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Session Access
|
|
138
|
+
|
|
139
|
+
Sessions injected via Litestar's SQLAlchemy plugin:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
143
|
+
|
|
144
|
+
@get("/")
|
|
145
|
+
async def handler(self, db_session: AsyncSession) -> dict:
|
|
146
|
+
# db_session is auto-injected
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Core Models
|
|
151
|
+
|
|
152
|
+
| Model | Table | Purpose |
|
|
153
|
+
|-------|-------|---------|
|
|
154
|
+
| `User` | `users` | User accounts |
|
|
155
|
+
| `OAuthAccount` | `oauth_accounts` | Linked OAuth providers |
|
|
156
|
+
| `Role` | `roles` | Permission roles |
|
|
157
|
+
| `Page` | `pages` | Content pages |
|
|
158
|
+
| `PageRevision` | `page_revisions` | Content history |
|
|
159
|
+
| `Setting` | `settings` | Key-value site settings |
|
|
160
|
+
|
|
161
|
+
### Migrations
|
|
162
|
+
|
|
163
|
+
Uses Alembic via CLI wrapper:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
skrift db upgrade head # Apply all
|
|
167
|
+
skrift db downgrade -1 # Rollback one
|
|
168
|
+
skrift db revision -m "add table" --autogenerate # Generate
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Migration files in: `skrift/migrations/versions/`
|
|
172
|
+
|
|
173
|
+
## Authentication & Authorization
|
|
174
|
+
|
|
175
|
+
### OAuth Flow
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
/auth/{provider}/login → Provider → /auth/{provider}/callback → Session created
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Providers configured in `app.yaml` under `auth.providers`.
|
|
182
|
+
|
|
183
|
+
### Session Management
|
|
184
|
+
|
|
185
|
+
Client-side encrypted cookies (Litestar's CookieBackendConfig):
|
|
186
|
+
- 7-day expiry
|
|
187
|
+
- HttpOnly, Secure (in production), SameSite=Lax
|
|
188
|
+
|
|
189
|
+
### Role-Based Authorization
|
|
190
|
+
|
|
191
|
+
**Built-in Roles:**
|
|
192
|
+
- `admin`: Full access (`administrator` permission bypasses all checks)
|
|
193
|
+
- `editor`: Can manage pages
|
|
194
|
+
- `author`: Can view drafts
|
|
195
|
+
- `moderator`: Can moderate content
|
|
196
|
+
|
|
197
|
+
**Guard System:**
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from skrift.auth import auth_guard, Permission, Role
|
|
201
|
+
|
|
202
|
+
# Basic auth required
|
|
203
|
+
guards = [auth_guard]
|
|
204
|
+
|
|
205
|
+
# With permission
|
|
206
|
+
guards = [auth_guard, Permission("manage-pages")]
|
|
207
|
+
|
|
208
|
+
# With role
|
|
209
|
+
guards = [auth_guard, Role("editor")]
|
|
210
|
+
|
|
211
|
+
# Combinations
|
|
212
|
+
guards = [auth_guard, Permission("edit") & Permission("publish")] # AND
|
|
213
|
+
guards = [auth_guard, Role("admin") | Role("editor")] # OR
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Custom Roles:**
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from skrift.auth import register_role
|
|
220
|
+
|
|
221
|
+
register_role(
|
|
222
|
+
"support",
|
|
223
|
+
"view-tickets",
|
|
224
|
+
"respond-tickets",
|
|
225
|
+
display_name="Support Agent",
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Template System
|
|
230
|
+
|
|
231
|
+
### Resolution Order
|
|
232
|
+
|
|
233
|
+
The `Template` class resolves templates from most to least specific:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
Template("page", "services", "web")
|
|
237
|
+
# Tries: page-services-web.html → page-services.html → page.html
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Search Directories
|
|
241
|
+
|
|
242
|
+
1. `./templates/` (project root - user overrides)
|
|
243
|
+
2. `skrift/templates/` (package - defaults)
|
|
244
|
+
|
|
245
|
+
### Template Globals
|
|
246
|
+
|
|
247
|
+
Available in all templates:
|
|
248
|
+
- `now()` - Current datetime
|
|
249
|
+
- `site_name()` - From settings
|
|
250
|
+
- `site_tagline()` - From settings
|
|
251
|
+
- `site_copyright_holder()` - From settings
|
|
252
|
+
- `site_copyright_start_year()` - From settings
|
|
253
|
+
|
|
254
|
+
### Template Filters
|
|
255
|
+
|
|
256
|
+
- `markdown` - Render markdown to HTML
|
|
257
|
+
|
|
258
|
+
## Hook/Filter System
|
|
259
|
+
|
|
260
|
+
### Concept
|
|
261
|
+
|
|
262
|
+
WordPress-inspired extensibility:
|
|
263
|
+
- **Actions**: Side effects, no return value
|
|
264
|
+
- **Filters**: Transform and return values
|
|
265
|
+
|
|
266
|
+
### Registration Methods
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
# Decorator (auto-registers on import)
|
|
270
|
+
@action("hook_name", priority=10)
|
|
271
|
+
async def my_action(arg1, arg2):
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
@filter("hook_name", priority=10)
|
|
275
|
+
async def my_filter(value, arg1) -> Any:
|
|
276
|
+
return modified_value
|
|
277
|
+
|
|
278
|
+
# Direct registration
|
|
279
|
+
hooks.add_action("hook_name", callback, priority=10)
|
|
280
|
+
hooks.add_filter("hook_name", callback, priority=10)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Triggering
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# Actions (fire and forget)
|
|
287
|
+
await hooks.do_action("hook_name", arg1, arg2)
|
|
288
|
+
|
|
289
|
+
# Filters (chain transforms)
|
|
290
|
+
result = await hooks.apply_filters("hook_name", initial_value, arg1)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Priority
|
|
294
|
+
|
|
295
|
+
Lower numbers execute first. Default is 10.
|
|
296
|
+
|
|
297
|
+
### Built-in Hook Points
|
|
298
|
+
|
|
299
|
+
**Actions:**
|
|
300
|
+
- `before_page_save(page, is_new)` - Before saving
|
|
301
|
+
- `after_page_save(page, is_new)` - After saving
|
|
302
|
+
- `before_page_delete(page)` - Before deletion
|
|
303
|
+
- `after_page_delete(page)` - After deletion
|
|
304
|
+
|
|
305
|
+
**Filters:**
|
|
306
|
+
- `page_seo_meta(meta, page)` - Modify SEO metadata
|
|
307
|
+
- `page_og_meta(meta, page)` - Modify OpenGraph metadata
|
|
308
|
+
- `sitemap_urls(urls)` - Modify sitemap URLs
|
|
309
|
+
- `sitemap_page(page_data, page)` - Modify sitemap entry
|
|
310
|
+
- `robots_txt(content)` - Modify robots.txt
|
|
311
|
+
- `template_context(context)` - Modify template context
|
|
312
|
+
|
|
313
|
+
## Static Files
|
|
314
|
+
|
|
315
|
+
Served from `/static/` with same priority as templates:
|
|
316
|
+
1. `./static/` (project root - user overrides)
|
|
317
|
+
2. `skrift/static/` (package - defaults)
|
|
318
|
+
|
|
319
|
+
## Error Handling
|
|
320
|
+
|
|
321
|
+
Custom exception handlers in `skrift/lib/exceptions.py`:
|
|
322
|
+
- `HTTPException` → `http_exception_handler`
|
|
323
|
+
- `Exception` → `internal_server_error_handler`
|
|
324
|
+
|
|
325
|
+
Error templates:
|
|
326
|
+
- `error.html` - Generic error
|
|
327
|
+
- `error-404.html` - Not found
|
|
328
|
+
- `error-500.html` - Server error
|