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.
Files changed (68) hide show
  1. skrift/__init__.py +1 -0
  2. skrift/__main__.py +17 -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 +91 -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.ini +77 -0
  14. skrift/asgi.py +545 -0
  15. skrift/auth/__init__.py +58 -0
  16. skrift/auth/guards.py +130 -0
  17. skrift/auth/roles.py +94 -0
  18. skrift/auth/services.py +184 -0
  19. skrift/cli.py +45 -0
  20. skrift/config.py +192 -0
  21. skrift/controllers/__init__.py +4 -0
  22. skrift/controllers/auth.py +371 -0
  23. skrift/controllers/web.py +67 -0
  24. skrift/db/__init__.py +3 -0
  25. skrift/db/base.py +7 -0
  26. skrift/db/models/__init__.py +6 -0
  27. skrift/db/models/page.py +26 -0
  28. skrift/db/models/role.py +56 -0
  29. skrift/db/models/setting.py +13 -0
  30. skrift/db/models/user.py +36 -0
  31. skrift/db/services/__init__.py +1 -0
  32. skrift/db/services/page_service.py +217 -0
  33. skrift/db/services/setting_service.py +206 -0
  34. skrift/lib/__init__.py +3 -0
  35. skrift/lib/exceptions.py +168 -0
  36. skrift/lib/template.py +108 -0
  37. skrift/setup/__init__.py +14 -0
  38. skrift/setup/config_writer.py +211 -0
  39. skrift/setup/controller.py +751 -0
  40. skrift/setup/middleware.py +89 -0
  41. skrift/setup/providers.py +163 -0
  42. skrift/setup/state.py +134 -0
  43. skrift/static/css/style.css +998 -0
  44. skrift/templates/admin/admin.html +19 -0
  45. skrift/templates/admin/base.html +24 -0
  46. skrift/templates/admin/pages/edit.html +32 -0
  47. skrift/templates/admin/pages/list.html +62 -0
  48. skrift/templates/admin/settings/site.html +32 -0
  49. skrift/templates/admin/users/list.html +58 -0
  50. skrift/templates/admin/users/roles.html +42 -0
  51. skrift/templates/auth/login.html +125 -0
  52. skrift/templates/base.html +52 -0
  53. skrift/templates/error-404.html +19 -0
  54. skrift/templates/error-500.html +19 -0
  55. skrift/templates/error.html +19 -0
  56. skrift/templates/index.html +9 -0
  57. skrift/templates/page.html +26 -0
  58. skrift/templates/setup/admin.html +24 -0
  59. skrift/templates/setup/auth.html +110 -0
  60. skrift/templates/setup/base.html +407 -0
  61. skrift/templates/setup/complete.html +17 -0
  62. skrift/templates/setup/database.html +125 -0
  63. skrift/templates/setup/restart.html +28 -0
  64. skrift/templates/setup/site.html +39 -0
  65. skrift-0.1.0a1.dist-info/METADATA +233 -0
  66. skrift-0.1.0a1.dist-info/RECORD +68 -0
  67. skrift-0.1.0a1.dist-info/WHEEL +4 -0
  68. skrift-0.1.0a1.dist-info/entry_points.txt +3 -0
@@ -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())