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.
Files changed (74) hide show
  1. {skrift-0.1.0a8 → skrift-0.1.0a10}/PKG-INFO +1 -1
  2. {skrift-0.1.0a8 → skrift-0.1.0a10}/pyproject.toml +5 -1
  3. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260129_add_oauth_accounts.py +9 -2
  4. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/asgi.py +108 -1
  5. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/roles.py +36 -1
  6. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/static/css/style.css +5 -0
  7. {skrift-0.1.0a8 → skrift-0.1.0a10}/.gitignore +0 -0
  8. {skrift-0.1.0a8 → skrift-0.1.0a10}/README.md +0 -0
  9. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/__init__.py +0 -0
  10. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/__main__.py +0 -0
  11. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/admin/__init__.py +0 -0
  12. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/admin/controller.py +0 -0
  13. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/admin/navigation.py +0 -0
  14. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/env.py +0 -0
  15. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/script.py.mako +0 -0
  16. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
  17. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
  18. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
  19. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
  20. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
  21. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
  22. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/alembic.ini +0 -0
  23. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/__init__.py +0 -0
  24. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/guards.py +0 -0
  25. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/auth/services.py +0 -0
  26. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/cli.py +0 -0
  27. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/config.py +0 -0
  28. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/controllers/__init__.py +0 -0
  29. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/controllers/auth.py +0 -0
  30. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/controllers/web.py +0 -0
  31. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/__init__.py +0 -0
  32. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/base.py +0 -0
  33. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/__init__.py +0 -0
  34. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/oauth_account.py +0 -0
  35. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/page.py +0 -0
  36. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/role.py +0 -0
  37. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/setting.py +0 -0
  38. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/models/user.py +0 -0
  39. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/__init__.py +0 -0
  40. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/oauth_service.py +0 -0
  41. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/page_service.py +0 -0
  42. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/db/services/setting_service.py +0 -0
  43. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/lib/__init__.py +0 -0
  44. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/lib/exceptions.py +0 -0
  45. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/lib/template.py +0 -0
  46. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/__init__.py +0 -0
  47. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/config_writer.py +0 -0
  48. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/controller.py +0 -0
  49. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/middleware.py +0 -0
  50. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/providers.py +0 -0
  51. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/setup/state.py +0 -0
  52. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/admin.html +0 -0
  53. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/base.html +0 -0
  54. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/pages/edit.html +0 -0
  55. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/pages/list.html +0 -0
  56. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/settings/site.html +0 -0
  57. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/users/list.html +0 -0
  58. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/admin/users/roles.html +0 -0
  59. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/auth/dummy_login.html +0 -0
  60. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/auth/login.html +0 -0
  61. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/base.html +0 -0
  62. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/error-404.html +0 -0
  63. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/error-500.html +0 -0
  64. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/error.html +0 -0
  65. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/index.html +0 -0
  66. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/page.html +0 -0
  67. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/admin.html +0 -0
  68. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/auth.html +0 -0
  69. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/base.html +0 -0
  70. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/complete.html +0 -0
  71. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/configuring.html +0 -0
  72. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/database.html +0 -0
  73. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/restart.html +0 -0
  74. {skrift-0.1.0a8 → skrift-0.1.0a10}/skrift/templates/setup/site.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skrift
3
- Version: 0.1.0a8
3
+ Version: 0.1.0a10
4
4
  Summary: A lightweight async Python CMS for crafting modern websites
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: advanced-alchemy>=0.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "skrift"
3
- version = "0.1.0a8"
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.execute(sa.text("""
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
- randomblob(16),
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(role: RoleDefinition) -> None:
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