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,26 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,70 @@
1
+ """initial schema
2
+
3
+ Revision ID: 09b0364dbb7b
4
+ Revises:
5
+ Create Date: 2026-01-20 21:01:54.470260
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ from advanced_alchemy.types import GUID, DateTimeUTC
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = '09b0364dbb7b'
17
+ down_revision: Union[str, None] = None
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # Create users table
24
+ op.create_table(
25
+ 'users',
26
+ sa.Column('id', GUID(length=16), nullable=False),
27
+ sa.Column('created_at', DateTimeUTC(timezone=True), nullable=False),
28
+ sa.Column('updated_at', DateTimeUTC(timezone=True), nullable=False),
29
+ sa.Column('oauth_provider', sa.String(length=50), nullable=False),
30
+ sa.Column('oauth_id', sa.String(length=255), nullable=False),
31
+ sa.Column('email', sa.String(length=255), nullable=False),
32
+ sa.Column('name', sa.String(length=255), nullable=True),
33
+ sa.Column('picture_url', sa.String(length=512), nullable=True),
34
+ sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
35
+ sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True),
36
+ sa.PrimaryKeyConstraint('id'),
37
+ sa.UniqueConstraint('oauth_id'),
38
+ sa.UniqueConstraint('email'),
39
+ )
40
+
41
+ # Create pages table
42
+ op.create_table(
43
+ 'pages',
44
+ sa.Column('id', GUID(length=16), nullable=False),
45
+ sa.Column('created_at', DateTimeUTC(timezone=True), nullable=False),
46
+ sa.Column('updated_at', DateTimeUTC(timezone=True), nullable=False),
47
+ sa.Column('user_id', GUID(length=16), nullable=True),
48
+ sa.Column('type', sa.String(length=50), nullable=False, default='page'),
49
+ sa.Column('slug', sa.String(length=255), nullable=False),
50
+ sa.Column('title', sa.String(length=500), nullable=False),
51
+ sa.Column('content', sa.Text(), nullable=False, default=''),
52
+ sa.Column('is_published', sa.Boolean(), nullable=False, default=False),
53
+ sa.Column('published_at', sa.DateTime(timezone=True), nullable=True),
54
+ sa.PrimaryKeyConstraint('id'),
55
+ sa.ForeignKeyConstraint(['user_id'], ['users.id']),
56
+ sa.UniqueConstraint('slug'),
57
+ )
58
+
59
+ # Create indexes
60
+ op.create_index('ix_pages_slug', 'pages', ['slug'])
61
+ op.create_index('ix_pages_user_id', 'pages', ['user_id'])
62
+ op.create_index('ix_pages_type_published', 'pages', ['type', 'is_published'])
63
+
64
+
65
+ def downgrade() -> None:
66
+ op.drop_index('ix_pages_type_published', 'pages')
67
+ op.drop_index('ix_pages_user_id', 'pages')
68
+ op.drop_index('ix_pages_slug', 'pages')
69
+ op.drop_table('pages')
70
+ op.drop_table('users')
@@ -0,0 +1,57 @@
1
+ """add_roles_and_permissions
2
+
3
+ Revision ID: 0b7c927d2591
4
+ Revises: 09b0364dbb7b
5
+ Create Date: 2026-01-22 15:27:44.922770
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import advanced_alchemy.types
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = '0b7c927d2591'
17
+ down_revision: Union[str, None] = '09b0364dbb7b'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ op.create_table('roles',
24
+ sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
25
+ sa.Column('name', sa.String(length=50), nullable=False),
26
+ sa.Column('display_name', sa.String(length=100), nullable=True),
27
+ sa.Column('description', sa.String(length=500), nullable=True),
28
+ sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
29
+ sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
30
+ sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
31
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_roles')),
32
+ sa.UniqueConstraint('name', name=op.f('uq_roles_name'))
33
+ )
34
+ op.create_table('role_permissions',
35
+ sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
36
+ sa.Column('role_id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
37
+ sa.Column('permission', sa.String(length=100), nullable=False),
38
+ sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
39
+ sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
40
+ sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
41
+ sa.ForeignKeyConstraint(['role_id'], ['roles.id'], name=op.f('fk_role_permissions_role_id_roles'), ondelete='CASCADE'),
42
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_role_permissions')),
43
+ sa.UniqueConstraint('role_id', 'permission', name='uq_role_permission')
44
+ )
45
+ op.create_table('user_roles',
46
+ sa.Column('user_id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
47
+ sa.Column('role_id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
48
+ sa.ForeignKeyConstraint(['role_id'], ['roles.id'], name=op.f('fk_user_roles_role_id_roles'), ondelete='CASCADE'),
49
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_roles_user_id_users'), ondelete='CASCADE'),
50
+ sa.PrimaryKeyConstraint('user_id', 'role_id', name=op.f('pk_user_roles'))
51
+ )
52
+
53
+
54
+ def downgrade() -> None:
55
+ op.drop_table('user_roles')
56
+ op.drop_table('role_permissions')
57
+ op.drop_table('roles')
@@ -0,0 +1,31 @@
1
+ """add sa_orm_sentinel column
2
+
3
+ Revision ID: cdf734a5b847
4
+ Revises: 0b7c927d2591
5
+ Create Date: 2026-01-22 17:28:36.586660
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'cdf734a5b847'
16
+ down_revision: Union[str, None] = '0b7c927d2591'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # Add sa_orm_sentinel column to users table
23
+ op.add_column('users', sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True))
24
+
25
+ # Add sa_orm_sentinel column to pages table
26
+ op.add_column('pages', sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True))
27
+
28
+
29
+ def downgrade() -> None:
30
+ op.drop_column('pages', 'sa_orm_sentinel')
31
+ op.drop_column('users', 'sa_orm_sentinel')
@@ -0,0 +1,43 @@
1
+ """remove page type column
2
+
3
+ Revision ID: a9c55348eae7
4
+ Revises: cdf734a5b847
5
+ Create Date: 2026-01-22 17:56:37.000000
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'a9c55348eae7'
16
+ down_revision: Union[str, None] = 'cdf734a5b847'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # Delete any posts (convert to pages not needed since we're removing posts entirely)
23
+ op.execute("DELETE FROM pages WHERE type = 'post'")
24
+
25
+ # Drop the composite index on type and is_published
26
+ op.drop_index('ix_pages_type_published', table_name='pages')
27
+
28
+ # Drop the type column
29
+ op.drop_column('pages', 'type')
30
+
31
+
32
+ def downgrade() -> None:
33
+ # Add the type column back
34
+ op.add_column('pages', sa.Column('type', sa.String(50), nullable=True))
35
+
36
+ # Set all existing pages to type 'page'
37
+ op.execute("UPDATE pages SET type = 'page'")
38
+
39
+ # Make the column non-nullable
40
+ op.alter_column('pages', 'type', nullable=False)
41
+
42
+ # Recreate the composite index
43
+ op.create_index('ix_pages_type_published', 'pages', ['type', 'is_published'])
@@ -0,0 +1,38 @@
1
+ """add settings table
2
+
3
+ Revision ID: 8f3a5c2d1e0b
4
+ Revises: a9c55348eae7
5
+ Create Date: 2026-01-22 20:00:00.000000
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import advanced_alchemy.types
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = '8f3a5c2d1e0b'
17
+ down_revision: Union[str, None] = 'a9c55348eae7'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ op.create_table('settings',
24
+ sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
25
+ sa.Column('key', sa.String(length=255), nullable=False),
26
+ sa.Column('value', sa.Text(), nullable=True),
27
+ sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
28
+ sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
29
+ sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
30
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_settings')),
31
+ sa.UniqueConstraint('key', name=op.f('uq_settings_key'))
32
+ )
33
+ op.create_index(op.f('ix_settings_key'), 'settings', ['key'], unique=True)
34
+
35
+
36
+ def downgrade() -> None:
37
+ op.drop_index(op.f('ix_settings_key'), table_name='settings')
38
+ op.drop_table('settings')
skrift/alembic.ini ADDED
@@ -0,0 +1,77 @@
1
+ # Alembic Configuration File
2
+
3
+ [alembic]
4
+ # Path to migration scripts
5
+ script_location = alembic
6
+
7
+ # Template used to generate migration files
8
+ file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(rev)s_%%(slug)s
9
+
10
+ # Prepend sys.path with this value before running migrations
11
+ prepend_sys_path = .
12
+
13
+ # Timezone for timestamps in migration files (uses system timezone if not set)
14
+ # timezone =
15
+
16
+ # Max length of characters to apply to the "slug" field
17
+ truncate_slug_length = 40
18
+
19
+ # Set to 'true' to run the environment during revision generation (for autogenerate support)
20
+ revision_environment = false
21
+
22
+ # Set to 'true' to allow running in "offline" mode
23
+ # sourceless = false
24
+
25
+ # Version path separator (os, space, :, ;)
26
+ # version_path_separator = os
27
+
28
+ # Output encoding used when revision files are written from script.py.mako
29
+ # output_encoding = utf-8
30
+
31
+ # Database URL - override with environment variable DATABASE_URL
32
+ # The env.py file reads this from the Settings class, so this is just a placeholder
33
+ sqlalchemy.url = sqlite+aiosqlite:///./app.db
34
+
35
+
36
+ [post_write_hooks]
37
+ # Format newly generated revision files using ruff
38
+ # hooks = ruff_format
39
+ # ruff_format.type = exec
40
+ # ruff_format.executable = ruff
41
+ # ruff_format.options = format REVISION_SCRIPT_FILENAME
42
+
43
+
44
+ # Logging configuration
45
+ [loggers]
46
+ keys = root,sqlalchemy,alembic
47
+
48
+ [handlers]
49
+ keys = console
50
+
51
+ [formatters]
52
+ keys = generic
53
+
54
+ [logger_root]
55
+ level = WARN
56
+ handlers = console
57
+ qualname =
58
+
59
+ [logger_sqlalchemy]
60
+ level = WARN
61
+ handlers =
62
+ qualname = sqlalchemy.engine
63
+
64
+ [logger_alembic]
65
+ level = INFO
66
+ handlers =
67
+ qualname = alembic
68
+
69
+ [handler_console]
70
+ class = StreamHandler
71
+ args = (sys.stderr,)
72
+ level = NOTSET
73
+ formatter = generic
74
+
75
+ [formatter_generic]
76
+ format = %(levelname)-5.5s [%(name)s] %(message)s
77
+ datefmt = %H:%M:%S