diff --git a/Dockerfile b/Dockerfile
index 992d426..1ca502b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,8 @@ WORKDIR /code
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
-COPY ./app /code/app
COPY alembic.ini ./
+COPY ./app /code/app
+COPY ./tests /code/tests
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7939961
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+.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/app/database.py b/app/database.py
index 0199d4a..f6e7d41 100644
--- a/app/database.py
+++ b/app/database.py
@@ -6,7 +6,7 @@ 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)
+# SQLModel.metadata.create_all(engine)
# Get database session
def get_session():
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..dd79520
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,54 @@
+
+
+
+ Добро пожаловать в API
+
+
+
+ Добро пожаловать в API!
+
+
+
diff --git a/app/main.py b/app/main.py
index c09d5d7..e70e791 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,8 +1,13 @@
+from datetime import datetime
+from pathlib import Path
+from typing import List
+
from alembic import command
from alembic.config import Config
-from fastapi import FastAPI, Depends, HTTPException
+from fastapi import FastAPI, Depends, Request, HTTPException
+from fastapi.responses import HTMLResponse, JSONResponse
from sqlmodel import SQLModel, Session, select
-from typing import List
+
from .database import engine, get_session
from .models import Author, AuthorBase, Book, BookBase, AuthorBookLink
@@ -20,6 +25,14 @@ app = FastAPI(
"name": "books",
"description": "Operations with books.",
},
+ {
+ "name": "relations",
+ "description": "Operations with relations.",
+ },
+ {
+ "name": "misc",
+ "description": "Miscellaneous operations.",
+ }
]
)
@@ -32,9 +45,21 @@ def on_startup():
command.upgrade(alembic_cfg, "head")
# Root endpoint
-@app.get("/", tags=["authors", "books"])
-async def hello_world():
- return {"message": "Hello world!"}
+@app.get("/", tags=["misc"])
+async def root(request: Request, html: str = ""):
+
+ if html != "": # API response
+ data = {
+ "title": app.title,
+ "version": app.version,
+ "description": app.description,
+ "status": "ok"
+ }
+ return JSONResponse({"message": "Hello world!", "data": data, "time": datetime.now(), })
+ else: # Browser response
+ with open(Path(__file__).parent / "index.html", 'r', encoding='utf-8') as file:
+ html_content = file.read()
+ return HTMLResponse(html_content)
# Create an author
@app.post("/authors/", response_model=Author, tags=["authors"])
@@ -75,17 +100,11 @@ def delete_author(author_id: int, session: Session = Depends(get_session)):
# Create a book with authors
@app.post("/books/", response_model=Book, tags=["books"])
-def create_book(book: BookBase, author_ids: List[int] | None = None, session: Session = Depends(get_session)):
+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)
- # Create relationships if author_ids are provided
- if author_ids:
- for author_id in author_ids:
- link = AuthorBookLink(author_id=author_id, book_id=db_book.id)
- session.add(link)
- session.commit()
return db_book
# Read books
@@ -96,7 +115,7 @@ def read_books(session: Session = Depends(get_session)):
# Update a book with authors
@app.put("/books/{book_id}", response_model=Book, tags=["books"])
-def update_book(book_id: int, book: BookBase, author_ids: List[int] | None = None, session: Session = Depends(get_session)):
+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")
@@ -105,17 +124,6 @@ def update_book(book_id: int, book: BookBase, author_ids: List[int] | None = Non
db_book.description = book.description
session.commit()
session.refresh(db_book)
- # Update relationships if author_ids are provided
- if author_ids is not None:
- # Clear existing relationships
- existing_links = session.exec(select(AuthorBookLink).where(AuthorBookLink.book_id == book_id)).all()
- for link in existing_links:
- session.delete(link)
- # Create new relationships
- for author_id in author_ids:
- link = AuthorBookLink(author_id=author_id, book_id=db_book.id)
- session.add(link)
- session.commit()
return db_book
# Delete a book
@@ -128,3 +136,79 @@ def delete_book(book_id: int, session: Session = Depends(get_session)):
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/docker-compose.yml b/docker-compose.yml
index e301d3b..c2bf913 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,5 +21,13 @@ services:
depends_on:
- db
+ test:
+ build: .
+ command: pytest
+ environment:
+ DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
+ depends_on:
+ - db
+
volumes:
postgres_data:
diff --git a/requirements.txt b/requirements.txt
index b263068..985a34d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,5 @@ sqlmodel
psycopg2-binary
python-decouple
alembic
+pytest
+httpx
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..1b46e73
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,70 @@
+import pytest # pyright: ignore
+from fastapi.testclient import TestClient
+from app.main import app
+
+@pytest.fixture()
+def client():
+ with TestClient(app) as test_client:
+ yield test_client
+
+# Тесты для авторов
+def test_create_author(client):
+ response = client.post("/authors/", json={"name": "Author Name"})
+ assert response.status_code == 200
+ assert response.json()["name"] == "Author Name"
+
+def test_read_authors(client):
+ response = client.get("/authors/")
+ assert response.status_code == 200
+ assert isinstance(response.json(), list) # Проверяем, что ответ - это список
+
+def test_update_author(client):
+ # Сначала создаем автора, чтобы его обновить
+ create_response = client.post("/authors/", json={"name": "Author Name"})
+ author_id = create_response.json()["id"]
+
+ response = client.put(f"/authors/{author_id}", json={"name": "Updated Author Name"})
+ assert response.status_code == 200
+ assert response.json()["name"] == "Updated Author Name"
+
+def test_delete_author(client):
+ # Сначала создаем автора, чтобы его удалить
+ create_response = client.post("/authors/", json={"name": "Author Name"})
+ author_id = create_response.json()["id"]
+ author_name = create_response.json()["name"]
+
+ response = client.delete(f"/authors/{author_id}")
+ assert response.status_code == 200
+ assert response.json()["name"] == author_name
+
+# Тесты для книг
+def test_create_book(client):
+ response = client.post("/books/", json={"title": "Book Title", "description": "Book Description"})
+ assert response.status_code == 200
+ assert response.json()["title"] == "Book Title"
+
+def test_read_books(client):
+ response = client.get("/books/")
+ assert response.status_code == 200
+ assert isinstance(response.json(), list) # Проверяем, что ответ - это список
+
+def test_update_book(client):
+ # Сначала создаем книгу, чтобы ее обновить
+ create_response = client.post("/books/", json={"title": "Book Title", "description": "Book Description"})
+ book_id = create_response.json()["id"]
+
+ response = client.put(f"/books/{book_id}", json={"title": "Updated Book Title", "description": "Updated Description"})
+ assert response.status_code == 200
+ assert response.json()["title"] == "Updated Book Title"
+
+def test_delete_book(client):
+ # Сначала создаем книгу, чтобы ее удалить
+ create_response = client.post("/books/", json={"title": "Book Title", "description": "Book Description"})
+ book_id = create_response.json()["id"]
+ book_title = create_response.json()["title"]
+ book_description = create_response.json()["description"]
+
+ response = client.delete(f"/books/{book_id}")
+ assert response.status_code == 200
+ assert response.json()["title"] == book_title
+ assert response.json()["description"] == book_description