skrift 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skrift/__init__.py +1 -0
- skrift/__main__.py +17 -0
- skrift/admin/__init__.py +11 -0
- skrift/admin/controller.py +452 -0
- skrift/admin/navigation.py +105 -0
- skrift/alembic/env.py +91 -0
- skrift/alembic/script.py.mako +26 -0
- skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +70 -0
- skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +57 -0
- skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +31 -0
- skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +43 -0
- skrift/alembic/versions/20260122_200000_add_settings_table.py +38 -0
- skrift/alembic.ini +77 -0
- skrift/asgi.py +545 -0
- skrift/auth/__init__.py +58 -0
- skrift/auth/guards.py +130 -0
- skrift/auth/roles.py +94 -0
- skrift/auth/services.py +184 -0
- skrift/cli.py +45 -0
- skrift/config.py +192 -0
- skrift/controllers/__init__.py +4 -0
- skrift/controllers/auth.py +371 -0
- skrift/controllers/web.py +67 -0
- skrift/db/__init__.py +3 -0
- skrift/db/base.py +7 -0
- skrift/db/models/__init__.py +6 -0
- skrift/db/models/page.py +26 -0
- skrift/db/models/role.py +56 -0
- skrift/db/models/setting.py +13 -0
- skrift/db/models/user.py +36 -0
- skrift/db/services/__init__.py +1 -0
- skrift/db/services/page_service.py +217 -0
- skrift/db/services/setting_service.py +206 -0
- skrift/lib/__init__.py +3 -0
- skrift/lib/exceptions.py +168 -0
- skrift/lib/template.py +108 -0
- skrift/setup/__init__.py +14 -0
- skrift/setup/config_writer.py +211 -0
- skrift/setup/controller.py +751 -0
- skrift/setup/middleware.py +89 -0
- skrift/setup/providers.py +163 -0
- skrift/setup/state.py +134 -0
- skrift/static/css/style.css +998 -0
- skrift/templates/admin/admin.html +19 -0
- skrift/templates/admin/base.html +24 -0
- skrift/templates/admin/pages/edit.html +32 -0
- skrift/templates/admin/pages/list.html +62 -0
- skrift/templates/admin/settings/site.html +32 -0
- skrift/templates/admin/users/list.html +58 -0
- skrift/templates/admin/users/roles.html +42 -0
- skrift/templates/auth/login.html +125 -0
- skrift/templates/base.html +52 -0
- skrift/templates/error-404.html +19 -0
- skrift/templates/error-500.html +19 -0
- skrift/templates/error.html +19 -0
- skrift/templates/index.html +9 -0
- skrift/templates/page.html +26 -0
- skrift/templates/setup/admin.html +24 -0
- skrift/templates/setup/auth.html +110 -0
- skrift/templates/setup/base.html +407 -0
- skrift/templates/setup/complete.html +17 -0
- skrift/templates/setup/database.html +125 -0
- skrift/templates/setup/restart.html +28 -0
- skrift/templates/setup/site.html +39 -0
- skrift-0.1.0a1.dist-info/METADATA +233 -0
- skrift-0.1.0a1.dist-info/RECORD +68 -0
- skrift-0.1.0a1.dist-info/WHEEL +4 -0
- skrift-0.1.0a1.dist-info/entry_points.txt +3 -0
skrift/setup/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Setup wizard package for first-time Skrift configuration."""
|
|
2
|
+
|
|
3
|
+
from skrift.setup.state import is_setup_complete, get_setup_step
|
|
4
|
+
from skrift.setup.middleware import SetupMiddleware, create_dynamic_setup_middleware_factory
|
|
5
|
+
from skrift.setup.controller import SetupController, SetupAuthController
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"is_setup_complete",
|
|
9
|
+
"get_setup_step",
|
|
10
|
+
"SetupMiddleware",
|
|
11
|
+
"SetupController",
|
|
12
|
+
"SetupAuthController",
|
|
13
|
+
"create_dynamic_setup_middleware_factory",
|
|
14
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Safe app.yaml configuration writer using ruamel.yaml to preserve comments."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
|
|
10
|
+
# Default app.yaml structure
|
|
11
|
+
DEFAULT_CONFIG = {
|
|
12
|
+
"controllers": [
|
|
13
|
+
"skrift.controllers.auth:AuthController",
|
|
14
|
+
"skrift.admin.controller:AdminController",
|
|
15
|
+
"skrift.controllers.web:WebController",
|
|
16
|
+
],
|
|
17
|
+
"db": {
|
|
18
|
+
"url": "sqlite+aiosqlite:///./app.db",
|
|
19
|
+
"pool_size": 5,
|
|
20
|
+
"pool_overflow": 10,
|
|
21
|
+
"pool_timeout": 30,
|
|
22
|
+
"echo": False,
|
|
23
|
+
},
|
|
24
|
+
"auth": {
|
|
25
|
+
"redirect_base_url": "http://localhost:8000",
|
|
26
|
+
"providers": {},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_config_path() -> Path:
|
|
32
|
+
"""Get the path to app.yaml."""
|
|
33
|
+
return Path.cwd() / "app.yaml"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def backup_config() -> Path | None:
|
|
37
|
+
"""Create a backup of app.yaml if it exists.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Path to backup file or None if no backup was created
|
|
41
|
+
"""
|
|
42
|
+
config_path = get_config_path()
|
|
43
|
+
if not config_path.exists():
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
47
|
+
backup_path = config_path.with_suffix(f".yaml.backup.{timestamp}")
|
|
48
|
+
shutil.copy2(config_path, backup_path)
|
|
49
|
+
return backup_path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_config() -> dict[str, Any]:
|
|
53
|
+
"""Load current config or return default structure."""
|
|
54
|
+
yaml = YAML()
|
|
55
|
+
yaml.preserve_quotes = True
|
|
56
|
+
|
|
57
|
+
config_path = get_config_path()
|
|
58
|
+
if not config_path.exists():
|
|
59
|
+
return DEFAULT_CONFIG.copy()
|
|
60
|
+
|
|
61
|
+
with open(config_path, "r") as f:
|
|
62
|
+
config = yaml.load(f)
|
|
63
|
+
return config if config else DEFAULT_CONFIG.copy()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_config(config: dict[str, Any]) -> None:
|
|
67
|
+
"""Save configuration to app.yaml.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Configuration dictionary to save
|
|
71
|
+
"""
|
|
72
|
+
yaml = YAML()
|
|
73
|
+
yaml.preserve_quotes = True
|
|
74
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
75
|
+
|
|
76
|
+
config_path = get_config_path()
|
|
77
|
+
|
|
78
|
+
# Validate YAML can be serialized before writing
|
|
79
|
+
from io import StringIO
|
|
80
|
+
|
|
81
|
+
test_stream = StringIO()
|
|
82
|
+
yaml.dump(config, test_stream)
|
|
83
|
+
|
|
84
|
+
# Write to file
|
|
85
|
+
with open(config_path, "w") as f:
|
|
86
|
+
yaml.dump(config, f)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def update_database_config(
|
|
90
|
+
db_type: str,
|
|
91
|
+
url: str | None = None,
|
|
92
|
+
host: str | None = None,
|
|
93
|
+
port: int | None = None,
|
|
94
|
+
database: str | None = None,
|
|
95
|
+
username: str | None = None,
|
|
96
|
+
password: str | None = None,
|
|
97
|
+
use_env_vars: dict[str, bool] | None = None,
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""Update database configuration in app.yaml.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
db_type: Either "sqlite" or "postgresql"
|
|
103
|
+
url: Direct URL (for sqlite file path)
|
|
104
|
+
host: PostgreSQL host
|
|
105
|
+
port: PostgreSQL port
|
|
106
|
+
database: PostgreSQL database name
|
|
107
|
+
username: PostgreSQL username
|
|
108
|
+
password: PostgreSQL password
|
|
109
|
+
use_env_vars: Dict mapping field names to whether they should use env vars
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Updated configuration
|
|
113
|
+
"""
|
|
114
|
+
backup_config()
|
|
115
|
+
config = load_config()
|
|
116
|
+
|
|
117
|
+
if "db" not in config:
|
|
118
|
+
config["db"] = {}
|
|
119
|
+
|
|
120
|
+
use_env_vars = use_env_vars or {}
|
|
121
|
+
|
|
122
|
+
if db_type == "sqlite":
|
|
123
|
+
file_path = url or "./app.db"
|
|
124
|
+
if use_env_vars.get("url"):
|
|
125
|
+
config["db"]["url"] = f"${file_path}"
|
|
126
|
+
else:
|
|
127
|
+
config["db"]["url"] = f"sqlite+aiosqlite:///{file_path}"
|
|
128
|
+
else:
|
|
129
|
+
# PostgreSQL
|
|
130
|
+
if use_env_vars.get("url"):
|
|
131
|
+
# Use a single env var for the whole URL
|
|
132
|
+
config["db"]["url"] = f"${url}"
|
|
133
|
+
else:
|
|
134
|
+
# Build URL from components
|
|
135
|
+
host_str = f"${host}" if use_env_vars.get("host") else host
|
|
136
|
+
port_str = f"${port}" if use_env_vars.get("port") else port
|
|
137
|
+
db_str = f"${database}" if use_env_vars.get("database") else database
|
|
138
|
+
user_str = f"${username}" if use_env_vars.get("username") else username
|
|
139
|
+
pass_str = f"${password}" if use_env_vars.get("password") else password
|
|
140
|
+
|
|
141
|
+
# For env vars in components, we need to store them as env var references
|
|
142
|
+
if any(use_env_vars.values()):
|
|
143
|
+
# Store the URL with env var placeholders
|
|
144
|
+
config["db"]["url"] = f"$DATABASE_URL"
|
|
145
|
+
else:
|
|
146
|
+
config["db"]["url"] = (
|
|
147
|
+
f"postgresql+asyncpg://{user_str}:{pass_str}@{host_str}:{port_str}/{db_str}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
save_config(config)
|
|
151
|
+
return config
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def update_auth_config(
|
|
155
|
+
redirect_base_url: str,
|
|
156
|
+
providers: dict[str, dict[str, Any]],
|
|
157
|
+
use_env_vars: dict[str, dict[str, bool]] | None = None,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""Update authentication configuration in app.yaml.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
redirect_base_url: Base URL for OAuth callbacks
|
|
163
|
+
providers: Dict of provider configs {provider: {client_id, client_secret, ...}}
|
|
164
|
+
use_env_vars: Dict of {provider: {field: use_env_var}} for env var toggles
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Updated configuration
|
|
168
|
+
"""
|
|
169
|
+
backup_config()
|
|
170
|
+
config = load_config()
|
|
171
|
+
|
|
172
|
+
if "auth" not in config:
|
|
173
|
+
config["auth"] = {}
|
|
174
|
+
|
|
175
|
+
config["auth"]["redirect_base_url"] = redirect_base_url
|
|
176
|
+
|
|
177
|
+
if "providers" not in config["auth"]:
|
|
178
|
+
config["auth"]["providers"] = {}
|
|
179
|
+
|
|
180
|
+
use_env_vars = use_env_vars or {}
|
|
181
|
+
|
|
182
|
+
for provider, provider_config in providers.items():
|
|
183
|
+
provider_env_vars = use_env_vars.get(provider, {})
|
|
184
|
+
|
|
185
|
+
processed_config = {}
|
|
186
|
+
for field, value in provider_config.items():
|
|
187
|
+
if provider_env_vars.get(field):
|
|
188
|
+
# Store as env var reference
|
|
189
|
+
processed_config[field] = f"${value}"
|
|
190
|
+
else:
|
|
191
|
+
processed_config[field] = value
|
|
192
|
+
|
|
193
|
+
# Add default scopes if not specified
|
|
194
|
+
from skrift.setup.providers import get_provider_info
|
|
195
|
+
|
|
196
|
+
provider_info = get_provider_info(provider)
|
|
197
|
+
if provider_info and "scopes" not in processed_config:
|
|
198
|
+
processed_config["scopes"] = provider_info.scopes
|
|
199
|
+
|
|
200
|
+
config["auth"]["providers"][provider] = processed_config
|
|
201
|
+
|
|
202
|
+
save_config(config)
|
|
203
|
+
return config
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_configured_providers() -> list[str]:
|
|
207
|
+
"""Get list of providers currently configured in app.yaml."""
|
|
208
|
+
config = load_config()
|
|
209
|
+
auth = config.get("auth", {})
|
|
210
|
+
providers = auth.get("providers", {})
|
|
211
|
+
return list(providers.keys())
|