From a88cd778f9854a7e549b407a0145c779e8deffc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2=20?= =?UTF-8?q?=D0=A2=D0=B5=D0=BB=D1=8C=D0=BC=D0=B0=D0=BD?= Date: Fri, 13 Sep 2024 17:26:28 +0500 Subject: [PATCH] commit with completed test case --- .env | 1 + .env-non-dev | 1 + .idea/.gitignore | 3 + .idea/inspectionProfiles/Project_Default.xml | 37 +++++++++ .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/misc.xml | 4 + .idea/modules.xml | 8 ++ .idea/task-python.iml | 17 ++++ .idea/toolchains.xml | 8 ++ .idea/vcs.xml | 6 ++ Dockerfile | 15 ++++ README.md | 76 +++++++----------- app/SLA_calculation.py | 49 +++++++++++ .../SLA_calculation.cpython-310.pyc | Bin 0 -> 1191 bytes app/__pycache__/main.cpython-310.pyc | Bin 0 -> 1995 bytes app/db/__pycache__/crud.cpython-310.pyc | Bin 0 -> 1700 bytes app/db/__pycache__/database.cpython-310.pyc | Bin 0 -> 1321 bytes .../sqlalchemy_models.cpython-310.pyc | Bin 0 -> 1238 bytes app/db/crud.py | 57 +++++++++++++ app/db/database.py | 35 ++++++++ app/db/sqlalchemy_models.py | 31 +++++++ app/main.py | 59 ++++++++++++++ .../pydantic_models.cpython-310.pyc | Bin 0 -> 669 bytes app/models/pydantic_models.py | 14 ++++ docker-compose.yml | 20 +++++ requirements.txt | 25 ++++++ 26 files changed, 425 insertions(+), 47 deletions(-) create mode 100644 .env create mode 100644 .env-non-dev create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/task-python.iml create mode 100644 .idea/toolchains.xml create mode 100644 .idea/vcs.xml create mode 100644 Dockerfile create mode 100644 app/SLA_calculation.py create mode 100644 app/__pycache__/SLA_calculation.cpython-310.pyc create mode 100644 app/__pycache__/main.cpython-310.pyc create mode 100644 app/db/__pycache__/crud.cpython-310.pyc create mode 100644 app/db/__pycache__/database.cpython-310.pyc create mode 100644 app/db/__pycache__/sqlalchemy_models.cpython-310.pyc create mode 100644 app/db/crud.py create mode 100644 app/db/database.py create mode 100644 app/db/sqlalchemy_models.py create mode 100644 app/main.py create mode 100644 app/models/__pycache__/pydantic_models.cpython-310.pyc create mode 100644 app/models/pydantic_models.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/.env b/.env new file mode 100644 index 0000000..d17514f --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DB_URL="postgresql+asyncpg://postgres:rootroot@localhost/itpc_test" \ No newline at end of file diff --git a/.env-non-dev b/.env-non-dev new file mode 100644 index 0000000..fc15e7d --- /dev/null +++ b/.env-non-dev @@ -0,0 +1 @@ +DB_URL="postgresql+asyncpg://postgres:mypassword@database/postgres" \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..20c85e2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,37 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..bbbba04 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ec5e7af --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/task-python.iml b/.idea/task-python.iml new file mode 100644 index 0000000..5d8ff06 --- /dev/null +++ b/.idea/task-python.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/toolchains.xml b/.idea/toolchains.xml new file mode 100644 index 0000000..4581420 --- /dev/null +++ b/.idea/toolchains.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e46acd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.10 + +RUN mkdir /fast_app + +WORKDIR /fast_app + +COPY requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + + + +CMD gunicorn app.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind=0.0.0.0:8000 \ No newline at end of file diff --git a/README.md b/README.md index cfb476f..bdaa22c 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,5 @@ # Вакансия :: Программист 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С, то эта вакансия не для вас. - +## (Для корректной работы приложения - состояние/status сервиса необходимо прописывать как up/down/unstable) ## Тестовое задание Решение принимается в виде PR к текущему проекту. @@ -53,7 +8,7 @@ Требуется написать API который: -1. Получает и сохраняет данные: имя, состояние, описание +1. Получает и сохраняет данные: имя, состояние, описание 2. Выводит список сервисов с актуальным состоянием 3. По имени сервиса выдает историю изменения состояния и все данные по каждому состоянию @@ -62,3 +17,30 @@ 1. По указанному интервалу выдается информация о том сколько не работал сервис и считать SLA в процентах до 3-й запятой Вывод всех данных должен быть в формате JSON + +## Стек +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) Для создания API/backend на Python. + - 🧰 [SQLAlchemy](https://www.sqlalchemy.org/) Для взаимодействия с базой данных (ORM). + - 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI для валидации данных и управления настройками. + - 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQL БД. + - :unicorn: [Uvicorn](https://www.uvicorn.org/), Веб-сервер ASGI для Python. (при запуске приложения/сервиса через docker используется gunicorn) + - [pip](https://pip.pypa.io/en/stable/) в качестве стандартного пакетного менеджера. ![Зависимости зафиксированы](https://img.shields.io/badge/зависимости_зафиксированы-using%20pip%20freeze-blue) + - [yapf](https://github.com/google/yapf) для автоматического форматирования кода в проекте. + - 🐋[Docker](https://www.docker.com/) Запуск сервиса и инфраструктуры проводится в docker контейнерах. + + +## Инструкция по запуску сервиса с помощью 🐋Docker🐋 +### 1. Клонируйте репозиторий + bash команда: git clone https://github.com/Telmann/task-python.git +### 2. Далее для запуска приложения + выполните: docker compose up --build app +### 3. Готово! Приложение будет доступно по адресу: http://127.0.0.1:9990/docs#/ + +## Интерактивная документация по API +![image](https://github.com/user-attachments/assets/6872f860-be9e-4e6b-90dd-79f1513d65aa) + +## Кратко про существующие эндпоинты: + - POST /services: Создает/обновляет информацию о сервисе в базе данных. При успешном выполнении возвращает сообщение об успешном создании/обновлении. + - GET /services: Получает список всех сервисов из базы данных. Если в БД нет сервисов на данный момент, возвращает сообщение о том, что в БД нет сервисов. + - GET /services/{service_name}/history: Получает историю изменений для указанного сервиса по его имени. Если сервис не найден/у него нет истории, возвращает сообщение об этом. + - GET /services/{service_id}/SLA_downtime_info: Рассчитывает время даунтайма для указанного сервиса (по id) в заданном временном диапазоне. Возвращает данные о времени даунтайма и процент SLA, либо поднимает ошибку о неверном формате даты-времени. diff --git a/app/SLA_calculation.py b/app/SLA_calculation.py new file mode 100644 index 0000000..ad35282 --- /dev/null +++ b/app/SLA_calculation.py @@ -0,0 +1,49 @@ +from datetime import datetime, timedelta +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from .db import sqlalchemy_models as models + + +async def calculate_service_downtime(db: AsyncSession, service_id: int, + start_date: datetime, + end_date: datetime) -> dict[str, Any]: + # Получаем все записи о статусах сервиса за указанный интервал + result = await db.execute( + select(models.ServiceHistory).where( + models.ServiceHistory.service_id == service_id, + models.ServiceHistory.timestamp >= start_date, + models.ServiceHistory.timestamp <= end_date)) + history_records = result.scalars().all() + + total_downtime: timedelta = timedelta() + last_status: str | None = None + last_status_time: datetime | None = None + + # Проходим по всем записям и считаем время простоя + for record in history_records: + if last_status: + if last_status == 'down' and (record.status == 'up' + or record.status == 'unstable'): + # Если был статус 'down' и теперь 'up'/'unstable', добавляем время простоя + downtime_duration: timedelta = record.timestamp - last_status_time + total_downtime += downtime_duration + + # Обновляем последний статус и его время + last_status = record.status + last_status_time = record.timestamp + + # Расчет SLA + total_time: timedelta = end_date - start_date + if total_time.total_seconds() > 0: + sla = (1 - (total_downtime.total_seconds() / + total_time.total_seconds())) * 100 + else: + sla: int | float = 0 + + return { + "total_downtime": str(total_downtime), + "sla_percentage": round(sla, 3) + } diff --git a/app/__pycache__/SLA_calculation.cpython-310.pyc b/app/__pycache__/SLA_calculation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc09833c4d0839a34eec5a225e5986fc284cd636 GIT binary patch literal 1191 zcmYjQUyI~K5buBaGnvfn-XU_XqUYlrx=(^2a=0jXf*c2nFJUC~rF(BO(@EyKJKhZ> zpsXLmhZ*q2&*8V|NA+dDg5aO(WR}|wRrRZ`uBvoZbu*hK2-f`mUsu0I2>t1myDJ6d z3m~%wCXm1iRVYIh&M@{mQKX_79WYg_3bJ6pOodgHMSuels<@hD3E)TuB9>Dzkw_#! zQ<=({q*vq}bd`uy%zkFsT+A=g;^YSMp#^cV;8xnQl5XO;kjnA}cJ!j&fk-dRt}ZU6 zF=bPO9vZ1+VQ;W^#_pbZskkauvfAZU1HQ%)@!Taw>L1EN=A{5^xwg5_;$m5g5rtaX zR@Vjde{ozhkTKBjK;{o%Kj9ATk;Rs*={4RnFV{hbH)KFzM>fF`(G5P(1O5g|_5}5; zWBcfU_VEGrft_5VJ`|{rfZ{Ir3Vr`!Kj|X$IEhrw1h1hG{8+F7(>C_bz&XMEDl!)0zm@r#9`vYiws- zDxzy5;$H}~Ve&L%qIp$kq}^t5TSKv)E2({XHH-_o5uQO$6*zmSW$i{xurv{vd6JY0Be$^3F`$CJ|7M(8%vS?}n2aMG&&`n#5#Y|7&{YHw2bf3DN1olJUycS1Eau0K>z>% literal 0 HcmV?d00001 diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7af5ac0cad99cf8c3298e69febec7df63fe7b25a GIT binary patch literal 1995 zcmaJ>TW=#Z6drpfm*(C!O`En40|L)JiB@kphxcDVmO*%`9^Xw$pAx zDs?HZmG~R{4j%XqeubC)3%rzL&rFr>qV;HeIrcd|{?7SMQmu-eg zs*_9Cv`I~hmEL7no*0iqE)hrEQW3@vc}e)uhSLQrj=XFFst975u#OAp*2>Z zb=GDZY-5DG)~HAuwE0`%29XZvY=X`ft8}dgdZ~_)j~v>ftLA*yoL__U)vm?bk00u+ zvyJSoY(Ky8vA(lSH;j#KW25t$pPOulZm}Zm!k_s&s?hCRhwcol-tH@nbkA0$Z#*Fn zPmWdTYc^pq6{>!8dV2EYh0i83Na7w+wL>wB{Q(mqU6-jRnG7QK3gTkHfol& zpTk$WvY=)jshVQJ_E(#GbV@h9NcL5oGt0AIxJ~!O{m@F<#3!D z7@p6Du@|xV%0YIm=poYJOiYqkuwj%?7OGOVr!YNNMb6}u$3E7t&zcRR1OK1!i#_OG zB55tq0ut^PA48HISyy<0d5dCd2WS9EM;$%(3jH34L4!A;Q>6$X@}9Bz``MLl`Y3uZ zooL9YE2h2=XGYU7oV_#uC_femxBv9??WcQ)Yd-XBbCr3B}hH-)5nvU94GOaH<_G8UJ&n3W~w$xgv^aKz>?79 z#Mf)sL1p;(1}uFY@VsdZHFgHK4onPg8a0%mh87wn+y~z^N-)&*kYYy4+4F^QJnh=B zuWC9g@=a}66rsb{EpUbF_1wJ=_`xsPL6Zqr2U%*U+OqhKaNoQAE?CRMeX|s7(WHoC z9pnXuC)j^HH-H}YLQ@1ou$?T}&cgZ88UT|dn(ZP> ze-dlTQ0Er^iY^UZoRltIFvPcYw*y`9JEYZo^Ekfn!hpJYbpe2RI#n1;r zlOko(3qpWW`=u&Bc;S>i;XL77JKihsht^ZDS9CGb&+}f(HUr;#peS^N@in%~7^B)< z5!X-+;k$lsmy||OBfUw$b;@&{JmxCW5kH4e`Nk}o?Xwqh-_vz6NVo=#>zpM`jelMp zp?VshNyk(s0koqYR7pRrRD!Q)Jxx1OrRV$F*>ew~Rn?LbNPIq}rV1DynT%34keE1{ zyvm`0FO@AMH)K^dB#?4KD3*pb=B3IGAlr1MSn3Sx%AbQ*(~y0kIW9hgu5AGX07DD@ cHUdEGJ=7|>s9pF3)k;O&y2AhvMy|x<#Bh2Gk#>do!zmN zkw9>T-;hXL_QIdw#!u)gTt?y-AP`j9$vj{kb#-@D^`pM}s#6AmN8tO*`+f3VIRgF(czxx&jQ{R{w$c-+~>jfX1LQ>x?&f#M`U919`A#GuhH*_ zeeh?Eb#O$$Zu4Ef2ljh>e?*3ZE4VWmGR>Y8wRT71`9c(l(C!!WDlLmR1Cy;pCX!0C zFUyzO1Mzu0pQpvwL#q8JsxFd|Pzs!{C~igdu6mxuSuz!Q9pxn#nF6IV67ofwh%2LY z>d$K)7gd^UT(rT@w4+)lZA(!tWRalr_OoDML;D4q`W3{tbV*jEqLndY-_ezc{mfd@ znRRZg>6(59z&|CI#?n~XYqE0IuOpSW1!lYpN$i_;3xB1_Yx`MN^H--xYZ*N#y2KCq~1S1GpT7n zgP;AkXSvL!J-P$4f#GdN@@-gAkDI&(V(W?4J7IY1o;*^1rQD22BY2umBP0Mqj51BL%xG<{uXdd z?VX~)TVEH&6ef3Xep3{$D+w$Ob)Q2lK-RX9oJ}D)P$I^xb7vJ4&&q@7yBNKE0EZ3z z*T(+_nB*Y{z1OlZDhm;nGAf{#_tCnG;ur;jB@y$X*-{KqXn3akw+WPr|Dkt$8@*`j zd{##Y(_`31VNMXt0|3)y4Ka*{CZ>E3OsB&GZC5ouW)e9d84AP{it+jpYtxG;O9xE{ zH>MMDQJuv22BxLN{{c-TCh!&+0@Ok^!gCm_hI-n}pfK9ZG8nXo;C*hBuJg_<2e-1;U`s{PMcu+$@}f} z7l)8vEx6hm2yWp}f8caNIN`J)C5t7a~U&B;EmvsEf!))Q2{r2$2UpaQP^9Q=;?k|8zzSD@2_RHs8`wlfK-h9p&UktHC`asejTv*-wv$}52Zfm)hn zdLJa>h(lJSGS&G5NG7QQb71p7jfdJgH zlCgKNpPYRCr3nu9j`#NWj_z1=j3F*$)5>_OQ~D+D%P|e(?&BFg9XiIHK8Z8_@<51t>^o(lqg1#glk}psZpOYW^Psn5P zf@$a6J#W0EOO}#van;4XaMlbbN92Q5lzR6eD$4eIIPA(RdN}lCh;J~B31Fp-FE2!5 zYb_&WW!EmX*EdoWHs!1m;uVntT>A6l+aHZj6i7AJ)3ngzyYpvDVw}!qV?G(%`q &E}?=i(Dss@&@jue4NNBRv z;<6$;Pl!f!uJe9OzQG*-rylRTUJtp08>0q0OYyBlRv!j4Z?3UTz0P0SgV(yjByOOs z?1#>tMl_&49sMoeMmS`$hmXVIz_iu(MOtK2DCZLMAf|swjNp+T)$h$K>ENPttw(sp zqmrex|B85Ug|uN;)Gs|7+O(4`sPUHamR3)ORZ7|u8c)w>xj2&>)-SM(qqH>LBuRy+ zG=4{wP)SltqZYwZ4sTg^do^`p%M!d+4F6_X@rwdJu_9LQ;S>^Ej#X#)WFCuXkA*a( Oj4^7v_gA|)X#5M1NkHBJ literal 0 HcmV?d00001 diff --git a/app/db/__pycache__/sqlalchemy_models.cpython-310.pyc b/app/db/__pycache__/sqlalchemy_models.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a399de2d5cd0a7165dc86827db5a095997be660a GIT binary patch literal 1238 zcmaJ=%Wm8@6czP;jXZIiAWaOki*9BSe?SlfMI5*d0vG}8ZioQ{j>vdqi;_%IPAA)R z>kr8n1>iDWWuH}6xmOxLHVp+`+$VLYbJ)#hsROJ0`uXiA&vE|4;O+___yb;j35GkI zG>#;cNSC_Oqn`AsF9RA7thr4nBN~D4aleUWLX$BcG^tE!N}N9(9`fj4hesm$;L!}? zm?sb?W1Nc!y!>p&Q=VOUA}b4?A3KZL7x=cb2yA%JHG^zz^r|)DR45xB8&$WbHa+Z= zs88G1Vq@c%%!rd(iUqN`5)CtT*XniMe<8RFvHlCDu^4k`G|*V#_IKCKpT@Lx>{qeI7s*kCD&AE0-pB{FFyDi=l-ubkrm4NAvCQ;OX*j z0Irv2%^I_Ov#Hih$v3Jy6P1B1)8|k6jahf?lJ)(Pua^3vVNJCba)anZqxbub%3!ky zUY&!99iJ3#;pSvc#>)Q_v4Zv=5)l9u{s+PXAYdXR8GV?^M9?yWL+!kA!{;IX<)Q0YC*(%pl0!ULc0Aqu?<>H_B*)^N{H9u7MpaRN@xOmHk z#*Bxu(RLIH#fnwuW#9D!$WrK|#je_g5%m+ApV8b$bHkH*fFa7!rq@8W%&CW1`UOph z2Cp&b#Z+zg&L${p;8gYs|F(^`!?%o1-ea8H1B1XdDr@3om*nsiWbTet^*dBRj&Y~B zP~~c)euKo(Vh2#&uzUwKDr-bNLZiiP(|)Jq1PN~+Tft3K33em=@pnxh9MAnL=5922 ai`d4`CcQt$8?6g4dkz8yA$jxkLH-@Zel5=c literal 0 HcmV?d00001 diff --git a/app/db/crud.py b/app/db/crud.py new file mode 100644 index 0000000..384cdec --- /dev/null +++ b/app/db/crud.py @@ -0,0 +1,57 @@ +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from . import sqlalchemy_models as models +from .sqlalchemy_models import Service +from ..models import pydantic_models as schemas + + +async def create_service(db: AsyncSession, + service: schemas.Service) -> models.Service: + service_exist_check = await db.execute( + select(models.Service).where(models.Service.name == service.name)) + existing_service = service_exist_check.scalars().first() + if existing_service: # если сервис с таким названием уже существует, то обновляем его данные + # 1) записываем существующие данные в историю + history_entry = models.ServiceHistory( + service_id=existing_service.id, + status=existing_service.status, + description=existing_service.description) + db.add(history_entry) + + # 2) обновляем актуальные данные о сервисе + existing_service.status = service.status + existing_service.description = service.description + await db.commit() + await db.refresh(existing_service) + return existing_service + else: # иначе добавляем новый сервис в БД + service = models.Service(**service.model_dump()) + db.add(service) + await db.commit() + await db.refresh(service) + return service + + +async def read_services(db: AsyncSession) -> list[models.Service]: ## ## + services = await db.execute(select(models.Service)) + return services.scalars().all() + + +async def read_service_history_by_name( + db: AsyncSession, + service_name: str) -> Optional[list[models.ServiceHistory]]: + service = await db.execute( + select(models.Service).where(models.Service.name == service_name)) + service = service.scalar_one_or_none() + + if not service: + return None + + history_entries = await db.execute( + select(models.ServiceHistory).where( + models.ServiceHistory.service_id == service.id)) + + return history_entries.scalars().all() diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 0000000..96a9af6 --- /dev/null +++ b/app/db/database.py @@ -0,0 +1,35 @@ +from sqlalchemy import MetaData +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.asyncio import AsyncEngine +from sqlalchemy.ext.declarative import DeclarativeMeta + +from dotenv import load_dotenv +from os import getenv + +from typing import AsyncGenerator + +load_dotenv() +DATABASE_URL: str = getenv("DB_URL") + +engine: AsyncEngine = create_async_engine(DATABASE_URL, echo=True) +metadata: MetaData = MetaData() +Base: DeclarativeMeta = declarative_base() + +async_session = sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + + +async def init_db() -> None: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with async_session() as session: + await init_db() + yield session diff --git a/app/db/sqlalchemy_models.py b/app/db/sqlalchemy_models.py new file mode 100644 index 0000000..0e7de94 --- /dev/null +++ b/app/db/sqlalchemy_models.py @@ -0,0 +1,31 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, DateTime +from sqlalchemy.orm import relationship +from .database import Base +from datetime import datetime + + +class Service(Base): + __tablename__ = 'services' + + id = Column(Integer, primary_key=True, autoincrement=True) + + name = Column(String, unique=True, nullable=False) + status = Column(String, nullable=False) + description = Column(String, nullable=False) + + +class ServiceHistory(Base): + __tablename__ = 'service_history' + + id = Column(Integer, primary_key=True, index=True) + service_id = Column(Integer, ForeignKey('services.id'), nullable=False) + status = Column(String, nullable=False) + description = Column(String, nullable=False) + timestamp = Column(DateTime, default=datetime.utcnow) + + service = relationship("Service", back_populates="history") + + +Service.history = relationship("ServiceHistory", + order_by=ServiceHistory.id, + back_populates="service") diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..1c80117 --- /dev/null +++ b/app/main.py @@ -0,0 +1,59 @@ +from fastapi import FastAPI, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from datetime import datetime +from .SLA_calculation import calculate_service_downtime + +from .models.pydantic_models import Service, ServiceHistory +from .db.database import get_db +from .db.crud import create_service, read_services, read_service_history_by_name + +app = FastAPI() + + +@app.post("/services", response_model=dict) +async def post_service( + service: Service, db: AsyncSession = Depends(get_db)) -> dict[str, str]: + new_service = await create_service(db=db, service=service) + if new_service: + return {"message": "service created/updated successfully!"} + return {"message": "service is NOT created/updated successfully!"} # + + +@app.get("/services", response_model=list[Service] | dict[str, str]) +async def get_services(db: AsyncSession = Depends(get_db)) -> list[Service] | dict[str, str]: + services = await read_services(db=db) + if services: + return services + return {"message": "There are no services in the database"} + + +@app.get("/services/{service_name}/history", + response_model=list[ServiceHistory] | dict) +async def get_service_history( + service_name: str, db: AsyncSession = Depends(get_db) +) -> list[ServiceHistory] | dict[str, str]: + history_entries = await read_service_history_by_name(db, service_name) + if not history_entries: + return { + "message": + "Service not found OR Service doesn't have a history yet." + } + return history_entries + + +@app.get("/services/{service_id}/SLA_downtime_info", response_model=dict) +async def get_service_history( + service_id: int, + start_date: datetime, + end_date: datetime, + db: AsyncSession = Depends(get_db)) -> dict: + try: + # Вызываем функцию для расчета времени простоя|SLA + time_data = await calculate_service_downtime(db=db, + service_id=service_id, + start_date=start_date, + end_date=end_date) + return time_data + except ValueError: + raise HTTPException(status_code=400, detail="Invalid datetime format") diff --git a/app/models/__pycache__/pydantic_models.cpython-310.pyc b/app/models/__pycache__/pydantic_models.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4aa525ff0faeaed8e35dfca294a926153a2cad02 GIT binary patch literal 669 zcmaJHgr823`Dw~zX430&dW1e%@;e1`nO^*8>=l2_t z?=s9-?G88>MNvW*s5G2E6WxRNH1IjSJWG~FX_MHM$ZayO)0NQcxh`*2Y9T7j-APs3 zRaqpWsuH+J83;7wI;DV_9K!7b*T5KRRr8eT)qN rFli9Bb2O7+d^Yr5^gX-wAkqxj)cn@WMdR(4Ij7hWAJrp=>?r&Ke$JDF literal 0 HcmV?d00001 diff --git a/app/models/pydantic_models.py b/app/models/pydantic_models.py new file mode 100644 index 0000000..881254f --- /dev/null +++ b/app/models/pydantic_models.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + + +class Service(BaseModel): + name: str + status: str + description: str + + +class ServiceHistory(BaseModel): + id: int + service_id: int + status: str + description: str diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1247c55 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.7" +services: + database: + image: postgres:15 + environment: + POSTGRES_PASSWORD: mypassword + container_name: db_app + env_file: + - .env-non-dev + + app: + build: + context: . + env_file: + - .env-non-dev + container_name: itpc_app + ports: + - 9990:8000 + depends_on: + - database diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..affad40 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,25 @@ +annotated-types==0.7.0 +anyio==4.4.0 +async-timeout==4.0.3 +asyncpg==0.29.0 +click==8.1.7 +colorama==0.4.6 +exceptiongroup==1.2.2 +fastapi==0.114.1 +greenlet==3.1.0 +h11==0.14.0 +idna==3.8 +importlib_metadata==8.5.0 +platformdirs==4.3.2 +pydantic==2.9.1 +pydantic_core==2.23.3 +python-dotenv==1.0.1 +sniffio==1.3.1 +SQLAlchemy==2.0.34 +starlette==0.38.5 +tomli==2.0.1 +typing_extensions==4.12.2 +uvicorn==0.30.6 +yapf==0.40.2 +zipp==3.20.1 +gunicorn==23.0.0