diff --git a/kashin_maxim_lab_3/.gitignore b/kashin_maxim_lab_3/.gitignore new file mode 100644 index 0000000..b694934 --- /dev/null +++ b/kashin_maxim_lab_3/.gitignore @@ -0,0 +1 @@ +.venv \ No newline at end of file diff --git a/kashin_maxim_lab_3/docker-compose.yml b/kashin_maxim_lab_3/docker-compose.yml new file mode 100644 index 0000000..b4887c5 --- /dev/null +++ b/kashin_maxim_lab_3/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' +services: + university_service: + build: + context: ./university_service + depends_on: + - university_db + environment: + - FLASK_APP=app.py + expose: + - 8081 + + faculty_service: + build: + context: ./faculty_service + depends_on: + - faculty_db + environment: + - FLASK_APP=app.py + expose: + - 8082 + + university_db: + image: postgres + environment: + POSTGRES_USER: university_user + POSTGRES_PASSWORD: password + POSTGRES_DB: universitydb + ports: + - "5433:5432" + volumes: + - ./university_service/init.sql:/docker-entrypoint-initdb.d/init.sql + + faculty_db: + image: postgres + environment: + POSTGRES_USER: faculty_user + POSTGRES_PASSWORD: password + POSTGRES_DB: facultydb + ports: + - "5434:5432" + volumes: + - ./faculty_service/init.sql:/docker-entrypoint-initdb.d/init.sql + + nginx: + image: nginx + ports: + - 8086:8086 + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + depends_on: + - university_service + - faculty_service diff --git a/kashin_maxim_lab_3/faculty_service/Dockerfile b/kashin_maxim_lab_3/faculty_service/Dockerfile new file mode 100644 index 0000000..aa1488a --- /dev/null +++ b/kashin_maxim_lab_3/faculty_service/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.9-slim + +# Установка зависимостей +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Установка рабочей директории +WORKDIR /app + +# Копирование файлов +COPY requirements.txt requirements.txt + +# Установка Python-зависимостей +RUN pip install --no-cache-dir -r requirements.txt + +# Копирование всех файлов приложения +COPY . . + +# Запуск приложения +CMD ["python", "app.py"] diff --git a/kashin_maxim_lab_3/faculty_service/app.py b/kashin_maxim_lab_3/faculty_service/app.py new file mode 100644 index 0000000..97678eb --- /dev/null +++ b/kashin_maxim_lab_3/faculty_service/app.py @@ -0,0 +1,57 @@ +from flask import Flask, jsonify, request, Response +from service import FacultyService +from db import db + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://faculty_user:password@faculty_db:5432/facultydb' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) + +faculty_service = FacultyService() + + +@app.route('/faculties', methods=['GET', 'POST']) +def handle_faculties(): + if request.method == 'POST': + dto = request.get_json()['dto'] + faculty = faculty_service.create_faculty(dto['name'], dto['university_id']) + + if isinstance(faculty, dict): + return jsonify({"id": faculty['id'], "name": faculty['name'], "university_id": faculty['university_id']}) + + return jsonify({"id": faculty.id, "name": faculty.name, "university_id": faculty.university_id}) + + return jsonify(faculty_service.get_all_faculties()) + +@app.route('/faculties/', methods=['GET', 'PUT', 'DELETE']) +def handle_faculty(faculty_id): + if request.method == 'PUT': + dto = request.get_json()['dto'] + updated_faculty = faculty_service.update_faculty(faculty_id, dto['name']) + if updated_faculty: + return jsonify(updated_faculty) + else: + return Response(status=404) + + elif request.method == 'DELETE': + faculty_service.delete_faculty(faculty_id) + return Response(status=200) + + university = faculty_service.get_faculty(faculty_id) + return jsonify({"id": university.id, "name": university.name, "university_id": university.university_id}) + +@app.route('/faculties//universities', methods=['POST']) +def add_university(faculty_id): + dto = request.get_json() + university_id = dto['university_id'] + faculty_service.add_university_to_faculty(faculty_id, university_id) + return Response(status=204) + +@app.route('/faculties//universities/', methods=['DELETE']) +def remove_university(faculty_id, university_id): + faculty_service.remove_university_from_faculty(faculty_id, university_id) + return Response(status=204) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', use_reloader=False, port=8082) diff --git a/kashin_maxim_lab_3/faculty_service/db.py b/kashin_maxim_lab_3/faculty_service/db.py new file mode 100644 index 0000000..23e6b0d --- /dev/null +++ b/kashin_maxim_lab_3/faculty_service/db.py @@ -0,0 +1,19 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class Faculty(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + university_id = db.Column(db.Integer, nullable=False) + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "university_id": self.university_id + } + +class FacultyUniversity(db.Model): + faculty_id = db.Column(db.Integer, db.ForeignKey('faculty.id'), primary_key=True) + university_id = db.Column(db.Integer, db.ForeignKey('university.id'), primary_key=True) diff --git a/kashin_maxim_lab_3/faculty_service/init.sql b/kashin_maxim_lab_3/faculty_service/init.sql new file mode 100644 index 0000000..6a6319e --- /dev/null +++ b/kashin_maxim_lab_3/faculty_service/init.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS faculty ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + university_id INT NOT NULL +); + +CREATE TABLE IF NOT EXISTS university ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); + +CREATE TABLE IF NOT EXISTS faculty_university ( + faculty_id INT REFERENCES faculty(id), + university_id INT REFERENCES university(id), + PRIMARY KEY (faculty_id, university_id) +); diff --git a/kashin_maxim_lab_3/faculty_service/requirements.txt b/kashin_maxim_lab_3/faculty_service/requirements.txt new file mode 100644 index 0000000..c9b3f66 --- /dev/null +++ b/kashin_maxim_lab_3/faculty_service/requirements.txt @@ -0,0 +1,3 @@ +flask +flask_sqlalchemy +psycopg2 \ No newline at end of file diff --git a/kashin_maxim_lab_3/faculty_service/service.py b/kashin_maxim_lab_3/faculty_service/service.py new file mode 100644 index 0000000..65d98db --- /dev/null +++ b/kashin_maxim_lab_3/faculty_service/service.py @@ -0,0 +1,39 @@ +from db import db, Faculty, FacultyUniversity + + +class FacultyService: + def get_all_faculties(self): + return [faculty.to_dict() for faculty in Faculty.query.all()] + + def get_faculty(self, faculty_id): + return Faculty.query.get(faculty_id) + + def create_faculty(self, name, university_id): + faculty = Faculty(name=name, university_id=university_id) + db.session.add(faculty) + db.session.commit() + return faculty + + def update_faculty(self, faculty_id, name): + faculty = self.get_faculty(faculty_id) + if faculty: + faculty.name = name + db.session.commit() + return faculty.to_dict() + return None + + def delete_faculty(self, faculty_id): + faculty = self.get_faculty(faculty_id) + if faculty: + db.session.delete(Faculty.query.get(faculty_id)) + db.session.commit() + def add_university_to_faculty(self, faculty_id, university_id): + association = FacultyUniversity(faculty_id=faculty_id, university_id=university_id) + db.session.add(association) + db.session.commit() + + def remove_university_from_faculty(self, faculty_id, university_id): + association = FacultyUniversity.query.filter_by(faculty_id=faculty_id, university_id=university_id).first() + if association: + db.session.delete(association) + db.session.commit() \ No newline at end of file diff --git a/kashin_maxim_lab_3/nginx.conf b/kashin_maxim_lab_3/nginx.conf new file mode 100644 index 0000000..492592f --- /dev/null +++ b/kashin_maxim_lab_3/nginx.conf @@ -0,0 +1,27 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 8086; + listen [::]:8086; + server_name localhost; + + location /university_service/ { + proxy_pass http://university_service:8081/; + 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 /faculty_service/ { + proxy_pass http://faculty_service:8082/; + 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/kashin_maxim_lab_3/readme.md b/kashin_maxim_lab_3/readme.md new file mode 100644 index 0000000..0dab095 --- /dev/null +++ b/kashin_maxim_lab_3/readme.md @@ -0,0 +1,105 @@ +# Кашин Максим ПИбд-42 + +# Проект "Университеты и Факультеты" + +## Часть 1: Создание Docker + +### Установка Docker и Docker Compose +Перед началом нужно проверить, что Docker работает, иначе использовать инструцию для 1-ой лабораторной. + +### 1.1 Файл `docker-compose.yml` +В корне проекта файл `docker-compose.yml`. Этот файл будет описывать, как должны быть организованы сервисы. В данном случае рассматриваем только университет, в факультетах анологично. + +```yaml +version: '3.8' +services: + university_service: + build: + context: ./university_service + depends_on: + - university_db + environment: + - FLASK_APP=app.py + expose: + - 8081 + university_db: + image: postgres:13 + environment: + POSTGRES_USER: university_user + POSTGRES_PASSWORD: password + POSTGRES_DB: universitydb + ports: + - "5432:5432" +``` + +Объяснение: +- `version`: Указывает версию Docker Compose. +- `services`: Определяет микросервисы. В данном случае, мы начинаем с `university_service`. +- `build`: Указывает, что нужно построить образ для этого сервиса, используя Dockerfile из указанной директории. +- `depends_on`: Указывает, что этот сервис зависит от `university_db` (базы данных), которая будет запущена первой. +- `environment`: Определяет переменные окружения для сервиса, в данном случае указывая файл Flask. +- `expose`: Указывает, что сервис будет доступен на порту 8081 внутри Docker-сети. + +### 1.2 Файл Dockerfile для сервиса университетов +В каталоге `university_service` файл `Dockerfile`. Этот файл будет содержать инструкции для сборки образа сервиса университетов. В данном случае рассматриваем только университет, в факультетах анологично. + +```dockerfile +FROM python:3.9-slim + +# Установка зависимостей +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Установка рабочей директории +WORKDIR /app + +# Копирование файлов +COPY requirements.txt requirements.txt + +# Установка Python-зависимостей +RUN pip install --no-cache-dir -r requirements.txt + +# Копирование всех файлов приложения +COPY . . + +# Запуск приложения +CMD ["python", "app.py"] +``` + + +### 1.3 Создание файла requirements.txt +В каталоге `university_service` файл `requirements.txt`, в котором перечислите все необходимые библиотеки для приложения. В данном случае рассматриваем только университет, в факультетах анологично. + +```txt +flask +flask_sqlalchemy +psycopg2 +``` + +### 1.5 Запуск Docker Compose +Когда все файлы созданы, можно запустить сервисы с помощью команды: + +```bash +docker-compose up +``` + +Эта команда создаст и запустит все указанные в `docker-compose.yml` сервисы. + +## Часть 2: Реализация функциональности + +### 2.1 Создание сервиса для университетов +В каталоге `university_service` файл `app.py`,`service.py`,`db.py`. Этb файлы будут содержать код Flask приложения для управления университетами и базы данных для него. + +### 2.2 Создание сервиса для факультетов +Каталог `faculty_service` и в нем файл `app.py`,`service.py`,`db.py`. Эти файлы будут содержать код для управления факультетами и базы данных для него. + +### 2.3 Создание таблиц базы данных +Нужно создать таблицы базы данных для университетов и факультетов. Для этого созданы файлы init.sql для университета и для факультетов. + +### 2.4 Тестирование API +Тестирование производится через расширение RestMan. В нем мы можем убедиться, что весь CRUD работает исправно. + +## Часть 3: Ссылка на видео +[Видео-отчёт Кашин Максим ПИбд-42](https://disk.yandex.ru/i/F0Dh7RnpVL1Stg) \ No newline at end of file diff --git a/kashin_maxim_lab_3/university_service/Dockerfile b/kashin_maxim_lab_3/university_service/Dockerfile new file mode 100644 index 0000000..aa1488a --- /dev/null +++ b/kashin_maxim_lab_3/university_service/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.9-slim + +# Установка зависимостей +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Установка рабочей директории +WORKDIR /app + +# Копирование файлов +COPY requirements.txt requirements.txt + +# Установка Python-зависимостей +RUN pip install --no-cache-dir -r requirements.txt + +# Копирование всех файлов приложения +COPY . . + +# Запуск приложения +CMD ["python", "app.py"] diff --git a/kashin_maxim_lab_3/university_service/app.py b/kashin_maxim_lab_3/university_service/app.py new file mode 100644 index 0000000..ccc9a72 --- /dev/null +++ b/kashin_maxim_lab_3/university_service/app.py @@ -0,0 +1,54 @@ +from flask import Flask, jsonify, request, Response +from service import UniversityService +from db import db + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://university_user:password@university_db:5432/universitydb' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) + +university_service = UniversityService() + + +@app.route('/universities', methods=['GET', 'POST']) +def handle_universities(): + if request.method == 'POST': + dto = request.get_json()['dto'] + university = university_service.create_university(dto['name']) + return jsonify(university) + + universities = university_service.get_all_universities() + return jsonify(universities) + + +@app.route('/universities/', methods=['GET', 'PUT', 'DELETE']) +def handle_university(university_id): + if request.method == 'PUT': + dto = request.get_json()['dto'] + updated_university = university_service.update_university(university_id, dto['name']) + return jsonify(updated_university) if updated_university else Response(status=404) + + elif request.method == 'DELETE': + university_service.delete_university(university_id) + return Response(status=200) + + university = university_service.get_university(university_id) + if university is None: + return Response(status=404) + return jsonify(university) + +@app.route('/universities//faculties', methods=['POST']) +def add_faculty(university_id): + dto = request.get_json() + faculty_id = dto['faculty_id'] + university_service.add_faculty_to_university(university_id, faculty_id) + return Response(status=204) + +@app.route('/universities//faculties/', methods=['DELETE']) +def remove_faculty(university_id, faculty_id): + university_service.remove_faculty_from_university(university_id, faculty_id) + return Response(status=204) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', use_reloader=False, port=8081) diff --git a/kashin_maxim_lab_3/university_service/db.py b/kashin_maxim_lab_3/university_service/db.py new file mode 100644 index 0000000..4c218b1 --- /dev/null +++ b/kashin_maxim_lab_3/university_service/db.py @@ -0,0 +1,20 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + +class University(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + established_at = db.Column(db.DateTime, default=datetime.utcnow) + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "established_at": self.established_at.isoformat() + } + +class FacultyUniversity(db.Model): + faculty_id = db.Column(db.Integer, db.ForeignKey('faculty.id'), primary_key=True) + university_id = db.Column(db.Integer, db.ForeignKey('university.id'), primary_key=True) diff --git a/kashin_maxim_lab_3/university_service/init.sql b/kashin_maxim_lab_3/university_service/init.sql new file mode 100644 index 0000000..4bfd43b --- /dev/null +++ b/kashin_maxim_lab_3/university_service/init.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS university ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + established_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE IF NOT EXISTS faculty ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); + +CREATE TABLE IF NOT EXISTS faculty_university ( + faculty_id INT REFERENCES faculty(id), + university_id INT REFERENCES university(id), + PRIMARY KEY (faculty_id, university_id) +); diff --git a/kashin_maxim_lab_3/university_service/requirements.txt b/kashin_maxim_lab_3/university_service/requirements.txt new file mode 100644 index 0000000..c9b3f66 --- /dev/null +++ b/kashin_maxim_lab_3/university_service/requirements.txt @@ -0,0 +1,3 @@ +flask +flask_sqlalchemy +psycopg2 \ No newline at end of file diff --git a/kashin_maxim_lab_3/university_service/service.py b/kashin_maxim_lab_3/university_service/service.py new file mode 100644 index 0000000..2884c38 --- /dev/null +++ b/kashin_maxim_lab_3/university_service/service.py @@ -0,0 +1,42 @@ +from db import db, University, FacultyUniversity + + +class UniversityService: + def get_all_universities(self): + return [university.to_dict() for university in University.query.all()] + + def get_university(self, university_id): + university = University.query.get(university_id) + return university.to_dict() if university else None + + def create_university(self, name): + university = University(name=name) + db.session.add(university) + db.session.commit() + return university.to_dict() + + def update_university(self, university_id, name): + university = self.get_university(university_id) + if university: + university_obj = University.query.get(university_id) + university_obj.name = name + db.session.commit() + return university_obj.to_dict() + return None + + def delete_university(self, university_id): + university = self.get_university(university_id) + if university: + db.session.delete(University.query.get(university_id)) + db.session.commit() + + def add_faculty_to_university(self, university_id, faculty_id): + association = FacultyUniversity(university_id=university_id, faculty_id=faculty_id) + db.session.add(association) + db.session.commit() + + def remove_faculty_from_university(self, university_id, faculty_id): + association = FacultyUniversity.query.filter_by(university_id=university_id, faculty_id=faculty_id).first() + if association: + db.session.delete(association) + db.session.commit() \ No newline at end of file