From 6658d773bf6712e00a0193b24f5bc0fdd732de0f Mon Sep 17 00:00:00 2001 From: wowlikon Date: Tue, 24 Jun 2025 13:30:35 +0300 Subject: [PATCH] Global refactoring of the project to use poetry and implement tests, fixing bugs, changing the handling of dto and db models, preparing to add new functionality --- .env | 4 + .env.example | 3 - .gitignore | 143 +- Dockerfile | 30 +- Makefile | 20 - README.md | 6 +- alembic.ini | 24 +- app/database.py | 14 - app/main.py | 220 --- app/migrations/README | 1 - .../d266fdc61e99_init.cpython-311.pyc | Bin 3408 -> 0 bytes app/models.py | 41 - docker-compose.yml | 35 +- {app => library_service}/__init__.py | 0 library_service/api.py | 6 + library_service/main.py | 28 + library_service/models/__init__.py | 2 + library_service/models/db/__init__.py | 7 + library_service/models/db/author.py | 14 + library_service/models/db/book.py | 14 + library_service/models/db/links.py | 15 + library_service/models/dto/__init__.py | 15 + library_service/models/dto/author.py | 25 + library_service/models/dto/book.py | 30 + .../routers/__init__.py | 3 +- library_service/routers/authors.py | 80 + library_service/routers/books.py | 77 + {src/app => library_service}/routers/misc.py | 18 +- .../routers/relationships.py | 10 +- library_service/settings.py | 52 + {app => library_service}/templates/index.html | 12 +- migrations/README | 1 + {app/migrations => migrations}/env.py | 7 +- {app/migrations => migrations}/script.py.mako | 0 .../versions/d266fdc61e99_init.py | 0 poetry.lock | 1753 +++++++++++++++++ pyproject.toml | 29 + requirements.txt | 9 - src/alembic.ini | 116 -- src/app/__init__.py | 0 src/app/database.py | 14 - src/app/main.py | 45 - src/app/migrations/README | 1 - src/app/migrations/env.py | 81 - src/app/migrations/script.py.mako | 27 - .../migrations/versions/d266fdc61e99_init.py | 54 - src/app/models/__init__.py | 5 - src/app/models/author.py | 20 - src/app/models/book.py | 22 - src/app/models/links.py | 6 - src/app/routers/authors.py | 45 - src/app/routers/books.py | 62 - src/app/templates/index.html | 56 - tests/test_authors.py | 12 + tests/test_books.py | 58 + tests/test_main.py | 70 - tests/test_misc.py | 75 + tests/test_relationships.py | 12 + 58 files changed, 2521 insertions(+), 1008 deletions(-) create mode 100644 .env delete mode 100644 .env.example delete mode 100644 Makefile mode change 100644 => 100755 alembic.ini delete mode 100644 app/database.py delete mode 100644 app/main.py delete mode 100644 app/migrations/README delete mode 100644 app/migrations/versions/__pycache__/d266fdc61e99_init.cpython-311.pyc delete mode 100644 app/models.py rename {app => library_service}/__init__.py (100%) create mode 100644 library_service/api.py create mode 100644 library_service/main.py create mode 100644 library_service/models/__init__.py create mode 100644 library_service/models/db/__init__.py create mode 100644 library_service/models/db/author.py create mode 100644 library_service/models/db/book.py create mode 100644 library_service/models/db/links.py create mode 100644 library_service/models/dto/__init__.py create mode 100644 library_service/models/dto/author.py create mode 100644 library_service/models/dto/book.py rename {src/app => library_service}/routers/__init__.py (85%) create mode 100644 library_service/routers/authors.py create mode 100644 library_service/routers/books.py rename {src/app => library_service}/routers/misc.py (61%) rename {src/app => library_service}/routers/relationships.py (84%) create mode 100644 library_service/settings.py rename {app => library_service}/templates/index.html (82%) create mode 100755 migrations/README rename {app/migrations => migrations}/env.py (91%) mode change 100644 => 100755 rename {app/migrations => migrations}/script.py.mako (100%) mode change 100644 => 100755 rename {app/migrations => migrations}/versions/d266fdc61e99_init.py (100%) create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 src/alembic.ini delete mode 100644 src/app/__init__.py delete mode 100644 src/app/database.py delete mode 100644 src/app/main.py delete mode 100644 src/app/migrations/README delete mode 100644 src/app/migrations/env.py delete mode 100644 src/app/migrations/script.py.mako delete mode 100644 src/app/migrations/versions/d266fdc61e99_init.py delete mode 100644 src/app/models/__init__.py delete mode 100644 src/app/models/author.py delete mode 100644 src/app/models/book.py delete mode 100644 src/app/models/links.py delete mode 100644 src/app/routers/authors.py delete mode 100644 src/app/routers/books.py delete mode 100644 src/app/templates/index.html create mode 100644 tests/test_authors.py create mode 100644 tests/test_books.py delete mode 100644 tests/test_main.py create mode 100644 tests/test_misc.py create mode 100644 tests/test_relationships.py diff --git a/.env b/.env new file mode 100644 index 0000000..610aa73 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +POSTGRES_USER = "postgres" +POSTGRES_PASSWORD = "password" +POSTGRES_DB = "mydatabase" +POSTGRES_SERVER = "db" diff --git a/.env.example b/.env.example deleted file mode 100644 index 787729d..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -POSTGRES_USER=postgres -POSTGRES_PASSWORD=password -POSTGRES_DB=mydatabase diff --git a/.gitignore b/.gitignore index 11a3b84..0db9584 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,141 @@ -!.env.example -.env - +# Byte-compiled / optimized / DLL files __pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +# .env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Jetbrains' IDEs +.idea + +# VS code +.vscode + +# JUPITER +*.ipynb + +# Postgres data +data/ diff --git a/Dockerfile b/Dockerfile index ab9e788..e8d8370 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,22 @@ -FROM python:3.11 +FROM python:3.13 as requirements-stage +WORKDIR /tmp +RUN pip install poetry +RUN poetry self add poetry-plugin-export +COPY ./pyproject.toml ./poetry.lock* /tmp/ +RUN poetry export -f requirements.txt --output requirements.txt --with dev --without-hashes + +FROM python:3.13 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +RUN apt-get update \ + && apt-get -y install gcc postgresql \ + && apt-get clean # netcat + +RUN pip install --upgrade pip WORKDIR /code - -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./src/alembic.ini ./ -COPY ./src/app /code/app -COPY ./tests /code/tests - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +COPY --from=requirements-stage /tmp/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir --upgrade -r ./requirements.txt +COPY . . +ENV PYTHONPATH=. diff --git a/Makefile b/Makefile deleted file mode 100644 index 7939961..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -.PHONY: help build up start down destroy stop restart logs db-shell - -help: - @echo "Available commands:" - @echo " make build - Build the Docker images" - @echo " make up - Start the containers" - @echo " make down - Stop and remove the containers" - @echo " make db-shell - Access the database shell" - -build: - docker-compose -f docker-compose.yml build - -up: - docker-compose -f docker-compose.yml up -d - -down: - docker-compose -f docker-compose.yml down - -db-shell: - docker-compose -f docker-compose.yml exec timescale psql -Upostgres diff --git a/README.md b/README.md index c2bc0fd..45a1bcd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Book API +# LibraryAPI + +## WARNING: The documentation is now out of date. This project is a test web application built using FastAPI, a modern web framework for creating APIs in Python. It showcases the use of Pydantic for data validation, SQLModel for database interactions, Alembic for migration management, PostgreSQL as the database system, and Docker Compose for easy deployment. @@ -18,7 +20,7 @@ For development: 1. Clone the repository: ```bash - git clone https://github.com/wowlikon/bookapi.git + git clone https://github.com/wowlikon/libraryapi.git ``` 2. Navigate to the project directory: diff --git a/alembic.ini b/alembic.ini old mode 100644 new mode 100755 index e63faa7..4da05b0 --- a/alembic.ini +++ b/alembic.ini @@ -2,23 +2,22 @@ [alembic] # path to migration scripts -script_location = app/migrations +script_location = migrations # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. prepend_sys_path = . +path_separator = os # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library. -# Any required deps can installed by adding `alembic[tz]` to the pip requirements -# string value is passed to ZoneInfo() +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() # leave blank for localtime # timezone = @@ -51,16 +50,11 @@ prepend_sys_path = . # version_path_separator = space version_path_separator = os # Use os.pathsep. Default configuration used for new projects. -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - # the output encoding used when revision files # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = driver://user:pass@localhost/dbname +sqlalchemy.url = postgresql+asyncpg://user:pwd@db:5432/foo [post_write_hooks] @@ -74,12 +68,6 @@ sqlalchemy.url = driver://user:pass@localhost/dbname # black.entrypoint = black # black.options = -l 79 REVISION_SCRIPT_FILENAME -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - # Logging configuration [loggers] keys = root,sqlalchemy,alembic diff --git a/app/database.py b/app/database.py deleted file mode 100644 index f6e7d41..0000000 --- a/app/database.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlmodel import create_engine, SQLModel, Session -from decouple import config - -# Get database configuration -DATABASE_URL = config('DATABASE_URL', cast=str, default='sqlite:///./bookapi.db') - -# Create database engine -engine = create_engine(str(DATABASE_URL), echo=True) -# SQLModel.metadata.create_all(engine) - -# Get database session -def get_session(): - with Session(engine) as session: - yield session diff --git a/app/main.py b/app/main.py deleted file mode 100644 index a072847..0000000 --- a/app/main.py +++ /dev/null @@ -1,220 +0,0 @@ -from datetime import datetime -from pathlib import Path -from typing import Dict, List - -from alembic import command -from alembic.config import Config -from fastapi import FastAPI, Depends, Request, HTTPException -from fastapi.responses import HTMLResponse, JSONResponse -from fastapi.templating import Jinja2Templates -from sqlmodel import SQLModel, Session, select - -from .database import engine, get_session -from .models import Author, AuthorBase, Book, BookBase, AuthorBookLink - -alembic_cfg = Config("alembic.ini") -templates = Jinja2Templates(directory=Path(__file__).parent / "templates") -app = FastAPI( - title="LibraryAPI", - description="This is a sample API for managing authors and books.", - version="1.0.1", - openapi_tags=[ - { - "name": "authors", - "description": "Operations with authors.", - }, - { - "name": "books", - "description": "Operations with books.", - }, - { - "name": "relations", - "description": "Operations with relations.", - }, - { - "name": "misc", - "description": "Miscellaneous operations.", - } - ] -) - -def get_info() -> Dict: - return { - "status": "ok", - "app_info": { - "title": app.title, - "version": app.version, - "description": app.description, - }, - "server_time": datetime.now().isoformat(), - } - -# Initialize the database -@app.on_event("startup") -def on_startup(): - # Apply database migrations - with engine.begin() as connection: - alembic_cfg.attributes['connection'] = connection - command.upgrade(alembic_cfg, "head") - -# Root endpoint -@app.get("/", response_class=HTMLResponse) -async def root(request: Request): - return templates.TemplateResponse("index.html", {"request": request, "data": get_info()}) - -# API Information endpoint -@app.get("/api/info", tags=["misc"]) -async def api_info(): - return JSONResponse(content=get_info()) - -# Create an author -@app.post("/authors/", response_model=Author, tags=["authors"]) -def create_author(author: AuthorBase, session: Session = Depends(get_session)): - db_author = Author(name=author.name) - session.add(db_author) - session.commit() - session.refresh(db_author) - return db_author - -# Read authors -@app.get("/authors/", response_model=List[Author], tags=["authors"]) -def read_authors(session: Session = Depends(get_session)): - authors = session.exec(select(Author)).all() - return authors - -# Update an author -@app.put("/authors/{author_id}", response_model=Author, tags=["authors"]) -def update_author(author_id: int, author: AuthorBase, session: Session = Depends(get_session)): - db_author = session.get(Author, author_id) - if not db_author: - raise HTTPException(status_code=404, detail="Author not found") - db_author.name = author.name - session.commit() - session.refresh(db_author) - return db_author - -# Delete an author -@app.delete("/authors/{author_id}", response_model=AuthorBase, tags=["authors"]) -def delete_author(author_id: int, session: Session = Depends(get_session)): - db_author = session.get(Author, author_id) - if not db_author: - raise HTTPException(status_code=404, detail="Author not found") - session.delete(db_author) - author = AuthorBase(name=db_author.name) - session.commit() - return author - -# Create a book with authors -@app.post("/books/", response_model=Book, tags=["books"]) -def create_book(book: BookBase, session: Session = Depends(get_session)): - db_book = Book(title=book.title, description=book.description) - session.add(db_book) - session.commit() - session.refresh(db_book) - return db_book - -# Read books -@app.get("/books/", response_model=List[Book], tags=["books"]) -def read_books(session: Session = Depends(get_session)): - books = session.exec(select(Book)).all() - return books - -# Update a book with authors -@app.put("/books/{book_id}", response_model=Book, tags=["books"]) -def update_book(book_id: int, book: BookBase, session: Session = Depends(get_session)): - db_book = session.get(Book, book_id) - if not db_book: - raise HTTPException(status_code=404, detail="Book not found") - - db_book.title = book.title - db_book.description = book.description - session.commit() - session.refresh(db_book) - return db_book - -# Delete a book -@app.delete("/books/{book_id}", response_model=BookBase, tags=["books"]) -def delete_book(book_id: int, session: Session = Depends(get_session)): - db_book = session.get(Book, book_id) - if not db_book: - raise HTTPException(status_code=404, detail="Book not found") - book = Book(title=db_book.title, description=db_book.description) - session.delete(db_book) - session.commit() - return book - -# Add author to book -@app.post("/relationships/", response_model=AuthorBookLink, tags=["relations"]) -def add_author_to_book(author_id: int, book_id: int, session: Session = Depends(get_session)): - # Check if author and book exist - author = session.get(Author, author_id) - if not author: - raise HTTPException(status_code=404, detail="Author not found") - - book = session.get(Book, book_id) - if not book: - raise HTTPException(status_code=404, detail="Book not found") - - # Check if relationship already exists - existing_link = session.exec( - select(AuthorBookLink) - .where(AuthorBookLink.author_id == author_id) - .where(AuthorBookLink.book_id == book_id) - ).first() - - if existing_link: - raise HTTPException(status_code=400, detail="Relationship already exists") - - # Create new relationship - link = AuthorBookLink(author_id=author_id, book_id=book_id) - session.add(link) - session.commit() - session.refresh(link) - return link - -# Remove author from book -@app.delete("/relationships/", tags=["relations"]) -def remove_author_from_book(author_id: int, book_id: int, session: Session = Depends(get_session)): - # Find the relationship - link = session.exec( - select(AuthorBookLink) - .where(AuthorBookLink.author_id == author_id) - .where(AuthorBookLink.book_id == book_id) - ).first() - - if not link: - raise HTTPException(status_code=404, detail="Relationship not found") - - session.delete(link) - session.commit() - return {"message": "Relationship removed successfully"} - -# Get all authors for a book -@app.get("/books/{book_id}/authors/", response_model=List[Author], tags=["books", "relations"]) -def get_authors_for_book(book_id: int, session: Session = Depends(get_session)): - book = session.get(Book, book_id) - if not book: - raise HTTPException(status_code=404, detail="Book not found") - - authors = session.exec( - select(Author) - .join(AuthorBookLink) - .where(AuthorBookLink.book_id == book_id) - ).all() - - return authors - -# Get all books for an author -@app.get("/authors/{author_id}/books/", response_model=List[Book], tags=["authors", "relations"]) -def get_books_for_author(author_id: int, session: Session = Depends(get_session)): - author = session.get(Author, author_id) - if not author: - raise HTTPException(status_code=404, detail="Author not found") - - books = session.exec( - select(Book) - .join(AuthorBookLink) - .where(AuthorBookLink.author_id == author_id) - ).all() - - return books diff --git a/app/migrations/README b/app/migrations/README deleted file mode 100644 index 98e4f9c..0000000 --- a/app/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/app/migrations/versions/__pycache__/d266fdc61e99_init.cpython-311.pyc b/app/migrations/versions/__pycache__/d266fdc61e99_init.cpython-311.pyc deleted file mode 100644 index c54e06a0a562151a058a537f94738628aec0b52b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3408 zcmcguO-vg{6kglowOMQk)G?y`gaCDMQ~peCenJ|x0aPF%s5DaJRx?jsp_FOQkqK+Ro|@bHKrvEYLxNJ^USn$LFYbqjl*1+!xn6fTN2i=HDL?eENpCx zql7(dH~J{fhaE77;?A%W+rt$^AeERO1`Qa6UBpHNjOMKMMH?J&9R;qNafy`(B`(O? zN+XbU7G$elL$<<@b>qr=C|r%**aiQmFq+}~Rd;4YNt6ZQb8<^ei%F?vd}OEv2STAa z91Hc5!NEL-q=#CBOO!-q(lP>Ms3p)72zK@aI|KbKy#qr%9}NWpU4j0=p1z7w(iEs+ft-7oTX7Iwh)O&XGuJ*3z(#c zgwv6vr1O->87jq0??I;$nhn~)62$91CtFI)MYE4Bvi;CJlQ#;smdzI<&g}qqw;%(N zvpqsPux8g>SGLNW#dg`qru@Q1zNlakGAm!UoyFea{}WlOD6YiT9XQ{vdBz>s{jPZe zOSvb^eU%6Uk>izGP21Rcj7Q$Ae=FXCe@oF6-h#E`qY3%CYv$|; zqgi<*n$8nO({)TV-DUG;uAL}2D>3&tLH5{R`46Nd?~X~lC1*SQ?HaQ z#l?-Q%60C*z1}tNHlRKSh*~qH^U;jFkfgd*#A5(?PAL-y2#XUPNznw+UE)$CFA)(j z@cE1+0!->?HkrJk+htJ((Mn9xF)F5HfK6S16^se;dR2ZY8^%THhF)Q+(j5hvJB7I} z5#KT9pyPK_mP5KLW`KA^Ha28UM|J*EGM-6Dx?^0D$vmODGkr6jNMaJ#ZO}7@@^Xp* z0DqE^lQS|ErFq>mO~pi%E>Du>OGzm$)2JxPrgl-nWJ$No>7KD9CE~nPCiVk&z=HBJ z&>aIa-O*I4J0Z@~C_8Jq`xc?>Axd{2ek3C7mFh|@>yAtcBr&0W(BOrZegfFD!u^K$ zdso*!`x*YmSMkQD>l5EyUca0TDyUaQy&CGxbLKV{*5jJ@+%s?JsW+s02Q=@%V-Olw z(YS`jK}bUlnc9mvlA$pP7C1cS$aX92Y`Z}A8F}FO}hMpZPbAuu;; zY*+EL=MCO?7}y$mFuXOK11ke68qm-{o+Cc2-Fo*y<5pwNuAq>LLK+Gc?0>a+B|DkD zprZ2{I$xN(zB!roXB$-1uc7|J++?<1MSU9TdtS-2odVe}C-i-YXbhTJSkf|}IB142 zAOEPA!`u;&EP;UeOmW;|34V?|pT;9!1|CQBnOd;zf7!K{uf{xlT)dv$Bad_edK^=< z4qQdqwV>|+0dNtFiymRXLu=T)o6Vy<7zmgqQw1o<_l=87+nJz`5f%^H0YtB0-)8x% z*9GHlXzANP@>lPQQroJcHVw5YsEsAem5tW*_O-ieZM#<6p7WZ?1RaCD=2KC^g`+?> Dict: return { "status": "ok", @@ -23,10 +29,10 @@ def get_info(app) -> Dict: # Root endpoint @router.get("/", response_class=HTMLResponse) -async def root(request: Request, app=None): - return templates.TemplateResponse("index.html", {"request": request, "data": get_info(app)}) +async def root(request: Request, app=Depends(get_app)): + return templates.TemplateResponse(request, "index.html", get_info(app)) # API Information endpoint @router.get("/api/info") -async def api_info(app=None): - return JSONResponse(content=get_info(app)) \ No newline at end of file +async def api_info(app=Depends(get_app)): + return JSONResponse(content=get_info(app)) diff --git a/src/app/routers/relationships.py b/library_service/routers/relationships.py similarity index 84% rename from src/app/routers/relationships.py rename to library_service/routers/relationships.py index 42c3091..9aa45b4 100644 --- a/src/app/routers/relationships.py +++ b/library_service/routers/relationships.py @@ -2,15 +2,14 @@ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from typing import List, Dict -from ..database import get_session -from ..models import Author, Book, AuthorBookLink +from library_service.settings import get_session +from library_service.models.db import Book, Author, AuthorBookLink router = APIRouter(prefix="/relationships", tags=["relations"]) # Add author to book @router.post("/", response_model=AuthorBookLink) def add_author_to_book(author_id: int, book_id: int, session: Session = Depends(get_session)): - # Check if author and book exist author = session.get(Author, author_id) if not author: raise HTTPException(status_code=404, detail="Author not found") @@ -19,7 +18,6 @@ def add_author_to_book(author_id: int, book_id: int, session: Session = Depends( if not book: raise HTTPException(status_code=404, detail="Book not found") - # Check if relationship already exists existing_link = session.exec( select(AuthorBookLink) .where(AuthorBookLink.author_id == author_id) @@ -29,7 +27,6 @@ def add_author_to_book(author_id: int, book_id: int, session: Session = Depends( if existing_link: raise HTTPException(status_code=400, detail="Relationship already exists") - # Create new relationship link = AuthorBookLink(author_id=author_id, book_id=book_id) session.add(link) session.commit() @@ -39,7 +36,6 @@ def add_author_to_book(author_id: int, book_id: int, session: Session = Depends( # Remove author from book @router.delete("/", response_model=Dict[str, str]) def remove_author_from_book(author_id: int, book_id: int, session: Session = Depends(get_session)): - # Find the relationship link = session.exec( select(AuthorBookLink) .where(AuthorBookLink.author_id == author_id) @@ -51,4 +47,4 @@ def remove_author_from_book(author_id: int, book_id: int, session: Session = Dep session.delete(link) session.commit() - return {"message": "Relationship removed successfully"} \ No newline at end of file + return {"message": "Relationship removed successfully"} diff --git a/library_service/settings.py b/library_service/settings.py new file mode 100644 index 0000000..5177490 --- /dev/null +++ b/library_service/settings.py @@ -0,0 +1,52 @@ +import os +from dotenv import load_dotenv +from fastapi import FastAPI +from sqlmodel import create_engine, SQLModel, Session +from toml import load + +load_dotenv() + +with open("pyproject.toml") as f: + config = load(f) + +# Dependency to get the FastAPI application instance +def get_app() -> FastAPI: + return FastAPI( + title=config["tool"]["poetry"]["name"], + description=config["tool"]["poetry"]["description"], + version=config["tool"]["poetry"]["version"], + openapi_tags=[ + { + "name": "authors", + "description": "Operations with authors.", + }, + { + "name": "books", + "description": "Operations with books.", + }, + { + "name": "relations", + "description": "Operations with relations.", + }, + { + "name": "misc", + "description": "Miscellaneous operations.", + } + ] + ) + +USER = os.getenv("POSTGRES_USER") +PASSWORD = os.getenv("POSTGRES_PASSWORD") +DATABASE = os.getenv("POSTGRES_DB") +HOST = os.getenv("POSTGRES_SERVER") + +if not USER or not PASSWORD or not DATABASE or not HOST: + raise ValueError("Missing environment variables") + +POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:5432/{DATABASE}" +engine = create_engine(POSTGRES_DATABASE_URL, echo=True, future=True) + +# Dependency to get a database session +def get_session(): + with Session(engine) as session: + yield session diff --git a/app/templates/index.html b/library_service/templates/index.html similarity index 82% rename from app/templates/index.html rename to library_service/templates/index.html index c9b6997..4c37657 100644 --- a/app/templates/index.html +++ b/library_service/templates/index.html @@ -3,7 +3,7 @@ - {{ data.title }} + {{ app_info.title }} -

Welcome to {{ data.title }}!

-

Description: {{ data.description }}

-

Version: {{ data.version }}

-

Current Time: {{ data.time }}

-

Status: {{ data.status }}

+

Welcome to {{ app_info.title }}!

+

Description: {{ app_info.description }}

+

Version: {{ app_info.version }}

+

Current Time: {{ server_time }}

+

Status: {{ status }}