code format

This commit is contained in:
Jakub Miazek 2024-04-24 10:37:08 +02:00
parent 43fe665608
commit c2975fd260
19 changed files with 358 additions and 173 deletions

View File

@ -29,6 +29,10 @@ docker-create-db-migration: ## Create new alembic database migration aka databa
docker-test: ## Run project tests docker-test: ## Run project tests
docker-compose -f docker-compose.yml -f docker-compose.test.yml run --rm app pytest docker-compose -f docker-compose.yml -f docker-compose.test.yml run --rm app pytest
.PHONY: docker-test-snapshot
docker-test-snapshot: ## Run project tests with inline snapshot
docker-compose -f docker-compose.yml -f docker-compose.test.yml run --rm app pytest --inline-snapshot=create
.PHONY: safety .PHONY: safety
safety: ## Check project and dependencies with safety https://github.com/pyupio/safety safety: ## Check project and dependencies with safety https://github.com/pyupio/safety
docker-compose run --rm app safety check docker-compose run --rm app safety check

View File

@ -31,7 +31,9 @@ async def run_migrations_online():
and associate a connection with the context. and associate a connection with the context.
""" """
connectable = create_async_engine(settings.asyncpg_url.unicode_string(), future=True) connectable = create_async_engine(
settings.asyncpg_url.unicode_string(), future=True
)
async with connectable.connect() as connection: async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations) await connection.run_sync(do_run_migrations)

View File

@ -5,12 +5,13 @@ Revises:
Create Date: 2024-01-02 20:12:46.214651 Create Date: 2024-01-02 20:12:46.214651
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '68cd4c3a0af0' revision = "68cd4c3a0af0"
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -18,125 +19,168 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('nonsense', op.create_table(
sa.Column('id', sa.UUID(), autoincrement=True, nullable=False), "nonsense",
sa.Column('name', sa.String(), nullable=False), sa.Column("id", sa.UUID(), autoincrement=True, nullable=False),
sa.Column('description', sa.String(), nullable=True), sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint('name'), sa.Column("description", sa.String(), nullable=True),
sa.UniqueConstraint('id'), sa.PrimaryKeyConstraint("name"),
sa.UniqueConstraint('name'), sa.UniqueConstraint("id"),
schema='happy_hog' sa.UniqueConstraint("name"),
schema="happy_hog",
) )
op.create_table('stuff', op.create_table(
sa.Column('id', sa.UUID(), autoincrement=True, nullable=False), "stuff",
sa.Column('name', sa.String(), nullable=False), sa.Column("id", sa.UUID(), autoincrement=True, nullable=False),
sa.Column('description', sa.String(), nullable=True), sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint('name'), sa.Column("description", sa.String(), nullable=True),
sa.UniqueConstraint('id'), sa.PrimaryKeyConstraint("name"),
sa.UniqueConstraint('name'), sa.UniqueConstraint("id"),
schema='happy_hog' sa.UniqueConstraint("name"),
schema="happy_hog",
) )
op.create_table('character', op.create_table(
sa.Column('id', sa.String(length=32), nullable=False), "character",
sa.Column('name', sa.String(length=64), nullable=False), sa.Column("id", sa.String(length=32), nullable=False),
sa.Column('speech_count', sa.Integer(), nullable=False), sa.Column("name", sa.String(length=64), nullable=False),
sa.Column('abbrev', sa.String(length=32), nullable=True), sa.Column("speech_count", sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=2056), nullable=True), sa.Column("abbrev", sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id', name='character_pkey'), sa.Column("description", sa.String(length=2056), nullable=True),
schema='shakespeare' sa.PrimaryKeyConstraint("id", name="character_pkey"),
schema="shakespeare",
) )
op.create_table('wordform', op.create_table(
sa.Column('id', sa.Integer(), nullable=False), "wordform",
sa.Column('plain_text', sa.String(length=64), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('phonetic_text', sa.String(length=64), nullable=False), sa.Column("plain_text", sa.String(length=64), nullable=False),
sa.Column('stem_text', sa.String(length=64), nullable=False), sa.Column("phonetic_text", sa.String(length=64), nullable=False),
sa.Column('occurences', sa.Integer(), nullable=False), sa.Column("stem_text", sa.String(length=64), nullable=False),
sa.PrimaryKeyConstraint('id', name='wordform_pkey'), sa.Column("occurences", sa.Integer(), nullable=False),
schema='shakespeare' sa.PrimaryKeyConstraint("id", name="wordform_pkey"),
schema="shakespeare",
) )
op.create_table('work', op.create_table(
sa.Column('id', sa.String(length=32), nullable=False), "work",
sa.Column('title', sa.String(length=32), nullable=False), sa.Column("id", sa.String(length=32), nullable=False),
sa.Column('long_title', sa.String(length=64), nullable=False), sa.Column("title", sa.String(length=32), nullable=False),
sa.Column('year', sa.Integer(), nullable=False), sa.Column("long_title", sa.String(length=64), nullable=False),
sa.Column('genre_type', sa.String(length=1), nullable=False), sa.Column("year", sa.Integer(), nullable=False),
sa.Column('source', sa.String(length=16), nullable=False), sa.Column("genre_type", sa.String(length=1), nullable=False),
sa.Column('total_words', sa.Integer(), nullable=False), sa.Column("source", sa.String(length=16), nullable=False),
sa.Column('total_paragraphs', sa.Integer(), nullable=False), sa.Column("total_words", sa.Integer(), nullable=False),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("total_paragraphs", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id', name='work_pkey'), sa.Column("notes", sa.Text(), nullable=True),
schema='shakespeare' sa.PrimaryKeyConstraint("id", name="work_pkey"),
schema="shakespeare",
) )
op.create_table('user', op.create_table(
sa.Column('id', sa.UUID(), nullable=False), "user",
sa.Column('email', sa.String(), nullable=False), sa.Column("id", sa.UUID(), nullable=False),
sa.Column('first_name', sa.String(), nullable=False), sa.Column("email", sa.String(), nullable=False),
sa.Column('last_name', sa.String(), nullable=False), sa.Column("first_name", sa.String(), nullable=False),
sa.Column('_password', sa.LargeBinary(), nullable=False), sa.Column("last_name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id'), sa.Column("_password", sa.LargeBinary(), nullable=False),
sa.UniqueConstraint('email') sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
) )
op.create_table('stuff_full_of_nonsense', op.create_table(
sa.Column('id', sa.UUID(), nullable=False), "stuff_full_of_nonsense",
sa.Column('stuff_id', sa.UUID(), nullable=False), sa.Column("id", sa.UUID(), nullable=False),
sa.Column('nonsense_id', sa.UUID(), nullable=False), sa.Column("stuff_id", sa.UUID(), nullable=False),
sa.Column('but_why', sa.String(), nullable=True), sa.Column("nonsense_id", sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(['nonsense_id'], ['happy_hog.nonsense.id'], ), sa.Column("but_why", sa.String(), nullable=True),
sa.ForeignKeyConstraint(['stuff_id'], ['happy_hog.stuff.id'], ), sa.ForeignKeyConstraint(
sa.PrimaryKeyConstraint('id'), ["nonsense_id"],
schema='happy_hog' ["happy_hog.nonsense.id"],
),
sa.ForeignKeyConstraint(
["stuff_id"],
["happy_hog.stuff.id"],
),
sa.PrimaryKeyConstraint("id"),
schema="happy_hog",
) )
op.create_table('chapter', op.create_table(
sa.Column('id', sa.Integer(), nullable=False), "chapter",
sa.Column('work_id', sa.String(length=32), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('section_number', sa.Integer(), nullable=False), sa.Column("work_id", sa.String(length=32), nullable=False),
sa.Column('chapter_number', sa.Integer(), nullable=False), sa.Column("section_number", sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=256), nullable=False), sa.Column("chapter_number", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['work_id'], ['shakespeare.work.id'], name='chapter_work_id_fkey'), sa.Column("description", sa.String(length=256), nullable=False),
sa.PrimaryKeyConstraint('id', name='chapter_pkey'), sa.ForeignKeyConstraint(
sa.UniqueConstraint('work_id', 'section_number', 'chapter_number', name='chapter_work_id_section_number_chapter_number_key'), ["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"
schema='shakespeare' ),
sa.PrimaryKeyConstraint("id", name="chapter_pkey"),
sa.UniqueConstraint(
"work_id",
"section_number",
"chapter_number",
name="chapter_work_id_section_number_chapter_number_key",
),
schema="shakespeare",
) )
op.create_table('character_work', op.create_table(
sa.Column('character_id', sa.String(length=32), nullable=False), "character_work",
sa.Column('work_id', sa.String(length=32), nullable=False), sa.Column("character_id", sa.String(length=32), nullable=False),
sa.ForeignKeyConstraint(['character_id'], ['shakespeare.character.id'], name='character_work_character_id_fkey'), sa.Column("work_id", sa.String(length=32), nullable=False),
sa.ForeignKeyConstraint(['work_id'], ['shakespeare.work.id'], name='character_work_work_id_fkey'), sa.ForeignKeyConstraint(
sa.PrimaryKeyConstraint('character_id', 'work_id', name='character_work_pkey'), ["character_id"],
schema='shakespeare' ["shakespeare.character.id"],
name="character_work_character_id_fkey",
),
sa.ForeignKeyConstraint(
["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"
),
sa.PrimaryKeyConstraint("character_id", "work_id", name="character_work_pkey"),
schema="shakespeare",
) )
op.create_table('paragraph', op.create_table(
sa.Column('id', sa.Integer(), nullable=False), "paragraph",
sa.Column('work_id', sa.String(length=32), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('paragraph_num', sa.Integer(), nullable=False), sa.Column("work_id", sa.String(length=32), nullable=False),
sa.Column('character_id', sa.String(length=32), nullable=False), sa.Column("paragraph_num", sa.Integer(), nullable=False),
sa.Column('plain_text', sa.Text(), nullable=False), sa.Column("character_id", sa.String(length=32), nullable=False),
sa.Column('phonetic_text', sa.Text(), nullable=False), sa.Column("plain_text", sa.Text(), nullable=False),
sa.Column('stem_text', sa.Text(), nullable=False), sa.Column("phonetic_text", sa.Text(), nullable=False),
sa.Column('paragraph_type', sa.String(length=1), nullable=False), sa.Column("stem_text", sa.Text(), nullable=False),
sa.Column('section_number', sa.Integer(), nullable=False), sa.Column("paragraph_type", sa.String(length=1), nullable=False),
sa.Column('chapter_number', sa.Integer(), nullable=False), sa.Column("section_number", sa.Integer(), nullable=False),
sa.Column('char_count', sa.Integer(), nullable=False), sa.Column("chapter_number", sa.Integer(), nullable=False),
sa.Column('word_count', sa.Integer(), nullable=False), sa.Column("char_count", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['character_id'], ['shakespeare.character.id'], name='paragraph_character_id_fkey'), sa.Column("word_count", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['work_id', 'section_number', 'chapter_number'], ['shakespeare.chapter.work_id', 'shakespeare.chapter.section_number', 'shakespeare.chapter.chapter_number'], name='paragraph_chapter_fkey'), sa.ForeignKeyConstraint(
sa.ForeignKeyConstraint(['work_id'], ['shakespeare.work.id'], name='paragraph_work_id_fkey'), ["character_id"],
sa.PrimaryKeyConstraint('id', name='paragraph_pkey'), ["shakespeare.character.id"],
schema='shakespeare' name="paragraph_character_id_fkey",
),
sa.ForeignKeyConstraint(
["work_id", "section_number", "chapter_number"],
[
"shakespeare.chapter.work_id",
"shakespeare.chapter.section_number",
"shakespeare.chapter.chapter_number",
],
name="paragraph_chapter_fkey",
),
sa.ForeignKeyConstraint(
["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"
),
sa.PrimaryKeyConstraint("id", name="paragraph_pkey"),
schema="shakespeare",
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('paragraph', schema='shakespeare') op.drop_table("paragraph", schema="shakespeare")
op.drop_table('character_work', schema='shakespeare') op.drop_table("character_work", schema="shakespeare")
op.drop_table('chapter', schema='shakespeare') op.drop_table("chapter", schema="shakespeare")
op.drop_table('stuff_full_of_nonsense', schema='happy_hog') op.drop_table("stuff_full_of_nonsense", schema="happy_hog")
op.drop_table('user') op.drop_table("user")
op.drop_table('work', schema='shakespeare') op.drop_table("work", schema="shakespeare")
op.drop_table('wordform', schema='shakespeare') op.drop_table("wordform", schema="shakespeare")
op.drop_table('character', schema='shakespeare') op.drop_table("character", schema="shakespeare")
op.drop_table('stuff', schema='happy_hog') op.drop_table("stuff", schema="happy_hog")
op.drop_table('nonsense', schema='happy_hog') op.drop_table("nonsense", schema="happy_hog")
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -12,7 +12,9 @@ router = APIRouter(prefix="/v1/nonsense")
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=NonsenseResponse) @router.post("/", status_code=status.HTTP_201_CREATED, response_model=NonsenseResponse)
async def create_nonsense(payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)): async def create_nonsense(
payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)
):
nonsense = Nonsense(**payload.model_dump()) nonsense = Nonsense(**payload.model_dump())
await nonsense.save(db_session) await nonsense.save(db_session)
return nonsense return nonsense
@ -103,7 +105,9 @@ async def import_nonsense(
# If an error occurs, roll back the session # If an error occurs, roll back the session
await db_session.rollback() await db_session.rollback()
# Raise an HTTP exception with a 422 status code # Raise an HTTP exception with a 422 status code
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
finally: finally:
# Ensure that the database session is closed, regardless of whether an error occurred or not # Ensure that the database session is closed, regardless of whether an error occurred or not
await db_session.close() await db_session.close()
@ -147,4 +151,4 @@ async def import_nonsense(
# #
# TODO: https://medium.com/@amitosh/full-text-search-fts-with-postgresql-and-sqlalchemy-edc436330a0c # TODO: https://medium.com/@amitosh/full-text-search-fts-with-postgresql-and-sqlalchemy-edc436330a0c
# TODO: https://www.postgresql.org/docs/13/textsearch-intro.html # TODO: https://www.postgresql.org/docs/13/textsearch-intro.html

View File

@ -13,21 +13,29 @@ router = APIRouter(prefix="/v1/stuff")
@router.post("/add_many", status_code=status.HTTP_201_CREATED) @router.post("/add_many", status_code=status.HTTP_201_CREATED)
async def create_multi_stuff(payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)): async def create_multi_stuff(
payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)
):
try: try:
stuff_instances = [Stuff(**stuff.model_dump()) for stuff in payload] stuff_instances = [Stuff(**stuff.model_dump()) for stuff in payload]
db_session.add_all(stuff_instances) db_session.add_all(stuff_instances)
await db_session.commit() await db_session.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
# logger.exception(ex) # logger.exception(ex)
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
else: else:
logger.info(f"{len(stuff_instances)} instances of Stuff inserted into database.") logger.info(
f"{len(stuff_instances)} instances of Stuff inserted into database."
)
return True return True
@router.post("", status_code=status.HTTP_201_CREATED, response_model=StuffResponse) @router.post("", status_code=status.HTTP_201_CREATED, response_model=StuffResponse)
async def create_stuff(payload: StuffSchema, db_session: AsyncSession = Depends(get_db)): async def create_stuff(
payload: StuffSchema, db_session: AsyncSession = Depends(get_db)
):
stuff = Stuff(**payload.model_dump()) stuff = Stuff(**payload.model_dump())
await stuff.save(db_session) await stuff.save(db_session)
return stuff return stuff

View File

@ -13,7 +13,9 @@ router = APIRouter(prefix="/v1/user")
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse) @router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
async def create_user(payload: UserSchema, request: Request, db_session: AsyncSession = Depends(get_db)): async def create_user(
payload: UserSchema, request: Request, db_session: AsyncSession = Depends(get_db)
):
logger.info(f"Creating user: {payload}") logger.info(f"Creating user: {payload}")
_user: User = User(**payload.model_dump()) _user: User = User(**payload.model_dump())
await _user.save(db_session) await _user.save(db_session)
@ -23,15 +25,23 @@ async def create_user(payload: UserSchema, request: Request, db_session: AsyncSe
return _user return _user
@router.post("/token", status_code=status.HTTP_201_CREATED, response_model=TokenResponse) @router.post(
async def get_token_for_user(user: UserLogin, request: Request, db_session: AsyncSession = Depends(get_db)): "/token", status_code=status.HTTP_201_CREATED, response_model=TokenResponse
)
async def get_token_for_user(
user: UserLogin, request: Request, db_session: AsyncSession = Depends(get_db)
):
_user: User = await User.find(db_session, [User.email == user.email]) _user: User = await User.find(db_session, [User.email == user.email])
# TODO: out exception handling to external module # TODO: out exception handling to external module
if not _user: if not _user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
if not _user.check_password(user.password): if not _user.check_password(user.password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Password is incorrect") raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Password is incorrect"
)
# TODO: add refresh token # TODO: add refresh token
_token = await create_access_token(_user, request) _token = await create_access_token(_user, request)

View File

@ -7,9 +7,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file=".env", env_file=".env", env_ignore_empty=True, extra="ignore"
env_ignore_empty=True,
extra="ignore"
) )
jwt_algorithm: str = os.getenv("JWT_ALGORITHM") jwt_algorithm: str = os.getenv("JWT_ALGORITHM")
jwt_expire: int = os.getenv("JWT_EXPIRE") jwt_expire: int = os.getenv("JWT_EXPIRE")
@ -46,7 +44,6 @@ class Settings(BaseSettings):
host=self.REDIS_HOST, host=self.REDIS_HOST,
port=self.REDIS_PORT, port=self.REDIS_PORT,
path=self.REDIS_DB, path=self.REDIS_DB,
) )
@computed_field @computed_field

View File

@ -21,8 +21,6 @@ async def lifespan(app: FastAPI):
# Load the redis connection # Load the redis connection
app.state.redis = await get_redis() app.state.redis = await get_redis()
try: try:
# Initialize the cache with the redis connection # Initialize the cache with the redis connection
redis_cache = await get_cache() redis_cache = await get_cache()
@ -43,4 +41,9 @@ app.include_router(user_router)
app.include_router(health_router, prefix="/v1/public/health", tags=["Health, Public"]) app.include_router(health_router, prefix="/v1/public/health", tags=["Health, Public"])
app.include_router(health_router, prefix="/v1/health", tags=["Health, Bearer"], dependencies=[Depends(AuthBearer())]) app.include_router(
health_router,
prefix="/v1/health",
tags=["Health, Bearer"],
dependencies=[Depends(AuthBearer())],
)

View File

@ -26,7 +26,9 @@ class Base(DeclarativeBase):
db_session.add(self) db_session.add(self)
return await db_session.commit() return await db_session.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
async def delete(self, db_session: AsyncSession): async def delete(self, db_session: AsyncSession):
""" """
@ -39,7 +41,9 @@ class Base(DeclarativeBase):
await db_session.commit() await db_session.commit()
return True return True
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
async def update(self, db: AsyncSession, **kwargs): async def update(self, db: AsyncSession, **kwargs):
""" """
@ -53,7 +57,9 @@ class Base(DeclarativeBase):
setattr(self, k, v) setattr(self, k, v)
return await db.commit() return await db.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
async def save_or_update(self, db: AsyncSession): async def save_or_update(self, db: AsyncSession):
try: try:

View File

@ -12,7 +12,9 @@ from app.models.base import Base
class Nonsense(Base): class Nonsense(Base):
__tablename__ = "nonsense" __tablename__ = "nonsense"
__table_args__ = ({"schema": "happy_hog"},) __table_args__ = ({"schema": "happy_hog"},)
id: Mapped[uuid:UUID] = mapped_column(UUID(as_uuid=True), unique=True, default=uuid.uuid4, autoincrement=True) id: Mapped[uuid:UUID] = mapped_column(
UUID(as_uuid=True), unique=True, default=uuid.uuid4, autoincrement=True
)
name: Mapped[str] = mapped_column(String, primary_key=True, unique=True) name: Mapped[str] = mapped_column(String, primary_key=True, unique=True)
description: Mapped[str | None] description: Mapped[str | None]
# TODO: apply relation to other tables # TODO: apply relation to other tables
@ -31,7 +33,9 @@ class Nonsense(Base):
if instance is None: if instance is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail={"Record not found": f"There is no record for requested name value : {name}"}, detail={
"Record not found": f"There is no record for requested name value : {name}"
},
) )
else: else:
return instance return instance

View File

@ -34,7 +34,10 @@ class Character(Base):
""" """
__tablename__ = "character" __tablename__ = "character"
__table_args__ = (PrimaryKeyConstraint("id", name="character_pkey"), {"schema": "shakespeare"}) __table_args__ = (
PrimaryKeyConstraint("id", name="character_pkey"),
{"schema": "shakespeare"},
)
id: Mapped[str] = mapped_column(String(32), primary_key=True) id: Mapped[str] = mapped_column(String(32), primary_key=True)
name: Mapped[str] = mapped_column(String(64)) name: Mapped[str] = mapped_column(String(64))
@ -45,7 +48,9 @@ class Character(Base):
work: Mapped[list["Work"]] = relationship( work: Mapped[list["Work"]] = relationship(
"Work", secondary="shakespeare.character_work", back_populates="character" "Work", secondary="shakespeare.character_work", back_populates="character"
) )
paragraph: Mapped[list["Paragraph"]] = relationship("Paragraph", back_populates="character") paragraph: Mapped[list["Paragraph"]] = relationship(
"Paragraph", back_populates="character"
)
class Wordform(Base): class Wordform(Base):
@ -65,7 +70,10 @@ class Wordform(Base):
""" """
__tablename__ = "wordform" __tablename__ = "wordform"
__table_args__ = (PrimaryKeyConstraint("id", name="wordform_pkey"), {"schema": "shakespeare"}) __table_args__ = (
PrimaryKeyConstraint("id", name="wordform_pkey"),
{"schema": "shakespeare"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
plain_text: Mapped[str] = mapped_column(String(64)) plain_text: Mapped[str] = mapped_column(String(64))
@ -98,7 +106,10 @@ class Work(Base):
""" """
__tablename__ = "work" __tablename__ = "work"
__table_args__ = (PrimaryKeyConstraint("id", name="work_pkey"), {"schema": "shakespeare"}) __table_args__ = (
PrimaryKeyConstraint("id", name="work_pkey"),
{"schema": "shakespeare"},
)
id: Mapped[str] = mapped_column(String(32), primary_key=True) id: Mapped[str] = mapped_column(String(32), primary_key=True)
title: Mapped[str] = mapped_column(String(32)) title: Mapped[str] = mapped_column(String(32))
@ -114,7 +125,9 @@ class Work(Base):
"Character", secondary="shakespeare.character_work", back_populates="work" "Character", secondary="shakespeare.character_work", back_populates="work"
) )
chapter: Mapped[list["Chapter"]] = relationship("Chapter", back_populates="work") chapter: Mapped[list["Chapter"]] = relationship("Chapter", back_populates="work")
paragraph: Mapped[list["Paragraph"]] = relationship("Paragraph", back_populates="work") paragraph: Mapped[list["Paragraph"]] = relationship(
"Paragraph", back_populates="work"
)
class Chapter(Base): class Chapter(Base):
@ -137,10 +150,15 @@ class Chapter(Base):
__tablename__ = "chapter" __tablename__ = "chapter"
__table_args__ = ( __table_args__ = (
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"), ForeignKeyConstraint(
["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"
),
PrimaryKeyConstraint("id", name="chapter_pkey"), PrimaryKeyConstraint("id", name="chapter_pkey"),
UniqueConstraint( UniqueConstraint(
"work_id", "section_number", "chapter_number", name="chapter_work_id_section_number_chapter_number_key" "work_id",
"section_number",
"chapter_number",
name="chapter_work_id_section_number_chapter_number_key",
), ),
{"schema": "shakespeare"}, {"schema": "shakespeare"},
) )
@ -152,7 +170,9 @@ class Chapter(Base):
description: Mapped[str] = mapped_column(String(256)) description: Mapped[str] = mapped_column(String(256))
work: Mapped["Work"] = relationship("Work", back_populates="chapter") work: Mapped["Work"] = relationship("Work", back_populates="chapter")
paragraph: Mapped[list["Paragraph"]] = relationship("Paragraph", back_populates="chapter") paragraph: Mapped[list["Paragraph"]] = relationship(
"Paragraph", back_populates="chapter"
)
t_character_work = Table( t_character_work = Table(
@ -160,8 +180,14 @@ t_character_work = Table(
Base.metadata, Base.metadata,
Column("character_id", String(32), primary_key=True, nullable=False), Column("character_id", String(32), primary_key=True, nullable=False),
Column("work_id", String(32), primary_key=True, nullable=False), Column("work_id", String(32), primary_key=True, nullable=False),
ForeignKeyConstraint(["character_id"], ["shakespeare.character.id"], name="character_work_character_id_fkey"), ForeignKeyConstraint(
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"), ["character_id"],
["shakespeare.character.id"],
name="character_work_character_id_fkey",
),
ForeignKeyConstraint(
["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"
),
PrimaryKeyConstraint("character_id", "work_id", name="character_work_pkey"), PrimaryKeyConstraint("character_id", "work_id", name="character_work_pkey"),
schema="shakespeare", schema="shakespeare",
) )
@ -198,13 +224,23 @@ class Paragraph(Base):
__tablename__ = "paragraph" __tablename__ = "paragraph"
__table_args__ = ( __table_args__ = (
ForeignKeyConstraint(["character_id"], ["shakespeare.character.id"], name="paragraph_character_id_fkey"), ForeignKeyConstraint(
["character_id"],
["shakespeare.character.id"],
name="paragraph_character_id_fkey",
),
ForeignKeyConstraint( ForeignKeyConstraint(
["work_id", "section_number", "chapter_number"], ["work_id", "section_number", "chapter_number"],
["shakespeare.chapter.work_id", "shakespeare.chapter.section_number", "shakespeare.chapter.chapter_number"], [
"shakespeare.chapter.work_id",
"shakespeare.chapter.section_number",
"shakespeare.chapter.chapter_number",
],
name="paragraph_chapter_fkey", name="paragraph_chapter_fkey",
), ),
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"), ForeignKeyConstraint(
["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"
),
PrimaryKeyConstraint("id", name="paragraph_pkey"), PrimaryKeyConstraint("id", name="paragraph_pkey"),
{"schema": "shakespeare"}, {"schema": "shakespeare"},
) )
@ -222,7 +258,9 @@ class Paragraph(Base):
char_count: Mapped[int] = mapped_column(Integer) char_count: Mapped[int] = mapped_column(Integer)
word_count: Mapped[int] = mapped_column(Integer) word_count: Mapped[int] = mapped_column(Integer)
character: Mapped["Character"] = relationship("Character", back_populates="paragraph") character: Mapped["Character"] = relationship(
"Character", back_populates="paragraph"
)
chapter: Mapped["Chapter"] = relationship("Chapter", back_populates="paragraph") chapter: Mapped["Chapter"] = relationship("Chapter", back_populates="paragraph")
work: Mapped["Work"] = relationship("Work", back_populates="paragraph") work: Mapped["Work"] = relationship("Work", back_populates="paragraph")
@ -244,6 +282,12 @@ class Paragraph(Base):
- List[Paragraph]: A list of `Paragraph` objects that are associated with the specified character. - List[Paragraph]: A list of `Paragraph` objects that are associated with the specified character.
""" """
stmt = select(cls).join(Character).join(Chapter).join(Work).where(Character.name == character) stmt = (
select(cls)
.join(Character)
.join(Chapter)
.join(Work)
.where(Character.name == character)
)
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
return result.scalars().all() return result.scalars().all()

View File

@ -13,11 +13,15 @@ from app.models.nonsense import Nonsense
class Stuff(Base): class Stuff(Base):
__tablename__ = "stuff" __tablename__ = "stuff"
__table_args__ = ({"schema": "happy_hog"},) __table_args__ = ({"schema": "happy_hog"},)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), unique=True, default=uuid.uuid4, autoincrement=True) id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), unique=True, default=uuid.uuid4, autoincrement=True
)
name: Mapped[str] = mapped_column(String, primary_key=True, unique=True) name: Mapped[str] = mapped_column(String, primary_key=True, unique=True)
description: Mapped[str | None] description: Mapped[str | None]
nonsense: Mapped["Nonsense"] = relationship("Nonsense", secondary="happy_hog.stuff_full_of_nonsense") nonsense: Mapped["Nonsense"] = relationship(
"Nonsense", secondary="happy_hog.stuff_full_of_nonsense"
)
@classmethod @classmethod
async def find(cls, db_session: AsyncSession, name: str): async def find(cls, db_session: AsyncSession, name: str):
@ -42,7 +46,11 @@ class Stuff(Base):
class StuffFullOfNonsense(Base): class StuffFullOfNonsense(Base):
__tablename__ = "stuff_full_of_nonsense" __tablename__ = "stuff_full_of_nonsense"
__table_args__ = ({"schema": "happy_hog"},) __table_args__ = ({"schema": "happy_hog"},)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), default=uuid.uuid4, primary_key=True) id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), default=uuid.uuid4, primary_key=True
)
stuff_id: Mapped[Stuff] = mapped_column(UUID, ForeignKey("happy_hog.stuff.id")) stuff_id: Mapped[Stuff] = mapped_column(UUID, ForeignKey("happy_hog.stuff.id"))
nonsense_id: Mapped["Nonsense"] = mapped_column(UUID, ForeignKey("happy_hog.nonsense.id")) nonsense_id: Mapped["Nonsense"] = mapped_column(
UUID, ForeignKey("happy_hog.nonsense.id")
)
but_why: Mapped[str | None] but_why: Mapped[str | None]

View File

@ -15,7 +15,9 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(Base): class User(Base):
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), default=uuid.uuid4, primary_key=True) id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), default=uuid.uuid4, primary_key=True
)
email: Mapped[str] = mapped_column(String, nullable=False, unique=True) email: Mapped[str] = mapped_column(String, nullable=False, unique=True)
first_name: Mapped[str] = mapped_column(String, nullable=False) first_name: Mapped[str] = mapped_column(String, nullable=False)
last_name: Mapped[str] = mapped_column(String, nullable=False) last_name: Mapped[str] = mapped_column(String, nullable=False)
@ -28,7 +30,9 @@ class User(Base):
@password.setter @password.setter
def password(self, password: SecretStr): def password(self, password: SecretStr):
_password_string = password.get_secret_value() _password_string = password.get_secret_value()
self._password = bcrypt.hashpw(_password_string.encode("utf-8"), bcrypt.gensalt()) self._password = bcrypt.hashpw(
_password_string.encode("utf-8"), bcrypt.gensalt()
)
def check_password(self, password: SecretStr): def check_password(self, password: SecretStr):
return pwd_context.verify(password.get_secret_value(), self.password) return pwd_context.verify(password.get_secret_value(), self.password)

View File

@ -8,10 +8,20 @@ config = ConfigDict(from_attributes=True)
# TODO: add pydantic field validator for strong password # TODO: add pydantic field validator for strong password
class UserSchema(BaseModel): class UserSchema(BaseModel):
model_config = config model_config = config
email: EmailStr = Field(title="Users email", description="Users email", examples=["john@domain.com"]) email: EmailStr = Field(
first_name: str = Field(title="Users first name", description="Users first name", examples=["John"]) title="Users email", description="Users email", examples=["john@domain.com"]
last_name: str = Field(title="Users last name", description="Users last name", examples=["Doe"]) )
password: SecretStr = Field(title="Users password", description="Users password", examples=["@SuperSecret123"]) first_name: str = Field(
title="Users first name", description="Users first name", examples=["John"]
)
last_name: str = Field(
title="Users last name", description="Users last name", examples=["Doe"]
)
password: SecretStr = Field(
title="Users password",
description="Users password",
examples=["@SuperSecret123"],
)
class UserResponse(BaseModel): class UserResponse(BaseModel):
@ -23,11 +33,19 @@ class UserResponse(BaseModel):
class TokenResponse(BaseModel): class TokenResponse(BaseModel):
access_token: str = Field(title="Users access token", description="Users access token") access_token: str = Field(
title="Users access token", description="Users access token"
)
token_type: str = Field(title="Users token type", description="Users token type") token_type: str = Field(title="Users token type", description="Users token type")
class UserLogin(BaseModel): class UserLogin(BaseModel):
model_config = config model_config = config
email: EmailStr = Field(title="Users email", description="Users email", examples=["john@domain.com"]) email: EmailStr = Field(
password: SecretStr = Field(title="Users password", description="Users password", examples=["@SuperSecret123"]) title="Users email", description="Users email", examples=["john@domain.com"]
)
password: SecretStr = Field(
title="Users password",
description="Users password",
examples=["@SuperSecret123"],
)

View File

@ -30,9 +30,13 @@ class AuthBearer(HTTPBearer):
if not credentials: if not credentials:
raise HTTPException(status_code=403, detail="Invalid authorization code.") raise HTTPException(status_code=403, detail="Invalid authorization code.")
if credentials.scheme != "Bearer": if credentials.scheme != "Bearer":
raise HTTPException(status_code=403, detail="Invalid authentication scheme.") raise HTTPException(
status_code=403, detail="Invalid authentication scheme."
)
if not await verify_jwt(request, credentials.credentials): if not await verify_jwt(request, credentials.credentials):
raise HTTPException(status_code=403, detail="Invalid token or expired token.") raise HTTPException(
status_code=403, detail="Invalid token or expired token."
)
return credentials.credentials return credentials.credentials
@ -43,8 +47,12 @@ async def create_access_token(user: User, request: Request):
"expiry": time.time() + global_settings.jwt_expire, "expiry": time.time() + global_settings.jwt_expire,
"platform": request.headers.get("User-Agent"), "platform": request.headers.get("User-Agent"),
} }
token = jwt.encode(payload, str(user.password), algorithm=global_settings.jwt_algorithm) token = jwt.encode(
payload, str(user.password), algorithm=global_settings.jwt_algorithm
)
_bool = await set_to_redis(request, token, str(payload), ex=global_settings.jwt_expire) _bool = await set_to_redis(
request, token, str(payload), ex=global_settings.jwt_expire
)
if _bool: if _bool:
return token return token

View File

@ -19,4 +19,6 @@ class AppLogger(metaclass=SingletonMeta):
class RichConsoleHandler(RichHandler): class RichConsoleHandler(RichHandler):
def __init__(self, width=200, style=None, **kwargs): def __init__(self, width=200, style=None, **kwargs):
super().__init__(console=Console(color_system="256", width=width, style=style), **kwargs) super().__init__(
console=Console(color_system="256", width=width, style=style), **kwargs
)

View File

@ -2,18 +2,36 @@ import pytest
from httpx import AsyncClient from httpx import AsyncClient
from starlette import status from starlette import status
import jwt import jwt
from inline_snapshot import snapshot
from dirty_equals import IsStr, IsUUID, IsPositiveFloat
pytestmark = pytest.mark.anyio pytestmark = pytest.mark.anyio
# TODO: parametrize test with diff urls # TODO: parametrize test with diff urls
async def test_add_user(client: AsyncClient): async def test_add_user(client: AsyncClient):
payload = {"email": "joe@grillazz.com", "first_name": "Joe", "last_name": "Garcia", "password": "s1lly"} payload = {
"email": "joe@grillazz.com",
"first_name": "Joe",
"last_name": "Garcia",
"password": "s1lly",
}
response = await client.post("/user/", json=payload) response = await client.post("/user/", json=payload)
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
claimset = jwt.decode(response.json()["access_token"], options={"verify_signature": False}) assert response.json() == snapshot(
assert claimset["email"] == payload["email"] {
assert claimset["expiry"] > 0 "id": IsUUID(4),
"email": "joe@grillazz.com",
"first_name": "Joe",
"last_name": "Garcia",
"access_token": IsStr(),
}
)
claimset = jwt.decode(
response.json()["access_token"], options={"verify_signature": False}
)
assert claimset["expiry"] == IsPositiveFloat()
assert claimset["platform"] == "python-httpx/0.27.0" assert claimset["platform"] == "python-httpx/0.27.0"
@ -22,9 +40,11 @@ async def test_get_token(client: AsyncClient):
payload = {"email": "joe@grillazz.com", "password": "s1lly"} payload = {"email": "joe@grillazz.com", "password": "s1lly"}
response = await client.post("/user/token", json=payload) response = await client.post("/user/token", json=payload)
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
claimset = jwt.decode(response.json()["access_token"], options={"verify_signature": False}) claimset = jwt.decode(
response.json()["access_token"], options={"verify_signature": False}
)
assert claimset["email"] == payload["email"] assert claimset["email"] == payload["email"]
assert claimset["expiry"] > 0 assert claimset["expiry"] == IsPositiveFloat()
assert claimset["platform"] == "python-httpx/0.27.0" assert claimset["platform"] == "python-httpx/0.27.0"

View File

@ -24,4 +24,4 @@ async def test_import_animals(client: AsyncClient):
) )
assert response.status_code == expected_status assert response.status_code == expected_status
assert response.json() == {'filename': 'nonsense.xlsx', 'nonsense_records': 10} assert response.json() == {"filename": "nonsense.xlsx", "nonsense_records": 10}

View File

@ -32,7 +32,6 @@ async def client(start_db) -> AsyncClient:
transport = ASGITransport( transport = ASGITransport(
app=app, app=app,
) )
async with AsyncClient( async with AsyncClient(
# app=app, # app=app,