mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2025-08-26 16:40:40 +03:00
refactor
This commit is contained in:
parent
794cca19c7
commit
b736de6710
0
tests/app/__init__.py
Normal file
0
tests/app/__init__.py
Normal file
0
tests/app/api/__init__.py
Normal file
0
tests/app/api/__init__.py
Normal file
40
tests/app/api/nonsense.py
Normal file
40
tests/app/api/nonsense.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from the_app.database import get_db
|
||||||
|
from the_app.models.nonsense import Nonsense
|
||||||
|
from the_app.schemas.nnonsense import NonsenseResponse, NonsenseSchema
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/v1/nonsense")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=NonsenseResponse)
|
||||||
|
async def create_nonsense(payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)):
|
||||||
|
nonsense = Nonsense(**payload.dict())
|
||||||
|
await nonsense.save(db_session)
|
||||||
|
return nonsense
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=NonsenseResponse)
|
||||||
|
async def find_nonsense(
|
||||||
|
name: str,
|
||||||
|
db_session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
return await Nonsense.find(db_session, name)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/")
|
||||||
|
async def delete_nonsense(name: str, db_session: AsyncSession = Depends(get_db)):
|
||||||
|
nonsense = await Nonsense.find(db_session, name)
|
||||||
|
return await nonsense.delete(nonsense, db_session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/", response_model=NonsenseResponse)
|
||||||
|
async def update_nonsense(
|
||||||
|
payload: NonsenseSchema,
|
||||||
|
name: str,
|
||||||
|
db_session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
nonsense = await Nonsense.find(db_session, name)
|
||||||
|
await nonsense.update(db_session, **payload.dict())
|
||||||
|
return nonsense
|
15
tests/app/api/shakespeare.py
Normal file
15
tests/app/api/shakespeare.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from the_app.database import get_db
|
||||||
|
from the_app.models.shakespeare import Paragraph
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/v1/shakespeare")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/",)
|
||||||
|
async def find_paragraph(
|
||||||
|
character: str,
|
||||||
|
db_session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
return await Paragraph.find(db_session=db_session, character=character)
|
58
tests/app/api/stuff.py
Normal file
58
tests/app/api/stuff.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from the_app.database import get_db
|
||||||
|
from the_app.models.stuff import Stuff
|
||||||
|
from the_app.schemas.stuff import StuffResponse, StuffSchema
|
||||||
|
from the_app.utils import get_logger
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/v1/stuff")
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add_many", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_multi_stuff(payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
stuff_instances = [Stuff(name=stuf.name, description=stuf.description) for stuf in payload]
|
||||||
|
db_session.add_all(stuff_instances)
|
||||||
|
await db_session.commit()
|
||||||
|
except SQLAlchemyError as ex:
|
||||||
|
# logger.exception(ex)
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex))
|
||||||
|
else:
|
||||||
|
logger.info(f"{len(stuff_instances)} instances of Stuff inserted into database.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", status_code=status.HTTP_201_CREATED, response_model=StuffResponse)
|
||||||
|
async def create_stuff(payload: StuffSchema, db_session: AsyncSession = Depends(get_db)):
|
||||||
|
stuff = Stuff(name=payload.name, description=payload.description)
|
||||||
|
await stuff.save(db_session)
|
||||||
|
return stuff
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{name}", response_model=StuffResponse)
|
||||||
|
async def find_stuff(
|
||||||
|
name: str,
|
||||||
|
db_session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
return await Stuff.find(db_session, name)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{name}")
|
||||||
|
async def delete_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
|
||||||
|
stuff = await Stuff.find(db_session, name)
|
||||||
|
return await Stuff.delete(stuff, db_session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{name}", response_model=StuffResponse)
|
||||||
|
async def update_stuff(
|
||||||
|
payload: StuffSchema,
|
||||||
|
name: str,
|
||||||
|
db_session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
stuff = await Stuff.find(db_session, name)
|
||||||
|
await stuff.update(db_session, **payload.dict())
|
||||||
|
return stuff
|
44
tests/app/config.py
Normal file
44
tests/app/config.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import os
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
from the_app.utils import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""
|
||||||
|
|
||||||
|
BaseSettings, from Pydantic, validates the data so that when we create an instance of Settings,
|
||||||
|
environment and testing will have types of str and bool, respectively.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
pg_user (str):
|
||||||
|
pg_pass (str):
|
||||||
|
pg_database: (str):
|
||||||
|
pg_test_database: (str):
|
||||||
|
asyncpg_url: AnyUrl:
|
||||||
|
asyncpg_test_url: AnyUrl:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
instance of Settings
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
pg_user: str = os.getenv("SQL_USER", "")
|
||||||
|
pg_pass: str = os.getenv("POSTGRES_PASSWORD", "")
|
||||||
|
pg_host: str = os.getenv("SQL_HOST", "")
|
||||||
|
pg_database: str = os.getenv("SQL_DB", "")
|
||||||
|
asyncpg_url: str = f"postgresql+asyncpg://{pg_user}:{pg_pass}@{pg_host}:5432/{pg_database}"
|
||||||
|
|
||||||
|
jwt_secret_key: str = os.getenv("SECRET_KEY", "")
|
||||||
|
jwt_algorithm: str = os.getenv("ALGORITHM", "")
|
||||||
|
jwt_access_toke_expire_minutes: int = os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 1)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_settings():
|
||||||
|
logger.info("Loading config settings from the environment...")
|
||||||
|
return Settings()
|
37
tests/app/database.py
Normal file
37
tests/app/database.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from the_app import config
|
||||||
|
|
||||||
|
global_settings = config.get_settings()
|
||||||
|
url = global_settings.asyncpg_url
|
||||||
|
|
||||||
|
engine = create_async_engine(
|
||||||
|
url,
|
||||||
|
future=True,
|
||||||
|
echo=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# expire_on_commit=False will prevent attributes from being expired
|
||||||
|
# after commit.
|
||||||
|
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
||||||
|
|
||||||
|
|
||||||
|
# Dependency
|
||||||
|
async def get_db() -> AsyncGenerator:
|
||||||
|
async with async_session() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
await session.commit()
|
||||||
|
except SQLAlchemyError as sql_ex:
|
||||||
|
await session.rollback()
|
||||||
|
raise sql_ex
|
||||||
|
except HTTPException as http_ex:
|
||||||
|
await session.rollback()
|
||||||
|
raise http_ex
|
||||||
|
finally:
|
||||||
|
await session.close()
|
59
tests/app/exceptions.py
Normal file
59
tests/app/exceptions.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestHTTPException(HTTPException):
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=msg if msg else "Bad request",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthFailedHTTPException(HTTPException):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Not authenticated",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenExpiredHTTPException(HTTPException):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Expired token",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenHTTPException(HTTPException):
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=msg if msg else "Requested resource is forbidden",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundHTTPException(HTTPException):
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=msg if msg else "Requested resource is not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictHTTPException(HTTPException):
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=msg if msg else "Conflicting resource request",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceNotAvailableHTTPException(HTTPException):
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=msg if msg else "Service not available",
|
||||||
|
)
|
24
tests/app/main.py
Normal file
24
tests/app/main.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from the_app.api.nonsense import router as nonsense_router
|
||||||
|
from the_app.api.stuff import router as stuff_router
|
||||||
|
from the_app.api.shakespeare import router as shakespeare_router
|
||||||
|
from the_app.utils import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
app = FastAPI(title="Stuff And Nonsense API", version="0.4")
|
||||||
|
|
||||||
|
app.include_router(stuff_router)
|
||||||
|
app.include_router(nonsense_router)
|
||||||
|
app.include_router(shakespeare_router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
logger.info("Starting up...")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
logger.info("Shutting down...")
|
4
tests/app/models/__init__.py
Normal file
4
tests/app/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# for Alembic and unit tests
|
||||||
|
from the_app.models.stuff import * # noqa
|
||||||
|
from the_app.models.nonsense import * # noqa
|
||||||
|
from the_app.models.shakespeare import * # noqa
|
64
tests/app/models/base.py
Normal file
64
tests/app/models/base.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.ext.declarative import as_declarative, declared_attr
|
||||||
|
|
||||||
|
|
||||||
|
@as_declarative()
|
||||||
|
class BaseReadOnly:
|
||||||
|
id: Any
|
||||||
|
__name__: str
|
||||||
|
# Generate __tablename__ automatically
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def __tablename__(cls) -> str:
|
||||||
|
return cls.__name__.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@as_declarative()
|
||||||
|
class Base:
|
||||||
|
id: Any
|
||||||
|
__name__: str
|
||||||
|
# Generate __tablename__ automatically
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def __tablename__(cls) -> str:
|
||||||
|
return cls.__name__.lower()
|
||||||
|
|
||||||
|
async def save(self, db_session: AsyncSession):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param db_session:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db_session.add(self)
|
||||||
|
return await db_session.commit()
|
||||||
|
except SQLAlchemyError as ex:
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex))
|
||||||
|
|
||||||
|
async def delete(self, db_session: AsyncSession):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param db_session:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await db_session.delete(self)
|
||||||
|
await db_session.commit()
|
||||||
|
return True
|
||||||
|
except SQLAlchemyError as ex:
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex))
|
||||||
|
|
||||||
|
async def update(self, db_session: AsyncSession, **kwargs):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param db_session:
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
await self.save(db_session)
|
42
tests/app/models/nonsense.py
Normal file
42
tests/app/models/nonsense.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import Column, String, select
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from the_app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Nonsense(Base):
|
||||||
|
__tablename__ = "nonsense"
|
||||||
|
__table_args__ = (
|
||||||
|
{"schema": "happy_hog"},
|
||||||
|
)
|
||||||
|
id = Column(UUID(as_uuid=True), unique=True, default=uuid.uuid4, autoincrement=True)
|
||||||
|
name = Column(String, nullable=False, primary_key=True, unique=True)
|
||||||
|
description = Column(String, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, name: str, description: str):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def find(cls, db_session: AsyncSession, name: str):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param db_session:
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stmt = select(cls).where(cls.name == name)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
instance = result.scalars().first()
|
||||||
|
if instance is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail={"Record not found": f"There is no record for requested name value : {name}"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return instance
|
134
tests/app/models/shakespeare.py
Normal file
134
tests/app/models/shakespeare.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
ForeignKey,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
Integer,
|
||||||
|
PrimaryKeyConstraint,
|
||||||
|
String,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
UniqueConstraint,
|
||||||
|
select,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from the_app.models.base import Base
|
||||||
|
|
||||||
|
metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
class Character(Base):
|
||||||
|
__tablename__ = "character"
|
||||||
|
__table_args__ = (PrimaryKeyConstraint("id", name="character_pkey"), {"schema": "shakespeare"})
|
||||||
|
|
||||||
|
id = Column(String(32))
|
||||||
|
name = Column(String(64), nullable=False)
|
||||||
|
speech_count = Column(Integer, nullable=False)
|
||||||
|
abbrev = Column(String(32))
|
||||||
|
description = Column(String(2056))
|
||||||
|
|
||||||
|
work = relationship("Work", secondary="shakespeare.character_work", back_populates="character")
|
||||||
|
paragraph = relationship("Paragraph", back_populates="character")
|
||||||
|
|
||||||
|
|
||||||
|
class Wordform(Base):
|
||||||
|
__tablename__ = "wordform"
|
||||||
|
__table_args__ = (PrimaryKeyConstraint("id", name="wordform_pkey"), {"schema": "shakespeare"})
|
||||||
|
|
||||||
|
id = Column(Integer)
|
||||||
|
plain_text = Column(String(64), nullable=False)
|
||||||
|
phonetic_text = Column(String(64), nullable=False)
|
||||||
|
stem_text = Column(String(64), nullable=False)
|
||||||
|
occurences = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Work(Base):
|
||||||
|
__tablename__ = "work"
|
||||||
|
__table_args__ = (PrimaryKeyConstraint("id", name="work_pkey"), {"schema": "shakespeare"})
|
||||||
|
|
||||||
|
id = Column(String(32))
|
||||||
|
title = Column(String(32), nullable=False)
|
||||||
|
long_title = Column(String(64), nullable=False)
|
||||||
|
year = Column(Integer, nullable=False)
|
||||||
|
genre_type = Column(String(1), nullable=False)
|
||||||
|
source = Column(String(16), nullable=False)
|
||||||
|
total_words = Column(Integer, nullable=False)
|
||||||
|
total_paragraphs = Column(Integer, nullable=False)
|
||||||
|
notes = Column(Text)
|
||||||
|
|
||||||
|
character = relationship("Character", secondary="shakespeare.character_work", back_populates="work")
|
||||||
|
chapter = relationship("Chapter", back_populates="work")
|
||||||
|
paragraph = relationship("Paragraph", back_populates="work")
|
||||||
|
|
||||||
|
|
||||||
|
class Chapter(Base):
|
||||||
|
__tablename__ = "chapter"
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"),
|
||||||
|
PrimaryKeyConstraint("id", name="chapter_pkey"),
|
||||||
|
UniqueConstraint(
|
||||||
|
"work_id", "section_number", "chapter_number", name="chapter_work_id_section_number_chapter_number_key"
|
||||||
|
),
|
||||||
|
{"schema": "shakespeare"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id = Column(Integer)
|
||||||
|
work_id = Column(ForeignKey("shakespeare.work.id"), nullable=False)
|
||||||
|
section_number = Column(Integer, nullable=False)
|
||||||
|
chapter_number = Column(Integer, nullable=False)
|
||||||
|
description = Column(String(256), nullable=False)
|
||||||
|
|
||||||
|
work = relationship("Work", back_populates="chapter")
|
||||||
|
paragraph = relationship("Paragraph", back_populates="chapter")
|
||||||
|
|
||||||
|
|
||||||
|
t_character_work = Table(
|
||||||
|
"character_work",
|
||||||
|
metadata,
|
||||||
|
Column("character_id", ForeignKey("shakespeare.character.id"), nullable=False),
|
||||||
|
Column("work_id", ForeignKey("shakespeare.work.id"), nullable=False),
|
||||||
|
ForeignKeyConstraint(["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"),
|
||||||
|
schema="shakespeare",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Paragraph(Base):
|
||||||
|
__tablename__ = "paragraph"
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["character_id"], ["shakespeare.character.id"], name="paragraph_character_id_fkey"),
|
||||||
|
ForeignKeyConstraint(
|
||||||
|
["work_id", "section_number", "chapter_number"],
|
||||||
|
["shakespeare.chapter.work_id", "shakespeare.chapter.section_number", "shakespeare.chapter.chapter_number"],
|
||||||
|
name="paragraph_chapter_fkey",
|
||||||
|
),
|
||||||
|
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"),
|
||||||
|
PrimaryKeyConstraint("id", name="paragraph_pkey"),
|
||||||
|
{"schema": "shakespeare"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id = Column(Integer)
|
||||||
|
work_id = Column(ForeignKey("shakespeare.work.id"), nullable=False)
|
||||||
|
paragraph_num = Column(Integer, nullable=False)
|
||||||
|
character_id = Column(ForeignKey("shakespeare.character.id"), nullable=False)
|
||||||
|
plain_text = Column(Text, nullable=False)
|
||||||
|
phonetic_text = Column(Text, nullable=False)
|
||||||
|
stem_text = Column(Text, nullable=False)
|
||||||
|
paragraph_type = Column(String(1), nullable=False)
|
||||||
|
section_number = Column(Integer, nullable=False)
|
||||||
|
chapter_number = Column(Integer, nullable=False)
|
||||||
|
char_count = Column(Integer, nullable=False)
|
||||||
|
word_count = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
character = relationship("Character", back_populates="paragraph", lazy="selectin")
|
||||||
|
chapter = relationship("Chapter", back_populates="paragraph", lazy="selectin")
|
||||||
|
work = relationship("Work", back_populates="paragraph", lazy="selectin")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def find(cls, db_session: AsyncSession, character: str):
|
||||||
|
stmt = select(cls).join(Character).join(Chapter).join(Work).where(Character.name == character)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
instance = result.scalars().all()
|
||||||
|
return instance
|
41
tests/app/models/stuff.py
Normal file
41
tests/app/models/stuff.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import Column, String, select
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from the_app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Stuff(Base):
|
||||||
|
__tablename__ = "stuff"
|
||||||
|
__table_args__ = (
|
||||||
|
{"schema": "happy_hog"},
|
||||||
|
)
|
||||||
|
id = Column(UUID(as_uuid=True), unique=True, default=uuid.uuid4, autoincrement=True)
|
||||||
|
name = Column(String, nullable=False, primary_key=True, unique=True)
|
||||||
|
description = Column(String, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, name: str, description: str):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def find(cls, db_session: AsyncSession, name: str):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param db_session:
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stmt = select(cls).where(cls.name == name)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
instance = result.scalars().first()
|
||||||
|
if instance is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail={"Not found": f"There is no record for name: {name}"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return instance
|
0
tests/app/schemas/__init__.py
Normal file
0
tests/app/schemas/__init__.py
Normal file
48
tests/app/schemas/nnonsense.py
Normal file
48
tests/app/schemas/nnonsense.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class NonsenseSchema(BaseModel):
|
||||||
|
name: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
description: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"name": "Name for Some Nonsense",
|
||||||
|
"description": "Some Nonsense Description",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NonsenseResponse(BaseModel):
|
||||||
|
id: UUID = Field(
|
||||||
|
title="Id",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
name: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
description: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"config_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"name": "Name for Some Nonsense",
|
||||||
|
"description": "Some Nonsense Description",
|
||||||
|
}
|
||||||
|
}
|
51
tests/app/schemas/shakespeare.py
Normal file
51
tests/app/schemas/shakespeare.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Character(BaseModel):
|
||||||
|
id: str
|
||||||
|
abbrev: str
|
||||||
|
speech_count: int
|
||||||
|
name: str
|
||||||
|
description: Any
|
||||||
|
|
||||||
|
|
||||||
|
class Chapter(BaseModel):
|
||||||
|
work_id: str
|
||||||
|
section_number: int
|
||||||
|
description: str
|
||||||
|
id: int
|
||||||
|
chapter_number: int
|
||||||
|
|
||||||
|
|
||||||
|
class Work(BaseModel):
|
||||||
|
id: str
|
||||||
|
year: int
|
||||||
|
source: str
|
||||||
|
total_paragraphs: int
|
||||||
|
title: str
|
||||||
|
long_title: str
|
||||||
|
genre_type: str
|
||||||
|
total_words: int
|
||||||
|
notes: Any
|
||||||
|
|
||||||
|
|
||||||
|
class Paragraph(BaseModel):
|
||||||
|
id: int
|
||||||
|
character_id: str
|
||||||
|
phonetic_text: str
|
||||||
|
paragraph_type: str
|
||||||
|
section_number: int
|
||||||
|
char_count: int
|
||||||
|
work_id: str
|
||||||
|
paragraph_num: int
|
||||||
|
plain_text: str
|
||||||
|
stem_text: str
|
||||||
|
chapter_number: int
|
||||||
|
word_count: int
|
||||||
|
character: Character
|
||||||
|
chapter: Chapter
|
||||||
|
work: Work
|
48
tests/app/schemas/stuff.py
Normal file
48
tests/app/schemas/stuff.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class StuffSchema(BaseModel):
|
||||||
|
name: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
description: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"name": "Name for Some Stuff",
|
||||||
|
"description": "Some Stuff Description",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StuffResponse(BaseModel):
|
||||||
|
id: UUID = Field(
|
||||||
|
title="Id",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
name: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
description: str = Field(
|
||||||
|
title="",
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"config_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"name": "Name for Some Stuff",
|
||||||
|
"description": "Some Stuff Description",
|
||||||
|
}
|
||||||
|
}
|
17
tests/app/utils.py
Normal file
17
tests/app/utils.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import logging
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
console = Console(color_system="256", width=200, style="blue")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_logger(module_name):
|
||||||
|
logger = logging.getLogger(module_name)
|
||||||
|
handler = RichHandler(rich_tracebacks=True, console=console, tracebacks_show_locals=True)
|
||||||
|
handler.setFormatter(logging.Formatter("[ %(threadName)s:%(funcName)s:%(lineno)d ] - %(message)s"))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
return logger
|
Loading…
x
Reference in New Issue
Block a user