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.
- skrift/__init__.py +1 -0
- skrift/__main__.py +17 -0
- skrift/admin/__init__.py +11 -0
- skrift/admin/controller.py +452 -0
- skrift/admin/navigation.py +105 -0
- skrift/alembic/env.py +91 -0
- skrift/alembic/script.py.mako +26 -0
- skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +70 -0
- skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +57 -0
- skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +31 -0
- skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +43 -0
- skrift/alembic/versions/20260122_200000_add_settings_table.py +38 -0
- skrift/alembic.ini +77 -0
- skrift/asgi.py +545 -0
- skrift/auth/__init__.py +58 -0
- skrift/auth/guards.py +130 -0
- skrift/auth/roles.py +94 -0
- skrift/auth/services.py +184 -0
- skrift/cli.py +45 -0
- skrift/config.py +192 -0
- skrift/controllers/__init__.py +4 -0
- skrift/controllers/auth.py +371 -0
- skrift/controllers/web.py +67 -0
- skrift/db/__init__.py +3 -0
- skrift/db/base.py +7 -0
- skrift/db/models/__init__.py +6 -0
- skrift/db/models/page.py +26 -0
- skrift/db/models/role.py +56 -0
- skrift/db/models/setting.py +13 -0
- skrift/db/models/user.py +36 -0
- skrift/db/services/__init__.py +1 -0
- skrift/db/services/page_service.py +217 -0
- skrift/db/services/setting_service.py +206 -0
- skrift/lib/__init__.py +3 -0
- skrift/lib/exceptions.py +168 -0
- skrift/lib/template.py +108 -0
- skrift/setup/__init__.py +14 -0
- skrift/setup/config_writer.py +211 -0
- skrift/setup/controller.py +751 -0
- skrift/setup/middleware.py +89 -0
- skrift/setup/providers.py +163 -0
- skrift/setup/state.py +134 -0
- skrift/static/css/style.css +998 -0
- skrift/templates/admin/admin.html +19 -0
- skrift/templates/admin/base.html +24 -0
- skrift/templates/admin/pages/edit.html +32 -0
- skrift/templates/admin/pages/list.html +62 -0
- skrift/templates/admin/settings/site.html +32 -0
- skrift/templates/admin/users/list.html +58 -0
- skrift/templates/admin/users/roles.html +42 -0
- skrift/templates/auth/login.html +125 -0
- skrift/templates/base.html +52 -0
- skrift/templates/error-404.html +19 -0
- skrift/templates/error-500.html +19 -0
- skrift/templates/error.html +19 -0
- skrift/templates/index.html +9 -0
- skrift/templates/page.html +26 -0
- skrift/templates/setup/admin.html +24 -0
- skrift/templates/setup/auth.html +110 -0
- skrift/templates/setup/base.html +407 -0
- skrift/templates/setup/complete.html +17 -0
- skrift/templates/setup/database.html +125 -0
- skrift/templates/setup/restart.html +28 -0
- skrift/templates/setup/site.html +39 -0
- skrift-0.1.0a1.dist-info/METADATA +233 -0
- skrift-0.1.0a1.dist-info/RECORD +68 -0
- skrift-0.1.0a1.dist-info/WHEEL +4 -0
- 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
|