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.
Files changed (91) hide show
  1. {skrift-0.1.0a15 → skrift-0.1.0a17}/PKG-INFO +1 -1
  2. {skrift-0.1.0a15 → skrift-0.1.0a17}/pyproject.toml +2 -1
  3. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/asgi.py +6 -2
  4. skrift-0.1.0a17/skrift/claude_skill/SKILL.md +205 -0
  5. skrift-0.1.0a17/skrift/claude_skill/__init__.py +0 -0
  6. skrift-0.1.0a17/skrift/claude_skill/architecture.md +328 -0
  7. skrift-0.1.0a17/skrift/claude_skill/patterns.md +552 -0
  8. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/cli.py +47 -0
  9. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/config.py +1 -0
  10. skrift-0.1.0a17/skrift/db/session.py +90 -0
  11. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/config_writer.py +1 -0
  12. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/state.py +4 -4
  13. {skrift-0.1.0a15 → skrift-0.1.0a17}/.gitignore +0 -0
  14. {skrift-0.1.0a15 → skrift-0.1.0a17}/README.md +0 -0
  15. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/__init__.py +0 -0
  16. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/__main__.py +0 -0
  17. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/admin/__init__.py +0 -0
  18. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/admin/controller.py +0 -0
  19. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/admin/navigation.py +0 -0
  20. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/env.py +0 -0
  21. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/script.py.mako +0 -0
  22. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
  23. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
  24. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
  25. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
  26. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
  27. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260129_add_oauth_accounts.py +0 -0
  28. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
  29. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_content_scheduling.py +0 -0
  30. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_page_ordering.py +0 -0
  31. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_page_revisions.py +0 -0
  32. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_seo_fields.py +0 -0
  33. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/alembic.ini +0 -0
  34. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/__init__.py +0 -0
  35. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/guards.py +0 -0
  36. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/roles.py +0 -0
  37. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/auth/services.py +0 -0
  38. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/__init__.py +0 -0
  39. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/auth.py +0 -0
  40. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/sitemap.py +0 -0
  41. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/controllers/web.py +0 -0
  42. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/__init__.py +0 -0
  43. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/base.py +0 -0
  44. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/__init__.py +0 -0
  45. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/oauth_account.py +0 -0
  46. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/page.py +0 -0
  47. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/page_revision.py +0 -0
  48. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/role.py +0 -0
  49. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/setting.py +0 -0
  50. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/models/user.py +0 -0
  51. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/__init__.py +0 -0
  52. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/oauth_service.py +0 -0
  53. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/page_service.py +0 -0
  54. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/revision_service.py +0 -0
  55. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/db/services/setting_service.py +0 -0
  56. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/__init__.py +0 -0
  57. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/exceptions.py +0 -0
  58. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/flash.py +0 -0
  59. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/hooks.py +0 -0
  60. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/markdown.py +0 -0
  61. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/seo.py +0 -0
  62. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/lib/template.py +0 -0
  63. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/__init__.py +0 -0
  64. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/controller.py +0 -0
  65. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/middleware.py +0 -0
  66. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/setup/providers.py +0 -0
  67. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/static/css/style.css +0 -0
  68. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/admin.html +0 -0
  69. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/base.html +0 -0
  70. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/pages/edit.html +0 -0
  71. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/pages/list.html +0 -0
  72. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/pages/revisions.html +0 -0
  73. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/settings/site.html +0 -0
  74. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/users/list.html +0 -0
  75. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/admin/users/roles.html +0 -0
  76. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/auth/dummy_login.html +0 -0
  77. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/auth/login.html +0 -0
  78. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/base.html +0 -0
  79. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/error-404.html +0 -0
  80. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/error-500.html +0 -0
  81. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/error.html +0 -0
  82. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/index.html +0 -0
  83. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/page.html +0 -0
  84. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/admin.html +0 -0
  85. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/auth.html +0 -0
  86. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/base.html +0 -0
  87. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/complete.html +0 -0
  88. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/configuring.html +0 -0
  89. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/database.html +0 -0
  90. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/restart.html +0 -0
  91. {skrift-0.1.0a15 → skrift-0.1.0a17}/skrift/templates/setup/site.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skrift
3
- Version: 0.1.0a15
3
+ Version: 0.1.0a17
4
4
  Summary: A lightweight async Python CMS for crafting modern websites
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: advanced-alchemy>=0.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "skrift"
3
- version = "0.1.0a15"
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 = SQLAlchemyAsyncConfig(
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 = SQLAlchemyAsyncConfig(
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