build: add ruff and format code

This commit is contained in:
sam 2024-03-23 01:39:08 +01:00
parent afadffbaac
commit 9d24f79436
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
20 changed files with 122 additions and 60 deletions

View file

@ -1,10 +1,8 @@
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config, pool
from sqlalchemy import pool
from alembic import context from alembic import context
from foxnouns.db import Base from foxnouns.db import Base
from foxnouns.db.sync import SYNC_DATABASE_URL from foxnouns.db.sync import SYNC_DATABASE_URL
@ -70,9 +68,7 @@ def run_migrations_online() -> None:
) )
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure( context.configure(connection=connection, target_metadata=target_metadata)
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()

View file

@ -5,11 +5,12 @@ Revises:
Create Date: 2024-03-09 16:32:28.590145 Create Date: 2024-03-09 16:32:28.590145
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "b39613fd7327" revision: str = "b39613fd7327"

View file

@ -5,12 +5,14 @@ Revises: b39613fd7327
Create Date: 2024-03-13 17:01:50.434602 Create Date: 2024-03-13 17:01:50.434602
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "0b63f7c8ab96" revision: str = "0b63f7c8ab96"
down_revision: Union[str, None] = "b39613fd7327" down_revision: Union[str, None] = "b39613fd7327"

View file

@ -5,12 +5,14 @@ Revises: 0b63f7c8ab96
Create Date: 2024-03-20 15:36:08.756635 Create Date: 2024-03-20 15:36:08.756635
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "1d8f8443a7f5" revision: str = "1d8f8443a7f5"
down_revision: Union[str, None] = "0b63f7c8ab96" down_revision: Union[str, None] = "0b63f7c8ab96"

View file

@ -5,37 +5,43 @@ Revises: 1d8f8443a7f5
Create Date: 2024-03-20 16:00:59.251354 Create Date: 2024-03-20 16:00:59.251354
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '17cc8cb77be5' revision: str = "17cc8cb77be5"
down_revision: Union[str, None] = '1d8f8443a7f5' down_revision: Union[str, None] = "1d8f8443a7f5"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('members', op.create_table(
sa.Column('id', sa.BigInteger(), nullable=False), "members",
sa.Column('name', sa.Text(), nullable=False), sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column('display_name', sa.Text(), nullable=True), sa.Column("name", sa.Text(), nullable=False),
sa.Column('bio', sa.Text(), nullable=True), sa.Column("display_name", sa.Text(), nullable=True),
sa.Column('names', postgresql.JSONB(astext_type=sa.Text()), nullable=False), sa.Column("bio", sa.Text(), nullable=True),
sa.Column('pronouns', postgresql.JSONB(astext_type=sa.Text()), nullable=False), sa.Column("names", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('fields', postgresql.JSONB(astext_type=sa.Text()), nullable=False), sa.Column("pronouns", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('user_id', sa.BigInteger(), nullable=False), sa.Column("fields", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('members') op.drop_table("members")
# ### end Alembic commands ### # ### end Alembic commands ###

View file

@ -5,11 +5,12 @@ Revises: 17cc8cb77be5
Create Date: 2024-03-21 15:52:09.403257 Create Date: 2024-03-21 15:52:09.403257
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "a000d800f45f" revision: str = "a000d800f45f"

View file

@ -1,7 +1,7 @@
from quart import Quart, request, g from quart import Quart, g, request
from quart_schema import QuartSchema, RequestSchemaValidationError from quart_schema import QuartSchema, RequestSchemaValidationError
from .blueprints import users_blueprint, members_blueprint from .blueprints import members_blueprint, users_blueprint
from .db.aio import async_session from .db.aio import async_session
from .db.util import validate_token from .db.util import validate_token
from .exceptions import ExpectedError from .exceptions import ExpectedError

View file

@ -1,14 +1,15 @@
from functools import wraps from functools import wraps
from quart import g from quart import g
from foxnouns.exceptions import ForbiddenError, ErrorCode from foxnouns.exceptions import ErrorCode, ForbiddenError
def require_auth(*, scope: str | None = None): def require_auth(*, scope: str | None = None):
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
if not ("user" in g) or not ("token" in g): if "user" not in g or "token" not in g:
raise ForbiddenError("Not authenticated", type=ErrorCode.Forbidden) raise ForbiddenError("Not authenticated", type=ErrorCode.Forbidden)
if scope and not g.token.has_scope(scope): if scope and not g.token.has_scope(scope):

View file

@ -1,2 +1,4 @@
from .v2.users import bp as users_blueprint
from .v2.members import bp as members_blueprint from .v2.members import bp as members_blueprint
from .v2.users import bp as users_blueprint
__all__ = [users_blueprint, members_blueprint]

View file

@ -1,13 +1,12 @@
from pydantic import Field from pydantic import Field
from quart import Blueprint, g from quart import Blueprint, g
from quart_schema import validate_response, validate_request from quart_schema import validate_request, validate_response
from sqlalchemy import select
from foxnouns.auth import require_auth from foxnouns.auth import require_auth
from foxnouns.db import Member from foxnouns.db import Member
from foxnouns.db.aio import async_session from foxnouns.db.aio import async_session
from foxnouns.db.util import user_from_ref, is_self from foxnouns.db.util import user_from_ref
from foxnouns.exceptions import NotFoundError, ErrorCode from foxnouns.exceptions import ErrorCode, NotFoundError
from foxnouns.models.member import FullMemberModel, MemberPatchModel from foxnouns.models.member import FullMemberModel, MemberPatchModel
from foxnouns.settings import BASE_DOMAIN from foxnouns.settings import BASE_DOMAIN
@ -50,6 +49,9 @@ async def create_member(data: MemberCreateModel):
session.add(member) session.add(member)
await session.commit() await session.commit()
# This has to be fetched before we can pass the model to Pydantic.
# In a normal SELECT this is automatically fetched, but because we just created the object,
# we have to do it manually.
await member.awaitable_attrs.user await member.awaitable_attrs.user
return FullMemberModel.model_validate(member) return FullMemberModel.model_validate(member)

View file

@ -1,16 +1,16 @@
from pydantic import Field, field_validator from pydantic import Field, field_validator
from quart import Blueprint, g from quart import Blueprint, g
from quart_schema import validate_response, validate_request from quart_schema import validate_request, validate_response
from sqlalchemy import select from sqlalchemy import select
from foxnouns.auth import require_auth from foxnouns.auth import require_auth
from foxnouns.db import User from foxnouns.db import User
from foxnouns.db.aio import async_session from foxnouns.db.aio import async_session
from foxnouns.db.snowflake import Snowflake from foxnouns.db.snowflake import Snowflake
from foxnouns.db.util import user_from_ref, is_self, create_token, generate_token from foxnouns.db.util import create_token, generate_token, is_self, user_from_ref
from foxnouns.exceptions import NotFoundError, ErrorCode from foxnouns.exceptions import ErrorCode, NotFoundError
from foxnouns.models import BasePatchModel from foxnouns.models import BasePatchModel
from foxnouns.models.user import UserModel, SelfUserModel, check_username from foxnouns.models.user import SelfUserModel, UserModel, check_username
from foxnouns.settings import BASE_DOMAIN from foxnouns.settings import BASE_DOMAIN
bp = Blueprint("users_v2", __name__) bp = Blueprint("users_v2", __name__)

View file

@ -1,3 +1,5 @@
from .base import Base from .base import Base
from .user import User, Token, AuthMethod, FediverseApp
from .member import Member from .member import Member
from .user import AuthMethod, FediverseApp, Token, User
__all__ = [Base, User, Token, AuthMethod, FediverseApp, Member]

View file

@ -1,5 +1,5 @@
from sqlalchemy import URL from sqlalchemy import URL
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from foxnouns.settings import DATABASE, ECHO_SQL from foxnouns.settings import DATABASE, ECHO_SQL

View file

@ -1,6 +1,6 @@
from typing import Any from typing import Any
from sqlalchemy import Text, BigInteger, ForeignKey, Index, func, text from sqlalchemy import BigInteger, ForeignKey, Index, Text, func, text
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship

View file

@ -1,8 +1,8 @@
from datetime import datetime
import enum import enum
from datetime import datetime
from typing import Any from typing import Any
from sqlalchemy import Text, Integer, BigInteger, ForeignKey, DateTime from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -38,7 +38,9 @@ class User(Base):
return f"User(id={self.id!r}, username={self.username!r})" return f"User(id={self.id!r}, username={self.username!r})"
from .member import Member # Import Member here--it's needed for the back reference in User, but Member references User, so we can only import it
# after User is initialized.
from .member import Member # noqa: E402
class Token(Base): class Token(Base):

View file

@ -2,16 +2,17 @@ import datetime
from itsdangerous import BadSignature from itsdangerous import BadSignature
from itsdangerous.url_safe import URLSafeTimedSerializer from itsdangerous.url_safe import URLSafeTimedSerializer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, insert
from sqlalchemy.orm import selectinload
from quart import g from quart import g
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from .user import User, Token from foxnouns.exceptions import ErrorCode, ForbiddenError
from .member import Member
from foxnouns.exceptions import ForbiddenError, ErrorCode
from foxnouns.settings import SECRET_KEY from foxnouns.settings import SECRET_KEY
from .member import Member
from .user import Token, User
async def user_from_ref(session: AsyncSession, user_ref: str): async def user_from_ref(session: AsyncSession, user_ref: str):
"""Returns a user from a `user_ref` value. If `user_ref` is `@me`, returns the current user. """Returns a user from a `user_ref` value. If `user_ref` is `@me`, returns the current user.
@ -25,7 +26,7 @@ async def user_from_ref(session: AsyncSession, user_ref: str):
query = query.where(User.id == g.user.id) query = query.where(User.id == g.user.id)
else: else:
raise ForbiddenError( raise ForbiddenError(
f"Missing scope 'user.read'", type=ErrorCode.MissingScope "Missing scope 'user.read'", type=ErrorCode.MissingScope
) )
else: else:
raise ForbiddenError("Not authenticated") raise ForbiddenError("Not authenticated")
@ -63,7 +64,7 @@ async def create_token(session: AsyncSession, user: User, scopes: list[str] = ["
return await session.scalar(query) return await session.scalar(query)
async def validate_token(session: AsyncSession, header: str) -> (Token, User): async def validate_token(session: AsyncSession, header: str) -> tuple[Token, User]:
try: try:
token_id = serializer.loads(header) token_id = serializer.loads(header)
except BadSignature: except BadSignature:

View file

@ -1,7 +1,7 @@
from pydantic import Field from pydantic import Field
from . import BaseSnowflakeModel from . import BaseSnowflakeModel
from .fields import ProfileField, FieldEntry, PronounEntry from .fields import FieldEntry, ProfileField, PronounEntry
class BaseUserModel(BaseSnowflakeModel): class BaseUserModel(BaseSnowflakeModel):

29
poetry.lock generated
View file

@ -1010,6 +1010,33 @@ async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\
hiredis = ["hiredis (>=1.0.0)"] hiredis = ["hiredis (>=1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
name = "ruff"
version = "0.3.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -1213,4 +1240,4 @@ h11 = ">=0.9.0,<1"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "c4719965b47a0a85c480b6a26e0c7fd84d61e281a4bb012049fac9c61cd813df" content-hash = "a04cf866b1b5efd1f9484114b0e5924947a4044c7b7e70127f042778246c0ce6"

View file

@ -23,6 +23,12 @@ asyncpg = "^0.29.0"
environs = "^11.0.0" environs = "^11.0.0"
alembic = "^1.13.1" alembic = "^1.13.1"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
ruff = "^0.3.4"
[tool.poetry.group.test] [tool.poetry.group.test]
optional = true optional = true
@ -30,11 +36,23 @@ optional = true
pytest = "^8.0.2" pytest = "^8.0.2"
pytest-asyncio = "^0.23.5.post1" pytest-asyncio = "^0.23.5.post1"
[tool.poe.tasks.dev]
help = "Run a development server with auto-reload"
cmd = "env QUART_APP=foxnouns.app:app quart --debug run --reload"
[tool.poe.tasks.server]
help = "Run a production server"
cmd = "uvicorn 'foxnouns.app:app'"
[tool.poe.tasks.migrate]
help = "Migrate the database to the latest revision"
cmd = "alembic upgrade head"
[tool.poe.tasks] [tool.poe.tasks]
dev = "env QUART_APP=foxnouns.app:app quart --debug run --reload"
server = "uvicorn 'foxnouns.app:app'"
migrate = "alembic upgrade head"
test = "pytest" test = "pytest"
lint = "ruff check"
format = "ruff format"
"sort-imports" = "ruff check --select I --fix "
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = ["--import-mode=importlib"] addopts = ["--import-mode=importlib"]

View file

@ -1,11 +1,10 @@
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from sqlalchemy import text, delete from sqlalchemy import delete, text
from foxnouns.db import Base from foxnouns.db import Base
from foxnouns.settings import DATABASE from foxnouns.settings import DATABASE
# Override the database name to the testing database # Override the database name to the testing database
DATABASE["NAME"] = f"{DATABASE['NAME']}_test" DATABASE["NAME"] = f"{DATABASE['NAME']}_test"
@ -25,8 +24,8 @@ def pytest_collection_modifyitems(items):
def setup(): def setup():
"""Migrate the testing database to the latest migration, and once the tests complete, clear the database again.""" """Migrate the testing database to the latest migration, and once the tests complete, clear the database again."""
from foxnouns.db.sync import engine
from alembic import command, config from alembic import command, config
from foxnouns.db.sync import engine
cfg = config.Config("alembic.ini") cfg = config.Config("alembic.ini")
cfg.attributes["connection"] = engine.connect() cfg.attributes["connection"] = engine.connect()