skrift 0.1.0a12__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.
Files changed (74) hide show
  1. skrift/__init__.py +1 -0
  2. skrift/__main__.py +12 -0
  3. skrift/admin/__init__.py +11 -0
  4. skrift/admin/controller.py +452 -0
  5. skrift/admin/navigation.py +105 -0
  6. skrift/alembic/env.py +92 -0
  7. skrift/alembic/script.py.mako +26 -0
  8. skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +70 -0
  9. skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +57 -0
  10. skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +31 -0
  11. skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +43 -0
  12. skrift/alembic/versions/20260122_200000_add_settings_table.py +38 -0
  13. skrift/alembic/versions/20260129_add_oauth_accounts.py +141 -0
  14. skrift/alembic/versions/20260129_add_provider_metadata.py +29 -0
  15. skrift/alembic.ini +77 -0
  16. skrift/asgi.py +670 -0
  17. skrift/auth/__init__.py +58 -0
  18. skrift/auth/guards.py +130 -0
  19. skrift/auth/roles.py +129 -0
  20. skrift/auth/services.py +184 -0
  21. skrift/cli.py +143 -0
  22. skrift/config.py +259 -0
  23. skrift/controllers/__init__.py +4 -0
  24. skrift/controllers/auth.py +595 -0
  25. skrift/controllers/web.py +67 -0
  26. skrift/db/__init__.py +3 -0
  27. skrift/db/base.py +7 -0
  28. skrift/db/models/__init__.py +7 -0
  29. skrift/db/models/oauth_account.py +50 -0
  30. skrift/db/models/page.py +26 -0
  31. skrift/db/models/role.py +56 -0
  32. skrift/db/models/setting.py +13 -0
  33. skrift/db/models/user.py +36 -0
  34. skrift/db/services/__init__.py +1 -0
  35. skrift/db/services/oauth_service.py +195 -0
  36. skrift/db/services/page_service.py +217 -0
  37. skrift/db/services/setting_service.py +206 -0
  38. skrift/lib/__init__.py +3 -0
  39. skrift/lib/exceptions.py +168 -0
  40. skrift/lib/template.py +108 -0
  41. skrift/setup/__init__.py +14 -0
  42. skrift/setup/config_writer.py +213 -0
  43. skrift/setup/controller.py +888 -0
  44. skrift/setup/middleware.py +89 -0
  45. skrift/setup/providers.py +214 -0
  46. skrift/setup/state.py +315 -0
  47. skrift/static/css/style.css +1003 -0
  48. skrift/templates/admin/admin.html +19 -0
  49. skrift/templates/admin/base.html +24 -0
  50. skrift/templates/admin/pages/edit.html +32 -0
  51. skrift/templates/admin/pages/list.html +62 -0
  52. skrift/templates/admin/settings/site.html +32 -0
  53. skrift/templates/admin/users/list.html +58 -0
  54. skrift/templates/admin/users/roles.html +42 -0
  55. skrift/templates/auth/dummy_login.html +102 -0
  56. skrift/templates/auth/login.html +139 -0
  57. skrift/templates/base.html +52 -0
  58. skrift/templates/error-404.html +19 -0
  59. skrift/templates/error-500.html +19 -0
  60. skrift/templates/error.html +19 -0
  61. skrift/templates/index.html +9 -0
  62. skrift/templates/page.html +26 -0
  63. skrift/templates/setup/admin.html +24 -0
  64. skrift/templates/setup/auth.html +110 -0
  65. skrift/templates/setup/base.html +407 -0
  66. skrift/templates/setup/complete.html +17 -0
  67. skrift/templates/setup/configuring.html +158 -0
  68. skrift/templates/setup/database.html +125 -0
  69. skrift/templates/setup/restart.html +28 -0
  70. skrift/templates/setup/site.html +39 -0
  71. skrift-0.1.0a12.dist-info/METADATA +235 -0
  72. skrift-0.1.0a12.dist-info/RECORD +74 -0
  73. skrift-0.1.0a12.dist-info/WHEEL +4 -0
  74. skrift-0.1.0a12.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,158 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Configuring Database - Skrift{% endblock %}
4
+
5
+ {% block head %}
6
+ <style>
7
+ .configuring-content {
8
+ text-align: center;
9
+ padding: 2rem 0;
10
+ }
11
+
12
+ .spinner {
13
+ width: 48px;
14
+ height: 48px;
15
+ border: 4px solid var(--color-border);
16
+ border-top-color: var(--color-primary);
17
+ border-radius: 50%;
18
+ animation: spin 1s linear infinite;
19
+ margin: 0 auto 1.5rem;
20
+ }
21
+
22
+ .spinner.complete {
23
+ border-color: var(--color-success);
24
+ border-top-color: var(--color-success);
25
+ animation: none;
26
+ }
27
+
28
+ .spinner.error {
29
+ border-color: var(--color-error);
30
+ border-top-color: var(--color-error);
31
+ animation: none;
32
+ }
33
+
34
+ @keyframes spin {
35
+ to { transform: rotate(360deg); }
36
+ }
37
+
38
+ .status-icon {
39
+ width: 48px;
40
+ height: 48px;
41
+ margin: 0 auto 1.5rem;
42
+ font-size: 48px;
43
+ line-height: 1;
44
+ display: none;
45
+ }
46
+
47
+ .status-icon.complete {
48
+ display: block;
49
+ color: var(--color-success);
50
+ }
51
+
52
+ .status-icon.error {
53
+ display: block;
54
+ color: var(--color-error);
55
+ }
56
+
57
+ .status-message {
58
+ font-size: 1.125rem;
59
+ margin-bottom: 0.5rem;
60
+ }
61
+
62
+ .status-detail {
63
+ color: var(--color-text-muted);
64
+ font-size: 0.875rem;
65
+ min-height: 1.25rem;
66
+ }
67
+
68
+ .form-actions button:disabled {
69
+ opacity: 0.5;
70
+ cursor: not-allowed;
71
+ }
72
+ </style>
73
+ {% endblock %}
74
+
75
+ {% block content %}
76
+ <h2>Configuring Database</h2>
77
+ <p class="description">Setting up your database and running migrations.</p>
78
+
79
+ <div class="configuring-content">
80
+ <div class="spinner" id="spinner"></div>
81
+ <div class="status-icon" id="status-icon"></div>
82
+ <div class="status-message" id="status-message">Initializing...</div>
83
+ <div class="status-detail" id="status-detail"></div>
84
+ </div>
85
+
86
+ <div class="form-actions">
87
+ <a href="/setup/database" role="button" class="btn-secondary">Back</a>
88
+ <button type="button" id="next-button" disabled>Continue</button>
89
+ </div>
90
+ {% endblock %}
91
+
92
+ {% block scripts %}
93
+ <script>
94
+ document.addEventListener('DOMContentLoaded', function() {
95
+ const spinner = document.getElementById('spinner');
96
+ const statusIcon = document.getElementById('status-icon');
97
+ const statusMessage = document.getElementById('status-message');
98
+ const statusDetail = document.getElementById('status-detail');
99
+ const nextButton = document.getElementById('next-button');
100
+
101
+ let nextStep = 'auth'; // Default fallback
102
+
103
+ function setComplete(step) {
104
+ spinner.style.display = 'none';
105
+ statusIcon.textContent = '\u2713';
106
+ statusIcon.classList.add('complete');
107
+ statusMessage.textContent = 'Database configured successfully!';
108
+ statusDetail.textContent = '';
109
+ nextButton.disabled = false;
110
+ if (step) {
111
+ nextStep = step;
112
+ }
113
+ nextButton.onclick = function() {
114
+ window.location.href = '/setup/' + nextStep;
115
+ };
116
+ }
117
+
118
+ function setError(message) {
119
+ spinner.style.display = 'none';
120
+ statusIcon.textContent = '\u2717';
121
+ statusIcon.classList.add('error');
122
+ statusMessage.textContent = 'Configuration failed';
123
+ statusDetail.textContent = message || 'An error occurred during setup.';
124
+ }
125
+
126
+ function setStatus(message, detail) {
127
+ statusMessage.textContent = message;
128
+ statusDetail.textContent = detail || '';
129
+ }
130
+
131
+ // Connect to SSE endpoint
132
+ const eventSource = new EventSource('/setup/configuring/status');
133
+
134
+ eventSource.onmessage = function(event) {
135
+ const data = JSON.parse(event.data);
136
+
137
+ if (data.status === 'running') {
138
+ setStatus(data.message, data.detail);
139
+ } else if (data.status === 'complete') {
140
+ setComplete(data.next_step);
141
+ eventSource.close();
142
+ } else if (data.status === 'error') {
143
+ setError(data.message);
144
+ eventSource.close();
145
+ }
146
+ };
147
+
148
+ eventSource.onerror = function() {
149
+ // Check if we were successful before the connection closed
150
+ if (!nextButton.disabled) {
151
+ return; // Already complete, ignore the error
152
+ }
153
+ setError('Connection lost. Please refresh the page.');
154
+ eventSource.close();
155
+ };
156
+ });
157
+ </script>
158
+ {% endblock %}
@@ -0,0 +1,125 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Database Setup - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <h2>Database Configuration</h2>
7
+ <p class="description">Choose your database type and configure the connection.</p>
8
+
9
+ <form method="post" action="/setup/database">
10
+ <div class="radio-group">
11
+ <label class="radio-option{% if db_type == 'sqlite' %} selected{% endif %}" id="sqlite-option">
12
+ <input type="radio" name="db_type" value="sqlite" {% if db_type == 'sqlite' %}checked{% endif %}>
13
+ <div class="radio-content">
14
+ <strong>SQLite</strong>
15
+ <small>Perfect for personal sites. No setup required.</small>
16
+ </div>
17
+ </label>
18
+ <label class="radio-option{% if db_type == 'postgresql' %} selected{% endif %}" id="postgresql-option">
19
+ <input type="radio" name="db_type" value="postgresql" {% if db_type == 'postgresql' %}checked{% endif %}>
20
+ <div class="radio-content">
21
+ <strong>PostgreSQL</strong>
22
+ <small>For production deployments.</small>
23
+ </div>
24
+ </label>
25
+ </div>
26
+
27
+ <div class="conditional-fields{% if db_type == 'sqlite' %} visible{% endif %}" id="sqlite-fields">
28
+ <div class="form-group">
29
+ <label for="sqlite_path">Database File Path</label>
30
+ <input type="text" id="sqlite_path" name="sqlite_path" value="./app.db" placeholder="./app.db">
31
+ <small>Path relative to the application directory</small>
32
+ </div>
33
+ <div class="form-group env-toggle">
34
+ <input type="checkbox" id="sqlite_path_env" name="sqlite_path_env">
35
+ <label for="sqlite_path_env">Use environment variable (enter env var name in field above)</label>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="conditional-fields{% if db_type == 'postgresql' %} visible{% endif %}" id="postgresql-fields">
40
+ <div class="form-group env-toggle" style="margin-bottom: 1rem;">
41
+ <input type="checkbox" id="pg_url_env" name="pg_url_env">
42
+ <label for="pg_url_env">Use a single DATABASE_URL environment variable</label>
43
+ </div>
44
+
45
+ <div id="pg-env-field" style="display: none;">
46
+ <div class="form-group">
47
+ <label for="pg_url_envvar">Environment Variable Name</label>
48
+ <input type="text" id="pg_url_envvar" name="pg_url_envvar" value="DATABASE_URL">
49
+ <small>The environment variable containing the full connection URL</small>
50
+ </div>
51
+ </div>
52
+
53
+ <div id="pg-manual-fields">
54
+ <div class="form-group">
55
+ <label for="pg_host">Host</label>
56
+ <input type="text" id="pg_host" name="pg_host" value="localhost" placeholder="localhost">
57
+ </div>
58
+
59
+ <div class="form-group">
60
+ <label for="pg_port">Port</label>
61
+ <input type="number" id="pg_port" name="pg_port" value="5432" placeholder="5432">
62
+ </div>
63
+
64
+ <div class="form-group">
65
+ <label for="pg_database">Database Name</label>
66
+ <input type="text" id="pg_database" name="pg_database" value="skrift" placeholder="skrift">
67
+ </div>
68
+
69
+ <div class="form-group">
70
+ <label for="pg_username">Username</label>
71
+ <input type="text" id="pg_username" name="pg_username" value="postgres" placeholder="postgres">
72
+ </div>
73
+
74
+ <div class="form-group">
75
+ <label for="pg_password">Password</label>
76
+ <input type="password" id="pg_password" name="pg_password" placeholder="Enter password">
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="form-actions">
82
+ <button type="submit">Test Connection &amp; Continue</button>
83
+ </div>
84
+ </form>
85
+ {% endblock %}
86
+
87
+ {% block scripts %}
88
+ <script>
89
+ document.addEventListener('DOMContentLoaded', function() {
90
+ const sqliteOption = document.getElementById('sqlite-option');
91
+ const postgresqlOption = document.getElementById('postgresql-option');
92
+ const sqliteFields = document.getElementById('sqlite-fields');
93
+ const postgresqlFields = document.getElementById('postgresql-fields');
94
+ const radioInputs = document.querySelectorAll('input[name="db_type"]');
95
+
96
+ function updateVisibility() {
97
+ const selected = document.querySelector('input[name="db_type"]:checked').value;
98
+
99
+ sqliteOption.classList.toggle('selected', selected === 'sqlite');
100
+ postgresqlOption.classList.toggle('selected', selected === 'postgresql');
101
+
102
+ sqliteFields.classList.toggle('visible', selected === 'sqlite');
103
+ postgresqlFields.classList.toggle('visible', selected === 'postgresql');
104
+ }
105
+
106
+ radioInputs.forEach(input => {
107
+ input.addEventListener('change', updateVisibility);
108
+ });
109
+
110
+ // PostgreSQL env var toggle
111
+ const pgEnvCheckbox = document.getElementById('pg_url_env');
112
+ const pgEnvField = document.getElementById('pg-env-field');
113
+ const pgManualFields = document.getElementById('pg-manual-fields');
114
+
115
+ function updatePgFields() {
116
+ const useEnv = pgEnvCheckbox.checked;
117
+ pgEnvField.style.display = useEnv ? 'block' : 'none';
118
+ pgManualFields.style.display = useEnv ? 'none' : 'block';
119
+ }
120
+
121
+ pgEnvCheckbox.addEventListener('change', updatePgFields);
122
+ updatePgFields();
123
+ });
124
+ </script>
125
+ {% endblock %}
@@ -0,0 +1,28 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Restart Required - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <h2>Database Configured Successfully!</h2>
7
+ <p class="description">The database has been set up and migrations have been run. To continue with setup, you need to restart the server.</p>
8
+
9
+ <div style="background: var(--color-bg); padding: 1.5rem; border-radius: var(--radius-md); margin: 1.5rem 0; border: 1px solid var(--color-border);">
10
+ <h3 style="margin-top: 0;">Why restart?</h3>
11
+ <p style="margin-bottom: 0; color: var(--color-text-muted);">
12
+ The server needs to reload to enable authentication features that require the database connection you just configured.
13
+ </p>
14
+ </div>
15
+
16
+ <div style="background: var(--color-surface); padding: 1.5rem; border-radius: var(--radius-md); margin: 1.5rem 0; border: 1px solid var(--color-border);">
17
+ <h3 style="margin-top: 0;">To restart:</h3>
18
+ <ol style="margin-bottom: 0; padding-left: 1.5rem;">
19
+ <li>Stop the current server (press <code>Ctrl+C</code> in the terminal)</li>
20
+ <li>Start it again with the same command you used before</li>
21
+ <li>Return to this page - setup will continue automatically</li>
22
+ </ol>
23
+ </div>
24
+
25
+ <div class="form-actions">
26
+ <a href="/setup/restart" role="button" class="button">I've Restarted - Continue Setup</a>
27
+ </div>
28
+ {% endblock %}
@@ -0,0 +1,39 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Site Settings - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <h2>Site Settings</h2>
7
+ <p class="description">Configure your site's basic information.</p>
8
+
9
+ <form method="post" action="/setup/site">
10
+ <div class="form-group">
11
+ <label for="site_name">Site Name *</label>
12
+ <input type="text" id="site_name" name="site_name" value="{{ settings.site_name }}" required>
13
+ <small>The name displayed in the header and browser title</small>
14
+ </div>
15
+
16
+ <div class="form-group">
17
+ <label for="site_tagline">Site Tagline</label>
18
+ <input type="text" id="site_tagline" name="site_tagline" value="{{ settings.site_tagline }}">
19
+ <small>A short description or slogan for your site</small>
20
+ </div>
21
+
22
+ <div class="form-group">
23
+ <label for="site_copyright_holder">Copyright Holder</label>
24
+ <input type="text" id="site_copyright_holder" name="site_copyright_holder" value="{{ settings.site_copyright_holder }}">
25
+ <small>Name displayed in the copyright notice (defaults to site name if empty)</small>
26
+ </div>
27
+
28
+ <div class="form-group">
29
+ <label for="site_copyright_start_year">Copyright Start Year</label>
30
+ <input type="number" id="site_copyright_start_year" name="site_copyright_start_year" value="{{ settings.site_copyright_start_year }}" min="1900" max="2100">
31
+ <small>If different from current year, will show a range (e.g., 2020-2026)</small>
32
+ </div>
33
+
34
+ <div class="form-actions">
35
+ <a href="/setup/auth" class="btn-secondary" role="button">Back</a>
36
+ <button type="submit">Save &amp; Continue</button>
37
+ </div>
38
+ </form>
39
+ {% endblock %}
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: skrift
3
+ Version: 0.1.0a12
4
+ Summary: A lightweight async Python CMS for crafting modern websites
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: advanced-alchemy>=0.26.0
7
+ Requires-Dist: aiosqlite>=0.20.0
8
+ Requires-Dist: alembic>=1.14.0
9
+ Requires-Dist: asyncpg>=0.30.0
10
+ Requires-Dist: httpx>=0.28.0
11
+ Requires-Dist: litestar[cryptography,jinja,standard]>=2.14.0
12
+ Requires-Dist: pydantic-settings>=2.7.0
13
+ Requires-Dist: python-dotenv>=1.0.0
14
+ Requires-Dist: pyyaml>=6.0.0
15
+ Requires-Dist: ruamel-yaml>=0.18.0
16
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.36
17
+ Requires-Dist: uvicorn>=0.34.0
18
+ Provides-Extra: docs
19
+ Requires-Dist: zensical>=0.0.19; extra == 'docs'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Skrift
23
+
24
+ A modern Litestar-powered content management framework with multi-provider OAuth authentication, role-based access control, and WordPress-like template resolution.
25
+
26
+ ## Features
27
+
28
+ - **Multi-Provider OAuth**: Authenticate with Google, GitHub, Microsoft, Discord, Facebook, or Twitter/X
29
+ - **Role-Based Access Control**: Flexible permission system with Admin, Editor, Author, and Moderator roles
30
+ - **Setup Wizard**: Guided first-time configuration without manual file editing
31
+ - **Admin Interface**: Web-based management for users, pages, and site settings
32
+ - **WordPress-like Templates**: Hierarchical template resolution for content pages
33
+ - **Dynamic Controllers**: Load controllers from `app.yaml` configuration
34
+ - **SQLAlchemy Integration**: Async database support with SQLite/PostgreSQL
35
+ - **Client-Side Sessions**: Encrypted cookie sessions for horizontal scalability
36
+
37
+ ## Quick Start
38
+
39
+ ### Prerequisites
40
+
41
+ - Python 3.13+
42
+
43
+ ### Installation
44
+
45
+ ```bash
46
+ # Install Skrift
47
+ pip install skrift
48
+
49
+ # Or install from git
50
+ pip install git+https://github.com/ZechCodes/skrift.git
51
+ ```
52
+
53
+ ### Getting Started
54
+
55
+ Create a project directory and set up your environment:
56
+
57
+ ```bash
58
+ mkdir mysite && cd mysite
59
+
60
+ # Create minimal environment file
61
+ echo "SECRET_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" > .env
62
+
63
+ # Start Skrift
64
+ skrift
65
+ ```
66
+
67
+ Open http://localhost:8080 to launch the setup wizard.
68
+
69
+ ### Setup Wizard
70
+
71
+ The setup wizard guides you through initial configuration:
72
+
73
+ 1. **Database Configuration**: Choose SQLite (dev) or PostgreSQL (production)
74
+ 2. **Authentication Providers**: Configure OAuth credentials
75
+ 3. **Site Settings**: Set site name, tagline, and copyright info
76
+ 4. **Admin Account**: Create your first admin user via OAuth login
77
+
78
+ After completing the wizard, an `app.yaml` configuration file is created in your project directory.
79
+
80
+ ### Manual Configuration
81
+
82
+ Alternatively, create `app.yaml` manually:
83
+
84
+ ```yaml
85
+ controllers:
86
+ - skrift.controllers.auth:AuthController
87
+ - skrift.admin.controller:AdminController
88
+ - skrift.controllers.web:WebController
89
+
90
+ db:
91
+ url: sqlite+aiosqlite:///./app.db
92
+
93
+ auth:
94
+ redirect_base_url: http://localhost:8080
95
+ providers:
96
+ google:
97
+ client_id: $GOOGLE_CLIENT_ID
98
+ client_secret: $GOOGLE_CLIENT_SECRET
99
+ scopes: [openid, email, profile]
100
+ ```
101
+
102
+ Then run migrations and start the server:
103
+
104
+ ```bash
105
+ skrift-db upgrade head
106
+ skrift
107
+ ```
108
+
109
+ ## Documentation
110
+
111
+ - **[Full Documentation](docs/README.md)**: Comprehensive guide covering all features
112
+ - **[Deployment Guide](docs/deployment.md)**: VPS, Docker, and Kubernetes deployment
113
+ - **[CSS Framework](docs/css-framework.md)**: Styling documentation
114
+
115
+ ## Project Structure
116
+
117
+ ```
118
+ skrift/
119
+ ├── skrift/ # Main Python package
120
+ │ ├── asgi.py # Application factory
121
+ │ ├── config.py # Settings management
122
+ │ ├── controllers/ # Route handlers
123
+ │ ├── admin/ # Admin panel
124
+ │ ├── auth/ # RBAC and guards
125
+ │ ├── db/ # Models and services
126
+ │ ├── lib/ # Template resolver
127
+ │ └── setup/ # Setup wizard
128
+ ├── templates/ # Jinja2 templates
129
+ ├── static/ # Static assets
130
+ ├── alembic/ # Database migrations
131
+ ├── docs/ # Documentation
132
+ ├── app.yaml # Application config (generated)
133
+ └── main.py # Development entry point
134
+ ```
135
+
136
+ ## Configuration
137
+
138
+ ### Environment Variables
139
+
140
+ | Variable | Required | Description |
141
+ |----------|----------|-------------|
142
+ | `SECRET_KEY` | Yes | Session encryption key |
143
+ | `DEBUG` | No | Enable debug mode (default: false) |
144
+ | `DATABASE_URL` | No | Database connection string |
145
+ | `OAUTH_REDIRECT_BASE_URL` | No | OAuth callback base URL |
146
+
147
+ OAuth credentials are configured per-provider (e.g., `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`).
148
+
149
+ ### app.yaml
150
+
151
+ Application configuration is stored in `app.yaml` (generated by setup wizard):
152
+
153
+ ```yaml
154
+ controllers:
155
+ - skrift.controllers.auth:AuthController
156
+ - skrift.admin.controller:AdminController
157
+ - skrift.controllers.web:WebController
158
+
159
+ db:
160
+ url: $DATABASE_URL
161
+ pool_size: 5
162
+
163
+ auth:
164
+ redirect_base_url: $OAUTH_REDIRECT_BASE_URL
165
+ providers:
166
+ google:
167
+ client_id: $GOOGLE_CLIENT_ID
168
+ client_secret: $GOOGLE_CLIENT_SECRET
169
+ ```
170
+
171
+ Environment variables (prefixed with `$`) are interpolated at runtime.
172
+
173
+ ## Deployment
174
+
175
+ ### Minimal VPS Deployment
176
+
177
+ ```bash
178
+ # Install Skrift
179
+ pip install skrift
180
+
181
+ # Create project directory
182
+ mkdir -p /opt/skrift && cd /opt/skrift
183
+
184
+ # Configure environment
185
+ cat > .env << EOF
186
+ SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
187
+ DATABASE_URL=sqlite+aiosqlite:///./app.db
188
+ OAUTH_REDIRECT_BASE_URL=https://yourdomain.com
189
+ EOF
190
+
191
+ # Start server (use setup wizard or create app.yaml manually)
192
+ skrift
193
+ ```
194
+
195
+ ### Production with Gunicorn
196
+
197
+ ```bash
198
+ pip install gunicorn
199
+ gunicorn skrift.asgi:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8080
200
+ ```
201
+
202
+ See the [Deployment Guide](docs/deployment.md) for detailed instructions including Docker, Docker Compose, and Kubernetes deployments.
203
+
204
+ ## Database Migrations
205
+
206
+ ```bash
207
+ # Apply migrations
208
+ skrift-db upgrade head
209
+
210
+ # Create new migration
211
+ skrift-db revision --autogenerate -m "description"
212
+
213
+ # Rollback
214
+ skrift-db downgrade -1
215
+ ```
216
+
217
+ ## Template Resolution
218
+
219
+ Templates follow WordPress-like hierarchical resolution:
220
+
221
+ | URL Path | Templates Tried |
222
+ |----------|-----------------|
223
+ | `/about` | `page-about.html` -> `page.html` |
224
+ | `/services/web` | `page-services-web.html` -> `page-services.html` -> `page.html` |
225
+
226
+ ## Contributing
227
+
228
+ 1. Fork the repository
229
+ 2. Create a feature branch
230
+ 3. Make your changes
231
+ 4. Submit a pull request
232
+
233
+ ## License
234
+
235
+ MIT
@@ -0,0 +1,74 @@
1
+ skrift/__init__.py,sha256=eXE5PFVkJpH5XsV_ZlrTIeFPUPrmcHYAj4GpRS3R5PY,29
2
+ skrift/__main__.py,sha256=wt6JZL9nBhKU36vdyurhOEtWy7w3C9zohyy24PLcKho,164
3
+ skrift/alembic.ini,sha256=mYguI6CbMCTyfHctsGiTyf9Z5gv21FdeI3qtfgOHO3A,1815
4
+ skrift/asgi.py,sha256=qtP0aq9C6-bIZgbXiwJeF_3dY8O0cM2U_5hkajKMjBc,23032
5
+ skrift/cli.py,sha256=aT-7pXvOuuZC-Eypx1h-xCiqaBKhIFjSqd5Ky_dHna0,4214
6
+ skrift/config.py,sha256=O37SnnYkqTme-iMicMs2k4aD7Fv0wt7v3E_7fsiF40Y,7772
7
+ skrift/admin/__init__.py,sha256=x81Cj_ilVmv6slaMl16HHyT_AgrnLxKEWkS0RPa4V9s,289
8
+ skrift/admin/controller.py,sha256=5ZDypvKHXLNDESsKNsdsH2E3Si5OqlpzttFl7Ot8aF0,15651
9
+ skrift/admin/navigation.py,sha256=VwttFoIUIJy5rONKIkJd5w4CNkUpeK22_OfLGHecN34,3382
10
+ skrift/alembic/env.py,sha256=GaQx7D-3f0zVTV4YJNhN0GOfqHL99N4VBfMzjZJz0Bc,2673
11
+ skrift/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
12
+ skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py,sha256=X6w1vbVKFurhEcblTLGO4Nd_IKkMVtb8TsptV4gpAJ4,2750
13
+ skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py,sha256=yCGjnMTpfSMHVYQCsPTD6wdDI8pJSUtGRESO6bz0KfU,2672
14
+ skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py,sha256=96TCvvQbYk3RGpZdEQ0x4bIDXxBLc3Gv7VTItTq9T54,850
15
+ skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py,sha256=XJ55LhwzgN07MAREk6w4Bx2Vu3KvMe_bDkIjgE6EfqM,1223
16
+ skrift/alembic/versions/20260122_200000_add_settings_table.py,sha256=aG4pxp18fwonGAWt7EoT0XaiIEmu7zltSrj8e2a8QZU,1310
17
+ skrift/alembic/versions/20260129_add_oauth_accounts.py,sha256=KPuzJ5J13FEbSbyziuB-nfO67i4cRgBeQnygn-uNQx8,5412
18
+ skrift/alembic/versions/20260129_add_provider_metadata.py,sha256=prDfnYM0cODBSYb9xYqW5Dab2WDjHNkkEMRH-hI7BIA,846
19
+ skrift/auth/__init__.py,sha256=uHMqty3dgDSYlReVT96WhygzH6qNAWSVDWoxumzxmsA,1155
20
+ skrift/auth/guards.py,sha256=QePajHsGnJ4R_hlhzblr5IoAgZcY5jzeZ64bJwDL9hM,4451
21
+ skrift/auth/roles.py,sha256=Cvwv8v7Li6CJHhucTuzxRjaNmS530F9sbbQlCPRqhm4,3291
22
+ skrift/auth/services.py,sha256=h6GTXdN5UMRYglnaFz4asMoutVkSSAyL3_Vt56N26pA,5441
23
+ skrift/controllers/__init__.py,sha256=bVr0tsSGz7jBi002Lqd1AA1FQd7ZA_IagsqTKpiHiK0,147
24
+ skrift/controllers/auth.py,sha256=ohYjBJiRa5RDQKUQjL3nR6lzm4uUZcfIgnF2G-QY5o8,21905
25
+ skrift/controllers/web.py,sha256=vmoBS1u5G9gCBu65S49yqZn_WBKlmsqlcvX5tYXTKnE,2348
26
+ skrift/db/__init__.py,sha256=uSghyDFT2K4SFiEqUzdjCGzWpS-Oy6Sd1FUappau-v0,52
27
+ skrift/db/base.py,sha256=QJplFj9235kZdScASEpvyNHln6YW2hqbHwJEYZ3OSsc,173
28
+ skrift/db/models/__init__.py,sha256=wFF9YWe7rhIWIzRuMtAWVDcZdJsNcKnjfhKbLZw9JVs,341
29
+ skrift/db/models/oauth_account.py,sha256=fzmApdtrRYeEWLl35Y7H0YtF7CWjzrpG-lknBOuYRc8,1840
30
+ skrift/db/models/page.py,sha256=CI5W2sWq0zbKHxfwH5TpmGdJ4zlTicckzpdZtaqjJVE,1019
31
+ skrift/db/models/role.py,sha256=VkwkF3XWemmFtkUpQRk6RTIBfcukrU-0PUPCeo8x834,1768
32
+ skrift/db/models/setting.py,sha256=Am4HTyq2LFR408R6BZb--slys1xM8YLYq9HTq_gybD0,392
33
+ skrift/db/models/user.py,sha256=kkR3h0CphUyvOGJOpiZH6Qp28LqW2OriwVTfybs7vuY,1338
34
+ skrift/db/services/__init__.py,sha256=qAC24IPOYg6AampUWonmLeajljQRMwUw0lgXksQG6Nk,69
35
+ skrift/db/services/oauth_service.py,sha256=1065tYGvrSer0X28GblibrSz2BjwVjl3tndXk1plthE,5208
36
+ skrift/db/services/page_service.py,sha256=cXJ-urV7LjUDoVsT1P52f6wcOsur3T-pvqKkGG4xlvI,5246
37
+ skrift/db/services/setting_service.py,sha256=eqFxukn8QFrDHQbaBILOLjP1nr34JPIPFT_NnhmeaTY,5613
38
+ skrift/lib/__init__.py,sha256=PL66bS4_-90zJDMsQSnzqkkxwkO_3aLcYl9l9cDrE2Y,65
39
+ skrift/lib/exceptions.py,sha256=p8ceLIQCc7agCwW6-mhBDAuMAMxZDcf9TDLC6PfztU4,5803
40
+ skrift/lib/template.py,sha256=4_urkRfvth75yNeQ5TyGTHvkvs3vVef7TcwZx0k285k,4226
41
+ skrift/setup/__init__.py,sha256=3VjFPMES5y0M5cQ9R4C1xazqiEPEDqTPjX9-3rBMXnA,478
42
+ skrift/setup/config_writer.py,sha256=YFH3FVjXN7Rum2fzGVPAQRkjdc9b0bHECDqMKYiEkhg,6347
43
+ skrift/setup/controller.py,sha256=v0Ey8T7ptJ5A3vOqQ1TUAXH1bQwA0288J5uyUWMihsw,34250
44
+ skrift/setup/middleware.py,sha256=Nai8ZG2vHldngmAhq7kWzAwKRNcP5tHKhJHa5dCh404,2941
45
+ skrift/setup/providers.py,sha256=0BFKB6168NcmtXxFF6ofHgEDMQD2FbXkexsqrARVtDI,7967
46
+ skrift/setup/state.py,sha256=RMe9LtIjzDoOm9u-Nk5-KAnr_JBiQIjWDpTP9E30ezc,9304
47
+ skrift/static/css/style.css,sha256=-Ijp4PCugWzhHhtZTaRUAITHJDW-0AsbNpDCFI1njVQ,20943
48
+ skrift/templates/base.html,sha256=4bg4s4VdES0dSvhJYLgrfrN26ynqeq1-3jyKPkWWVWk,2065
49
+ skrift/templates/error-404.html,sha256=sJrDaF3Or3Nyki8mxo3wBxLLzgy4wkB9p9wdS8pRA6k,409
50
+ skrift/templates/error-500.html,sha256=MR0wJ1JKLqdmdvsoJbQnZxLkxDPE59LrlbtVPKLM8-A,401
51
+ skrift/templates/error.html,sha256=tg45Hj0wNc9QiZKBVVQ2WsqDiAkByXZnuehTdJPclXc,415
52
+ skrift/templates/index.html,sha256=lzZlnoZ2luY__OtRVa61mpQIgcopAbkKa2rS9XRfRlI,172
53
+ skrift/templates/page.html,sha256=kY1j7TH6dFnvWnYwBEGCIEvardjLhnFMnM3NbAnKszQ,759
54
+ skrift/templates/admin/admin.html,sha256=8sErpGKedl7Hf_CMeu53bUEHptH1FqdBX9LT3z-1HcU,454
55
+ skrift/templates/admin/base.html,sha256=JX1MPw9XTY8jCT2M6iHUwEj8NPNOdZPca2cDUEs9kfQ,659
56
+ skrift/templates/admin/pages/edit.html,sha256=s-uZ4BYG7BdnWYTBSS1mr5aFL8XfT8h-KNiSIraWmHw,1322
57
+ skrift/templates/admin/pages/list.html,sha256=LYh1vEwSHkyXihjcy5PIo33iA7UeIxprw4Gsn1WZMm4,2185
58
+ skrift/templates/admin/settings/site.html,sha256=xHZCZGj9XqyLKCe2eoBlpI3oFIx5VoG_5FP-VAF2mz4,1516
59
+ skrift/templates/admin/users/list.html,sha256=9GWql1la5Srm-OOgooHR9Eouk7ZL66K_FXY6HAAB5lA,1732
60
+ skrift/templates/admin/users/roles.html,sha256=pos-ZM-gXYCN_D8DZpzwEBEm_WvmOKMMPz-3xJqs7nY,1473
61
+ skrift/templates/auth/dummy_login.html,sha256=G2ykWSiOK7XFdr7dzD-7VvKLTXzYT6Lduq5Pc6B21I0,2430
62
+ skrift/templates/auth/login.html,sha256=kBOfKRvKqn0L_POtXgOdl_rzUTNbr9F5NKtqxOMXQdk,7057
63
+ skrift/templates/setup/admin.html,sha256=BSIztZT2iqxVSW23Tfg7ZM51SdGrKL422CR05DPjNic,804
64
+ skrift/templates/setup/auth.html,sha256=0DVL0kU6DlJ2pWe4zwc3DsIVfBAdqpMJxPYvS4zm4OM,4953
65
+ skrift/templates/setup/base.html,sha256=LTXqbnHMvx1wDsxFvo4BSieBPD9pcLMj6NM4ZGzErFM,10372
66
+ skrift/templates/setup/complete.html,sha256=oyT-rYPl0uuyOjPXgNeLr8YoptW9QjHTlScZSViDvTk,630
67
+ skrift/templates/setup/configuring.html,sha256=2KHW9h2BrJgL_kO5IizbAYs4pnFLyRf76IQvEj_cNRM,4607
68
+ skrift/templates/setup/database.html,sha256=gU4-315-QraHa2Eq4Fh3b55QpOM2CkJzh27_Yz13frA,5495
69
+ skrift/templates/setup/restart.html,sha256=GHg31F_e2uLFhWUzJoalk0Y0oYLqsFWyZXWKX3mblbY,1355
70
+ skrift/templates/setup/site.html,sha256=PSOH-q1-ZBl47iSW9-Ad6lEfJn_fzdGD3Pk4vb3xgK4,1680
71
+ skrift-0.1.0a12.dist-info/METADATA,sha256=Vqc35fHrSYSsEgJSZ1r46xjHr-mlE4YyEz_1H5RFutQ,6436
72
+ skrift-0.1.0a12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
73
+ skrift-0.1.0a12.dist-info/entry_points.txt,sha256=uquZ5Mumqr0xwYTpTcNiJtFSITGfF6_QCCy2DZJSZig,42
74
+ skrift-0.1.0a12.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ skrift = skrift.cli:cli