Skip to content
This repository was archived by the owner on Jul 3, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DB_URL="postgresql+asyncpg://postgres:rootroot@localhost/itpc_test"
1 change: 1 addition & 0 deletions .env-non-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DB_URL="postgresql+asyncpg://postgres:mypassword@database/postgres"
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions .idea/task-python.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/toolchains.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
76 changes: 29 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 к текущему проекту.
Expand All @@ -53,7 +8,7 @@

Требуется написать API который:

1. Получает и сохраняет данные: имя, состояние, описание
1. Получает и сохраняет данные: имя, состояние, описание
2. Выводит список сервисов с актуальным состоянием
3. По имени сервиса выдает историю изменения состояния и все данные по каждому состоянию

Expand All @@ -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, либо поднимает ошибку о неверном формате даты-времени.
49 changes: 49 additions & 0 deletions app/SLA_calculation.py
Original file line number Diff line number Diff line change
@@ -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)
}
Binary file added app/__pycache__/SLA_calculation.cpython-310.pyc
Binary file not shown.
Binary file added app/__pycache__/main.cpython-310.pyc
Binary file not shown.
Binary file added app/db/__pycache__/crud.cpython-310.pyc
Binary file not shown.
Binary file added app/db/__pycache__/database.cpython-310.pyc
Binary file not shown.
Binary file not shown.
57 changes: 57 additions & 0 deletions app/db/crud.py
Original file line number Diff line number Diff line change
@@ -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()
35 changes: 35 additions & 0 deletions app/db/database.py
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions app/db/sqlalchemy_models.py
Original file line number Diff line number Diff line change
@@ -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")
Loading