Merge pull request 'blokhin_dmitriy_lab_3' (#436) from blokhin_dmitriy_lab_3 into main

Reviewed-on: #436
This commit was merged in pull request #436.
This commit is contained in:
2025-12-08 23:08:54 +04:00
9 changed files with 425 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
## Описание
Целью лабораторной работы было изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля REST API. Работа реализована в виде системы из двух микросервисов, соединённых через шлюз nginx.
## Что делает программа
Программа состоит из трёх основных компонентов:
1. `user-service`: Микросервис, отвечающий за управление данными пользователей (например, продавцов автомобилей). Реализует CRUD-операции (создание, чтение, обновление, удаление) для сущности `User`.
2. `listing-service`: Микросервис, отвечающий за управление объявлениями о продаже автомобилей. Реализует CRUD-операции для сущности `Listing`. Связан с `user-service` отношением "один ко многим" (один пользователь может создать много объявлений).
3. `nginx-gateway`: Шлюз, реализованный с помощью nginx. Принимает внешние запросы и направляет их к соответствующим микросервисам. Запросы к `/users/` перенаправляются в `user-service`, а запросы к `/listings/` — в `listing-service`.
*Синхронный обмен:* При выполнении запроса `GET /listings/{uuid}` (получение деталей конкретного объявления) `listing-service` выполняет синхронный HTTP-запрос к `user-service`, чтобы получить информацию о пользователе, связанном с этим объявлением, и включает её в свой ответ.
## Как запустить лабораторную работу
1. Убедитесь, что Docker Desktop запущен
2. Перейдите в директорию проекта: `cd .\blokhin_dmitriy_lab_3\`
3. Выполните команду: `docker compose up --build`
4. После успешного запуска система будет доступна через шлюз nginx на порту `8080` хоста.
## Какие технологии использовались
`Python`: Язык программирования для реализации логики микросервисов.
`FastAPI`: Фреймворк для создания веб-API на Python. Автоматически генерирует документацию Swagger UI.
`Pydantic`: Библиотека для валидации данных, используемая FastAPI.
`httpx`: Библиотека для выполнения HTTP-запросов из `listing-service` в `user-service`.
`Nginx`: Веб-сервер, использованный в качестве API-шлюза для маршрутизации запросов.
`Docker`: Технология контейнеризации для изоляции и запуска микросервисов.
`Docker Compose`: Инструмент для определения и запуска многоконтейнерных Docker-приложений.
## Примеры взаимодействия через шлюз (порт 8080)
После запуска вы можете тестировать API через шлюз, например, с помощью `curl` или Postman.
* **Создать пользователя:**
```bash
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "Иванов Иван", "contact_info": "ivanov@example.com"}'
```
* **Создать объявление (используйте `uuid` созданного пользователя):**
```bash
curl -X POST http://localhost:8080/listings \
-H "Content-Type: application/json" \
-d '{"make": "Toyota", "model": "Camry", "year": 2020, "price": 1500000, "user_uuid": "UUID_ПОЛЬЗОВАТЕЛЯ"}'
```
* **Получить список пользователей:**
```bash
curl -X GET http://localhost:8080/users
```
* **Получить список объявлений:**
```bash
curl -X GET http://localhost:8080/listings
```
* **Получить объявление с деталями пользователя (демонстрация синхронного вызова):**
```bash
curl -X GET http://localhost:8080/listings/UUID_ОБЪЯВЛЕНИЯ
```
## Доступ к Swagger UI (напрямую, не через шлюз)
Для удобного просмотра и тестирования API каждого сервиса напрямую:
* `user-service` Swagger UI: `http://localhost:8001/docs`
* `listing-service` Swagger UI: `http://localhost:8002/docs`
Ссылка на видео:https://rutube.ru/video/private/60f1554c3716ec930f74a52d63100199/?p=lbZ6GVrBua9TRxDAyL1Uyw

View File

@@ -0,0 +1,32 @@
version: '3.8'
services:
user-service:
build:
context: ./user-service # Путь к папке с кодом user-service
dockerfile: Dockerfile # Убедитесь, что Dockerfile создан для user-service
ports:
- "8001:8001" # Для отладки, необязательно
# environment:
# - DATABASE_URL=sqlite:///./test.db # Пример переменной окружения
listing-service:
build:
context: ./listing-service # Путь к папке с кодом listing-service
dockerfile: Dockerfile # Убедитесь, что Dockerfile создан для listing-service
ports:
- "8002:8002" # Для отладки, необязательно
# environment:
# - USER_SERVICE_URL=http://user-service:8001 # URL для вызова user-service
depends_on:
- user-service # listing-service зависит от user-service
nginx-gateway:
image: nginx:alpine
ports:
- "8080:80" # Внешний порт для доступа к API через шлюз
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf # Монтируем наш конфиг
depends_on:
- user-service
- listing-service

View File

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

View File

@@ -0,0 +1,154 @@
from datetime import datetime
from typing import List, Optional
import uuid
import httpx # Импортируем httpx для асинхронного HTTP-запроса
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# Инициализируем FastAPI приложение с информацией о документации
app = FastAPI(
title="Listing Service API",
description="API for managing car listings",
version="1.0.0"
)
# --- Модели Pydantic ---
class UserInfo(BaseModel):
uuid: str
name: str
contact_info: str
registration_date: datetime
class ListingBase(BaseModel):
make: str
model: str
year: int
price: float
user_uuid: str
class ListingCreate(ListingBase):
pass
class ListingUpdate(ListingBase):
pass
class Listing(ListingBase):
uuid: str
created_at: datetime
class ListingDetail(Listing):
user_info: Optional[UserInfo] = None
# --- Хранилище данных (в памяти) ---
listings_db: List[Listing] = []
# --- CRUD операции ---
def get_listing_by_uuid(listing_uuid: str) -> Optional[Listing]:
for listing in listings_db:
if listing.uuid == listing_uuid:
return listing
return None
# --- Роуты API ---
@app.get("/", response_model=List[Listing])
def get_listings():
"""
Get a list of all listings.
Note: This does not include user details.
"""
return listings_db
# Исправленная функция: теперь она async
@app.get("/{listing_uuid}", response_model=ListingDetail)
async def get_listing(listing_uuid: str): # <-- Добавлен async
"""
Get a listing by its UUID.
This call performs a synchronous request to the User Service to fetch user details.
The 'user_info' field will contain user details if the user exists, otherwise it will be null.
"""
listing = get_listing_by_uuid(listing_uuid)
if not listing:
raise HTTPException(status_code=404, detail="Listing not found")
# Синхронный вызов user-service для получения информации о пользователе
user_info = None
try:
# Используем имя сервиса из docker-compose.yml
# Создаём асинхронный клиент и используем его внутри async функции
async with httpx.AsyncClient() as client:
response = await client.get(f"http://user-service:8001/{listing.user_uuid}") # <-- await
if response.status_code == 200:
user_data = response.json()
user_info = UserInfo(**user_data) # Создаем объект из JSON
else:
# Логика обработки ошибки, если пользователь не найден или сервис недоступен
print(f"Warning: Could not fetch user info for UUID {listing.user_uuid}. Status: {response.status_code}")
except httpx.RequestError as exc:
# Логика обработки ошибки соединения
print(f"Error fetching user info: {exc}")
# Возвращаем детали объявления, включая информацию о пользователе
return ListingDetail(
uuid=listing.uuid,
make=listing.make,
model=listing.model,
year=listing.year,
price=listing.price,
user_uuid=listing.user_uuid,
created_at=listing.created_at,
user_info=user_info
)
@app.post("/", response_model=Listing)
def create_listing(listing_data: ListingCreate): # <-- Исправлено: listing_data: ListingCreate
"""
Create a new listing.
"""
listing_uuid = str(uuid.uuid4())
created_at = datetime.now()
listing = Listing(
uuid=listing_uuid,
make=listing_data.make, # <-- Исправлено: listing_data.make
model=listing_data.model,
year=listing_data.year,
price=listing_data.price,
user_uuid=listing_data.user_uuid,
created_at=created_at
)
listings_db.append(listing)
return listing
@app.put("/{listing_uuid}", response_model=Listing)
def update_listing(listing_uuid: str, listing_data: ListingUpdate): # <-- Исправлено: listing_data: ListingUpdate
"""
Update an existing listing by its UUID.
"""
listing = get_listing_by_uuid(listing_uuid)
if not listing:
raise HTTPException(status_code=404, detail="Listing not found")
# Обновляем атрибуты
listing.make = listing_data.make # <-- Исправлено: listing_data.make
listing.model = listing_data.model
listing.year = listing_data.year
listing.price = listing_data.price
listing.user_uuid = listing_data.user_uuid
# created_at остается неизменным
return listing
@app.delete("/{listing_uuid}")
def delete_listing(listing_uuid: str):
"""
Delete a listing by its UUID.
"""
global listings_db
initial_len = len(listings_db)
listings_db = [l for l in listings_db if l.uuid != listing_uuid]
if len(listings_db) == initial_len:
raise HTTPException(status_code=404, detail="Listing not found")
return {"message": "Listing deleted successfully"}
# Точка входа для запуска с помощью uvicorn (если запускается напрямую)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8002)

View File

@@ -0,0 +1,3 @@
fastapi
uvicorn[standard]==0.32.0
httpx==0.27.2

View File

@@ -0,0 +1,41 @@
events {
worker_connections 1024;
}
http {
# Указываем upstream для каждого микросервиса
upstream user_service {
server user-service:8001; # Имя сервиса из docker-compose.yml
}
upstream listing_service {
server listing-service:8002; # Имя сервиса из docker-compose.yml
}
server {
listen 80; # Порт, на котором слушает nginx
# Маршрутизация запросов к user-service
location /users {
proxy_pass http://user_service; # Передаем запрос на upstream
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;
}
# Маршрутизация запросов к listing-service
location /listings {
proxy_pass http://listing_service;
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 / {
return 404; # Или перенаправить на документацию
}
}
}

View File

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

View File

@@ -0,0 +1,102 @@
from datetime import datetime
from typing import List, Optional
import uuid
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# Инициализируем FastAPI приложение с информацией о документации
app = FastAPI(
title="User Service API",
description="API for managing users (sellers)",
version="1.0.0"
)
# --- Модели Pydantic ---
class UserBase(BaseModel):
name: str
contact_info: str
class UserCreate(UserBase):
pass
class UserUpdate(UserBase):
pass
class User(UserBase):
uuid: str
registration_date: datetime
# --- Хранилище данных (в памяти) ---
users_db: List[User] = []
# --- CRUD операции ---
def get_user_by_uuid(user_uuid: str) -> Optional[User]:
for user in users_db:
if user.uuid == user_uuid:
return user
return None
# --- Роуты API ---
@app.get("/", response_model=List[User])
def get_users():
"""
Get a list of all users.
"""
return users_db
@app.get("/{user_uuid}", response_model=User)
def get_user(user_uuid: str):
"""
Get a user by its UUID.
"""
user = get_user_by_uuid(user_uuid)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.post("/", response_model=User)
def create_user(user_data: UserCreate):
"""
Create a new user.
"""
user_uuid = str(uuid.uuid4())
registration_date = datetime.now()
user = User(
uuid=user_uuid,
name=user_data.name,
contact_info=user_data.contact_info,
registration_date=registration_date
)
users_db.append(user)
return user
@app.put("/{user_uuid}", response_model=User)
def update_user(user_uuid: str, user_data: UserUpdate):
"""
Update an existing user by its UUID.
"""
user = get_user_by_uuid(user_uuid)
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Обновляем атрибуты
user.name = user_data.name
user.contact_info = user_data.contact_info
# registration_date остается неизменным
return user
@app.delete("/{user_uuid}")
def delete_user(user_uuid: str):
"""
Delete a user by its UUID.
"""
global users_db
initial_len = len(users_db)
users_db = [u for u in users_db if u.uuid != user_uuid]
if len(users_db) == initial_len:
raise HTTPException(status_code=404, detail="User not found")
return {"message": "User deleted successfully"}
# Точка входа для запуска с помощью uvicorn (если запускается напрямую)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)

View File

@@ -0,0 +1,2 @@
fastapi
uvicorn[standard]==0.32.0