zhirnova_alyona_lab_3

This commit is contained in:
2025-10-12 19:59:02 +04:00
parent 7731f1a120
commit 24cc437956
8 changed files with 387 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
## Цель
изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API.
### Задачи
1. Создать 2 микросервиса, реализующих CRUD на связанных сущностях.
2. Реализовать механизм синхронного обмена сообщениями между микросервисами.
3. Реализовать шлюз на основе прозрачного прокси-сервера nginx.
4. Оформить отчёт в формате Markdown
### Инструменты для выполнения:
1. Docker
2. Docker Compose
3. Python
4. Postman
## Компоненты системы:
1. Cartoon Service (cartoon_service/) - сервис для управления мультфильмами
2. Producer Service (producer_service/) - сервис для управления режиссерами
3. Nginx - обратный прокси и балансировщик нагрузки
4. Docker Compose - оркестрация контейнеров
## API Endpoints:
GET / - Получить все мультфильмы
GET /{cartoon_uuid} - Получить мультфильм по ID
GET /full - Получить все мультфильмы с информацией о продюсерах
GET /full/{cartoon_uuid} - Получить мультфильм с информацией о продюсере
GET /by-producer/{producer_uuid} - Получить мультфильмы по продюсеру
POST / - Создать новый мультфильм
PUT /{cartoon_uuid} - Обновить мультфильм
DELETE /{cartoon_uuid} - Удалить мультфильм
GET / - Получить всех режиссеров
GET /{producer_uuid} - Получить режиссера по ID
GET /with-cartoons/{producer_uuid} - Получить режиссера с его мультфильмами
GET /check/{producer_uuid} - Проверить существование режиссера
POST / - Создать нового режиссера
PUT /{producer_uuid} - Обновить режиссера
DELETE /{producer_uuid} - Удалить режиссера
Пример POST http://localhost/cartoon_service/:
```json
{
"title": "Король Лев",
"year": 1994,
"producer_id": "997aa4c5-ebb2-4794-ba81-e742f9f1fa30"
}
```
## Особенности реализации:
1. Микросервисная архитектура - каждый сервис независим и выполняет одну бизнес-функцию
2. Взаимодействие через HTTP - сервисы общаются через REST API вызовы
3. Контейнеризация - все компоненты запускаются в Docker контейнерах
4. Автоматическая сборка - Docker Compose автоматически собирает и запускает все сервисы
5. Единая точка входа - Nginx обеспечивает единый endpoint для всех сервисов
## Запуск:
1. На компьютере должны быть установлены Docker и Docker Compose
2. Скачайте папку с репозитория
3. Перейдите в директорию с yml
4. Запустите с помощью команды:
`docker-compose up --build`
5. Откройте Postman
## Что делает данная лабораторная работа:
Проект представляет собой микросервисное приложение для управления базой данных мультфильмов и их режиссеров. Система состоит из двух независимых сервисов, взаимодействующих через REST API, и обратного прокси на базе Nginx для маршрутизации запросов.
## Видео:
https://rutube.ru/video/private/22f82905c7e737e2863bd9726cedf3e5/?p=FkA762T6Z6K0TDyHZ5Yaeg

View File

@@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY cartoon_service/cartoon_service.py .
CMD ["python", "cartoon_service.py"]

View File

@@ -0,0 +1,94 @@
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from typing import List, Optional
from uuid import UUID, uuid4
import requests
import uvicorn
app = FastAPI(root_path="/cartoon_service", title="Cartoon Service", description="Service to manage cartoons and their producers", version="1.0.0")
producers_url = "http://producer_service:8080/"
class Cartoon(BaseModel):
uuid: UUID
title: str
year: int
producer_id: UUID
class CartoonCreate(BaseModel):
title: str
year: int
producer_id: UUID
cartoons: List[Cartoon] = [
Cartoon(uuid=UUID("89fa1e7a-7e88-445e-a4d8-6d4497ea8f19"), title="Большое путешествие", year=2006, producer_id=UUID("eb815350-c7b9-4446-8434-4c0640c21995")),
Cartoon(uuid=UUID("0351ee11-f11b-4d83-b2c8-1075b0c357dc"), title="Ходячий замок", year=2004, producer_id=UUID("997aa4c5-ebb2-4794-ba81-e742f9f1fa30")),
Cartoon(uuid=UUID("dfc17619-7690-47aa-ae8e-6a5068f8ddec"), title="Горбун из Нотр-Дама", year=1996, producer_id=UUID("694827e4-0f93-45a5-8f75-bad7ef2d21fe")),
]
@app.get("/", response_model=List[Cartoon], summary="Get all cartoons", description="Retrieve a list of all cartoons.")
def get_all_cartoons():
return cartoons
@app.get("/full", summary="Get all cartoons with producer info", description="Retrieve all cartoons with additional producer information.")
def get_all_cartoons_with_producers():
producers = requests.get(producers_url).json()
response = []
for cartoon in cartoons:
producer_info = next((p for p in producers if p["uuid"] == str(cartoon.producer_id)), None)
if producer_info:
response.append({**cartoon.dict(), "producer_info": producer_info})
return response
@app.get("/by-producer/{producer_uuid}", response_model=List[Cartoon], summary="Get cartoons by producer", description="Retrieve all cartoons for a given producer.")
def get_cartoons_by_producer(producer_uuid: UUID):
return [cartoon for cartoon in cartoons if cartoon.producer_id == producer_uuid]
@app.get("/{cartoon_uuid}", response_model=Cartoon, summary="Get a cartoon by ID", description="Retrieve a single cartoon by its ID.")
def get_cartoon_by_id(cartoon_uuid: UUID):
cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None)
if not cartoon:
raise HTTPException(status_code=404, detail="Cartoon not found")
return cartoon
@app.get("/full/{cartoon_uuid}", summary="Get cartoon with producer info", description="Retrieve a cartoon with additional producer information.")
def get_cartoon_with_producer_info(cartoon_uuid: UUID):
cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None)
if not cartoon:
raise HTTPException(status_code=404, detail="Cartoon not found")
producer_info = requests.get(f"{producers_url}{cartoon.producer_id}").json()
return {**cartoon.dict(), "producer_info": producer_info}
@app.post("/", response_model=Cartoon, summary="Create a new cartoon", description="Add a new cartoon to the database.")
def create_cartoon(cartoon: CartoonCreate):
# Check if producer exists
response = requests.get(f"{producers_url}check/{cartoon.producer_id}")
if response.status_code == 404:
raise HTTPException(status_code=404, detail="Producer not found")
new_cartoon = Cartoon(uuid=uuid4(), **cartoon.dict())
cartoons.append(new_cartoon)
return new_cartoon
@app.put("/{cartoon_uuid}", response_model=Cartoon, summary="Update a cartoon", description="Update an existing cartoon by its ID.")
def update_cartoon(cartoon_uuid: UUID, cartoon_update: CartoonCreate):
cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None)
if not cartoon:
raise HTTPException(status_code=404, detail="Cartoon not found")
cartoon.title = cartoon_update.title
cartoon.year = cartoon_update.year
cartoon.producer_id = cartoon_update.producer_id
return cartoon
@app.delete("/{cartoon_uuid}", summary="Delete a cartoon", description="Remove a cartoon by its ID.")
def delete_cartoon(cartoon_uuid: UUID):
global cartoons
cartoons = [cartoon for cartoon in cartoons if cartoon.uuid != cartoon_uuid]
return {"detail": "Cartoon deleted successfully"}
if __name__ == "__main__":
uvicorn.run("cartoon_service:app", host="0.0.0.0", port=8081, reload=True)

View File

@@ -0,0 +1,27 @@
services:
producer_service:
container_name: producer_service
build:
context: .
dockerfile: ./producer_service/Dockerfile
expose:
- 8080
cartoon_service:
container_name: cartoon_service
build:
context: .
dockerfile: ./cartoon_service/Dockerfile
expose:
- 8081
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- producer_service
- cartoon_service

View File

@@ -0,0 +1,49 @@
events { worker_connections 1024; }
http {
server {
listen 80;
listen [::]:80;
server_name localhost;
location /producer_service/ {
proxy_pass http://producer_service:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /cartoon_service/ {
proxy_pass http://cartoon_service:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /producer_service_service/docs {
proxy_pass http://producer_service:8080/docs;
}
location /cartoon_service/docs {
proxy_pass http://cartoon_service:8081/docs;
}
location /producer_service/openapi.json {
proxy_pass http://producer_service:8080/openapi.json;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /cartoon_service/openapi.json {
proxy_pass http://cartoon_service:8081/openapi.json;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY producer_service/producer_service.py .
CMD ["python", "producer_service.py"]

View File

@@ -0,0 +1,122 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from uuid import UUID, uuid4
import requests
from typing import List, Optional
import uvicorn
app = FastAPI(root_path="/producer_service", title="Producer API", description="API для управления продюсерами", version="1.0.0")
class Producer:
def __init__(self, name: str, surname: str, uuid_: Optional[UUID] = None):
self.uuid_ = uuid_ if uuid_ else uuid4()
self.name: str = name
self.surname: str = surname
def to_dict(self):
return {
"uuid": str(self.uuid_),
"name": self.name,
"surname": self.surname
}
def to_dict_with_cartoons(self, cartoons: list):
return {
"uuid": str(self.uuid_),
"name": self.name,
"surname": self.surname,
"cartoons": cartoons
}
class CreateProducerRequest(BaseModel):
name: str
surname: str
class UpdateProducerRequest(BaseModel):
name: Optional[str]
surname: Optional[str]
producers: List[Producer] = [
Producer(name="Хаяо", surname="Миядзаки", uuid_=UUID("997aa4c5-ebb2-4794-ba81-e742f9f1fa30")),
Producer(name="Гари", surname="Труздейл", uuid_=UUID("694827e4-0f93-45a5-8f75-bad7ef2d21fe")),
Producer(name="Стив", surname="Уильямс", uuid_=UUID("eb815350-c7b9-4446-8434-4c0640c21995"))
]
cartoons_url = "http://cartoon_service:8081/"
@app.get("/", response_model=List[dict])
def get_all():
"""Получение списка всех продюсеров."""
return [producer.to_dict() for producer in producers]
@app.get("/{uuid_}", response_model=dict)
def get_one(uuid_: UUID):
"""Получение продюсера по идентификатору."""
for producer in producers:
if producer.uuid_ == uuid_:
return producer.to_dict()
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
@app.get("/with-cartoons/{uuid_}", response_model=dict)
def get_one_with_cartoons(uuid_: UUID):
"""Получение продюсера со списком его мультфильмов."""
for producer in producers:
if producer.uuid_ == uuid_:
response = requests.get(f"{cartoons_url}by-producer/{uuid_}")
if response.status_code != 200:
raise HTTPException(status_code=404, detail="Не удалось получить мультфильмы продюсера")
return producer.to_dict_with_cartoons(response.json())
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
@app.get("/check/{uuid_}", response_class=JSONResponse)
def check_exist(uuid_: UUID):
"""Проверка наличия продюсера по идентификатору (для cartoon_service)."""
for producer in producers:
if producer.uuid_ == uuid_:
return JSONResponse(content="", status_code=200)
return JSONResponse(content="", status_code=404)
@app.post("/", response_model=dict)
def create(producer: CreateProducerRequest):
"""Создание нового продюсера."""
new_producer = Producer(name=producer.name, surname=producer.surname)
producers.append(new_producer)
return new_producer.to_dict()
@app.put("/{uuid_}", response_model=dict)
def update_by_id(uuid_: UUID, producer_update: UpdateProducerRequest):
"""Изменение данных продюсера по идентификатору."""
for producer in producers:
if producer.uuid_ == uuid_:
if producer_update.name is not None:
producer.name = producer_update.name
if producer_update.surname is not None:
producer.surname = producer_update.surname
return producer.to_dict()
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
@app.delete("/{uuid_}", response_class=JSONResponse)
def delete(uuid_: UUID):
"""Удаление продюсера по идентификатору."""
for producer in producers:
if producer.uuid_ == uuid_:
producers.remove(producer)
return JSONResponse(content="Продюсер успешно удалён", status_code=200)
raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден")
if __name__ == "__main__":
uvicorn.run("producer_service:app", host="0.0.0.0", port=8080, reload=True)

View File

@@ -0,0 +1,3 @@
fastapi
uvicorn
requests