From 03aa6c69a9e5123deb0b64fc163341db393ad691 Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Sun, 12 Nov 2023 16:22:13 +0500 Subject: [PATCH 01/10] Services Api --- .gitignore | 4 + requirements.txt | 11 + services/README.md | 35 ++ services/api/__init__.py | 0 services/api/admin.py | 21 ++ services/api/apps.py | 42 +++ services/api/migrations/0001_initial.py | 41 +++ services/api/migrations/__init__.py | 0 services/api/models.py | 68 ++++ services/api/serializers.py | 19 ++ services/api/urls.py | 17 + services/api/views.py | 63 ++++ services/config/__init__.py | 0 services/config/asgi.py | 7 + services/config/settings.py | 97 ++++++ services/config/urls.py | 7 + services/config/wsgi.py | 7 + services/manage.py | 21 ++ ...\272\321\206\320\270\320\270 Postman.json" | 320 ++++++++++++++++++ setup.cfg | 7 + 20 files changed, 787 insertions(+) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 services/README.md create mode 100644 services/api/__init__.py create mode 100644 services/api/admin.py create mode 100644 services/api/apps.py create mode 100644 services/api/migrations/0001_initial.py create mode 100644 services/api/migrations/__init__.py create mode 100644 services/api/models.py create mode 100644 services/api/serializers.py create mode 100644 services/api/urls.py create mode 100644 services/api/views.py create mode 100644 services/config/__init__.py create mode 100644 services/config/asgi.py create mode 100644 services/config/settings.py create mode 100644 services/config/urls.py create mode 100644 services/config/wsgi.py create mode 100644 services/manage.py create mode 100644 "services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..432bd0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.vscode/ +venv/ +db.sqlite3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..86eef43 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +asgiref==3.7.2 +Django==3.2.23 +djangorestframework==3.14.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 diff --git a/services/README.md b/services/README.md new file mode 100644 index 0000000..6a7fd3c --- /dev/null +++ b/services/README.md @@ -0,0 +1,35 @@ + + + +# Описание проекта: +Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно. + +Написан API который: + +1. Получает и сохраняет данные сервиса: имя, состояние, описание. +2. Выводит список сервисов с актуальным состоянием. +3. По id сервиса выдает историю изменения состояния и все данные по каждому состоянию. +4. По указанному интервалу выдаёт информация о том сколько не работал сервис и считает SLA в процентах до 3-й запятой. + +# Описание API: +- **GET /api/services/** - выводит список сервисов с актуальным состоянием. +- **POST /api/services/** - Добавление нового сервиса. +- **GET /api/history/{id_сервиса}/** - выдает историю изменения состояния. +- **GET /api/{id_сервиса}/{start_date}/{end_date}/**: выдаёт информация о том сколько не работал сервис и считает SLA. +``` +url: /api/4/2023-10-29/2023-11-13/ +Response: [ + { + "Информация для сервиса": "Fourth", + "Service level agreement(в процентах)": 96.913, + "Общее время недоступности сервиса(в секундах)": "5334.910411" + } +] +``` + +Выполнил Конышкин Иван \ 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/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/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..4e32866 --- /dev/null +++ b/services/api/urls.py @@ -0,0 +1,17 @@ +from api.views import ServiceViewSet, StatusHistoryViewSet, sla_calculation +from django.urls import include, path +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), +] diff --git a/services/api/views.py b/services/api/views.py new file mode 100644 index 0000000..9d4c74c --- /dev/null +++ b/services/api/views.py @@ -0,0 +1,63 @@ +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 .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 + + return Response( + { + 'Информация для сервиса': Service.objects.get(id=service_id).name, + 'Service level agreement(в процентах)': round(sla, 3), + 'Общее время недоступности сервиса(в секундах)': total_downtime + }, + 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..02b5523 --- /dev/null +++ b/services/config/settings.py @@ -0,0 +1,97 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + + +SECRET_KEY = 'django-insecure-)$n9-ip!)gr3cn=zv7#k@_#7%+%uuwuyoo2qkbgntd299s*66_' + +DEBUG = True + +ALLOWED_HOSTS = [] + + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'api.apps.ApiConfig', +] + +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': [], + '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' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +TIME_ZONE = 'Europe/Moscow' +USE_TZ = True + + +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/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" "b/services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" new file mode 100644 index 0000000..6a953f0 --- /dev/null +++ "b/services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" @@ -0,0 +1,320 @@ +{ + "info": { + "_postman_id": "b10ffa6c-1a06-49e0-811c-2a31a4a01018", + "name": "StatusHistory", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "22354635" + }, + "item": [ + { + "name": "Список сервисов", + "request": { + "method": "GET", + "header": [], + "url": "http://127.0.0.1:8000/api/services/", + "description": "Выводит список сервисов с актуальным состоянием" + }, + "response": [ + { + "name": "Пример response", + "originalRequest": { + "method": "GET", + "header": [], + "url": "http://127.0.0.1:8000/api/services/" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Date", + "value": "Sun, 12 Nov 2023 10:36:58 GMT" + }, + { + "key": "Server", + "value": "WSGIServer/0.2 CPython/3.7.9" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Vary", + "value": "Accept, Cookie" + }, + { + "key": "Allow", + "value": "GET, POST, HEAD, OPTIONS" + }, + { + "key": "X-Frame-Options", + "value": "DENY" + }, + { + "key": "Content-Length", + "value": "677" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "same-origin" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 7,\n \"name\": \"СAPI2\",\n \"status\": \"работает\",\n \"description\": \"Создание сервиса через API\"\n },\n {\n \"id\": 6,\n \"name\": \"СAPI\",\n \"status\": \"работает\",\n \"description\": \"Создание сервиса через API\"\n },\n {\n \"id\": 5,\n \"name\": \"Fifth\",\n \"status\": \"работает\",\n \"description\": \"Очень крутой сервис\"\n },\n {\n \"id\": 4,\n \"name\": \"Fourth\",\n \"status\": \"не работает\",\n \"description\": \"Крутооооой\"\n },\n {\n \"id\": 3,\n \"name\": \"Third\",\n \"status\": \"работает\",\n \"description\": \"Ещё круче\"\n },\n {\n \"id\": 2,\n \"name\": \"Second\",\n \"status\": \"работает\",\n \"description\": \"Крутой\"\n },\n {\n \"id\": 1,\n \"name\": \"First\",\n \"status\": \"работает\",\n \"description\": \"Крут\"\n }\n]" + } + ] + }, + { + "name": "История изменения состояния сервиса", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://127.0.0.1:8000/api/history/1/", + "description": "По имени сервиса выдает историю изменения состояния и все данные по каждому состоянию" + }, + "response": [ + { + "name": "Пример response", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://127.0.0.1:8000/api/history/1/" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Date", + "value": "Sun, 12 Nov 2023 10:36:36 GMT" + }, + { + "key": "Server", + "value": "WSGIServer/0.2 CPython/3.7.9" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Vary", + "value": "Accept, Cookie" + }, + { + "key": "Allow", + "value": "GET, HEAD, OPTIONS" + }, + { + "key": "X-Frame-Options", + "value": "DENY" + }, + { + "key": "Content-Length", + "value": "440" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "same-origin" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 8,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"работает\",\n \"last_modified\": \"12.11.2023 12:05:52\"\n },\n {\n \"id\": 7,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"нестабильно\",\n \"last_modified\": \"12.11.2023 12:03:40\"\n },\n {\n \"id\": 6,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"не работает\",\n \"last_modified\": \"12.11.2023 12:02:37\"\n },\n {\n \"id\": 1,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"работает\",\n \"last_modified\": \"12.11.2023 11:59:14\"\n }\n]" + } + ] + }, + { + "name": "SLA и общее время недоступности сервиса", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://127.0.0.1:8000/api/4/2023-11-11/2023-11-13/", + "description": "По указанному интервалу выдается информация о том сколько не работал сервис и рассчитывается SLA в процентах до 3-й запятой" + }, + "response": [ + { + "name": "Пример response", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://127.0.0.1:8000/api/4/2023-11-11/2023-11-13/" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Date", + "value": "Sun, 12 Nov 2023 10:35:09 GMT" + }, + { + "key": "Server", + "value": "WSGIServer/0.2 CPython/3.7.9" + }, + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + }, + { + "key": "Vary", + "value": "Accept, Cookie" + }, + { + "key": "Allow", + "value": "GET, OPTIONS" + }, + { + "key": "X-Frame-Options", + "value": "DENY" + }, + { + "key": "Content-Length", + "value": "212" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "same-origin" + } + ], + "cookie": [], + "body": "{\n \"Информация для сервиса\": \"Fourth\",\n \"Service level agreement(в процентах)\": 96.913,\n \"Общее время недоступности сервиса(в секундах)\": \"5334.910411\"\n}" + } + ] + }, + { + "name": "Создание сервиса", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"СAPI\",\r\n \"description\": \"Создание сервиса через API\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://127.0.0.1:8000/api/services/" + }, + "response": [ + { + "name": "Пример response", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"СAPI2\",\r\n \"description\": \"Создание сервиса через API\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://127.0.0.1:8000/api/services/" + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Date", + "value": "Sun, 12 Nov 2023 10:34:32 GMT" + }, + { + "key": "Server", + "value": "WSGIServer/0.2 CPython/3.7.9" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Vary", + "value": "Accept, Cookie" + }, + { + "key": "Allow", + "value": "GET, POST, HEAD, OPTIONS" + }, + { + "key": "X-Frame-Options", + "value": "DENY" + }, + { + "key": "Content-Length", + "value": "115" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "same-origin" + } + ], + "cookie": [], + "body": "{\n \"id\": 7,\n \"name\": \"СAPI2\",\n \"status\": \"работает\",\n \"description\": \"Создание сервиса через API\"\n}" + } + ] + } + ] +} \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..447c5b1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +exclude = + */migrations/, + venv/ +per-file-ignores = + */settings.py:E501 +max-complexity = 10 \ No newline at end of file From c67ccf4f2ba40f70dfb01da31e0dbfac9c7c5d30 Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Mon, 13 Nov 2023 22:07:49 +0500 Subject: [PATCH 02/10] fix --- LICENSE | 21 -- README.md | 91 ++--- services/README.md | 35 -- ...\272\321\206\320\270\320\270 Postman.json" | 320 ------------------ 4 files changed, 31 insertions(+), 436 deletions(-) delete mode 100644 LICENSE delete mode 100644 services/README.md delete mode 100644 "services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" 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..b3b2caf 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,35 @@ -# Вакансия :: Программист 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 который: - -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/{id_сервиса}/** - выдает историю изменения состояния. +- **GET /api/{id_сервиса}/{start_date}/{end_date}/**: выдаёт информация о том сколько не работал сервис и считает SLA. +``` +EXAMPLE GET - /api/{id_сервиса}/{start_date}/{end_date}/ + +URL: /api/4/2023-10-29/2023-11-13/ +Response: [ + { + "Информация для сервиса": "Fourth", + "Service level agreement(в процентах)": 96.913, + "Общее время недоступности сервиса(в секундах)": "5334.910411" + } +] +``` \ No newline at end of file diff --git a/services/README.md b/services/README.md deleted file mode 100644 index 6a7fd3c..0000000 --- a/services/README.md +++ /dev/null @@ -1,35 +0,0 @@ - - - -# Описание проекта: -Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно. - -Написан API который: - -1. Получает и сохраняет данные сервиса: имя, состояние, описание. -2. Выводит список сервисов с актуальным состоянием. -3. По id сервиса выдает историю изменения состояния и все данные по каждому состоянию. -4. По указанному интервалу выдаёт информация о том сколько не работал сервис и считает SLA в процентах до 3-й запятой. - -# Описание API: -- **GET /api/services/** - выводит список сервисов с актуальным состоянием. -- **POST /api/services/** - Добавление нового сервиса. -- **GET /api/history/{id_сервиса}/** - выдает историю изменения состояния. -- **GET /api/{id_сервиса}/{start_date}/{end_date}/**: выдаёт информация о том сколько не работал сервис и считает SLA. -``` -url: /api/4/2023-10-29/2023-11-13/ -Response: [ - { - "Информация для сервиса": "Fourth", - "Service level agreement(в процентах)": 96.913, - "Общее время недоступности сервиса(в секундах)": "5334.910411" - } -] -``` - -Выполнил Конышкин Иван \ No newline at end of file diff --git "a/services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" "b/services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" deleted file mode 100644 index 6a953f0..0000000 --- "a/services/\320\272\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\270 Postman.json" +++ /dev/null @@ -1,320 +0,0 @@ -{ - "info": { - "_postman_id": "b10ffa6c-1a06-49e0-811c-2a31a4a01018", - "name": "StatusHistory", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", - "_exporter_id": "22354635" - }, - "item": [ - { - "name": "Список сервисов", - "request": { - "method": "GET", - "header": [], - "url": "http://127.0.0.1:8000/api/services/", - "description": "Выводит список сервисов с актуальным состоянием" - }, - "response": [ - { - "name": "Пример response", - "originalRequest": { - "method": "GET", - "header": [], - "url": "http://127.0.0.1:8000/api/services/" - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Date", - "value": "Sun, 12 Nov 2023 10:36:58 GMT" - }, - { - "key": "Server", - "value": "WSGIServer/0.2 CPython/3.7.9" - }, - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Vary", - "value": "Accept, Cookie" - }, - { - "key": "Allow", - "value": "GET, POST, HEAD, OPTIONS" - }, - { - "key": "X-Frame-Options", - "value": "DENY" - }, - { - "key": "Content-Length", - "value": "677" - }, - { - "key": "X-Content-Type-Options", - "value": "nosniff" - }, - { - "key": "Referrer-Policy", - "value": "same-origin" - } - ], - "cookie": [], - "body": "[\n {\n \"id\": 7,\n \"name\": \"СAPI2\",\n \"status\": \"работает\",\n \"description\": \"Создание сервиса через API\"\n },\n {\n \"id\": 6,\n \"name\": \"СAPI\",\n \"status\": \"работает\",\n \"description\": \"Создание сервиса через API\"\n },\n {\n \"id\": 5,\n \"name\": \"Fifth\",\n \"status\": \"работает\",\n \"description\": \"Очень крутой сервис\"\n },\n {\n \"id\": 4,\n \"name\": \"Fourth\",\n \"status\": \"не работает\",\n \"description\": \"Крутооооой\"\n },\n {\n \"id\": 3,\n \"name\": \"Third\",\n \"status\": \"работает\",\n \"description\": \"Ещё круче\"\n },\n {\n \"id\": 2,\n \"name\": \"Second\",\n \"status\": \"работает\",\n \"description\": \"Крутой\"\n },\n {\n \"id\": 1,\n \"name\": \"First\",\n \"status\": \"работает\",\n \"description\": \"Крут\"\n }\n]" - } - ] - }, - { - "name": "История изменения состояния сервиса", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": "http://127.0.0.1:8000/api/history/1/", - "description": "По имени сервиса выдает историю изменения состояния и все данные по каждому состоянию" - }, - "response": [ - { - "name": "Пример response", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": "http://127.0.0.1:8000/api/history/1/" - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Date", - "value": "Sun, 12 Nov 2023 10:36:36 GMT" - }, - { - "key": "Server", - "value": "WSGIServer/0.2 CPython/3.7.9" - }, - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Vary", - "value": "Accept, Cookie" - }, - { - "key": "Allow", - "value": "GET, HEAD, OPTIONS" - }, - { - "key": "X-Frame-Options", - "value": "DENY" - }, - { - "key": "Content-Length", - "value": "440" - }, - { - "key": "X-Content-Type-Options", - "value": "nosniff" - }, - { - "key": "Referrer-Policy", - "value": "same-origin" - } - ], - "cookie": [], - "body": "[\n {\n \"id\": 8,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"работает\",\n \"last_modified\": \"12.11.2023 12:05:52\"\n },\n {\n \"id\": 7,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"нестабильно\",\n \"last_modified\": \"12.11.2023 12:03:40\"\n },\n {\n \"id\": 6,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"не работает\",\n \"last_modified\": \"12.11.2023 12:02:37\"\n },\n {\n \"id\": 1,\n \"name\": \"First\",\n \"service_id\": \"1\",\n \"status\": \"работает\",\n \"last_modified\": \"12.11.2023 11:59:14\"\n }\n]" - } - ] - }, - { - "name": "SLA и общее время недоступности сервиса", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": "http://127.0.0.1:8000/api/4/2023-11-11/2023-11-13/", - "description": "По указанному интервалу выдается информация о том сколько не работал сервис и рассчитывается SLA в процентах до 3-й запятой" - }, - "response": [ - { - "name": "Пример response", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": "http://127.0.0.1:8000/api/4/2023-11-11/2023-11-13/" - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Date", - "value": "Sun, 12 Nov 2023 10:35:09 GMT" - }, - { - "key": "Server", - "value": "WSGIServer/0.2 CPython/3.7.9" - }, - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "description": "", - "type": "text" - }, - { - "key": "Vary", - "value": "Accept, Cookie" - }, - { - "key": "Allow", - "value": "GET, OPTIONS" - }, - { - "key": "X-Frame-Options", - "value": "DENY" - }, - { - "key": "Content-Length", - "value": "212" - }, - { - "key": "X-Content-Type-Options", - "value": "nosniff" - }, - { - "key": "Referrer-Policy", - "value": "same-origin" - } - ], - "cookie": [], - "body": "{\n \"Информация для сервиса\": \"Fourth\",\n \"Service level agreement(в процентах)\": 96.913,\n \"Общее время недоступности сервиса(в секундах)\": \"5334.910411\"\n}" - } - ] - }, - { - "name": "Создание сервиса", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"СAPI\",\r\n \"description\": \"Создание сервиса через API\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": "http://127.0.0.1:8000/api/services/" - }, - "response": [ - { - "name": "Пример response", - "originalRequest": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"СAPI2\",\r\n \"description\": \"Создание сервиса через API\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": "http://127.0.0.1:8000/api/services/" - }, - "status": "Created", - "code": 201, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Date", - "value": "Sun, 12 Nov 2023 10:34:32 GMT" - }, - { - "key": "Server", - "value": "WSGIServer/0.2 CPython/3.7.9" - }, - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Vary", - "value": "Accept, Cookie" - }, - { - "key": "Allow", - "value": "GET, POST, HEAD, OPTIONS" - }, - { - "key": "X-Frame-Options", - "value": "DENY" - }, - { - "key": "Content-Length", - "value": "115" - }, - { - "key": "X-Content-Type-Options", - "value": "nosniff" - }, - { - "key": "Referrer-Policy", - "value": "same-origin" - } - ], - "cookie": [], - "body": "{\n \"id\": 7,\n \"name\": \"СAPI2\",\n \"status\": \"работает\",\n \"description\": \"Создание сервиса через API\"\n}" - } - ] - } - ] -} \ No newline at end of file From be44428ea6e4b313559fbc0095906b94d089843a Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Tue, 14 Nov 2023 00:24:05 +0500 Subject: [PATCH 03/10] fixed errors, added API documentation --- README.md | 34 +++-- services/api/docs/openapi-schema.yml | 194 +++++++++++++++++++++++++++ services/api/docs/swagger.html | 24 ++++ services/api/scripts.py | 25 ++++ services/api/urls.py | 5 + services/api/views.py | 4 +- services/config/settings.py | 13 +- 7 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 services/api/docs/openapi-schema.yml create mode 100644 services/api/docs/swagger.html create mode 100644 services/api/scripts.py diff --git a/README.md b/README.md index b3b2caf..29b6b63 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,23 @@ # Описание API: - **GET /api/services/** - выводит список сервисов с актуальным состоянием. - **POST /api/services/** - добавление нового сервиса. -- **GET /api/history/{id_сервиса}/** - выдает историю изменения состояния. -- **GET /api/{id_сервиса}/{start_date}/{end_date}/**: выдаёт информация о том сколько не работал сервис и считает SLA. -``` -EXAMPLE GET - /api/{id_сервиса}/{start_date}/{end_date}/ - -URL: /api/4/2023-10-29/2023-11-13/ -Response: [ - { - "Информация для сервиса": "Fourth", - "Service level agreement(в процентах)": 96.913, - "Общее время недоступности сервиса(в секундах)": "5334.910411" - } -] -``` \ No newline at end of file +- **GET /api/history/{service_id}/** - выдает историю изменений состояния. +- **GET /api/{service_id}/{start_date}/{end_date}/**: выдаёт информация о том сколько не работал сервис и считает SLA. + +# Запуск проекта: +- Клонируйте репозиторий и перейдите в него. +- Установите и активируйте виртуальное окружение. +- Установите зависимости из файла requirements.txt + ``` + python -m pip install --upgrade pip + pip install -r requirements.txt + ``` +- Перейдите в папку **services** с файлом **manage.py**, выполните миграции, и запустите сервер: + ``` + python manage.py migrate + python manage.py runserver + ``` + +После этого проект будет доступен по url-адресу [127.0.0.1:8000/api/](http://127.0.0.1:8000/api/) + +Документация к API будет доступна по url-адресу [127.0.0.1:8000/api/docs](http://127.0.0.1:8000/api/docs/) \ No newline at end of file diff --git a/services/api/docs/openapi-schema.yml b/services/api/docs/openapi-schema.yml new file mode 100644 index 0000000..bd50e45 --- /dev/null +++ b/services/api/docs/openapi-schema.yml @@ -0,0 +1,194 @@ +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: + 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 месяцев, 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/scripts.py b/services/api/scripts.py new file mode 100644 index 0000000..b81c0af --- /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} часов, {minutes} минут, ' + f'{seconds} секунд') + return result diff --git a/services/api/urls.py b/services/api/urls.py index 4e32866..80a3106 100644 --- a/services/api/urls.py +++ b/services/api/urls.py @@ -1,5 +1,6 @@ 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' @@ -14,4 +15,8 @@ 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 index 9d4c74c..dbe21da 100644 --- a/services/api/views.py +++ b/services/api/views.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from .models import Service, StatusHistory +from .scripts import seconds_to_time from .serializers import ServiceSerializer, StatusHistorySerializer @@ -52,12 +53,13 @@ def sla_calculation(request, service_id, start_date, end_date): 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_downtime + 'Общее время недоступности сервиса': total }, status=status.HTTP_200_OK ) diff --git a/services/config/settings.py b/services/config/settings.py index 02b5523..1779a4c 100644 --- a/services/config/settings.py +++ b/services/config/settings.py @@ -1,3 +1,4 @@ +import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent @@ -18,6 +19,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'api.apps.ApiConfig', + 'rest_framework' ] MIDDLEWARE = [ @@ -35,7 +37,9 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + os.path.join(BASE_DIR, 'api/docs'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -81,8 +85,6 @@ LANGUAGE_CODE = 'ru-RU' -TIME_ZONE = 'UTC' - USE_I18N = True USE_L10N = True @@ -90,8 +92,9 @@ TIME_ZONE = 'Europe/Moscow' USE_TZ = True - +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, 'api/docs'), +) STATIC_URL = '/static/' - DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' From 2e7c26e70b4cae4e2f3013e1f2074e9f003a9446 Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Tue, 21 Nov 2023 01:47:04 +0500 Subject: [PATCH 04/10] GitHub Pages --- docs/openapi-schema.yml | 194 ++++++++++++++++++++++++++++++++++++++++ docs/swagger.html | 23 +++++ 2 files changed, 217 insertions(+) create mode 100644 docs/openapi-schema.yml create mode 100644 docs/swagger.html diff --git a/docs/openapi-schema.yml b/docs/openapi-schema.yml new file mode 100644 index 0000000..bd50e45 --- /dev/null +++ b/docs/openapi-schema.yml @@ -0,0 +1,194 @@ +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: + 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 месяцев, 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/docs/swagger.html b/docs/swagger.html new file mode 100644 index 0000000..a966662 --- /dev/null +++ b/docs/swagger.html @@ -0,0 +1,23 @@ + + + Documentation API + + + + + + +
+ + + + \ No newline at end of file From b99c6167b35c33415194a6b6c52ecde6e9aa8e1d Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Tue, 21 Nov 2023 01:52:42 +0500 Subject: [PATCH 05/10] Rename swagger.html to index.html --- docs/{swagger.html => index.html} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/{swagger.html => index.html} (98%) diff --git a/docs/swagger.html b/docs/index.html similarity index 98% rename from docs/swagger.html rename to docs/index.html index a966662..329cd40 100644 --- a/docs/swagger.html +++ b/docs/index.html @@ -20,4 +20,4 @@ }) - \ No newline at end of file + From 4ee23abbd52daaa2787bad3223f19819867ae17b Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Tue, 21 Nov 2023 02:03:40 +0500 Subject: [PATCH 06/10] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29b6b63..d7b6e18 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ +# Документация API +[clownvkkaschenko.github.io/StatusHistory]([https://kaschenko.pythonanywhere.com/api/docs/](https://clownvkkaschenko.github.io/StatusHistory/)) # Описание проекта: Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно. @@ -38,4 +40,4 @@ После этого проект будет доступен по url-адресу [127.0.0.1:8000/api/](http://127.0.0.1:8000/api/) -Документация к API будет доступна по url-адресу [127.0.0.1:8000/api/docs](http://127.0.0.1:8000/api/docs/) \ No newline at end of file +Документация к API будет доступна по url-адресу [127.0.0.1:8000/api/docs](http://127.0.0.1:8000/api/docs/) From fb545f2e8151bc486589912e03e705add77b03f4 Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Tue, 21 Nov 2023 02:05:19 +0500 Subject: [PATCH 07/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7b6e18..702eac5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Документация API -[clownvkkaschenko.github.io/StatusHistory]([https://kaschenko.pythonanywhere.com/api/docs/](https://clownvkkaschenko.github.io/StatusHistory/)) +[clownvkkaschenko.github.io/StatusHistory](https://clownvkkaschenko.github.io/StatusHistory/) # Описание проекта: Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно. From dfc9957c45792bc3d69536b00496df643a39d693 Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Fri, 24 Nov 2023 00:00:18 +0500 Subject: [PATCH 08/10] added Docker and API docs --- .gitignore | 3 +- README.md | 34 +++++++++++-------- docs/index.html | 26 ++++++-------- docs/openapi-schema.yml | 3 +- infra/.env | 2 ++ infra/docker-compose.yml | 24 +++++++++++++ infra/nginx.conf | 25 ++++++++++++++ services/Dockerfile | 9 +++++ services/api/docs/openapi-schema.yml | 3 +- services/api/scripts.py | 4 +-- services/config/settings.py | 5 +-- requirements.txt => services/requirements.txt | 1 + setup.cfg | 7 ---- 13 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 infra/.env create mode 100644 infra/docker-compose.yml create mode 100644 infra/nginx.conf create mode 100644 services/Dockerfile rename requirements.txt => services/requirements.txt (92%) delete mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index 432bd0d..de676cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ .vscode/ venv/ -db.sqlite3 \ No newline at end of file +db.sqlite3 +setup.cfg \ No newline at end of file diff --git a/README.md b/README.md index 702eac5..84b62d9 100644 --- a/README.md +++ b/README.md @@ -26,18 +26,22 @@ # Запуск проекта: - Клонируйте репозиторий и перейдите в него. -- Установите и активируйте виртуальное окружение. -- Установите зависимости из файла requirements.txt - ``` - python -m pip install --upgrade pip - pip install -r requirements.txt - ``` -- Перейдите в папку **services** с файлом **manage.py**, выполните миграции, и запустите сервер: - ``` - python manage.py migrate - python manage.py runserver - ``` - -После этого проект будет доступен по url-адресу [127.0.0.1:8000/api/](http://127.0.0.1:8000/api/) - -Документация к API будет доступна по url-адресу [127.0.0.1:8000/api/docs](http://127.0.0.1:8000/api/docs/) +- Перейдите в папку **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 index 329cd40..52ed48a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,23 +1,19 @@ + Documentation API - - + + -
- - + + - + \ No newline at end of file diff --git a/docs/openapi-schema.yml b/docs/openapi-schema.yml index bd50e45..2eab914 100644 --- a/docs/openapi-schema.yml +++ b/docs/openapi-schema.yml @@ -77,6 +77,7 @@ paths: - История работы сервиса /api/{service_id}/{start_date}/{end_date}/: get: + operationId: Расчёт SLA description: 'Информация о том сколько не работал сервис и расчёт SLA.' parameters: - name: service_id @@ -118,7 +119,7 @@ paths: Общее время недоступности сервиса: type: string description: 'Общее время недоступности' - example: '0 месяцев, 10 часов, 12 минут, 31 секунд' + example: '0 месяцев, 1 дней, 10 часов, 12 минут, 31 секунд' description: '' tags: - Service level agreement 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/docs/openapi-schema.yml b/services/api/docs/openapi-schema.yml index bd50e45..2eab914 100644 --- a/services/api/docs/openapi-schema.yml +++ b/services/api/docs/openapi-schema.yml @@ -77,6 +77,7 @@ paths: - История работы сервиса /api/{service_id}/{start_date}/{end_date}/: get: + operationId: Расчёт SLA description: 'Информация о том сколько не работал сервис и расчёт SLA.' parameters: - name: service_id @@ -118,7 +119,7 @@ paths: Общее время недоступности сервиса: type: string description: 'Общее время недоступности' - example: '0 месяцев, 10 часов, 12 минут, 31 секунд' + example: '0 месяцев, 1 дней, 10 часов, 12 минут, 31 секунд' description: '' tags: - Service level agreement diff --git a/services/api/scripts.py b/services/api/scripts.py index b81c0af..20a5f38 100644 --- a/services/api/scripts.py +++ b/services/api/scripts.py @@ -20,6 +20,6 @@ def seconds_to_time(seconds: str) -> str: seconds -= minutes * seconds_in_minute result = ( - f'{months} месяцев, {hours} часов, {minutes} минут, ' - f'{seconds} секунд') + f'{months} месяцев, {hours} часов, {days} дней ' + f'{minutes} минут, {seconds} секунд') return result diff --git a/services/config/settings.py b/services/config/settings.py index 1779a4c..dd34875 100644 --- a/services/config/settings.py +++ b/services/config/settings.py @@ -6,9 +6,9 @@ SECRET_KEY = 'django-insecure-)$n9-ip!)gr3cn=zv7#k@_#7%+%uuwuyoo2qkbgntd299s*66_' -DEBUG = True +DEBUG = os.getenv('DEBUG') -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') INSTALLED_APPS = [ @@ -95,6 +95,7 @@ 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/requirements.txt b/services/requirements.txt similarity index 92% rename from requirements.txt rename to services/requirements.txt index 86eef43..d3804a2 100644 --- a/requirements.txt +++ b/services/requirements.txt @@ -1,6 +1,7 @@ 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 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 447c5b1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -exclude = - */migrations/, - venv/ -per-file-ignores = - */settings.py:E501 -max-complexity = 10 \ No newline at end of file From d6bccaa64ae8fa1b3f26e7137aff8db766586158 Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Fri, 24 Nov 2023 00:13:01 +0500 Subject: [PATCH 09/10] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 84b62d9..74d4c39 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ + # Документация API From f800450731ed5dfa5b4a3acc53230bbf3ab13dcf Mon Sep 17 00:00:00 2001 From: Ivan Konyshkin Date: Wed, 26 Jun 2024 01:14:24 +0500 Subject: [PATCH 10/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74d4c39..847799a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # Документация API -[clownvkkaschenko.github.io/StatusHistory](https://clownvkkaschenko.github.io/StatusHistory/) +Status History - **[API redoc](https://kaschenkkko.github.io/StatusHistory/)** # Описание проекта: Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно.