skrift 0.1.0a15__tar.gz → 0.1.0a17__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.
- {skrift-0.1.0a15 → skrift-0.1.0a17}/PKG-INFO +1 -1
- {skrift-0.1.0a15 → skrift-0.1.0a17}/pyproject.toml +2 -1
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/asgi.py +6 -2
- skrift-0.1.0a17/skrift/claude_skill/SKILL.md +205 -0
- skrift-0.1.0a17/skrift/claude_skill/__init__.py +0 -0
- skrift-0.1.0a17/skrift/claude_skill/architecture.md +328 -0
- skrift-0.1.0a17/skrift/claude_skill/patterns.md +552 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/cli.py +47 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/config.py +1 -0
- skrift-0.1.0a17/skrift/db/session.py +90 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/config_writer.py +1 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/state.py +4 -4
- {skrift-0.1.0a15 → skrift-0.1.0a17}/.gitignore +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/README.md +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/__main__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/admin/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/admin/controller.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/admin/navigation.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/env.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/script.py.mako +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260129_add_oauth_accounts.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_content_scheduling.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_page_ordering.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_page_revisions.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_seo_fields.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic.ini +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/guards.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/roles.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/services.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/auth.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/sitemap.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/web.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/base.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/oauth_account.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/page.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/page_revision.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/role.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/setting.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/user.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/oauth_service.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/page_service.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/revision_service.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/setting_service.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/exceptions.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/flash.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/hooks.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/markdown.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/seo.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/template.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/__init__.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/controller.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/middleware.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/providers.py +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/static/css/style.css +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/admin.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/base.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/pages/edit.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/pages/list.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/pages/revisions.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/settings/site.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/users/list.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/users/roles.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/auth/dummy_login.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/auth/login.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/base.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/error-404.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/error-500.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/error.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/index.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/page.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/admin.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/auth.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/base.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/complete.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/configuring.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/database.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/restart.html +0 -0
- {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/site.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "skrift"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.0a17"
|
|
4
4
|
description = "A lightweight async Python CMS for crafting modern websites"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -47,6 +47,7 @@ include = [
|
|
|
47
47
|
"skrift/static/**/*",
|
|
48
48
|
"skrift/alembic/**/*",
|
|
49
49
|
"skrift/alembic.ini",
|
|
50
|
+
"skrift/claude_skill/*.md",
|
|
50
51
|
]
|
|
51
52
|
exclude = [
|
|
52
53
|
"**/__pycache__",
|
|
@@ -22,6 +22,8 @@ from advanced_alchemy.extensions.litestar import (
|
|
|
22
22
|
SQLAlchemyAsyncConfig,
|
|
23
23
|
SQLAlchemyPlugin,
|
|
24
24
|
)
|
|
25
|
+
|
|
26
|
+
from skrift.db.session import SafeSQLAlchemyAsyncConfig
|
|
25
27
|
from litestar import Litestar
|
|
26
28
|
from litestar.config.compression import CompressionConfig
|
|
27
29
|
from litestar.contrib.jinja import JinjaTemplateEngine
|
|
@@ -398,10 +400,11 @@ def create_app() -> Litestar:
|
|
|
398
400
|
pool_size=settings.db.pool_size,
|
|
399
401
|
max_overflow=settings.db.pool_overflow,
|
|
400
402
|
pool_timeout=settings.db.pool_timeout,
|
|
403
|
+
pool_pre_ping=settings.db.pool_pre_ping,
|
|
401
404
|
echo=settings.db.echo,
|
|
402
405
|
)
|
|
403
406
|
|
|
404
|
-
db_config =
|
|
407
|
+
db_config = SafeSQLAlchemyAsyncConfig(
|
|
405
408
|
connection_string=settings.db.url,
|
|
406
409
|
metadata=Base.metadata,
|
|
407
410
|
create_all=False,
|
|
@@ -564,10 +567,11 @@ def create_setup_app() -> Litestar:
|
|
|
564
567
|
pool_size=5,
|
|
565
568
|
max_overflow=10,
|
|
566
569
|
pool_timeout=30,
|
|
570
|
+
pool_pre_ping=True,
|
|
567
571
|
echo=False,
|
|
568
572
|
)
|
|
569
573
|
|
|
570
|
-
db_config =
|
|
574
|
+
db_config = SafeSQLAlchemyAsyncConfig(
|
|
571
575
|
connection_string=db_url,
|
|
572
576
|
metadata=Base.metadata,
|
|
573
577
|
create_all=False,
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skrift
|
|
3
|
+
description: "Help working with Skrift CMS codebases - a Python async CMS built on Litestar with WordPress-style conventions. Use for creating controllers, models, hooks, pages, and templates."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skrift CMS Development Guide
|
|
7
|
+
|
|
8
|
+
Skrift is a lightweight async Python CMS built on Litestar, featuring WordPress-style template resolution, a hook/filter extensibility system, and SQLAlchemy async database access.
|
|
9
|
+
|
|
10
|
+
## Current Project State
|
|
11
|
+
|
|
12
|
+
**Configuration:**
|
|
13
|
+
!`cat app.yaml 2>/dev/null || echo "No app.yaml found"`
|
|
14
|
+
|
|
15
|
+
**Controllers:**
|
|
16
|
+
!`ls skrift/controllers/*.py 2>/dev/null | head -10`
|
|
17
|
+
|
|
18
|
+
**Models:**
|
|
19
|
+
!`ls skrift/db/models/*.py 2>/dev/null | head -10`
|
|
20
|
+
|
|
21
|
+
**Services:**
|
|
22
|
+
!`ls skrift/db/services/*.py 2>/dev/null | head -10`
|
|
23
|
+
|
|
24
|
+
**Templates:**
|
|
25
|
+
!`ls templates/*.html 2>/dev/null | head -10 || echo "No custom templates"`
|
|
26
|
+
|
|
27
|
+
## Quick Reference
|
|
28
|
+
|
|
29
|
+
### Core Architecture
|
|
30
|
+
|
|
31
|
+
- **Framework**: Litestar (async Python web framework)
|
|
32
|
+
- **Database**: SQLAlchemy async with Advanced Alchemy
|
|
33
|
+
- **Templates**: Jinja2 with WordPress-style template hierarchy
|
|
34
|
+
- **Config**: YAML (app.yaml) + environment variables (.env)
|
|
35
|
+
- **Auth**: OAuth providers + role-based permissions
|
|
36
|
+
|
|
37
|
+
### Key Files
|
|
38
|
+
|
|
39
|
+
| File | Purpose |
|
|
40
|
+
|------|---------|
|
|
41
|
+
| `skrift/asgi.py` | AppDispatcher, app creation, middleware loading |
|
|
42
|
+
| `skrift/config.py` | Settings management, YAML config loading |
|
|
43
|
+
| `skrift/cli.py` | CLI commands (serve, secret, db) |
|
|
44
|
+
| `skrift/lib/hooks.py` | WordPress-like hook/filter system |
|
|
45
|
+
| `skrift/lib/template.py` | Template resolution with fallbacks |
|
|
46
|
+
| `skrift/db/base.py` | SQLAlchemy Base class (UUIDAuditBase) |
|
|
47
|
+
| `skrift/auth/` | Guards, roles, permissions |
|
|
48
|
+
|
|
49
|
+
### CLI Commands
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Run development server
|
|
53
|
+
skrift serve --reload --port 8080
|
|
54
|
+
|
|
55
|
+
# Generate secret key
|
|
56
|
+
skrift secret # Print to stdout
|
|
57
|
+
skrift secret --write .env # Write to .env file
|
|
58
|
+
|
|
59
|
+
# Database migrations (wraps Alembic)
|
|
60
|
+
skrift db upgrade head # Apply all migrations
|
|
61
|
+
skrift db downgrade -1 # Rollback one migration
|
|
62
|
+
skrift db current # Show current revision
|
|
63
|
+
skrift db revision -m "desc" --autogenerate # Create migration
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Task-Specific Guidance
|
|
67
|
+
|
|
68
|
+
### Creating a Controller
|
|
69
|
+
|
|
70
|
+
Controllers are Litestar Controller classes registered in `app.yaml`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from litestar import Controller, get, post
|
|
74
|
+
from litestar.response import Template as TemplateResponse
|
|
75
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
76
|
+
|
|
77
|
+
class MyController(Controller):
|
|
78
|
+
path = "/my-path"
|
|
79
|
+
|
|
80
|
+
@get("/")
|
|
81
|
+
async def list_items(self, db_session: AsyncSession) -> TemplateResponse:
|
|
82
|
+
# db_session is injected automatically
|
|
83
|
+
return TemplateResponse("my-template.html", context={"items": []})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Register in `app.yaml`:
|
|
87
|
+
```yaml
|
|
88
|
+
controllers:
|
|
89
|
+
- myapp.controllers:MyController
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Creating a Database Model
|
|
93
|
+
|
|
94
|
+
Models inherit from `skrift.db.base.Base` (provides id, created_at, updated_at):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from sqlalchemy import String, Text
|
|
98
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
99
|
+
from skrift.db.base import Base
|
|
100
|
+
|
|
101
|
+
class MyModel(Base):
|
|
102
|
+
__tablename__ = "my_models"
|
|
103
|
+
|
|
104
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
105
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Creating a Service
|
|
109
|
+
|
|
110
|
+
Services are async functions that handle database operations:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from sqlalchemy import select
|
|
114
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
115
|
+
from skrift.db.models import MyModel
|
|
116
|
+
|
|
117
|
+
async def get_item_by_id(db_session: AsyncSession, item_id: UUID) -> MyModel | None:
|
|
118
|
+
result = await db_session.execute(select(MyModel).where(MyModel.id == item_id))
|
|
119
|
+
return result.scalar_one_or_none()
|
|
120
|
+
|
|
121
|
+
async def create_item(db_session: AsyncSession, name: str) -> MyModel:
|
|
122
|
+
item = MyModel(name=name)
|
|
123
|
+
db_session.add(item)
|
|
124
|
+
await db_session.commit()
|
|
125
|
+
await db_session.refresh(item)
|
|
126
|
+
return item
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Using Hooks
|
|
130
|
+
|
|
131
|
+
The hook system provides WordPress-like extensibility:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from skrift.lib.hooks import hooks, action, filter
|
|
135
|
+
|
|
136
|
+
# Action: Side effects (no return value needed)
|
|
137
|
+
@action("after_page_save", priority=10)
|
|
138
|
+
async def notify_on_save(page, is_new: bool):
|
|
139
|
+
print(f"Page saved: {page.title}")
|
|
140
|
+
|
|
141
|
+
# Filter: Transform values (must return value)
|
|
142
|
+
@filter("page_seo_meta", priority=10)
|
|
143
|
+
async def customize_meta(meta: dict, page) -> dict:
|
|
144
|
+
meta["author"] = "Custom Author"
|
|
145
|
+
return meta
|
|
146
|
+
|
|
147
|
+
# Trigger hooks programmatically
|
|
148
|
+
await hooks.do_action("my_action", arg1, arg2)
|
|
149
|
+
result = await hooks.apply_filters("my_filter", initial_value, arg1)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Built-in hooks:
|
|
153
|
+
- Actions: `before_page_save`, `after_page_save`, `before_page_delete`, `after_page_delete`
|
|
154
|
+
- Filters: `page_seo_meta`, `page_og_meta`, `sitemap_urls`, `sitemap_page`, `robots_txt`, `template_context`
|
|
155
|
+
|
|
156
|
+
### Using Guards (Authorization)
|
|
157
|
+
|
|
158
|
+
Protect routes with permission or role guards:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from skrift.auth import auth_guard, Permission, Role
|
|
162
|
+
|
|
163
|
+
class AdminController(Controller):
|
|
164
|
+
path = "/admin"
|
|
165
|
+
guards = [auth_guard, Permission("manage-pages")]
|
|
166
|
+
|
|
167
|
+
@get("/")
|
|
168
|
+
async def admin_dashboard(self) -> TemplateResponse:
|
|
169
|
+
return TemplateResponse("admin/dashboard.html")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Combine requirements:
|
|
173
|
+
```python
|
|
174
|
+
# AND: Both required
|
|
175
|
+
guards = [auth_guard, Permission("edit") & Permission("publish")]
|
|
176
|
+
|
|
177
|
+
# OR: Either sufficient
|
|
178
|
+
guards = [auth_guard, Role("admin") | Role("editor")]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Template Resolution
|
|
182
|
+
|
|
183
|
+
WordPress-style template hierarchy with fallbacks:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from skrift.lib.template import Template
|
|
187
|
+
|
|
188
|
+
# Tries: page-about.html -> page.html
|
|
189
|
+
template = Template("page", "about")
|
|
190
|
+
|
|
191
|
+
# Tries: post-news-2024.html -> post-news.html -> post.html
|
|
192
|
+
template = Template("post", "news", "2024")
|
|
193
|
+
|
|
194
|
+
return template.render(TEMPLATE_DIR, page=page, extra="context")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Templates searched in order:
|
|
198
|
+
1. `./templates/` (project directory - user overrides)
|
|
199
|
+
2. `skrift/templates/` (package directory - defaults)
|
|
200
|
+
|
|
201
|
+
## Reference Documentation
|
|
202
|
+
|
|
203
|
+
For detailed documentation, see:
|
|
204
|
+
- [Architecture Details](architecture.md)
|
|
205
|
+
- [Code Patterns](patterns.md)
|
|
File without changes
|
|
@@ -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
|