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.
@@ -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