Compare commits

..

2 Commits

13 changed files with 146 additions and 141 deletions

56
.gitignore vendored
View File

@@ -3,9 +3,6 @@ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions
*.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
@@ -27,12 +24,6 @@ share/python-wheels/
*.egg *.egg
MANIFEST 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 # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
@@ -51,36 +42,6 @@ coverage.xml
.hypothesis/ .hypothesis/
.pytest_cache/ .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 # pyenv
.python-version .python-version
@@ -94,13 +55,6 @@ ipython_config.py
# PEP 582; used by e.g. github.com/David-OConnor/pyflow # PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/ __pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments # Environments
# .env # .env
.venv .venv
@@ -110,16 +64,9 @@ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation
/site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
@@ -134,8 +81,5 @@ dmypy.json
# VS code # VS code
.vscode .vscode
# JUPITER
*.ipynb
# Postgres data # Postgres data
data/ data/

114
README.md
View File

@@ -1,98 +1,100 @@
# LibraryAPI # LibraryAPI
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. Это проект приложения на FastAPI - современном веб фреймворке для создания API на Python. Я использую Pydantic для валидации данных, SQLModel для взаимодействия с базой данных, Alembic для управления миграциями, PostgreSQL как систему базы данных и Docker Compose для легкого развертывания.
### **Key Components:** ### **Ключевые элементы:**
1. FastAPI: Provides high performance and simplicity for developing RESTful APIs, supporting asynchronous operations and automatic documentation generation. 1. FastAPI: Предоставляет высокопроизводительность и простоту для разработки RESTful API, поддерживает асинхронные операции и автоматическую генерацию документации.
2. Pydantic: Used for data validation and serialization, allowing easy definition of data schemas. 2. Pydantic: Используется для валидации данных и сериализации, позволяет легко определить схемы данных.
3. SQLModel: Combines SQLAlchemy and Pydantic, enabling database operations with Python classes. 3. SQLModel: Объединяет SQLAlchemy и Pydantic, включая операции с базой данных с помощью классов Python.
4. Alembic: A tool for managing database migrations, making it easy to track and apply changes to the database schema. 4. Alembic: Инструмент для управления миграциями базы данных, упрощающий отслеживание и применение изменений в схеме базы данных.
5. PostgreSQL: A reliable relational database used for data storage. 5. PostgreSQL: Надежная реляционная база данных для хранения данных.
6. Docker Compose: Simplifies the deployment of the application and its dependencies in containers. 6. Docker Compose: Упрощает развертывание приложения и его зависимостей в контейнерах.
### **Installation Instructions** ### **Инструкция по установке**
For development: 1. Клонируйте репозиторий:
1. Clone the repository:
```bash ```bash
git clone https://github.com/wowlikon/libraryapi.git git clone https://github.com/wowlikon/libraryapi.git
``` ```
2. Navigate to the project directory: 2. Перейдите в каталог проекта:
```bash ```bash
cd libraryapi cd libraryapi
``` ```
3. Configure environment variables: 3. Настройте переменные окружения:
```bash ```bash
edit .env edit .env
``` ```
4. Build the Docker containers: 4. Соберите контейнеры Docker:
```bash ```bash
docker compose build docker compose build
``` ```
5. Run the application: 5. Запустите приложение:
```bash ```bash
docker compose up api docker compose up api
``` ```
For make new migrations: Для создания новых миграций:
```bash ```bash
docker compose run --rm -T api alembic revision --autogenerate -m "Migration name" docker compose run --rm -T api alembic revision --autogenerate -m "Migration name"
``` ```
For run tests: Для запуска тестов:
```bash ```bash
docker compose up test docker compose up test
``` ```
### **API Endpoints** ### **Эндпоинты API**
**Authors** **Авторы**
| Method | Endpoint | Description | | Метод | Эндпоинты | Описание |
|--------|-----------------------|------------------------------------------------| |--------|-----------------------|---------------------------------------------|
| POST | `/authors` | Create a new author | | POST | `/authors` | Создать нового автора |
| GET | `/authors` | Retrieve a list of all authors | | GET | `/authors` | Получить список всех авторов |
| GET | `/authors/{id}` | Retrieve a specific author by ID with books | | GET | `/authors/{id}` | Получить конкретного автора по ID с книгами |
| PUT | `/authors/{id}` | Update a specific author by ID | | PUT | `/authors/{id}` | Обновить конкретного автора по ID |
| DELETE | `/authors/{id}` | Delete a specific author by ID | | DELETE | `/authors/{id}` | Удалить конкретного автора по ID |
| GET | `/authors/{id}/books` | Retrieve a list of books for a specific author | | GET | `/authors/{id}/books` | Получить список книг для конкретного автора |
**Books** **Книги**
| Method | Endpoint | Description | | Метод | Эндпоинты | Описание |
|--------|-----------------------|------------------------------------------------| |--------|-----------------------|----------------------------------------------|
| POST | `/books` | Create a new book | | POST | `/books` | Создать новую книгу |
| GET | `/books` | Retrieve a list of all books | | GET | `/books` | Получить список всех книг |
| GET | `/book/{id}` | Retrieve a specific book by ID with authors | | GET | `/book/{id}` | Получить конкретную книгу по ID с авторами |
| PUT | `/books/{id}` | Update a specific book by ID | | PUT | `/books/{id}` | Обновить конкретную книгу по ID |
| DELETE | `/books/{id}` | Delete a specific book by ID | | DELETE | `/books/{id}` | Удалить конкретную книгу по ID |
| GET | `/books/{id}/authors` | Retrieve a list of authors for a specific book | | GET | `/books/{id}/authors` | Получить список авторов для конкретной книги |
**Relationships** **Жанры**
| Method | Endpoint | Description | | Метод | Эндпоинты | Описание |
|--------|-----------------------|----------------------------------------------|
| POST | `/genres` | Создать новый жанр |
| GET | `/genres` | Получить список всех жанров |
| GET | `/genres/{id}` | Получить конкретный жанр по ID |
| PUT | `/genres/{id}` | Обновить конкретный жанр по ID |
| DELETE | `/genres/{id}` | Удалить конкретный жанр по ID |
| GET | `/books/{id}/genres` | Получить список жанров для конкретной книги |
**Связи**
| Метод | Эндпоинты | Описание |
|--------|------------------------------|-----------------------------------------| |--------|------------------------------|-----------------------------------------|
| GET | `/relationships/author-book` | Retrieve a list of all relationships | | GET | `/relationships/author-book` | Получить список всех связей автор-книга |
| POST | `/relationships/author-book` | Add author-book relationship | | POST | `/relationships/author-book` | Добавить связь автор-книга |
| DELETE | `/relationships/author-book` | Remove author-book relationship | | DELETE | `/relationships/author-book` | Удалить связь автор-книга |
### **Technologies Used** ### **Используемые технологии**
- **FastAPI**: A modern web framework for building APIs with Python, known for its speed and ease of use. - **FastAPI**: Современный web фреймворк для построения API с использованием Python, известный своей скоростью и простотой использования.
- **Pydantic**: A data validation and settings management library that uses Python type annotations. - **Pydantic**: Библиотека для валидации данных и управления настройками, использующая аннотации типов Python.
- **SQLModel**: A library for interacting with databases using Python classes, combining the features of SQLAlchemy and Pydantic. - **SQLModel**: Библиотека для взаимодействия с базами данных с использованием классов Python, объединяющая функции SQLAlchemy и Pydantic.
- **Alembic**: A lightweight database migration tool for use with SQLAlchemy. - **Alembic**: Легковесный инструмент для миграции базы данных на основе SQLAlchemy.
- **PostgreSQL**: A powerful, open-source relational database management system. - **PostgreSQL**: Сильная, открытая реляционная система управления базами данных.
- **Docker**: A platform for developing, shipping, and running applications in containers. - **Docker**: Платформа для разработки, распространения и запуска приложений в контейнерах.
- **Docker Compose**: A tool for defining and running multi-container Docker applications. - **Docker Compose**: Инструмент для определения и запуска многоконтейнерных приложений Docker.
### **TODO List**
- Geners table and endpoints
- Update tests

0
alembic.ini Executable file → Normal file
View File

View File

@@ -1,7 +1,7 @@
services: services:
db: db:
container_name: db container_name: db
image: postgres image: postgres:17
expose: expose:
- 5432 - 5432
volumes: volumes:

View File

@@ -11,18 +11,20 @@ from .routers.misc import get_info
app = get_app() app = get_app()
alembic_cfg = Config("alembic.ini") alembic_cfg = Config("alembic.ini")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
print("[+] Initializing...") print("[+] Initializing...")
# Initialize the database # Настройка базы данных
with engine.begin() as connection: with engine.begin() as connection:
alembic_cfg.attributes['connection'] = connection alembic_cfg.attributes["connection"] = connection
command.upgrade(alembic_cfg, "head") command.upgrade(alembic_cfg, "head")
print("[+] Starting...") print("[+] Starting...")
yield # Here FastAPI will start handling requests; yield # Обработка запросов
print("[+] Application shutdown") print("[+] Application shutdown")
# Include routers
# Подключение маршрутов
app.include_router(api_router) app.include_router(api_router)

View File

@@ -2,13 +2,15 @@ from fastapi import APIRouter
from .authors import router as authors_router from .authors import router as authors_router
from .books import router as books_router from .books import router as books_router
from .genres import router as genres_router
from .relationships import router as relationships_router from .relationships import router as relationships_router
from .misc import router as misc_router from .misc import router as misc_router
api_router = APIRouter() api_router = APIRouter()
# Including all routers # Подключение всех маршрутов
api_router.include_router(authors_router) api_router.include_router(authors_router)
api_router.include_router(books_router) api_router.include_router(books_router)
api_router.include_router(genres_router)
api_router.include_router(relationships_router) api_router.include_router(relationships_router)
api_router.include_router(misc_router) api_router.include_router(misc_router)

View File

@@ -10,29 +10,32 @@ from httpx import get
from library_service.settings import get_app from library_service.settings import get_app
# Templates initialization # Загрузка шаблонов
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
router = APIRouter(tags=["misc"]) router = APIRouter(tags=["misc"])
# Formatted information about the application
# Форматированная информация о приложении
def get_info(app) -> Dict: def get_info(app) -> Dict:
return { return {
"status": "ok", "status": "ok",
"app_info": { "app_info": {
"title": app.title, "title": app.title,
"version": app.version, "version": app.version,
"description": app.description, "description": app.description,
}, },
"server_time": datetime.now().isoformat(), "server_time": datetime.now().isoformat(),
} }
# Root endpoint
# Эндпоинт главной страницы
@router.get("/", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
async def root(request: Request, app=Depends(get_app)): async def root(request: Request, app=Depends(get_app)):
return templates.TemplateResponse(request, "index.html", get_info(app)) return templates.TemplateResponse(request, "index.html", get_info(app))
# API Information endpoint
# Эндпоинт информации об API
@router.get("/api/info") @router.get("/api/info")
async def api_info(app=Depends(get_app)): async def api_info(app=Depends(get_app)):
return JSONResponse(content=get_info(app)) return JSONResponse(content=get_info(app))

View File

@@ -51,9 +51,57 @@ def remove_author_from_book(author_id: int, book_id: int, session: Session = Dep
return {"message": "Relationship removed successfully"} return {"message": "Relationship removed successfully"}
# Get relationships # Get relationships
@router.get("/relationships/author-book", response_model=List[AuthorBookLink]) @router.get("/relationships/genre-book", response_model=List[GenreBookLink])
def get_relationships(session: Session = Depends(get_session)): def get_relationships(session: Session = Depends(get_session)):
relationships = session.exec(select(AuthorBookLink)).all() relationships = session.exec(select(GenreBookLink)).all()
return relationships
# Add author to book
@router.post("/relationships/genre-book", response_model=GenreBookLink)
def add_genre_to_book(genre_id: int, book_id: int, session: Session = Depends(get_session)):
genre = session.get(Genre, genre_id)
if not genre:
raise HTTPException(status_code=404, detail="Genre not found")
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
existing_link = session.exec(
select(GenreBookLink)
.where(GenreBookLink.genre_id == genre_id)
.where(GenreBookLink.book_id == book_id)
).first()
if existing_link:
raise HTTPException(status_code=400, detail="Relationship already exists")
link = GenreBookLink(genre_id=genre_id, book_id=book_id)
session.add(link)
session.commit()
session.refresh(link)
return link
# Remove author from book
@router.delete("/relationships/genre-book", response_model=Dict[str, str])
def remove_genre_from_book(genre_id: int, book_id: int, session: Session = Depends(get_session)):
link = session.exec(
select(GenreBookLink)
.where(GenreBookLink.genre_id == genre_id)
.where(GenreBookLink.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 relationships
@router.get("/relationships/genre-book", response_model=List[GenreBookLink])
def get__genre_relationships(session: Session = Depends(get_session)):
relationships = session.exec(select(GenreBookLink)).all()
return relationships return relationships
# Get author's books # Get author's books

0
migrations/README Executable file → Normal file
View File

0
migrations/env.py Executable file → Normal file
View File

0
migrations/script.py.mako Executable file → Normal file
View File

View File

@@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "library-service" name = "LibraryAPI"
version = "0.1.1" version = "0.1.2"
description = "This is a sample API for managing authors and books." description = "Это простое API для управления авторами и книгами."
authors = ["wowlikon"] authors = ["wowlikon"]
readme = "README.md" readme = "README.md"
packages = [{ include = "library_service" }] packages = [{ include = "library_service" }]

View File

@@ -9,10 +9,14 @@ from tests.test_misc import setup_database
client = TestClient(app) client = TestClient(app)
def make_relationship(author_id, book_id): def make_authorbook_relationship(author_id, book_id):
response = client.post("/relationships/author-book", params={"author_id": author_id, "book_id": book_id}) response = client.post("/relationships/author-book", params={"author_id": author_id, "book_id": book_id})
assert response.status_code == 200, "Invalid response status" assert response.status_code == 200, "Invalid response status"
def make_genrebook_relationship(author_id, book_id):
response = client.post("/relationships/genre-book", params={"genre_id": author_id, "book_id": book_id})
assert response.status_code == 200, "Invalid response status"
def test_prepare_data(setup_database): def test_prepare_data(setup_database):
response = client.post("/books", json={"title": "Test Book 1", "description": "Test Description 1"}) response = client.post("/books", json={"title": "Test Book 1", "description": "Test Description 1"})
response = client.post("/books", json={"title": "Test Book 2", "description": "Test Description 2"}) response = client.post("/books", json={"title": "Test Book 2", "description": "Test Description 2"})
@@ -22,11 +26,11 @@ def test_prepare_data(setup_database):
response = client.post("/authors", json={"name": "Test Author 2"}) response = client.post("/authors", json={"name": "Test Author 2"})
response = client.post("/authors", json={"name": "Test Author 3"}) response = client.post("/authors", json={"name": "Test Author 3"})
make_relationship(1, 1) make_authorbook_relationship(1, 1)
make_relationship(2, 1) make_authorbook_relationship(2, 1)
make_relationship(1, 2) make_authorbook_relationship(1, 2)
make_relationship(2, 3) make_authorbook_relationship(2, 3)
make_relationship(3, 3) make_authorbook_relationship(3, 3)
response = client.get("/relationships/author-book") response = client.get("/relationships/author-book")
assert response.status_code == 200, "Invalid response status" assert response.status_code == 200, "Invalid response status"