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,213 @@
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
+ from skrift.config import get_config_path as _get_config_path
11
+
12
+ # Default app.yaml structure
13
+ DEFAULT_CONFIG = {
14
+ "controllers": [
15
+ "skrift.controllers.auth:AuthController",
16
+ "skrift.admin.controller:AdminController",
17
+ "skrift.controllers.web:WebController",
18
+ ],
19
+ "db": {
20
+ "url": "sqlite+aiosqlite:///./app.db",
21
+ "pool_size": 5,
22
+ "pool_overflow": 10,
23
+ "pool_timeout": 30,
24
+ "echo": False,
25
+ },
26
+ "auth": {
27
+ "redirect_base_url": "http://localhost:8000",
28
+ "providers": {},
29
+ },
30
+ }
31
+
32
+
33
+ def get_config_path() -> Path:
34
+ """Get the path to the current environment's config file."""
35
+ return _get_config_path()
36
+
37
+
38
+ def backup_config() -> Path | None:
39
+ """Create a backup of app.yaml if it exists.
40
+
41
+ Returns:
42
+ Path to backup file or None if no backup was created
43
+ """
44
+ config_path = get_config_path()
45
+ if not config_path.exists():
46
+ return None
47
+
48
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
49
+ backup_path = config_path.with_suffix(f".yaml.backup.{timestamp}")
50
+ shutil.copy2(config_path, backup_path)
51
+ return backup_path
52
+
53
+
54
+ def load_config() -> dict[str, Any]:
55
+ """Load current config or return default structure."""
56
+ yaml = YAML()
57
+ yaml.preserve_quotes = True
58
+
59
+ config_path = get_config_path()
60
+ if not config_path.exists():
61
+ return DEFAULT_CONFIG.copy()
62
+
63
+ with open(config_path, "r") as f:
64
+ config = yaml.load(f)
65
+ return config if config else DEFAULT_CONFIG.copy()
66
+
67
+
68
+ def save_config(config: dict[str, Any]) -> None:
69
+ """Save configuration to app.yaml.
70
+
71
+ Args:
72
+ config: Configuration dictionary to save
73
+ """
74
+ yaml = YAML()
75
+ yaml.preserve_quotes = True
76
+ yaml.indent(mapping=2, sequence=4, offset=2)
77
+
78
+ config_path = get_config_path()
79
+
80
+ # Validate YAML can be serialized before writing
81
+ from io import StringIO
82
+
83
+ test_stream = StringIO()
84
+ yaml.dump(config, test_stream)
85
+
86
+ # Write to file
87
+ with open(config_path, "w") as f:
88
+ yaml.dump(config, f)
89
+
90
+
91
+ def update_database_config(
92
+ db_type: str,
93
+ url: str | None = None,
94
+ host: str | None = None,
95
+ port: int | None = None,
96
+ database: str | None = None,
97
+ username: str | None = None,
98
+ password: str | None = None,
99
+ use_env_vars: dict[str, bool] | None = None,
100
+ ) -> dict[str, Any]:
101
+ """Update database configuration in app.yaml.
102
+
103
+ Args:
104
+ db_type: Either "sqlite" or "postgresql"
105
+ url: Direct URL (for sqlite file path)
106
+ host: PostgreSQL host
107
+ port: PostgreSQL port
108
+ database: PostgreSQL database name
109
+ username: PostgreSQL username
110
+ password: PostgreSQL password
111
+ use_env_vars: Dict mapping field names to whether they should use env vars
112
+
113
+ Returns:
114
+ Updated configuration
115
+ """
116
+ backup_config()
117
+ config = load_config()
118
+
119
+ if "db" not in config:
120
+ config["db"] = {}
121
+
122
+ use_env_vars = use_env_vars or {}
123
+
124
+ if db_type == "sqlite":
125
+ file_path = url or "./app.db"
126
+ if use_env_vars.get("url"):
127
+ config["db"]["url"] = f"${file_path}"
128
+ else:
129
+ config["db"]["url"] = f"sqlite+aiosqlite:///{file_path}"
130
+ else:
131
+ # PostgreSQL
132
+ if use_env_vars.get("url"):
133
+ # Use a single env var for the whole URL
134
+ config["db"]["url"] = f"${url}"
135
+ else:
136
+ # Build URL from components
137
+ host_str = f"${host}" if use_env_vars.get("host") else host
138
+ port_str = f"${port}" if use_env_vars.get("port") else port
139
+ db_str = f"${database}" if use_env_vars.get("database") else database
140
+ user_str = f"${username}" if use_env_vars.get("username") else username
141
+ pass_str = f"${password}" if use_env_vars.get("password") else password
142
+
143
+ # For env vars in components, we need to store them as env var references
144
+ if any(use_env_vars.values()):
145
+ # Store the URL with env var placeholders
146
+ config["db"]["url"] = f"$DATABASE_URL"
147
+ else:
148
+ config["db"]["url"] = (
149
+ f"postgresql+asyncpg://{user_str}:{pass_str}@{host_str}:{port_str}/{db_str}"
150
+ )
151
+
152
+ save_config(config)
153
+ return config
154
+
155
+
156
+ def update_auth_config(
157
+ redirect_base_url: str,
158
+ providers: dict[str, dict[str, Any]],
159
+ use_env_vars: dict[str, dict[str, bool]] | None = None,
160
+ ) -> dict[str, Any]:
161
+ """Update authentication configuration in app.yaml.
162
+
163
+ Args:
164
+ redirect_base_url: Base URL for OAuth callbacks
165
+ providers: Dict of provider configs {provider: {client_id, client_secret, ...}}
166
+ use_env_vars: Dict of {provider: {field: use_env_var}} for env var toggles
167
+
168
+ Returns:
169
+ Updated configuration
170
+ """
171
+ backup_config()
172
+ config = load_config()
173
+
174
+ if "auth" not in config:
175
+ config["auth"] = {}
176
+
177
+ config["auth"]["redirect_base_url"] = redirect_base_url
178
+
179
+ if "providers" not in config["auth"]:
180
+ config["auth"]["providers"] = {}
181
+
182
+ use_env_vars = use_env_vars or {}
183
+
184
+ for provider, provider_config in providers.items():
185
+ provider_env_vars = use_env_vars.get(provider, {})
186
+
187
+ processed_config = {}
188
+ for field, value in provider_config.items():
189
+ if provider_env_vars.get(field):
190
+ # Store as env var reference
191
+ processed_config[field] = f"${value}"
192
+ else:
193
+ processed_config[field] = value
194
+
195
+ # Add default scopes if not specified
196
+ from skrift.setup.providers import get_provider_info
197
+
198
+ provider_info = get_provider_info(provider)
199
+ if provider_info and "scopes" not in processed_config:
200
+ processed_config["scopes"] = provider_info.scopes
201
+
202
+ config["auth"]["providers"][provider] = processed_config
203
+
204
+ save_config(config)
205
+ return config
206
+
207
+
208
+ def get_configured_providers() -> list[str]:
209
+ """Get list of providers currently configured in app.yaml."""
210
+ config = load_config()
211
+ auth = config.get("auth", {})
212
+ providers = auth.get("providers", {})
213
+ return list(providers.keys())