diff --git a/karamushko_maxim_lab_3/.gitignore b/karamushko_maxim_lab_3/.gitignore new file mode 100644 index 0000000..fb9df04 --- /dev/null +++ b/karamushko_maxim_lab_3/.gitignore @@ -0,0 +1 @@ +/.venv \ No newline at end of file diff --git a/karamushko_maxim_lab_3/README.md b/karamushko_maxim_lab_3/README.md new file mode 100644 index 0000000..a6f2fcb --- /dev/null +++ b/karamushko_maxim_lab_3/README.md @@ -0,0 +1,35 @@ +# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами +## ПИбд-42 || Карамушко Максим + +### Описание: +В данной лабораторной работе реализованы две сущности со связью "одик-ко-многим". Для доступа к кажой сущности реализован отдельный сервис. + +Сервис film: CRUD операции. Помимо этого реализована операция получения фильмов с информацие о жанре, получение фильмов по жанру и получение одного фильма с информацией о жанре + +Сервис genre: CRUD операции. Также реализована функция получения жанра со списком фильмов в этом жанре. + +### Цель лабораторной работы +изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API + +### Выбранные сущности: +- Жанр. Поля: Uuid, Name +- Фильм. Поля: Uuid, Name, Description + +### Инструкция для работы +1. Клонирование репозитория: +``` +git clone <ссылка-на-репозиторий> +cd <папка репозитория> +cd <папка лабораторной работы> +``` + +2. Запуск контейнеров: +``` +docker compose up --build +``` + +3. Результаты: +Можно применять описанные выше операции к сущностям через http запросы. + +### Видео с демонстрацией работы: +https://disk.yandex.ru/i/j7UxPcoU3lHx3Q \ No newline at end of file diff --git a/karamushko_maxim_lab_3/docker-compose.yaml b/karamushko_maxim_lab_3/docker-compose.yaml new file mode 100644 index 0000000..809419d --- /dev/null +++ b/karamushko_maxim_lab_3/docker-compose.yaml @@ -0,0 +1,26 @@ +services: + genre: + container_name: genre_service + build: + context: . + dockerfile: ./genre/Dockerfile + expose: + - 8001 + + film: + container_name: film_service + build: + context: . + dockerfile: ./film/Dockerfile + expose: + - 8002 + + nginx: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + depends_on: + - genre + - film \ No newline at end of file diff --git a/karamushko_maxim_lab_3/film/Dockerfile b/karamushko_maxim_lab_3/film/Dockerfile new file mode 100644 index 0000000..277d85c --- /dev/null +++ b/karamushko_maxim_lab_3/film/Dockerfile @@ -0,0 +1,14 @@ +# базовый образ +FROM python:3.12 + +# рабочая директория +WORKDIR /app + +COPY requirements.txt . + +# зависимости +RUN pip install --no-cache-dir -r requirements.txt + +COPY film/service.py . + +CMD ["python", "service.py"] \ No newline at end of file diff --git a/karamushko_maxim_lab_3/film/service.py b/karamushko_maxim_lab_3/film/service.py new file mode 100644 index 0000000..560d3d4 --- /dev/null +++ b/karamushko_maxim_lab_3/film/service.py @@ -0,0 +1,157 @@ +from flask import Flask, jsonify, request +import requests +from uuid import uuid4 +import uuid + + +class Film: + def __init__(self, name, description, uuid_: uuid, genre_id: uuid): + if uuid_ is None: + self.uuid_: uuid = uuid4() + else: + self.uuid_: uuid = uuid.UUID(uuid_) + self.name: str = name + self.description: str = description + self.genre_id: uuid = uuid.UUID(genre_id) + + def to_dict(self): + return { + 'name': self.name, + 'description': self.description, + 'genre_id': self.genre_id, + 'uuid': self.uuid_ + } + + def to_dict_for_genres(self): + return { + 'name': self.name, + 'description': self.description, + 'uuid': self.uuid_ + } + + def to_dict_with_info(self, genre: dict): + return { + 'name': self.name, + 'description': self.description, + 'genre_id': self.genre_id, + 'genre_info': genre, + 'uuid': self.uuid_ + } + + +app = Flask(__name__) + +specialities: list[Film] = [ + Film(name='Пираты карибского моря', description='фильм про пиратов', uuid_='51e5c5c1-1e6e-4455-b4e1-aec5774a2961', + genre_id='450a9c7c-fb6f-4a42-a354-acef65af4c9b'), + Film(name='1+1', description='=2', uuid_='660e4da0-abe9-4d33-9070-f3e8f67484f4', + genre_id='6f26879d-bdf2-4ccd-bd6f-23275c86a9ac'), + Film(name='Во все тяжкие', description='познавательный сериал про химию', uuid_='801d599b-ce70-4755-a2ef-00e1c092811d', + genre_id='139a4578-5922-4688-92e2-22e749ff8c47'), +] + +genres_url = 'http://genre_service:8001/' + + +def list_jsonify(): + return jsonify([speciality.to_dict() for speciality in specialities]) + + +# получение всех фильмов +@app.route('/', methods=['GET']) +def get_all(): + return list_jsonify(), 200 + + +# получение всех фильмов с информацие о жанре +@app.route('/full', methods=['GET']) +def get_all_full(): + genres: list[dict] = requests.get(genres_url).json() + response = [] + for speciality in specialities: + for genre in genres: + if speciality.genre_id == uuid.UUID(genre.get('uuid')): + response.append(speciality.to_dict_with_info(genre)) + + return response, 200 + + +# получение фильмов по uuid жанра +@app.route('/by-genre/', methods=['GET']) +def get_by_genre_id(genre_uuid): + return [speciality.to_dict_for_genres() for speciality in specialities if speciality.genre_id == genre_uuid], 200 + + +# получение фильма по uuid +@app.route('/', methods=['GET']) +def get_one(uuid_): + for speciality in specialities: + if speciality.uuid_ == uuid_: + return speciality.to_dict(), 200 + + return 'Специальность с указанным uuid не обнаружена', 404 + + +# получение фильма по uuid с информацие о жанре +@app.route('/full/', methods=['GET']) +def get_one_full(uuid_): + for speciality in specialities: + if speciality.uuid_ == uuid_: + response = requests.get(genres_url + str(speciality.genre_id)) + return speciality.to_dict_with_info(response.json()), 200 + + return 'Специальность с указанным uuid не обнаружена', 404 + + +# создание фильма +@app.route('/', methods=['POST']) +def create(): + data = request.json + name = data.get('name', None) + description = data.get('description', None) + genre_id = data.get('genre_id', None) + checking = requests.get(genres_url + f'/check/{genre_id}') + print(checking) + if checking.status_code == 200: + new_speciality = Film(name, description, None, genre_id) + specialities.append(new_speciality) + return get_one(new_speciality.uuid_) + if checking.status_code == 404: + return 'Факультета с таким uuid не существует', 404 + + return 'Неизвестная ошибка', 500 + + +# изменение фильма по uuid +@app.route('/', methods=['PUT']) +def update_by_id(uuid_): + data = request.json + new_name = data.get('name', None) + new_description = data.get('description', None) + + for speciality in specialities: + print(speciality.uuid_) + + if speciality.uuid_ == uuid_: + if new_name is not None: + speciality.name = new_name + if new_description is not None: + speciality.description = new_description + return get_one(speciality.uuid_) + + return 'Специальность с указанным uuid не обнаружена', 404 + + +# удаление фильма по uuid +@app.route('/', methods=['DELETE']) +def delete(uuid_): + for speciality in specialities: + if speciality.uuid_ == uuid_: + specialities.remove(speciality) + return 'Специальность была удалена', 200 + + return 'Специальность с указанным uuid не обнаружена', 404 + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8002, debug=True) \ No newline at end of file diff --git a/karamushko_maxim_lab_3/genre/Dockerfile b/karamushko_maxim_lab_3/genre/Dockerfile new file mode 100644 index 0000000..3b37840 --- /dev/null +++ b/karamushko_maxim_lab_3/genre/Dockerfile @@ -0,0 +1,14 @@ +# базовый образ +FROM python:3.12 + +# рабочая директория +WORKDIR /app + +COPY requirements.txt . + +# зависимости +RUN pip install --no-cache-dir -r requirements.txt + +COPY genre/service.py . + +CMD ["python", "service.py"] \ No newline at end of file diff --git a/karamushko_maxim_lab_3/genre/service.py b/karamushko_maxim_lab_3/genre/service.py new file mode 100644 index 0000000..25dc856 --- /dev/null +++ b/karamushko_maxim_lab_3/genre/service.py @@ -0,0 +1,125 @@ +from flask import Flask, jsonify, request +from uuid import uuid4 +import uuid +import requests + + +class Genre: + def __init__(self, name, uuid_: uuid): + if uuid_ is None: + self.uuid_: uuid = uuid4() + else: + self.uuid_: uuid = uuid.UUID(uuid_) + self.name: str = name + + def to_dict(self): + return { + "uuid": self.uuid_, + "name": self.name, + } + + def to_dict_with_films(self, films: list): + return { + "uuid": self.uuid_, + "name": self.name, + "films": films + } + + +app = Flask(__name__) + +genres: list[Genre] = [ + Genre(name='Комедия', uuid_='450a9c7c-fb6f-4a42-a354-acef65af4c9b'), + Genre(name='Драма', uuid_='6f26879d-bdf2-4ccd-bd6f-23275c86a9ac'), + Genre(name='Экшен', uuid_='139a4578-5922-4688-92e2-22e749ff8c47'), +] + +films_url = 'http://film_service:8002/' + + +def list_jsonify(): + return jsonify([genre.to_dict() for genre in genres]) + + +# получение всех жанров +@app.route('/', methods=['GET']) +def get_all(): + return list_jsonify(), 200 + + +# получение жанра по uuid +@app.route('/', methods=['GET']) +def get_one(uuid_): + for genre in genres: + if genre.uuid_ == uuid_: + return genre.to_dict(), 200 + + return 'Факультет с указанным uuid не обнаружен', 404 + + +# получение жанра со списком фильмов в этом жанре +@app.route('/with-films/', methods=['GET']) +def get_one_with_films(uuid_): + for genre in genres: + if genre.uuid_ == uuid_: + response = requests.get(films_url + f'by-genre/{uuid_}') + print(response.json()) + return genre.to_dict_with_films(response.json()), 200 + + return 'Факультет с указанным uuid не обнаружен', 404 + + +# проверка наличия жанра по uuid +@app.route('/check/', methods=['GET']) +def check_exist(uuid_): + for genre in genres: + if genre.uuid_ == uuid_: + return '', 200 + return '', 404 + + +# создание жанра +@app.route('/', methods=['POST']) +def create(): + data = request.json + name = data.get('name', None) + description = data.get('description', None) + if name is None or description is None: + return 'Не хватает данных для создания нового факультета', 404 + + new_genre = Genre(name, description, None) + genres.append(new_genre) + return get_one(new_genre.uuid_) + + +# изменения жанра по uuid +@app.route('/', methods=['PUT']) +def update_by_id(uuid_): + data = request.json + new_name = data.get('name', None) + new_description = data.get('description', None) + + for genre in genres: + if genre.uuid_ == uuid_: + if new_name is not None: + genre.name = new_name + if new_description is not None: + genre.description = new_description + return get_one(genre.uuid_) + + return 'Факультет с указанным uuid не обнаружен', 404 + + +# удаление жанра по uuid +@app.route('/', methods=['DELETE']) +def delete(uuid_): + for genre in genres: + if genre.uuid_ == uuid_: + genres.remove(genre) + return 'Факультет был удален', 200 + + return 'Факультет с указанным uuid не обнаружен', 404 + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8001, debug=True) \ No newline at end of file diff --git a/karamushko_maxim_lab_3/nginx.conf b/karamushko_maxim_lab_3/nginx.conf new file mode 100644 index 0000000..06f976c --- /dev/null +++ b/karamushko_maxim_lab_3/nginx.conf @@ -0,0 +1,25 @@ +events { worker_connections 1024; } + +http { + server { + listen 80; + listen [::]:80; + server_name localhost; + + location /genre_service/ { + proxy_pass http://genre_service:8001/; + 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 /film_service/ { + proxy_pass http://film_service:8002/; + 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; + } + } +} \ No newline at end of file diff --git a/karamushko_maxim_lab_3/requirements.txt b/karamushko_maxim_lab_3/requirements.txt new file mode 100644 index 0000000..494909e --- /dev/null +++ b/karamushko_maxim_lab_3/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.3 +requests==2.32.3 \ No newline at end of file