skrift 0.1.0a8__tar.gz → 0.1.0a10__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.
- {skrift-0.1.0a8 → skrift-0.1.0a10}/PKG-INFO +1 -1
- {skrift-0.1.0a8 → skrift-0.1.0a10}/pyproject.toml +5 -1
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260129_add_oauth_accounts.py +9 -2
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/asgi.py +108 -1
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/roles.py +36 -1
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/static/css/style.css +5 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/.gitignore +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/README.md +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/__main__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/admin/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/admin/controller.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/admin/navigation.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/env.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/script.py.mako +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic.ini +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/guards.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/services.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/cli.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/config.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/controllers/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/controllers/auth.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/controllers/web.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/base.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/oauth_account.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/page.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/role.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/setting.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/user.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/oauth_service.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/page_service.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/setting_service.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/lib/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/lib/exceptions.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/lib/template.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/__init__.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/config_writer.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/controller.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/middleware.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/providers.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/state.py +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/admin.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/base.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/pages/edit.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/pages/list.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/settings/site.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/users/list.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/users/roles.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/auth/dummy_login.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/auth/login.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/base.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/error-404.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/error-500.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/error.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/index.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/page.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/admin.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/auth.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/base.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/complete.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/configuring.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/database.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/restart.html +0 -0
- {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/site.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "skrift"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.0a10"
|
|
4
4
|
description = "A lightweight async Python CMS for crafting modern websites"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -30,6 +30,10 @@ requires = ["hatchling"]
|
|
|
30
30
|
build-backend = "hatchling.build"
|
|
31
31
|
|
|
32
32
|
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0.0",
|
|
35
|
+
"pytest-asyncio>=0.24.0",
|
|
36
|
+
]
|
|
33
37
|
docs = [
|
|
34
38
|
"zensical>=0.0.19",
|
|
35
39
|
]
|
|
@@ -63,10 +63,17 @@ def upgrade() -> None:
|
|
|
63
63
|
# Step 2: Migrate existing data from users to oauth_accounts
|
|
64
64
|
# Generate binary UUIDs (16 bytes) for new records and copy oauth data
|
|
65
65
|
conn = op.get_bind()
|
|
66
|
-
conn.
|
|
66
|
+
dialect = conn.dialect.name
|
|
67
|
+
|
|
68
|
+
if dialect == 'sqlite':
|
|
69
|
+
uuid_func = 'randomblob(16)'
|
|
70
|
+
else: # PostgreSQL and others
|
|
71
|
+
uuid_func = 'gen_random_uuid()'
|
|
72
|
+
|
|
73
|
+
conn.execute(sa.text(f"""
|
|
67
74
|
INSERT INTO oauth_accounts (id, created_at, updated_at, provider, provider_account_id, provider_email, user_id)
|
|
68
75
|
SELECT
|
|
69
|
-
|
|
76
|
+
{uuid_func},
|
|
70
77
|
created_at,
|
|
71
78
|
updated_at,
|
|
72
79
|
oauth_provider,
|
|
@@ -26,6 +26,7 @@ from litestar import Litestar
|
|
|
26
26
|
from litestar.config.compression import CompressionConfig
|
|
27
27
|
from litestar.contrib.jinja import JinjaTemplateEngine
|
|
28
28
|
from litestar.exceptions import HTTPException
|
|
29
|
+
from litestar.middleware import DefineMiddleware
|
|
29
30
|
from litestar.middleware.session.client_side import CookieBackendConfig
|
|
30
31
|
from litestar.static_files import create_static_files_router
|
|
31
32
|
from litestar.template import TemplateConfig
|
|
@@ -73,6 +74,109 @@ def load_controllers() -> list:
|
|
|
73
74
|
return controllers
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
def _load_middleware_factory(spec: str):
|
|
78
|
+
"""Import a single middleware factory from a module:name spec.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
spec: String in format "module.path:factory_name"
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The callable middleware factory
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If spec doesn't contain exactly one colon
|
|
88
|
+
ImportError: If the module cannot be imported
|
|
89
|
+
AttributeError: If the factory doesn't exist in the module
|
|
90
|
+
TypeError: If the factory is not callable
|
|
91
|
+
"""
|
|
92
|
+
if ":" not in spec:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Invalid middleware spec '{spec}': must be in format 'module:factory'"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
parts = spec.split(":")
|
|
98
|
+
if len(parts) != 2:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Invalid middleware spec '{spec}': must contain exactly one colon"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
module_path, factory_name = parts
|
|
104
|
+
module = importlib.import_module(module_path)
|
|
105
|
+
factory = getattr(module, factory_name)
|
|
106
|
+
|
|
107
|
+
if not callable(factory):
|
|
108
|
+
raise TypeError(
|
|
109
|
+
f"Middleware factory '{spec}' is not callable"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return factory
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_middleware() -> list:
|
|
116
|
+
"""Load middleware from app.yaml configuration.
|
|
117
|
+
|
|
118
|
+
Supports two formats in app.yaml:
|
|
119
|
+
|
|
120
|
+
Simple (no args):
|
|
121
|
+
middleware:
|
|
122
|
+
- myapp.middleware:create_logging_middleware
|
|
123
|
+
|
|
124
|
+
With kwargs:
|
|
125
|
+
middleware:
|
|
126
|
+
- factory: myapp.middleware:create_rate_limit_middleware
|
|
127
|
+
kwargs:
|
|
128
|
+
requests_per_minute: 100
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of middleware factories or DefineMiddleware instances
|
|
132
|
+
"""
|
|
133
|
+
config_path = get_config_path()
|
|
134
|
+
|
|
135
|
+
if not config_path.exists():
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
with open(config_path, "r") as f:
|
|
139
|
+
config = yaml.safe_load(f)
|
|
140
|
+
|
|
141
|
+
if not config:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
middleware_specs = config.get("middleware", [])
|
|
145
|
+
if not middleware_specs:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
# Add working directory to sys.path for local middleware imports
|
|
149
|
+
cwd = os.getcwd()
|
|
150
|
+
if cwd not in sys.path:
|
|
151
|
+
sys.path.insert(0, cwd)
|
|
152
|
+
|
|
153
|
+
middleware = []
|
|
154
|
+
for spec in middleware_specs:
|
|
155
|
+
if isinstance(spec, str):
|
|
156
|
+
# Simple format: "module:factory"
|
|
157
|
+
factory = _load_middleware_factory(spec)
|
|
158
|
+
middleware.append(factory)
|
|
159
|
+
elif isinstance(spec, dict):
|
|
160
|
+
# Dict format with optional kwargs
|
|
161
|
+
if "factory" not in spec:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Middleware dict spec must have 'factory' key: {spec}"
|
|
164
|
+
)
|
|
165
|
+
factory = _load_middleware_factory(spec["factory"])
|
|
166
|
+
kwargs = spec.get("kwargs", {})
|
|
167
|
+
if kwargs:
|
|
168
|
+
middleware.append(DefineMiddleware(factory, **kwargs))
|
|
169
|
+
else:
|
|
170
|
+
middleware.append(factory)
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Invalid middleware spec type: {type(spec).__name__}. "
|
|
174
|
+
"Must be string or dict."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return middleware
|
|
178
|
+
|
|
179
|
+
|
|
76
180
|
async def check_setup_complete(db_config: SQLAlchemyAsyncConfig) -> bool:
|
|
77
181
|
"""Check if setup has been completed."""
|
|
78
182
|
try:
|
|
@@ -282,6 +386,9 @@ def create_app() -> Litestar:
|
|
|
282
386
|
# Load controllers from app.yaml
|
|
283
387
|
controllers = load_controllers()
|
|
284
388
|
|
|
389
|
+
# Load middleware from app.yaml
|
|
390
|
+
user_middleware = load_middleware()
|
|
391
|
+
|
|
285
392
|
# Database configuration
|
|
286
393
|
if "sqlite" in settings.db.url:
|
|
287
394
|
engine_config = EngineConfig(echo=settings.db.echo)
|
|
@@ -350,7 +457,7 @@ def create_app() -> Litestar:
|
|
|
350
457
|
on_startup=[on_startup],
|
|
351
458
|
route_handlers=[*controllers, static_files_router],
|
|
352
459
|
plugins=[SQLAlchemyPlugin(config=db_config)],
|
|
353
|
-
middleware=[session_config.middleware],
|
|
460
|
+
middleware=[session_config.middleware, *user_middleware],
|
|
354
461
|
template_config=template_config,
|
|
355
462
|
compression_config=CompressionConfig(backend="gzip"),
|
|
356
463
|
exception_handlers={
|
|
@@ -86,9 +86,44 @@ def get_role_definition(name: str) -> RoleDefinition | None:
|
|
|
86
86
|
return ROLE_DEFINITIONS.get(name)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def register_role(
|
|
89
|
+
def register_role(
|
|
90
|
+
name: str,
|
|
91
|
+
*permissions: str,
|
|
92
|
+
display_name: str | None = None,
|
|
93
|
+
description: str | None = None,
|
|
94
|
+
) -> RoleDefinition:
|
|
90
95
|
"""Register a custom role definition.
|
|
91
96
|
|
|
92
97
|
This allows applications to add custom roles beyond the defaults.
|
|
98
|
+
Call this during application startup (e.g., in a custom controller module
|
|
99
|
+
or app initialization) before the database sync occurs.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
name: The unique identifier for the role
|
|
103
|
+
*permissions: Permission strings granted by this role
|
|
104
|
+
display_name: Human-readable name for the role
|
|
105
|
+
description: Description of the role's purpose
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The registered RoleDefinition instance
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
from skrift.auth.roles import register_role
|
|
112
|
+
|
|
113
|
+
# Register a custom role with permissions
|
|
114
|
+
register_role(
|
|
115
|
+
"support",
|
|
116
|
+
"view-tickets",
|
|
117
|
+
"respond-tickets",
|
|
118
|
+
display_name="Support Agent",
|
|
119
|
+
description="Can view and respond to support tickets",
|
|
120
|
+
)
|
|
93
121
|
"""
|
|
122
|
+
role = create_role(
|
|
123
|
+
name,
|
|
124
|
+
*permissions,
|
|
125
|
+
display_name=display_name,
|
|
126
|
+
description=description,
|
|
127
|
+
)
|
|
94
128
|
ROLE_DEFINITIONS[role.name] = role
|
|
129
|
+
return role
|
|
@@ -791,6 +791,11 @@ main.container.admin-layout {
|
|
|
791
791
|
color: var(--color-primary-text);
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
+
.admin-nav a.active:hover {
|
|
795
|
+
background-color: var(--color-primary-hover);
|
|
796
|
+
color: var(--color-primary-text);
|
|
797
|
+
}
|
|
798
|
+
|
|
794
799
|
.admin-nav a::after {
|
|
795
800
|
display: none;
|
|
796
801
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_200000_add_settings_table.py
RENAMED
|
File without changes
|
{skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260129_add_provider_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|