mirror of
https://github.com/wowlikon/LibraryAPI.git
synced 2025-12-11 21:30:46 +00:00
a
This commit is contained in:
@@ -5,7 +5,8 @@ WORKDIR /code
|
|||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY ./app /code/app
|
|
||||||
COPY alembic.ini ./
|
COPY alembic.ini ./
|
||||||
|
COPY ./app /code/app
|
||||||
|
COPY ./tests /code/tests
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
20
Makefile
Normal file
20
Makefile
Normal file
@@ -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
|
||||||
@@ -6,7 +6,7 @@ DATABASE_URL = config('DATABASE_URL', cast=str, default='sqlite:///./bookapi.db'
|
|||||||
|
|
||||||
# Create database engine
|
# Create database engine
|
||||||
engine = create_engine(str(DATABASE_URL), echo=True)
|
engine = create_engine(str(DATABASE_URL), echo=True)
|
||||||
SQLModel.metadata.create_all(engine)
|
# SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
# Get database session
|
# Get database session
|
||||||
def get_session():
|
def get_session():
|
||||||
|
|||||||
54
app/index.html
Normal file
54
app/index.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Добро пожаловать в API</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Добро пожаловать в API!</h1>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Попробуйте <a href="/docs">Swagger UI</a></p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Попробуйте <a href="/redoc">ReDoc</a></p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
132
app/main.py
132
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 import command
|
||||||
from alembic.config import Config
|
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 sqlmodel import SQLModel, Session, select
|
||||||
from typing import List
|
|
||||||
from .database import engine, get_session
|
from .database import engine, get_session
|
||||||
from .models import Author, AuthorBase, Book, BookBase, AuthorBookLink
|
from .models import Author, AuthorBase, Book, BookBase, AuthorBookLink
|
||||||
|
|
||||||
@@ -20,6 +25,14 @@ app = FastAPI(
|
|||||||
"name": "books",
|
"name": "books",
|
||||||
"description": "Operations with 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")
|
command.upgrade(alembic_cfg, "head")
|
||||||
|
|
||||||
# Root endpoint
|
# Root endpoint
|
||||||
@app.get("/", tags=["authors", "books"])
|
@app.get("/", tags=["misc"])
|
||||||
async def hello_world():
|
async def root(request: Request, html: str = ""):
|
||||||
return {"message": "Hello world!"}
|
|
||||||
|
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
|
# Create an author
|
||||||
@app.post("/authors/", response_model=Author, tags=["authors"])
|
@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
|
# Create a book with authors
|
||||||
@app.post("/books/", response_model=Book, tags=["books"])
|
@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)
|
db_book = Book(title=book.title, description=book.description)
|
||||||
session.add(db_book)
|
session.add(db_book)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(db_book)
|
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
|
return db_book
|
||||||
|
|
||||||
# Read books
|
# Read books
|
||||||
@@ -96,7 +115,7 @@ def read_books(session: Session = Depends(get_session)):
|
|||||||
|
|
||||||
# Update a book with authors
|
# Update a book with authors
|
||||||
@app.put("/books/{book_id}", response_model=Book, tags=["books"])
|
@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)
|
db_book = session.get(Book, book_id)
|
||||||
if not db_book:
|
if not db_book:
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
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
|
db_book.description = book.description
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(db_book)
|
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
|
return db_book
|
||||||
|
|
||||||
# Delete a book
|
# Delete a book
|
||||||
@@ -128,3 +136,79 @@ def delete_book(book_id: int, session: Session = Depends(get_session)):
|
|||||||
session.delete(db_book)
|
session.delete(db_book)
|
||||||
session.commit()
|
session.commit()
|
||||||
return book
|
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
|
||||||
|
|||||||
@@ -21,5 +21,13 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
|
test:
|
||||||
|
build: .
|
||||||
|
command: pytest
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ sqlmodel
|
|||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
python-decouple
|
python-decouple
|
||||||
alembic
|
alembic
|
||||||
|
pytest
|
||||||
|
httpx
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
70
tests/test_main.py
Normal file
70
tests/test_main.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user