Skip to content

Commit 1a183e5

Browse files
committed
feat: implement analytics logic with celery, improve tests, implement
locust and docker
1 parent fde3c5a commit 1a183e5

22 files changed

Lines changed: 1203 additions & 202 deletions

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
# Common
22
DEBUG=True
3+
HOST=0.0.0.0
4+
PORT=8000
5+
CELERY_RESULT_EXPIRES=43200
36

47
# PostgreSQL
58
POSTGRES_USER=
69
POSTGRES_PASSWORD=
710
POSTGRES_HOST=
811
POSTGRES_PORT=
9-
POSTGRES_DATABASE=
12+
POSTGRES_DB=
13+
14+
# Redis
15+
REDIS_HOST=
16+
REDIS_PORT=
17+
REDIS_DB=

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
*.db
2+
13
# Byte-compiled / optimized / DLL files
24
__pycache__/
35
*.py[cod]

Dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
FROM python:3.13.11
2-
WORKDIR /usr/src/python_template
2+
WORKDIR /usr/src/telemetry_api
33
COPY . .
44

55
RUN python -m pip install --upgrade pip
66
RUN python -m pip install poetry
77
RUN python -m poetry install --without dev
8-
9-
# CMD [ "some", "command" ]

docker-compose.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
services:
2+
telemetry-api:
3+
build: .
4+
env_file: ".env"
5+
container_name: "telemetry-api"
6+
command: >
7+
poetry run uvicorn telemetry_api.main:app
8+
--host ${HOST}
9+
--port ${PORT}
10+
--reload
11+
ports:
12+
- "${PORT}:${PORT}"
13+
depends_on:
14+
- "telemetry-postgres"
15+
- "telemetry-redis"
16+
networks:
17+
- app-net
18+
restart: always
19+
20+
telemetry-postgres:
21+
image: "postgres:16"
22+
env_file: ".env"
23+
container_name: "telemetry-postgres"
24+
volumes:
25+
- "telemetry-postgres:/var/lib/postgresql/data/"
26+
networks:
27+
- app-net
28+
restart: always
29+
30+
telemetry-redis:
31+
image: "redis:7-alpine"
32+
container_name: "telemetry-redis"
33+
command: "redis-server --save 60 1 --loglevel warning"
34+
volumes:
35+
- "telemetry-redis:/data"
36+
networks:
37+
- app-net
38+
restart: always
39+
40+
telemetry-celery-worker:
41+
build: .
42+
env_file: ".env"
43+
container_name: "telemetry-celery-worker"
44+
command: "poetry run celery -A telemetry_api.worker.celery_app:celery_instance worker --loglevel=INFO"
45+
depends_on:
46+
- "telemetry-postgres"
47+
- "telemetry-redis"
48+
networks:
49+
- app-net
50+
restart: always
51+
52+
volumes:
53+
telemetry-postgres:
54+
telemetry-redis:
55+
56+
networks:
57+
app-net:
58+
external: true

locustfile.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import uuid
2+
3+
from locust import HttpUser, between, task
4+
5+
6+
class TelemetryApiUser(HttpUser):
7+
wait_time = between(0.5, 3.0)
8+
9+
def on_start(self):
10+
self.user_id = self._create_user()
11+
self.device_id = self._create_device(self.user_id)
12+
self._add_measurement(self.device_id)
13+
14+
def _create_user(self) -> int:
15+
unique_suffix = uuid.uuid4().hex[:8]\
16+
payload = {
17+
"username": f"user-{unique_suffix}",
18+
"email": f"{unique_suffix}@example.com",
19+
}
20+
21+
with self.client.post(
22+
"/users/",
23+
json=payload,
24+
name="POST /users/",
25+
catch_response=True
26+
) as response:
27+
if response.status_code != 201:
28+
response.failure(f"Unexpected status: {response.status_code}, body={response.text}")
29+
return -1
30+
31+
data = response.json()
32+
return int(data["id"])
33+
34+
def _create_device(self, user_id: int) -> int:
35+
payload = {
36+
"name": f"device-{uuid.uuid4().hex[:6]}",
37+
"user_id": user_id,
38+
}
39+
40+
with self.client.post(
41+
"/devices/",
42+
json=payload,
43+
name="POST /devices/",
44+
catch_response=True
45+
) as response:
46+
if response.status_code != 201:
47+
response.failure(f"Unexpected status: {response.status_code}, body={response.text}")
48+
return -1
49+
50+
data = response.json()
51+
return int(data["id"])
52+
53+
def _add_measurement(self, device_id: int):
54+
payload = {
55+
"x": 1.0,
56+
"y": 2.0,
57+
"z": 3.0,
58+
}
59+
60+
with self.client.post(
61+
f"/analytics/{device_id}/data",
62+
json=payload,
63+
name="POST /analytics/{device_id}/data",
64+
catch_response=True,
65+
) as response:
66+
if response.status_code != 201:
67+
response.failure(f"Unexpected status: {response.status_code}, body={response.text}")
68+
69+
@task(5)
70+
def add_measurement(self):
71+
self._add_measurement(self.device_id)
72+
73+
@task(3)
74+
def get_device_analytics(self):
75+
params = {
76+
"device_id": self.device_id,
77+
"limit": 25,
78+
"offset": 0,
79+
}
80+
81+
with self.client.get("/analytics/", params=params, name="GET /analytics/?device_id", catch_response=True) as response:
82+
if response.status_code != 200:
83+
response.failure(f"Unexpected status: {response.status_code}, body={response.text}")
84+
return
85+
86+
payload = response.json()
87+
if "data" not in payload:
88+
response.failure("Response without data field")
89+
90+
@task(2)
91+
def get_users(self):
92+
with self.client.get(
93+
"/users/",
94+
params={"limit": 10, "offset": 0},
95+
name="GET /users/",
96+
catch_response=True,
97+
) as response:
98+
if response.status_code != 200:
99+
response.failure(f"Unexpected status: {response.status_code}, body={response.text}")
100+
101+
@task(2)
102+
def get_devices(self):
103+
with self.client.get(
104+
"/devices/",
105+
params={"limit": 10, "offset": 0},
106+
name="GET /devices/",
107+
catch_response=True,
108+
) as response:
109+
if response.status_code != 200:
110+
response.failure(f"Unexpected status: {response.status_code}, body={response.text}")

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ aiosqlite = "^0.22.1"
1717
asyncpg = "^0.31.0"
1818
greenlet = "^3.5.0"
1919
email-validator = "^2.3.0"
20+
celery = {extras = ["redis"], version = "^5.6.3"}
21+
redis = "^5.2.1"
2022

2123
[tool.poetry.group.dev.dependencies]
2224
black = "^25.1.0"
@@ -32,6 +34,7 @@ setuptools = "^75.8.0"
3234
pytest = "^9.0.3"
3335
pytest-asyncio = "^1.3.0"
3436
httpx = "^0.28.1"
37+
locust = "^2.37.10"
3538

3639
[build-system]
3740
requires = ["poetry-core"]

telemetry_api/api/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55

66
def create_api() -> FastAPI:
77
"""
8-
Создание инстенса приложения с подключением всех роутеров
9-
10-
Return:
11-
FastAPI - инстанс приложения
8+
Создаёт и настраивает экземпляр FastAPI-приложения.
129
"""
1310

1411
api = FastAPI(

0 commit comments

Comments
 (0)