skrift 0.1.0a16__tar.gz → 0.1.0a17__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 (91) hide show
  1. {skrift-0.1.0a16 → skrift-0.1.0a17}/PKG-INFO +1 -1
  2. {skrift-0.1.0a16 → skrift-0.1.0a17}/pyproject.toml +1 -1
  3. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/asgi.py +6 -2
  4. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/config.py +1 -0
  5. skrift-0.1.0a17/skrift/db/session.py +90 -0
  6. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/setup/config_writer.py +1 -0
  7. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/setup/state.py +4 -4
  8. {skrift-0.1.0a16 → skrift-0.1.0a17}/.gitignore +0 -0
  9. {skrift-0.1.0a16 → skrift-0.1.0a17}/README.md +0 -0
  10. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/__init__.py +0 -0
  11. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/__main__.py +0 -0
  12. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/admin/__init__.py +0 -0
  13. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/admin/controller.py +0 -0
  14. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/admin/navigation.py +0 -0
  15. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/env.py +0 -0
  16. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/script.py.mako +0 -0
  17. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
  18. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
  19. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
  20. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
  21. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
  22. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260129_add_oauth_accounts.py +0 -0
  23. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
  24. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_content_scheduling.py +0 -0
  25. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_page_ordering.py +0 -0
  26. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_page_revisions.py +0 -0
  27. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic/versions/20260202_add_seo_fields.py +0 -0
  28. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/alembic.ini +0 -0
  29. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/auth/__init__.py +0 -0
  30. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/auth/guards.py +0 -0
  31. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/auth/roles.py +0 -0
  32. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/auth/services.py +0 -0
  33. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/claude_skill/SKILL.md +0 -0
  34. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/claude_skill/__init__.py +0 -0
  35. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/claude_skill/architecture.md +0 -0
  36. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/claude_skill/patterns.md +0 -0
  37. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/cli.py +0 -0
  38. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/controllers/__init__.py +0 -0
  39. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/controllers/auth.py +0 -0
  40. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/controllers/sitemap.py +0 -0
  41. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/controllers/web.py +0 -0
  42. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/__init__.py +0 -0
  43. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/base.py +0 -0
  44. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/__init__.py +0 -0
  45. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/oauth_account.py +0 -0
  46. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/page.py +0 -0
  47. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/page_revision.py +0 -0
  48. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/role.py +0 -0
  49. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/setting.py +0 -0
  50. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/models/user.py +0 -0
  51. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/services/__init__.py +0 -0
  52. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/services/oauth_service.py +0 -0
  53. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/services/page_service.py +0 -0
  54. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/services/revision_service.py +0 -0
  55. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/db/services/setting_service.py +0 -0
  56. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/__init__.py +0 -0
  57. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/exceptions.py +0 -0
  58. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/flash.py +0 -0
  59. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/hooks.py +0 -0
  60. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/markdown.py +0 -0
  61. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/seo.py +0 -0
  62. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/lib/template.py +0 -0
  63. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/setup/__init__.py +0 -0
  64. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/setup/controller.py +0 -0
  65. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/setup/middleware.py +0 -0
  66. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/setup/providers.py +0 -0
  67. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/static/css/style.css +0 -0
  68. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/admin.html +0 -0
  69. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/base.html +0 -0
  70. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/pages/edit.html +0 -0
  71. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/pages/list.html +0 -0
  72. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/pages/revisions.html +0 -0
  73. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/settings/site.html +0 -0
  74. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/users/list.html +0 -0
  75. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/admin/users/roles.html +0 -0
  76. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/auth/dummy_login.html +0 -0
  77. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/auth/login.html +0 -0
  78. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/base.html +0 -0
  79. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/error-404.html +0 -0
  80. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/error-500.html +0 -0
  81. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/error.html +0 -0
  82. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/index.html +0 -0
  83. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/page.html +0 -0
  84. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/admin.html +0 -0
  85. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/auth.html +0 -0
  86. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/base.html +0 -0
  87. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/complete.html +0 -0
  88. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/configuring.html +0 -0
  89. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/database.html +0 -0
  90. {skrift-0.1.0a16 → skrift-0.1.0a17}/skrift/templates/setup/restart.html +0 -0
  91. {skrift-0.1.0a16 → skrift-0.1.0a17}/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.0a16
3
+ Version: 0.1.0a17
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.0a16"
3
+ version = "0.1.0a17"
4
4
  description = "A lightweight async Python CMS for crafting modern websites"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -22,6 +22,8 @@ from advanced_alchemy.extensions.litestar import (
22
22
  SQLAlchemyAsyncConfig,
23
23
  SQLAlchemyPlugin,
24
24
  )
25
+
26
+ from skrift.db.session import SafeSQLAlchemyAsyncConfig
25
27
  from litestar import Litestar
26
28
  from litestar.config.compression import CompressionConfig
27
29
  from litestar.contrib.jinja import JinjaTemplateEngine
@@ -398,10 +400,11 @@ def create_app() -> Litestar:
398
400
  pool_size=settings.db.pool_size,
399
401
  max_overflow=settings.db.pool_overflow,
400
402
  pool_timeout=settings.db.pool_timeout,
403
+ pool_pre_ping=settings.db.pool_pre_ping,
401
404
  echo=settings.db.echo,
402
405
  )
403
406
 
404
- db_config = SQLAlchemyAsyncConfig(
407
+ db_config = SafeSQLAlchemyAsyncConfig(
405
408
  connection_string=settings.db.url,
406
409
  metadata=Base.metadata,
407
410
  create_all=False,
@@ -564,10 +567,11 @@ def create_setup_app() -> Litestar:
564
567
  pool_size=5,
565
568
  max_overflow=10,
566
569
  pool_timeout=30,
570
+ pool_pre_ping=True,
567
571
  echo=False,
568
572
  )
569
573
 
570
- db_config = SQLAlchemyAsyncConfig(
574
+ db_config = SafeSQLAlchemyAsyncConfig(
571
575
  connection_string=db_url,
572
576
  metadata=Base.metadata,
573
577
  create_all=False,
@@ -110,6 +110,7 @@ class DatabaseConfig(BaseModel):
110
110
  pool_size: int = 5
111
111
  pool_overflow: int = 10
112
112
  pool_timeout: int = 30
113
+ pool_pre_ping: bool = True # Validate connections before use
113
114
  echo: bool = False
114
115
 
115
116
 
@@ -0,0 +1,90 @@
1
+ """Safe SQLAlchemy async session provider with CancelledError handling.
2
+
3
+ This module provides a custom session configuration that properly handles
4
+ connection cleanup when HTTP requests are cancelled (client disconnect, timeout).
5
+ Without this, CancelledError can prevent session cleanup, leading to connection
6
+ pool leaks.
7
+ """
8
+
9
+ import asyncio
10
+ from collections.abc import AsyncGenerator
11
+ from typing import TYPE_CHECKING, Callable, cast
12
+
13
+ from advanced_alchemy._listeners import set_async_context
14
+ from advanced_alchemy.extensions.litestar import SQLAlchemyAsyncConfig
15
+ from advanced_alchemy.extensions.litestar._utils import (
16
+ delete_aa_scope_state,
17
+ get_aa_scope_state,
18
+ set_aa_scope_state,
19
+ )
20
+ from sqlalchemy.ext.asyncio import AsyncSession
21
+
22
+ if TYPE_CHECKING:
23
+ from litestar.datastructures import State
24
+ from litestar.types import Scope
25
+
26
+
27
+ class SafeSQLAlchemyAsyncConfig(SQLAlchemyAsyncConfig):
28
+ """SQLAlchemy async config with safe session cleanup on request cancellation.
29
+
30
+ This subclass overrides `provide_session` to use an async generator that
31
+ catches CancelledError and ensures sessions are properly closed, preventing
32
+ connection pool leaks when HTTP requests are cancelled.
33
+
34
+ The standard advanced_alchemy session management relies on ASGI events
35
+ (http.response.body, http.disconnect) to trigger cleanup via before_send_handler.
36
+ However, CancelledError can prevent these events from firing, leaving sessions
37
+ in an unclosed state.
38
+
39
+ By using an async generator, Litestar's dependency injection system ensures
40
+ cleanup runs even when CancelledError is raised.
41
+ """
42
+
43
+ async def provide_session(
44
+ self,
45
+ state: "State",
46
+ scope: "Scope",
47
+ ) -> AsyncGenerator[AsyncSession, None]:
48
+ """Provide a database session with proper cleanup on cancellation.
49
+
50
+ This async generator wraps session creation to ensure that
51
+ CancelledError (raised when an HTTP request is cancelled) doesn't
52
+ prevent session cleanup, which would leak connections.
53
+
54
+ Args:
55
+ state: The application state
56
+ scope: The ASGI scope
57
+
58
+ Yields:
59
+ AsyncSession: The database session
60
+ """
61
+ # Check if we already have a session in scope
62
+ session = cast(
63
+ "AsyncSession | None",
64
+ get_aa_scope_state(scope, self.session_scope_key),
65
+ )
66
+
67
+ if session is None:
68
+ # Create a new session
69
+ session_maker = cast(
70
+ "Callable[[], AsyncSession]",
71
+ state[self.session_maker_app_state_key],
72
+ )
73
+ session = session_maker()
74
+ # Store in scope for reuse within this request
75
+ set_aa_scope_state(scope, self.session_scope_key, session)
76
+
77
+ set_async_context(True)
78
+
79
+ try:
80
+ yield session
81
+ except asyncio.CancelledError:
82
+ # Request was cancelled - ensure we clean up the session
83
+ # This prevents connection pool leaks
84
+ await session.close()
85
+ # Remove the session from scope state to prevent double-close
86
+ delete_aa_scope_state(scope, self.session_scope_key)
87
+ raise
88
+
89
+
90
+ __all__ = ["SafeSQLAlchemyAsyncConfig"]
@@ -21,6 +21,7 @@ DEFAULT_CONFIG = {
21
21
  "pool_size": 5,
22
22
  "pool_overflow": 10,
23
23
  "pool_timeout": 30,
24
+ "pool_pre_ping": True,
24
25
  "echo": False,
25
26
  },
26
27
  "auth": {
@@ -172,9 +172,9 @@ def run_migrations_if_needed() -> tuple[bool, str | None]:
172
172
  return True, None
173
173
 
174
174
  try:
175
- # Try skrift-db first
175
+ # Try skrift db first (the correct command)
176
176
  result = subprocess.run(
177
- ["skrift-db", "upgrade", "head"],
177
+ ["skrift", "db", "upgrade", "head"],
178
178
  capture_output=True,
179
179
  text=True,
180
180
  cwd=Path.cwd(),
@@ -183,7 +183,7 @@ def run_migrations_if_needed() -> tuple[bool, str | None]:
183
183
  if result.returncode == 0:
184
184
  _migrations_run = True
185
185
  return True, None
186
- # If skrift-db fails, try alembic directly
186
+ # If skrift db fails, try alembic directly
187
187
  except (subprocess.TimeoutExpired, FileNotFoundError):
188
188
  pass
189
189
 
@@ -202,7 +202,7 @@ def run_migrations_if_needed() -> tuple[bool, str | None]:
202
202
  except subprocess.TimeoutExpired:
203
203
  return False, "Migration timed out"
204
204
  except FileNotFoundError:
205
- return False, "Neither skrift-db nor alembic found"
205
+ return False, "skrift db command not found"
206
206
  except Exception as e:
207
207
  return False, str(e)
208
208
 
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