From cd4cc341a5b344f0b84b5d753bdbd6195751241f Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:40:11 +0200 Subject: [PATCH 1/9] temp isort drop --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 741e439..de99088 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ ENV PYTHONPATH=/home/code/ PYTHONHASHSEED=0 COPY tests/ tests/ COPY app/ app/ COPY alembic/ alembic/ -COPY .env alembic.ini ./ +COPY .env alembic.ini config.ini ./ # create a non-root user and switch to it, for security. RUN addgroup --system --gid 1001 "app-user" From 0b1dc340374f6ad19862cb17a8e4451ddc3b7144 Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:40:22 +0200 Subject: [PATCH 2/9] add logger config file --- config.ini | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 config.ini diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..cae41bc --- /dev/null +++ b/config.ini @@ -0,0 +1,55 @@ +[loggers] +keys = root, sqlalchemy + +[handlers] +keys = console, console_rich, error_file, access_file + +[formatters] +keys = generic, generic_rich, access + +[logger_root] +; Logging level for all loggers +level = NOTSET +handlers = console_rich, error_file + +[logger_sqlalchemy] +handlers = +qualname = sqlalchemy.engine + +[handler_console] +class = logging.StreamHandler +level = NOTSET +formatter = generic +stram = ext://sys.stdout + +[handler_error_file] +class = logging.FileHandler +formatter = generic +level = WARNING +args = ('/tmp/error.log','w') + +[handler_access_file] +class = logging.FileHandler +formatter = access +args = ('/tmp/access.log',) + +[formatter_generic] +format = [%(process)d|%(name)-12s|%(filename)s:%(lineno)d] %(levelname)-7s %(message)s +datefmt = %H:%M:%S +class = logging.Formatter + +[formatter_access] +format = %(message)s +class = logging.Formatter + + +[formatter_generic_rich] +format = [%(process)d %(name)s] %(message)s +datefmt = %H:%M:%S +class = logging.Formatter + +[handler_console_rich] +class = app.logging.RichConsoleHandler +args = (100, "blue") +kwargs = {"omit_repeated_times":False, "show_time": False, "enable_link_path": True, "tracebacks_show_locals": True} +level = NOTSET From 525c8c94b2e5f0e2d514cb6a8ad818f739a437fd Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:40:36 +0200 Subject: [PATCH 3/9] add logger config file --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 099f066..d43b30f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,8 @@ services: - .env - .secrets command: bash -c " - alembic upgrade head && uvicorn app.main:app + uvicorn app.main:app + --log-config ./config.ini --host 0.0.0.0 --port 8080 --lifespan=on --use-colors --loop uvloop --http httptools --reload --log-level debug From 8af785be98d8c29a44fb0577c4693dff031858ec Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:41:30 +0200 Subject: [PATCH 4/9] add singleton design pattern --- app/utils.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/app/utils.py b/app/utils.py index ec35e63..0c94176 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,21 +1,18 @@ -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") +from threading import Lock -@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 +class SingletonMeta(type): + """ + This is a thread-safe implementation of Singleton. + """ + + _instances = {} + + _lock: Lock = Lock() + + def __call__(cls, *args, **kwargs): + with cls._lock: + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] From 30bb877fccf2c97d04a8267b90b454ddb6f487a2 Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:41:45 +0200 Subject: [PATCH 5/9] drop isort --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index b5ee6e5..a2a7038 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,6 @@ lint: ## Lint project code. .PHONY: format format: ## Format project code. - isort app tests black app tests --line-length=120 .PHONY: slim-build From 429909344d58bb856fd949bab9d92384e978dc25 Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:42:00 +0200 Subject: [PATCH 6/9] unify logging with rich --- app/logging.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/logging.py diff --git a/app/logging.py b/app/logging.py new file mode 100644 index 0000000..9815e7c --- /dev/null +++ b/app/logging.py @@ -0,0 +1,22 @@ +import logging + +from rich.console import Console +from rich.logging import RichHandler + + +from app.utils import SingletonMeta + + +class AppLogger(metaclass=SingletonMeta): + _logger = None + + def __init__(self): + self._logger = logging.getLogger(__name__) + + def get_logger(self): + return self._logger + + +class RichConsoleHandler(RichHandler): + def __init__(self, width=200, style=None, **kwargs): + super().__init__(console=Console(color_system="256", width=width, style=style), **kwargs) From aa1b9ec867aee334e3d3725fc20e47890a1b1794 Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:42:06 +0200 Subject: [PATCH 7/9] unify logging with rich --- app/api/stuff.py | 26 ++++++++------------------ app/database.py | 11 ++++------- app/main.py | 4 ++-- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/app/api/stuff.py b/app/api/stuff.py index b28df6c..9de1917 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -5,39 +5,29 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.stuff import Stuff from app.schemas.stuff import StuffResponse, StuffSchema -from app.utils import get_logger +from app.logging import AppLogger + +logger = AppLogger.__call__().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) -): +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 - ] + 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) - ) from ex + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex 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 @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(name=payload.name, description=payload.description) await stuff.save(db_session) return stuff diff --git a/app/database.py b/app/database.py index b0c2783..621cbe1 100644 --- a/app/database.py +++ b/app/database.py @@ -4,11 +4,10 @@ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from app import config -from app.utils import get_logger - -logger = get_logger(__name__) +from app.logging import AppLogger global_settings = config.get_settings() +logger = AppLogger.__call__().get_logger() engine = create_async_engine( global_settings.asyncpg_url, @@ -18,13 +17,11 @@ engine = create_async_engine( # expire_on_commit=False will prevent attributes from being expired # after commit. -AsyncSessionFactory = sessionmaker( - engine, autoflush=False, expire_on_commit=False, class_=AsyncSession -) +AsyncSessionFactory = sessionmaker(engine, autoflush=False, expire_on_commit=False, class_=AsyncSession) # Dependency async def get_db() -> AsyncGenerator: async with AsyncSessionFactory() as session: - logger.debug(f"ASYNC Pool: {engine.pool.status()}") + # logger.debug(f"ASYNC Pool: {engine.pool.status()}") yield session diff --git a/app/main.py b/app/main.py index 80abb46..16ace8e 100644 --- a/app/main.py +++ b/app/main.py @@ -3,9 +3,9 @@ from fastapi import FastAPI from app.api.nonsense import router as nonsense_router from app.api.shakespeare import router as shakespeare_router from app.api.stuff import router as stuff_router -from app.utils import get_logger +from app.logging import AppLogger -logger = get_logger(__name__) +logger = AppLogger.__call__().get_logger() app = FastAPI(title="Stuff And Nonsense API", version="0.5") From ee1d241d2371a930c0305510ca27be58fc4933a2 Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:42:13 +0200 Subject: [PATCH 8/9] format --- app/api/nonsense.py | 14 +++++++++++--- app/config.py | 1 + app/models/base.py | 8 ++------ app/models/nonsense.py | 4 +--- app/models/shakespeare.py | 28 ++++++---------------------- 5 files changed, 21 insertions(+), 34 deletions(-) diff --git a/app/api/nonsense.py b/app/api/nonsense.py index 2638b48..7cf7ac6 100644 --- a/app/api/nonsense.py +++ b/app/api/nonsense.py @@ -9,9 +9,7 @@ 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) -): +async def create_nonsense(payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)): nonsense = Nonsense(**payload.dict()) await nonsense.save(db_session) return nonsense @@ -40,3 +38,13 @@ async def update_nonsense( nonsense = await Nonsense.find(db_session, name) await nonsense.update(db_session, **payload.dict()) return nonsense + + +@router.post("/", response_model=NonsenseResponse) +async def merge_nonsense( + payload: NonsenseSchema, + db_session: AsyncSession = Depends(get_db), +): + nonsense = Nonsense(**payload.dict()) + await nonsense.save_or_update(db_session) + return nonsense diff --git a/app/config.py b/app/config.py index 096d9b6..ded6e05 100644 --- a/app/config.py +++ b/app/config.py @@ -19,4 +19,5 @@ class Settings(BaseSettings): def get_settings(): return Settings() + settings = get_settings() diff --git a/app/models/base.py b/app/models/base.py index e359aad..028e0fe 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -38,9 +38,7 @@ class Base: 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) - ) from ex + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex async def delete(self, db_session: AsyncSession): """ @@ -53,9 +51,7 @@ class Base: await db_session.commit() return True 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_session: AsyncSession, **kwargs): """ diff --git a/app/models/nonsense.py b/app/models/nonsense.py index ffeb493..5f4477d 100644 --- a/app/models/nonsense.py +++ b/app/models/nonsense.py @@ -29,9 +29,7 @@ class Nonsense(Base): 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}" - }, + detail={"Record not found": f"There is no record for requested name value : {name}"}, ) else: return instance diff --git a/app/models/shakespeare.py b/app/models/shakespeare.py index b4e0d65..a42dd9e 100644 --- a/app/models/shakespeare.py +++ b/app/models/shakespeare.py @@ -31,9 +31,7 @@ class Character(Base): abbrev = Column(String(32)) description = Column(String(2056)) - work = relationship( - "Work", secondary="shakespeare.character_work", back_populates="character" - ) + work = relationship("Work", secondary="shakespeare.character_work", back_populates="character") paragraph = relationship("Paragraph", back_populates="character") @@ -68,9 +66,7 @@ class Work(Base): total_paragraphs = Column(Integer, nullable=False) notes = Column(Text) - character = relationship( - "Character", secondary="shakespeare.character_work", back_populates="work" - ) + character = relationship("Character", secondary="shakespeare.character_work", back_populates="work") chapter = relationship("Chapter", back_populates="work") paragraph = relationship("Paragraph", back_populates="work") @@ -78,9 +74,7 @@ class Work(Base): class Chapter(Base): __tablename__ = "chapter" __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"), UniqueConstraint( "work_id", @@ -111,9 +105,7 @@ t_character_work = Table( ["shakespeare.character.id"], name="character_work_character_id_fkey", ), - ForeignKeyConstraint( - ["work_id"], ["shakespeare.work.id"], name="character_work_work_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", ) @@ -136,9 +128,7 @@ class Paragraph(Base): ], 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"), {"schema": "shakespeare"}, ) @@ -162,13 +152,7 @@ class Paragraph(Base): @classmethod async def find(cls, db_session: AsyncSession, character: str): - 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) instance = result.scalars().all() return instance From 8a87f15ff5d97d0f9db660bf90a683c25dfffd4e Mon Sep 17 00:00:00 2001 From: Jakub Miazek Date: Mon, 10 Apr 2023 21:43:33 +0200 Subject: [PATCH 9/9] update change log --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7215cf4..dfa364c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Hope you enjoy it. - 3 OCT 2022 poetry added to project - 12 NOV 2022 ruff implemented to project as linting tool - 14 FEB 2023 bump project to Python 3.11 +- 10 APR 2023 implement logging with rich ### Local development with poetry