Skip to content

Commit fde3c5a

Browse files
committed
feat: implement core logic and tests
1 parent 64b631d commit fde3c5a

20 files changed

Lines changed: 693 additions & 22 deletions

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Common
2+
DEBUG=True
3+
4+
# PostgreSQL
5+
POSTGRES_USER=
6+
POSTGRES_PASSWORD=
7+
POSTGRES_HOST=
8+
POSTGRES_PORT=
9+
POSTGRES_DATABASE=

README.md

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
1-
# python-template
2-
![Static Badge](https://img.shields.io/badge/Python-0?style=flat-square&logo=python&color=010409)
3-
![Static Badge](https://img.shields.io/badge/Poetry-0?style=flat-square&logo=Poetry&color=010409)
4-
![Static Badge](https://img.shields.io/badge/Docker-0?style=flat-square&logo=Docker&color=010409)
5-
![Static Badge](https://img.shields.io/badge/Flake8-0?style=flat-square&logo=Flake8&color=010409)
6-
![Static Badge](https://img.shields.io/badge/Black-0?style=flat-square&logo=Black&color=010409)
1+
# telemetry-api
2+
При разработке предлагается использовать Python версии не ниже 3.8, pip / poetry.
73

8-
Template for the [Python 3.13.11](https://www.python.org/downloads/release/python-3110/) projects based on [Poetry](https://python-poetry.org/docs/) using [Flake8](https://flake8.pycqa.org/en/latest/), [Black](https://black.readthedocs.io/en/stable/), [pre-commit](https://pre-commit.com/), [Docker](https://docs.docker.com/) and more
4+
## Примечания:
5+
- Пункты отмеченные "\*" необязательны к выполнению
6+
- Необходимо подготовить описание реализованного сервиса
7+
- Приложить результаты нагрузочного тестирования
8+
- Формат предоставления: ссылка на репозиторий с кодом
99

10-
## Quick Start:
11-
1. Create a repository using this template
12-
2. Clone the created repository with `git clone`
13-
3. Rename the created folder
14-
4. Go to the created folder
15-
5. Create a local environment using the command `poetry env use PYTHON_PATH` and add it to your IDE
16-
6. Rename the `python_template` folder to the project name and mark it in the IDE as `Source Root`
17-
7. Configure the `pyproject.toml` file for the project
18-
8. Configure the Flake8 linter and other required tools in the `setup.cfg` file
19-
9. Configure the `Dockerfile` for the project
20-
10. Configure and install pre-commits with the command `pre-commit install`
21-
11. Add the required dependencies with the command `poetry add PACKAGE_NAME` and install them with the command `poetry install`
10+
## Описание системы:
11+
К реализации предлагается система учета и анализа данных, поступающих с условного устройства. Полученные данные привязываются к временной метке и устройству, с которого пришли данные, и сохраняются в БД. Набор данных используется для дальнейшего анализа.
12+
13+
## Требования к системе:
14+
15+
### Функциональные:
16+
- В системе реализован сбор статистики с устройства по его идентификатору
17+
- формат получаемой статистики - {“x”: float, “y”: float, “z”:float}
18+
- В системе реализован анализ собранной статистики с устройства за определенный период и за все время
19+
- Результатами анализа являются числовые характеристики величины:
20+
- минимальное значение
21+
- максимальное значение
22+
- количество
23+
- сумма
24+
- медиана
25+
- Система поддерживает добавление пользователей устройств*
26+
- В системе реализован функционал получения анализа показаний устройств по идентификатору пользователя*:
27+
- агрегированные результаты для всех устройств
28+
- для каждого устройства отдельно
29+
30+
### Нефункциональные:
31+
- архитектура REST
32+
- фреймворк реализации сервиса FastApi
33+
- собранные данные хранятся в БД на выбор разработчика
34+
- аналитика показателей происходит в асинхронном режиме при помощи фреймворка Celery*
35+
- Реализовано нагрузочное тестирование через инструмент locust*
36+
- Сервис и его окружение разворачивается средствами docker + docker-compose

pyproject.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
[tool.poetry]
2-
name = "python-template"
2+
name = "telemetry-api"
33
version = "0.0.0"
44
description = ""
5-
authors = ["Your Name <you@example.com>"]
5+
authors = ["Kirill Volychev <volychevk@gmail.com>"]
66
readme = "README.md"
7-
packages = [{include = "python_template"}]
7+
packages = [{include = "telemetry_api"}]
88

99
[tool.poetry.dependencies]
1010
python = "^3.13.11"
11+
fastapi = "^0.136.1"
12+
uvicorn = "^0.46.0"
13+
sqlalchemy = "^2.0.49"
14+
pydantic = "^2.13.4"
15+
pydantic-settings = "^2.14.1"
16+
aiosqlite = "^0.22.1"
17+
asyncpg = "^0.31.0"
18+
greenlet = "^3.5.0"
19+
email-validator = "^2.3.0"
1120

1221
[tool.poetry.group.dev.dependencies]
1322
black = "^25.1.0"
@@ -20,6 +29,9 @@ pep8-naming = "^0.14.1"
2029
reorder-python-imports = "^3.14.0"
2130
autoflake = "^2.3.1"
2231
setuptools = "^75.8.0"
32+
pytest = "^9.0.3"
33+
pytest-asyncio = "^1.3.0"
34+
httpx = "^0.28.1"
2335

2436
[build-system]
2537
requires = ["poetry-core"]

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
asyncio_mode = auto
3+
pythonpath = .

telemetry_api/api/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from fastapi import FastAPI
2+
3+
from telemetry_api.api import analytics, devices, users
4+
5+
6+
def create_api() -> FastAPI:
7+
"""
8+
Создание инстенса приложения с подключением всех роутеров
9+
10+
Return:
11+
FastAPI - инстанс приложения
12+
"""
13+
14+
api = FastAPI(
15+
title="Telemetry API",
16+
description="",
17+
version="1.0.0"
18+
)
19+
20+
api.include_router(analytics.router)
21+
api.include_router(devices.router)
22+
api.include_router(users.router)
23+
return api
24+
25+
26+
api = create_api()

telemetry_api/api/analytics.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from telemetry_api.database.database import get_db
2+
from telemetry_api.database.models import Measurement, Device
3+
from telemetry_api.schemas.analytics import MeasurementCreate, MeasurementRead
4+
5+
from fastapi import APIRouter, Depends, HTTPException, status
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
router = APIRouter(
9+
prefix="/analytics",
10+
tags=["Analytics"]
11+
)
12+
13+
14+
@router.post("/{device_id}/data", response_model=MeasurementRead, status_code=status.HTTP_201_CREATED)
15+
async def add_measurement(device_id: int, data: MeasurementCreate, db: AsyncSession = Depends(get_db)):
16+
device = await db.get(Device, device_id)
17+
if not device:
18+
raise HTTPException(status_code=404, detail="Device not found")
19+
20+
new_measurement = Measurement(**data.model_dump(), device_id=device_id)
21+
db.add(new_measurement)
22+
23+
await db.commit()
24+
await db.refresh(new_measurement)
25+
return new_measurement
26+
27+
28+
# TODO: analytics

telemetry_api/api/devices.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from telemetry_api.database.database import get_db
2+
from telemetry_api.database.models import Device
3+
from telemetry_api.schemas.devices import DeviceCreate, DeviceRead, DeviceUpdate
4+
5+
from fastapi import APIRouter, Depends, HTTPException, status
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
from sqlalchemy import select
8+
9+
router = APIRouter(prefix="/devices", tags=["Devices"])
10+
11+
12+
@router.post("/", response_model=DeviceRead, status_code=status.HTTP_201_CREATED)
13+
async def create_device(device_in: DeviceCreate, db: AsyncSession = Depends(get_db)):
14+
new_device = Device(**device_in.model_dump())
15+
db.add(new_device)
16+
17+
await db.commit()
18+
await db.refresh(new_device)
19+
return new_device
20+
21+
22+
@router.get("/{device_id}", response_model=DeviceRead)
23+
async def get_device(device_id: int, db: AsyncSession = Depends(get_db)):
24+
device = await db.get(Device, device_id)
25+
if not device:
26+
raise HTTPException(status_code=404, detail="Device not found")
27+
return device
28+
29+
30+
@router.patch("/{device_id}", response_model=DeviceRead)
31+
async def update_device(device_id: int, device_in: DeviceUpdate, db: AsyncSession = Depends(get_db)):
32+
device = await db.get(Device, device_id)
33+
if not device:
34+
raise HTTPException(status_code=404, detail="Device not found")
35+
36+
update_data = device_in.model_dump(exclude_unset=True)
37+
for key, value in update_data.items():
38+
setattr(device, key, value)
39+
40+
await db.commit()
41+
await db.refresh(device)
42+
return device
43+
44+
45+
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
46+
async def delete_device(device_id: int, db: AsyncSession = Depends(get_db)):
47+
device = await db.get(Device, device_id)
48+
if not device:
49+
raise HTTPException(status_code=404, detail="Device not found")
50+
51+
await db.delete(device)
52+
await db.commit()
53+
return None
54+
55+
56+
@router.get("/", response_model=list[DeviceRead])
57+
async def get_devices(limit: int = 25, offset: int = 0, db: AsyncSession = Depends(get_db)):
58+
result = await db.execute(select(Device).offset(offset).limit(limit))
59+
return result.scalars().all()

telemetry_api/api/users.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from telemetry_api.database.database import get_db
2+
from telemetry_api.database.models import User
3+
from telemetry_api.schemas.users import UserCreate, UserRead, UserUpdate
4+
5+
from fastapi import APIRouter, Depends, HTTPException, status
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
from sqlalchemy import select
8+
9+
router = APIRouter(
10+
prefix="/users",
11+
tags=["Users"]
12+
)
13+
14+
15+
@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
16+
async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
17+
new_user = User(**user_in.model_dump())
18+
db.add(new_user)
19+
20+
await db.commit()
21+
await db.refresh(new_user)
22+
return new_user
23+
24+
25+
@router.get("/{user_id}", response_model=UserRead)
26+
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
27+
user = await db.get(User, user_id)
28+
if not user:
29+
raise HTTPException(status_code=404, detail="User not found")
30+
return user
31+
32+
33+
@router.patch("/{user_id}", response_model=UserRead)
34+
async def update_user(user_id: int, user_in: UserUpdate, db: AsyncSession = Depends(get_db)):
35+
user = await db.get(User, user_id)
36+
if not user:
37+
raise HTTPException(status_code=404, detail="User not found")
38+
39+
update_data = user_in.model_dump(exclude_unset=True)
40+
for key, value in update_data.items():
41+
setattr(user, key, value)
42+
43+
await db.commit()
44+
await db.refresh(user)
45+
return user
46+
47+
48+
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
49+
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db)):
50+
user = await db.get(User, user_id)
51+
if not user:
52+
raise HTTPException(status_code=404, detail="User not found")
53+
54+
await db.delete(user)
55+
await db.commit()
56+
return None
57+
58+
59+
@router.get("/", response_model=list[UserRead])
60+
async def get_users(limit: int = 25, offset: int = 0, db: AsyncSession = Depends(get_db)):
61+
result = await db.execute(select(User).offset(offset).limit(limit))
62+
return result.scalars().all()

telemetry_api/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pydantic_settings import BaseSettings, SettingsConfigDict
2+
from pydantic import SecretStr
3+
4+
5+
class Settings(BaseSettings):
6+
debug: bool = False
7+
8+
postgres_user: SecretStr
9+
postgres_password: SecretStr
10+
postgres_host: SecretStr
11+
postgres_port: int
12+
postgres_database: SecretStr
13+
14+
model_config = SettingsConfigDict(
15+
env_file=".env",
16+
env_file_encoding="utf-8",
17+
extra="ignore"
18+
)
19+
20+
21+
config = Settings()

0 commit comments

Comments
 (0)