diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de676cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +.vscode/ +venv/ +db.sqlite3 +setup.cfg \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f0d3c98..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 tric-itpc - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index cfb476f..847799a 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,48 @@ -# Вакансия :: Программист Python - -Разработка бизнес-системы с использованием веб-технологий. Автоматизация сервисов с большим количеством пользователей - -## От вас - -### Обязательно - -- Знание синтаксиса языка Python -- Опыт разработки на Python не менее 1 года -- Базовые знания принципов работы Web -- Желание работать в команде и развиваться - -### Приветствуется - -- Навыки работы с Flask, Sanic, FastAPI -- Опыт работы с БД: PostgreSQL, MS SQL, MongoDB, ClickHouse -- Опыт разработки под ОС семейства GNU Linux, знание основных команд -- Работа с системами управления исходным кодом Git -- Знания базовых принципов разработки (тестирование, рефакторинг, Code Review, CI/CD) - -### Будет круто, но не обязательно - -- Знание английского языка на уровне чтения технической документации -- Участие в разработке Open Source проектов -- Наличие профиля на GitHub, Stack Overflow -- Наличие проектов которые можете показать нам -- Разработка с использованием TypeScript, знание современных frontend-библиотек и подходов к разработке - -## У нас - -- Полный рабочий день, гибкий обед и начало рабочего дня -- Полностью «белая» заработная плата с возможностью увеличения в процессе работы (зависит от отдачи сотрудника) -- Полис ДМС -- Дружелюбная команда с юмором, готовая поддержать и помочь -- Интересный проект и необычные задачи. Рутина тоже есть, но мы нацелены именно на продуктив -- Возможность одновременно участвовать в разных проектах и развивать другие компетенции (TS и все модное) -- Попробовать современные тренды и практики в разработке ПО -- Никаких опенспейсов и кубиклов, а комфортное пространство в центре Тюмени -- Готовы делиться опытом и знаниями, если вы готовы их получать - -  - -Если вакансия вас заинтересовала, но есть недопонимания и вопросы, свяжитесь с нами - обсудим, договоримся. -Большим плюсом будет выполнение тестового задания. -Если у вас есть опыт работы с 1С, то эта вакансия не для вас. - -## Тестовое задание - -Решение принимается в виде PR к текущему проекту. - + + +# Документация API +Status History - **[API redoc](https://kaschenkkko.github.io/StatusHistory/)** + +# Описание проекта: Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно. -Требуется написать API который: - -1. Получает и сохраняет данные: имя, состояние, описание -2. Выводит список сервисов с актуальным состоянием -3. По имени сервиса выдает историю изменения состояния и все данные по каждому состоянию - -Дополнительным плюсом будет - -1. По указанному интервалу выдается информация о том сколько не работал сервис и считать SLA в процентах до 3-й запятой - -Вывод всех данных должен быть в формате JSON +Написан API который: + +1. Получает и сохраняет данные сервиса: имя, состояние, описание. +2. Выводит список сервисов с актуальным состоянием. +3. По id сервиса выдает историю изменения состояния и все данные по каждому состоянию. +4. По указанному интервалу выдаёт информация о том сколько не работал сервис и считает SLA в процентах до 3-й запятой. + +# Описание API: +- **GET /api/services/** - выводит список сервисов с актуальным состоянием. +- **POST /api/services/** - добавление нового сервиса. +- **GET /api/history/{service_id}/** - выдает историю изменений состояния. +- **GET /api/{service_id}/{start_date}/{end_date}/**: выдаёт информация о том сколько не работал сервис и считает SLA. + +# Запуск проекта: +- Клонируйте репозиторий и перейдите в него. +- Перейдите в папку **infra** и проверьте, что файл .env заполнен данными представленными ниже: + ``` + DEBUG=True + ALLOWED_HOSTS=127.0.0.1 + ``` +- Из папки **infra** запустите docker-compose + ``` + ~$ docker-compose up -d --build + ``` +- В контейнере web выполните миграции, создайте суперпользователя и соберите статику. + ``` + ~$ docker-compose exec web python manage.py migrate + ~$ docker-compose exec web python manage.py createsuperuser + ~$ docker-compose exec web python manage.py collectstatic --no-input + ``` + +После этого API проекта будет доступно по url-адресу [127.0.0.1/api/](http://127.0.0.1/api/) + +Документация к API будет доступна по url-адресу [127.0.0.1/api/docs](http://127.0.0.1/api/docs/) diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..52ed48a --- /dev/null +++ b/docs/index.html @@ -0,0 +1,19 @@ + + + + Documentation API + + + + + + + + + + \ No newline at end of file diff --git a/docs/openapi-schema.yml b/docs/openapi-schema.yml new file mode 100644 index 0000000..2eab914 --- /dev/null +++ b/docs/openapi-schema.yml @@ -0,0 +1,195 @@ +openapi: 3.0.2 +info: + title: 'StatusHistory' + version: '' +paths: + /api/services/: + get: + description: 'Список сервисов.' + parameters: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Services' + description: '' + tags: + - Сервисы + post: + description: 'Создание сервиса.' + parameters: + - name: name + required: true + in: query + description: 'Название сервиса' + example: 'Крутой сервис' + schema: + type: string + - name: description + required: true + in: query + description: 'Описание сервиса' + example: 'Описание крутого сервиса' + schema: + type: string + - name: status + required: false + in: query + description: 'Статус работы сервиса' + example: 'работает' + schema: + default: 'работает' + type: string + enum: ['работает', 'не работает', 'нестабильно'] + responses: + '201': + content: + application/json: + schema: + $ref: "#/components/schemas/Service" + description: '' + tags: + - Сервисы + /api/history/{service_id}/: + get: + description: 'История изменений состояния работы сервиса.' + parameters: + - name: service_id + required: true + in: path + description: 'ID сервиса' + example: 3 + schema: + type: integer + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/History' + description: '' + tags: + - История работы сервиса + /api/{service_id}/{start_date}/{end_date}/: + get: + operationId: Расчёт SLA + description: 'Информация о том сколько не работал сервис и расчёт SLA.' + parameters: + - name: service_id + required: true + in: path + description: 'ID сервиса' + example: 4 + schema: + type: integer + - name: start_date + required: true + in: path + description: 'Начало периода' + example: '2023-10-29' + schema: + type: string + - name: end_date + required: true + in: path + description: 'Конец периода' + example: '2023-11-13' + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + Информация для сервиса: + type: string + description: 'Название сервиса' + example: 'Fourth' + Service level agreement(в процентах): + type: integer + description: 'SLA' + example: '96.913' + Общее время недоступности сервиса: + type: string + description: 'Общее время недоступности' + example: '0 месяцев, 1 дней, 10 часов, 12 минут, 31 секунд' + description: '' + tags: + - Service level agreement + +components: + schemas: + Services: + type: object + properties: + id: + type: integer + readOnly: true + example: '1' + name: + type: string + maxLength: 100 + description: 'Название' + example: 'СAPI2' + status: + type: string + maxLength: 50 + description: 'Состояние' + example: 'работает' + description: + type: string + description: 'Описание' + example: 'Крутой сервис' + Service: + type: object + properties: + id: + title: ID + type: integer + readOnly: true + example: 2 + name: + title: Название сервиса + type: string + example: "Крутой сервис" + maxLength: 100 + status: + title: Статус работы сервиса + type: string + example: "работает" + description: + title: Опсиание сервиса + type: string + example: "Описание крутого сервиса" + History: + type: object + properties: + id: + title: ID + type: integer + readOnly: true + example: 1 + name: + title: Название сервиса + type: string + example: "CAPI2" + maxLength: 100 + service_id: + title: ID сервиса + type: string + example: 3 + status: + title: Статус работы сервиса + type: string + example: "работает" + last_modified: + title: Дата последнего изменения работы сервиса + type: string + example: "12.11.2023 12:05:52" \ No newline at end of file diff --git a/infra/.env b/infra/.env new file mode 100644 index 0000000..8b3f4b2 --- /dev/null +++ b/infra/.env @@ -0,0 +1,2 @@ +DEBUG=True +ALLOWED_HOSTS=127.0.0.1 \ No newline at end of file diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..c395ee1 --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + web: + build: ../services/ + restart: always + volumes: + - static_value:/app/static/ + env_file: + - .env + + nginx: + image: nginx:1.21.3-alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ../services/api/docs/:/usr/share/nginx/html/api/docs/ + - static_value:/var/html/static/ + depends_on: + - web + +volumes: + static_value: \ No newline at end of file diff --git a/infra/nginx.conf b/infra/nginx.conf new file mode 100644 index 0000000..a03f3d2 --- /dev/null +++ b/infra/nginx.conf @@ -0,0 +1,25 @@ +server { + listen 80; + server_name 127.0.0.1; + server_tokens off; + + location /static/ { + autoindex on; + root /var/html/; + } + + location /api/ { + 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; + proxy_pass http://web:8000/api/; + } + + location /admin/ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_pass http://web:8000/admin/; + } +} \ No newline at end of file diff --git a/services/Dockerfile b/services/Dockerfile new file mode 100644 index 0000000..4aaa537 --- /dev/null +++ b/services/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.7-slim + +WORKDIR /app + +COPY . . + +RUN apt-get update && pip install --upgrade pip && pip install -r /app/requirements.txt --no-cache-dir + +CMD ["gunicorn", "config.wsgi:application", "--bind", "0:8000"] \ No newline at end of file diff --git a/services/api/__init__.py b/services/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/api/admin.py b/services/api/admin.py new file mode 100644 index 0000000..f75921c --- /dev/null +++ b/services/api/admin.py @@ -0,0 +1,21 @@ +from django.contrib import admin + +from .models import Service, StatusHistory + + +@admin.register(Service) +class ServiceAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'status', 'description') + search_fields = ('name', 'description') + list_filter = ('status',) + + +@admin.register(StatusHistory) +class StatusHistorysAdmin(admin.ModelAdmin): + list_display = ('id', 'service', 'status', 'date') + search_fields = ('service__name',) + list_filter = ('status',) + + def date(self, obj): + return obj.last_modified.strftime('%d.%m.%Y %H:%M:%S') + date.short_description = 'Дата изменения' diff --git a/services/api/apps.py b/services/api/apps.py new file mode 100644 index 0000000..9b148ce --- /dev/null +++ b/services/api/apps.py @@ -0,0 +1,42 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' + + def ready(self): + from django.db.models.signals import post_save, pre_save + from django.dispatch import receiver + from django.utils import timezone + + from .models import Service, StatusHistory + + @receiver(post_save, sender=Service) + def model_service_changed(sender, instance, **kwargs): + """ + При изменении статуса работы сервиса будет создаваться новый + объект StatusHistory. + """ + + related_instance = StatusHistory.objects.create(service=instance) + related_instance.status = related_instance.service.status + related_instance.save() + + @receiver(pre_save, sender=StatusHistory) + def update_start_end_time(sender, instance, **kwargs): + """ + Установка start_time и end_time в соответствии с + изменениями статуса сервиса. + """ + + if instance.status == 'не работает' and not instance.start_time: + instance.start_time = timezone.now() + + elif (instance.status == 'работает' or + instance.status == 'нестабильно'): + last_down_status = StatusHistory.objects.filter( + status='не работает', service=instance.service_id).first() + if last_down_status and not last_down_status.end_time: + last_down_status.end_time = timezone.now() + last_down_status.save() diff --git a/services/api/docs/openapi-schema.yml b/services/api/docs/openapi-schema.yml new file mode 100644 index 0000000..2eab914 --- /dev/null +++ b/services/api/docs/openapi-schema.yml @@ -0,0 +1,195 @@ +openapi: 3.0.2 +info: + title: 'StatusHistory' + version: '' +paths: + /api/services/: + get: + description: 'Список сервисов.' + parameters: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Services' + description: '' + tags: + - Сервисы + post: + description: 'Создание сервиса.' + parameters: + - name: name + required: true + in: query + description: 'Название сервиса' + example: 'Крутой сервис' + schema: + type: string + - name: description + required: true + in: query + description: 'Описание сервиса' + example: 'Описание крутого сервиса' + schema: + type: string + - name: status + required: false + in: query + description: 'Статус работы сервиса' + example: 'работает' + schema: + default: 'работает' + type: string + enum: ['работает', 'не работает', 'нестабильно'] + responses: + '201': + content: + application/json: + schema: + $ref: "#/components/schemas/Service" + description: '' + tags: + - Сервисы + /api/history/{service_id}/: + get: + description: 'История изменений состояния работы сервиса.' + parameters: + - name: service_id + required: true + in: path + description: 'ID сервиса' + example: 3 + schema: + type: integer + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/History' + description: '' + tags: + - История работы сервиса + /api/{service_id}/{start_date}/{end_date}/: + get: + operationId: Расчёт SLA + description: 'Информация о том сколько не работал сервис и расчёт SLA.' + parameters: + - name: service_id + required: true + in: path + description: 'ID сервиса' + example: 4 + schema: + type: integer + - name: start_date + required: true + in: path + description: 'Начало периода' + example: '2023-10-29' + schema: + type: string + - name: end_date + required: true + in: path + description: 'Конец периода' + example: '2023-11-13' + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + Информация для сервиса: + type: string + description: 'Название сервиса' + example: 'Fourth' + Service level agreement(в процентах): + type: integer + description: 'SLA' + example: '96.913' + Общее время недоступности сервиса: + type: string + description: 'Общее время недоступности' + example: '0 месяцев, 1 дней, 10 часов, 12 минут, 31 секунд' + description: '' + tags: + - Service level agreement + +components: + schemas: + Services: + type: object + properties: + id: + type: integer + readOnly: true + example: '1' + name: + type: string + maxLength: 100 + description: 'Название' + example: 'СAPI2' + status: + type: string + maxLength: 50 + description: 'Состояние' + example: 'работает' + description: + type: string + description: 'Описание' + example: 'Крутой сервис' + Service: + type: object + properties: + id: + title: ID + type: integer + readOnly: true + example: 2 + name: + title: Название сервиса + type: string + example: "Крутой сервис" + maxLength: 100 + status: + title: Статус работы сервиса + type: string + example: "работает" + description: + title: Опсиание сервиса + type: string + example: "Описание крутого сервиса" + History: + type: object + properties: + id: + title: ID + type: integer + readOnly: true + example: 1 + name: + title: Название сервиса + type: string + example: "CAPI2" + maxLength: 100 + service_id: + title: ID сервиса + type: string + example: 3 + status: + title: Статус работы сервиса + type: string + example: "работает" + last_modified: + title: Дата последнего изменения работы сервиса + type: string + example: "12.11.2023 12:05:52" \ No newline at end of file diff --git a/services/api/docs/swagger.html b/services/api/docs/swagger.html new file mode 100644 index 0000000..c340d13 --- /dev/null +++ b/services/api/docs/swagger.html @@ -0,0 +1,24 @@ +{% load static %} + + + Documentation API + + + + + + +
+ + + + \ No newline at end of file diff --git a/services/api/migrations/0001_initial.py b/services/api/migrations/0001_initial.py new file mode 100644 index 0000000..5171d72 --- /dev/null +++ b/services/api/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.23 on 2023-11-12 08:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Service', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Название')), + ('status', models.CharField(choices=[('работает', 'работает'), ('не работает', 'не работает'), ('нестабильно', 'нестабильно')], default='работает', max_length=50, verbose_name='Состояние')), + ('description', models.TextField(verbose_name='Описание')), + ], + options={ + 'ordering': ('-id',), + }, + ), + migrations.CreateModel( + name='StatusHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(max_length=50, verbose_name='Состояние')), + ('last_modified', models.DateTimeField(auto_now_add=True, verbose_name='Дата изменения')), + ('start_time', models.DateTimeField(blank=True, null=True, verbose_name='Время начала недоступности сервиса')), + ('end_time', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания недоступности сервиса')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='api.service', verbose_name='Название')), + ], + options={ + 'ordering': ('-id',), + }, + ), + ] diff --git a/services/api/migrations/__init__.py b/services/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/api/models.py b/services/api/models.py new file mode 100644 index 0000000..59daf00 --- /dev/null +++ b/services/api/models.py @@ -0,0 +1,68 @@ +from django.db import models +from django.utils import timezone + +tz = timezone.get_default_timezone() + + +class Service(models.Model): + """Модель для сервисов.""" + + IS_STABLE = 'работает' + NOT_WORKING = 'не работает' + INSTABILITY = 'нестабильно' + STATUSES = [ + (IS_STABLE, 'работает'), + (NOT_WORKING, 'не работает'), + ('нестабильно', 'нестабильно') + ] + name = models.CharField( + max_length=100, unique=True, + verbose_name='Название' + ) + status = models.CharField( + max_length=50, verbose_name='Состояние', + choices=STATUSES, default='работает' + ) + description = models.TextField(verbose_name='Описание') + + class Meta: + ordering = ('-id',) + + def __str__(self): + return self.name + + +class StatusHistory(models.Model): + """Модель для хранения истории изменений состояния работы сервиса.""" + + status = models.CharField(max_length=50, verbose_name='Состояние') + service = models.ForeignKey( + Service, related_name='history', + verbose_name='Название', on_delete=models.CASCADE + ) + last_modified = models.DateTimeField( + auto_now_add=True, + verbose_name='Дата изменения' + ) + start_time = models.DateTimeField( + null=True, blank=True, + verbose_name='Время начала недоступности сервиса' + ) + end_time = models.DateTimeField( + null=True, blank=True, + verbose_name='Время окончания недоступности сервиса' + ) + + @property + def downtime_duration(self): + if self.start_time and self.end_time: + return self.end_time - self.start_time + elif not self.end_time and self.start_time: + return timezone.now() - self.start_time + return None + + class Meta: + ordering = ('-id',) + + def __str__(self): + return f'Изменение сервиса "{self.service}"' diff --git a/services/api/scripts.py b/services/api/scripts.py new file mode 100644 index 0000000..20a5f38 --- /dev/null +++ b/services/api/scripts.py @@ -0,0 +1,25 @@ +def seconds_to_time(seconds: str) -> str: + """Перевод секунд в месяцы/дни/часы/секунды.""" + + seconds_in_minute = 60 + seconds_in_hour = 3600 + seconds_in_day = 86400 + seconds_in_month = 2629746 # среднее количество + seconds = int(seconds) + + months = seconds // seconds_in_month + seconds -= months * seconds_in_month + + days = seconds // seconds_in_day + seconds -= days * seconds_in_day + + hours = seconds // seconds_in_hour + seconds -= hours * seconds_in_hour + + minutes = seconds // seconds_in_minute + seconds -= minutes * seconds_in_minute + + result = ( + f'{months} месяцев, {hours} часов, {days} дней ' + f'{minutes} минут, {seconds} секунд') + return result diff --git a/services/api/serializers.py b/services/api/serializers.py new file mode 100644 index 0000000..e00c059 --- /dev/null +++ b/services/api/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from .models import Service, StatusHistory + + +class ServiceSerializer(serializers.ModelSerializer): + + class Meta: + model = Service + fields = ('id', 'name', 'status', 'description') + + +class StatusHistorySerializer(serializers.ModelSerializer): + name = serializers.CharField(source='service.name') + service_id = serializers.CharField(source='service.id') + + class Meta: + model = StatusHistory + fields = ('id', 'name', 'service_id', 'status', 'last_modified') diff --git a/services/api/urls.py b/services/api/urls.py new file mode 100644 index 0000000..80a3106 --- /dev/null +++ b/services/api/urls.py @@ -0,0 +1,22 @@ +from api.views import ServiceViewSet, StatusHistoryViewSet, sla_calculation +from django.urls import include, path +from django.views.generic import TemplateView +from rest_framework.routers import DefaultRouter + +app_name = 'api' + +router = DefaultRouter() +router.register('services', ServiceViewSet) +router.register(r'history/(?P.+)', + StatusHistoryViewSet, basename='statushistory') + + +urlpatterns = [ + path('', include(router.urls)), + path('///', + sla_calculation), + path('docs/', TemplateView.as_view( + template_name='swagger.html', + extra_context={'schema_url': 'openapi-schema'} + ), name='swagger-ui'), +] diff --git a/services/api/views.py b/services/api/views.py new file mode 100644 index 0000000..dbe21da --- /dev/null +++ b/services/api/views.py @@ -0,0 +1,65 @@ +from datetime import datetime, timedelta + +from rest_framework import decorators, mixins, status, viewsets +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from .models import Service, StatusHistory +from .scripts import seconds_to_time +from .serializers import ServiceSerializer, StatusHistorySerializer + + +class ServiceViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, + viewsets.GenericViewSet): + """Вьюсет для создания и получения информации о сервисах.""" + + permission_classes = [AllowAny] + queryset = Service.objects.all() + serializer_class = ServiceSerializer + + def perform_create(self, serializer): + serializer.save() + + +class StatusHistoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """Вьюсет для получения истории изменений состояния по id сервиса.""" + + serializer_class = StatusHistorySerializer + + def get_queryset(self): + service_id = self.kwargs.get('service_id') + return StatusHistory.objects.filter(service__id=service_id) + + +@decorators.api_view(['GET']) +def sla_calculation(request, service_id, start_date, end_date): + """ + Вьюсет для подсчета общего времени недоступности сервиса и расчёта + SLA за заданный интервал времени. + """ + + service_downtime = StatusHistory.objects.filter( + service_id=service_id, + status='не работает', + last_modified__range=[start_date, end_date] + ) + + total_downtime = timedelta() + for downtime in service_downtime: + total_downtime += downtime.downtime_duration + + end_date = datetime.strptime(end_date, '%Y-%m-%d') + start_date = datetime.strptime(start_date, '%Y-%m-%d') + total_time = end_date - start_date + availability = (total_time - total_downtime) / total_time + sla = availability * 100 + total = seconds_to_time(total_downtime.seconds) + + return Response( + { + 'Информация для сервиса': Service.objects.get(id=service_id).name, + 'Service level agreement(в процентах)': round(sla, 3), + 'Общее время недоступности сервиса': total + }, + status=status.HTTP_200_OK + ) diff --git a/services/config/__init__.py b/services/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/config/asgi.py b/services/config/asgi.py new file mode 100644 index 0000000..a00b3b5 --- /dev/null +++ b/services/config/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/services/config/settings.py b/services/config/settings.py new file mode 100644 index 0000000..dd34875 --- /dev/null +++ b/services/config/settings.py @@ -0,0 +1,101 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + + +SECRET_KEY = 'django-insecure-)$n9-ip!)gr3cn=zv7#k@_#7%+%uuwuyoo2qkbgntd299s*66_' + +DEBUG = os.getenv('DEBUG') + +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') + + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'api.apps.ApiConfig', + 'rest_framework' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'api/docs'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +REST_FRAMEWORK = { + 'DATETIME_FORMAT': '%d.%m.%Y %H:%M:%S', +} + + +LANGUAGE_CODE = 'ru-RU' + +USE_I18N = True + +USE_L10N = True + +TIME_ZONE = 'Europe/Moscow' +USE_TZ = True + +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, 'api/docs'), +) +STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_URL = '/static/' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/services/config/urls.py b/services/config/urls.py new file mode 100644 index 0000000..b2c5a3a --- /dev/null +++ b/services/config/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path('api/', include('api.urls', namespace='api')), + path('admin/', admin.site.urls) +] diff --git a/services/config/wsgi.py b/services/config/wsgi.py new file mode 100644 index 0000000..a9f185c --- /dev/null +++ b/services/config/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/services/manage.py b/services/manage.py new file mode 100644 index 0000000..fb3d917 --- /dev/null +++ b/services/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/services/requirements.txt b/services/requirements.txt new file mode 100644 index 0000000..d3804a2 --- /dev/null +++ b/services/requirements.txt @@ -0,0 +1,12 @@ +asgiref==3.7.2 +Django==3.2.23 +djangorestframework==3.14.0 +gunicorn==20.1.0 +inflection==0.5.1 +isort==5.11.5 +packaging==23.2 +pytz==2023.3.post1 +PyYAML==6.0.1 +sqlparse==0.4.4 +typing-extensions==4.7.1 +uritemplate==4.1.1