diff --git a/lazarev_andrey_lab_3/.gitignore b/lazarev_andrey_lab_3/.gitignore new file mode 100644 index 0000000..eba74f4 --- /dev/null +++ b/lazarev_andrey_lab_3/.gitignore @@ -0,0 +1 @@ +venv/ \ No newline at end of file diff --git a/lazarev_andrey_lab_3/README.md b/lazarev_andrey_lab_3/README.md new file mode 100644 index 0000000..0fd6a66 --- /dev/null +++ b/lazarev_andrey_lab_3/README.md @@ -0,0 +1,76 @@ +# Лабораторная работа №2 + +## Описание проекта + +Проект разворачивает 3 программы в отдельных контейнерах с использованием Docker Compose: +1. **author_service** - сервис, с CRUD операциями для авторов; +2. **publication_service** - сервис, с CRUD операциями для публикаций; +3. **nginx** - веб-сервер и прокси-сервер, является маршрутизатором. + +Между первыми двумя сервисами имеется связь один(`Автор`) ко многим(`Публикация`). + +## Струкутура проекта + +### Проект состоит из: + +- 2 папки(author_service, publication_service) + - Каждая папка содержит в себе файл с расширением `.py` с кодом программы; + - Кадлая папка сожержит в себе файл `Dockerfile` с инструкцией по созданию Docker образа. + +- Файл `.gitignore` для исключения временных файлов директории `venv/`; + +- Файл `docker-compose.yml` с конфигурацией Docker Compose; + +- Файл `nginx.conf` конфигурации для веб-сервера NGINX с параметрами работы сервера; + +- Файл `requirements.txt` с перечислением всех необходимых библиотек для запуска. + +Комментарии в файлах. + +## Запуск + +1. Скачать и установить Docker и Docker Compose; +2. Перейти в директорию с файлом docker-compose.yml; +3. Открыть консоль и запустить сервисы командой +```bash +docker-compose up --build -d +``` +4. Дождаться запуска всех сервисов +```bash + [+] Running 3/3 + ✔ Container lazarev_andrey_lab_2-generate-files-1 Started 0.5s + ✔ Container lazarev_andrey_lab_2-first-1 Started 1.3s + ✔ Container lazarev_andrey_lab_2-second-1 Started 2.0s +``` +5. Остановка всех сервисов +Для завершения работы с сервисами необходимо выполнить команду: + ```bash + docker-compose down + ``` +Дождаться завершения работы: +```bash +[+] Running 4/4 + ✔ Container lazarev_andrey_lab_2-second-1 Removed 0.0s + ✔ Container lazarev_andrey_lab_2-first-1 Removed 0.0s + ✔ Container lazarev_andrey_lab_2-generate-files-1 Removed 0.0s + ✔ Network lazarev_andrey_lab_2_default Removed 0.4s +``` + +## Cписок команд +- Author_service + - `http://localhost:8000/author_service/author` - список авторов + - `http://localhost:8000/author_service/author/{id автора}` - конкретный автор + - `http://localhost:8000/author_service/author/full/{id автора}` - автор и полный список его публикаций + - `http://localhost:8000/author_service/author?name={имя}&second_name={фамилия}&age={возраст}` - добавление нового автора + - `http://localhost:8000/author_service/author/{id автора}?name={новое имя}` - изменение имени автора + +- Publication_service + - `http://localhost:8000/publication_service/publication` - список публикаций + - `http://localhost:8000/publication_service/publication/{id публикации}` - конкретная публикация + - `http://localhost:8000/publication_service/publication/full/{id публикации}` - публикация и полная информация об авторе + - `http://localhost:8000/publication_service/publication?name={название}&public_year={год выпуска}&author_id={id автора}` - добавление новой публикации + - `http://localhost:8000/publication_service/publication/{id публикации}?name={новое название}` - изменение названия публикации +## Видеодемонстрация работоспособности + +[Демонстрация работы сервисов](https://files.ulstu.ru/s/5D2i6gbLn6r2jsA) + diff --git a/lazarev_andrey_lab_3/author_service/Dockerfile b/lazarev_andrey_lab_3/author_service/Dockerfile new file mode 100644 index 0000000..ea8f0e8 --- /dev/null +++ b/lazarev_andrey_lab_3/author_service/Dockerfile @@ -0,0 +1,17 @@ +# Использует базовый образ Python 3.9 на основе slim-версии +FROM python:3.10-slim + +# Устанавливаю рабочую директорию внутри контейнера +WORKDIR /app + +# Копирую файл requirements.txt в контейнер +COPY requirements.txt /app/ + +# Устанавливаю зависимости +RUN pip install --no-cache-dir -r requirements.txt + +#аналогично копирую +COPY author_service/main.py . + +# Задает команду для запуска контейнера +CMD ["python", "main.py"] \ No newline at end of file diff --git a/lazarev_andrey_lab_3/author_service/main.py b/lazarev_andrey_lab_3/author_service/main.py new file mode 100644 index 0000000..8107c08 --- /dev/null +++ b/lazarev_andrey_lab_3/author_service/main.py @@ -0,0 +1,110 @@ +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field +from uuid import UUID, uuid4 +import requests + +#Инициализация веб-приложения +app = FastAPI(title="Author service") + +#Строка подключения к публикациям +publication_url='http://publication_service:8009' + +#Сущность автор с полями имя, фамилия, возраст. +class Author(BaseModel): + uuid_: UUID = Field(default_factory=uuid4) + name: str | None + second_name: str | None + age: int | None + +#Заранее заполненный список авторов, в некоторых есть uuid они пригодятся при создании публикаций +data: list[Author] = [ + Author(uuid_="92e78b56-0026-4561-b9f6-ba628110c900", name="Андрей", second_name="Лазарев", age=21), + Author(uuid_="3203b355-d844-4d5a-ad91-a9e0135cd9d9",name="Павел", second_name="Сорокин", age=20), + Author(name="Дарья", second_name="Балберова", age=21), + Author(name="Дмитрий", second_name="Курило", age=24), + Author(name="Александр", second_name="Дырночкин", age=24) +] + +#Получить список всех авторов +@app.get("/author", tags=["Author"]) +def all_authors(): + return data + +#Получить одного автора по uuid +@app.get("/author/{author_id}", tags=["Author"]) +def get_author(author_id: UUID): + author = next((x for x in data if x.uuid_ == author_id), None) + + if not author: + return HTTPException(status_code=404, detail="Автор не найден") + + return author + +#Получить одного автора по uuid с списком его публикаций +@app.get("/author/full/{author_id}") +def get_publications_by_author(author_id: UUID): + author = get_author(author_id) + + if not author: + return HTTPException(status_code=404, detail="Автор не найден") + + publications = requests.get(f"{publication_url}/publication/author/{author_id}") + + if not publications: + return HTTPException(status_code=404, detail="Публикации не найдены") + + result = author.model_dump() + result['publications'] = publications.json() + + return result + +#Добавление нового автора, все поля обязательные +@app.post("/author", tags=["Author"]) +def add_author(name: str, second_name: str, age: int): + author = next((x for x in data if x.name == name and x.second_name == second_name and x.age == age), None) + + if author: + return HTTPException(status_code=404, detail="Такой автор уже существует") + + try: + data.append(Author(name=name, second_name=second_name, age=age)) + return JSONResponse(content={"message": "Автор успешно добавлен"}, status_code=200) + + except Exception as e: + return HTTPException(status_code=404, detail={"Автор не был добавлен с ошибкой": str(e)}) + +#Изменение автора по uuid +@app.put("/author/{author_id}", tags=["Author"]) +def update_author(author_id: UUID, name: str = None , second_name: str = None, age: int = None): + author = get_author(author_id) + if author: + index = data.index(author) + if name: + data[index].name = name + if second_name: + data[index].second_name = second_name + if age: + data[index].age = age + return JSONResponse(content={"message": "Автор успешно изменен"}, status_code=200) + + else: + return HTTPException(status_code=404, detail={"Автор не найден": {author}}) + +#Удаление автора по uuid +@app.delete("/author/{author_id}", tags=["Author"]) +def delete_author(author_id: UUID): + author = get_author(author_id) + + if author: + index = data.index(author) + del data[index] + return JSONResponse(content={"message": "Автор успешно удален"}, status_code=200) + + else: + return HTTPException(status_code=404, detail={"Автор не найден": {author}}) + +#Запуск +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8008) \ No newline at end of file diff --git a/lazarev_andrey_lab_3/docker-compose.yml b/lazarev_andrey_lab_3/docker-compose.yml new file mode 100644 index 0000000..7442dc3 --- /dev/null +++ b/lazarev_andrey_lab_3/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' +services: + + author_service: + build: + context: . #Контекст сборки — текущая директория (корневая папка проекта). + dockerfile: ./author_service/Dockerfile # Путь до Dockerfile для сборки контейнера. + expose: # Указывает, какой порт будет открыт внутри контейнера. + - 8008 + + publication_service: + build: + context: . + dockerfile: ./publication_service/Dockerfile + expose: + - 8009 + + nginx: # Третий сервис, называемый "nginx". + image: nginx # Используется готовый образ NGINX из Docker Hub. + ports: # Публикует порты для доступа к NGINX. + - 8000:8000 # Проброс порта: внешний порт 8000 связан с внутренним портом 8000. + volumes: # Монтирует локальные файлы/директории в контейнер. + - ./nginx.conf:/etc/nginx/nginx.conf # Локальный файл nginx.conf будет монтирован в контейнер по пути /etc/nginx/nginx.conf. + depends_on: # Зависимости. NGINX будет запускаться после запуска указанных сервисов. + - author_service # NGINX зависит от запуска author_service. + - publication_service # NGINX зависит от запуска publication_service. \ No newline at end of file diff --git a/lazarev_andrey_lab_3/nginx.conf b/lazarev_andrey_lab_3/nginx.conf new file mode 100644 index 0000000..b98a188 --- /dev/null +++ b/lazarev_andrey_lab_3/nginx.conf @@ -0,0 +1,27 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 8000; + listen [::]:8000; + server_name localhost; + + location /author_service/ { + proxy_pass http://author_service:8008/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Prefix $scheme; + } + + location /publication_service/ { + proxy_pass http://publication_service:8009/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Prefix $scheme; + } + } +} \ No newline at end of file diff --git a/lazarev_andrey_lab_3/publication_service/Dockerfile b/lazarev_andrey_lab_3/publication_service/Dockerfile new file mode 100644 index 0000000..8b7e4a6 --- /dev/null +++ b/lazarev_andrey_lab_3/publication_service/Dockerfile @@ -0,0 +1,12 @@ +#описание в Dockerfile author_service +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY publication_service/main.py . + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/lazarev_andrey_lab_3/publication_service/main.py b/lazarev_andrey_lab_3/publication_service/main.py new file mode 100644 index 0000000..ad5c239 --- /dev/null +++ b/lazarev_andrey_lab_3/publication_service/main.py @@ -0,0 +1,113 @@ +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field +from uuid import UUID, uuid4 +import requests + +#Инициализация веб-приложения +app = FastAPI(title="Publication service") + +#Строка подключения к авторам +author_url='http://author_service:8008' + +#Сущность публикации с полями название, год публикации, ид автора. +class Publication(BaseModel): + uuid_: UUID = Field(default_factory=uuid4) + name: str | None + public_year: int | None + author_id: UUID | None + +#Заранее заполненный список публикаций, в некоторых есть uuid они пригодятся при создании публикаций +data: list[Publication] = [ + Publication(uuid_="92e78b56-0026-4561-b9f6-ba628110c901", name="книга 1", public_year=2024, author_id="92e78b56-0026-4561-b9f6-ba628110c900"), + Publication(name="книга 2", public_year=2022, author_id="92e78b56-0026-4561-b9f6-ba628110c900"), + Publication(name="книга 3", public_year=2003, author_id="3203b355-d844-4d5a-ad91-a9e0135cd9d9"), + Publication(name="книга 4", public_year=2020, author_id="92e78b56-0026-4561-b9f6-ba628110c900"), + Publication(name="книга 5", public_year=2019, author_id="3203b355-d844-4d5a-ad91-a9e0135cd9d9") +] + +#Получить список всех публикаций +@app.get("/publication", tags=["Publication"]) +def all_publications(): + return data + + +#Получить одной публикации по uuid +@app.get("/publication/{publication_id}", tags=["Publication"]) +def get_publication(publication_id: UUID): + publication = next((x for x in data if x.uuid_ == publication_id), None) + + if not publication: + return HTTPException(status_code=404, detail="Публикация не найдена") + + return publication + +#Получить одной публикации по uuid с информацие об ее авторе +@app.get("/publication/full/{publication_id}") +def get_full_publication(publication_id: UUID): + publication = get_publication(publication_id) + + if not publication: + return HTTPException(status_code=404, detail="Публикаций не найдена") + + author = requests.get(f"{author_url}/author/{publication.author_id}") + + if not author: + return HTTPException(status_code=404, detail="Автор не найден") + + result = publication.model_dump() + result['author_info'] = author.json() + + return result + +#Добавление новой публикации, все поля обязательные +@app.post("/publication", tags=["Publication"]) +def add_publication(name: str, public_year: int, author_id: UUID): + author = next((x for x in data if x.name == name and x.public_year == public_year and x.author_id == author_id), None) + + if author: + return HTTPException(status_code=404, detail="Такая публикация уже существует") + + try: + data.append(Publication(name=name, public_year=public_year, author_id=author_id)) + return JSONResponse(content={"message": "Публикация успешно добавлена"}, status_code=200) + + except Exception as e: + return HTTPException(status_code=404, detail={"Публикация не была добавлена с ошибкой": str(e)}) + +#Изменение публикации по uuid +@app.put("/publication/{publication_id}", tags=["Publication"]) +def update_publication(publication_id: UUID, name: str = None, public_year: int = None, author_id: UUID = None): + publication = get_publication(publication_id) + + if publication: + index = data.index(publication) + if name: + data[index].name = name + if public_year: + data[index].public_year = public_year + if author_id: + data[index].author_id = author_id + return JSONResponse(content={"message": "Публикация успешно изменена"}, status_code=200) + + else: + return HTTPException(status_code=404, detail={"Публикация не найдена": {publication}}) + +#Удаление публикации по uuid +@app.delete("/publication/{publication_id}", tags=["Publication"]) +def delete_publication(publication_id: UUID): + publication = get_publication(publication_id) + + if publication: + index = data.index(publication) + del data[index] + + return JSONResponse(content={"message": "Публикация успешно удалена"}, status_code=200) + + else: + return HTTPException(status_code=404, detail={"Публикация не найдена": {publication}}) + +#Запуск +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8009) \ No newline at end of file diff --git a/lazarev_andrey_lab_3/requirements.txt b/lazarev_andrey_lab_3/requirements.txt new file mode 100644 index 0000000..5378a0d --- /dev/null +++ b/lazarev_andrey_lab_3/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.115.0 +uvicorn==0.31.0 +pydantic==2.9.2 +pydantic_core==2.23.4 +requests==2.32.3 \ No newline at end of file