Skip to content
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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r hw2/hw/requirements.txt
pip install pytest pytest-cov
- name: Run tests with coverage
run: |
PYTHONPATH=hw2/hw pytest -q hw2/hw --cov=hw2/hw --cov-report=term --cov-fail-under=95
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,4 @@ dmypy.json

# macOS
.DS_Store
*.conda
126 changes: 119 additions & 7 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,130 @@
import math
from typing import Any, Awaitable, Callable
from http import HTTPStatus

def factorial(data):
# 1. проверяем что dict не пустой
if not data or "n" not in data:
return HTTPStatus.UNPROCESSABLE_ENTITY, -1
raw_value = data.get("n", "")
if raw_value == "":
return HTTPStatus.UNPROCESSABLE_ENTITY, -1
try:
if int(raw_value) < 0:
return HTTPStatus.BAD_REQUEST, -1
res = math.factorial(int(raw_value))
return HTTPStatus.OK, res
except ValueError:
return HTTPStatus.UNPROCESSABLE_ENTITY, -1

def fibonacci(n):
try:
if int(n) < 0:
return HTTPStatus.BAD_REQUEST, -1
a, b = 0, 1
for _ in range(int(n)):
a,b = b, a + b
return HTTPStatus.OK, b
except ValueError:
return HTTPStatus.UNPROCESSABLE_ENTITY, -1

def mean(data):
print("data", data)
# 1. проверяем что dict не пустой
if not data or "numbers" not in data or data["numbers"] == 'null':
return HTTPStatus.UNPROCESSABLE_ENTITY, -1

# 3. парсим строку
raw_value = data.get("numbers", "")
items = raw_value.split(",") if raw_value else []

if len(items) == 0:
return HTTPStatus.BAD_REQUEST, -1

# 4. приводим к float
fl_data = [float(x) for x in items if x]
res = sum(fl_data) / len(fl_data)
return HTTPStatus.OK, res

def routing(path, query_string, json_body):
parts = path.split("/") # ['', 'fibonacci', '10']
entity = parts[1] if len(parts) > 1 else ""
param = parts[2] if len(parts) > 2 else ""
params: dict[str, str] = {}
if query_string:
for pair in query_string.split("&"):
if not pair:
continue
if "=" in pair:
k, v = pair.split("=", 1)
params[k] = v
else:
params[pair] = ""
print("all: ", entity, param, params)

match entity:
case "fibonacci":
return fibonacci(param)
case "factorial":
return factorial(params)
case "mean":
return mean({ "numbers": json_body.replace("[", "").replace("]", "") })
case _:
return HTTPStatus.NOT_FOUND, -1

async def application(
scope: dict[str, Any],
receive: Callable[[], Awaitable[dict[str, Any]]],
send: Callable[[dict[str, Any]], Awaitable[None]],
):
"""
Args:
scope: Словарь с информацией о запросе
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь
print("scope------>", scope)
if scope["type"] == "lifespan":
# просто отвечаем "готово"
while True:
message = await receive()
if message["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
else:
req_body = b""
more = True
while more:
event = await receive()
if event["type"] != "http.request":
break
req_body += event.get("body", b"")
more = event.get("more_body", False)

status_code, res = routing(scope["path"], scope["query_string"].decode("utf-8"), req_body.decode())

if status_code != HTTPStatus.OK:
await send({
"type": "http.response.start",
"status": status_code,
"headers": [],
})
await send({
"type": "http.response.body",
"body": b"",
})
return

res_body = ('{"result": %d}' % res).encode()

await send({
"type": "http.response.start",
"status": HTTPStatus.OK,
"headers": [
(b"content-type", b"application/json; charset=utf-8"),
(b"content-length", str(len(res_body)).encode()),
],
})
await send({
"type": "http.response.body",
"body": res_body,
})

if __name__ == "__main__":
import uvicorn
Expand Down
23 changes: 23 additions & 0 deletions hw2/hw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.12 AS base

ARG PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=on \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=500

RUN apt-get update && apt-get install -y gcc
RUN python -m pip install --upgrade pip

WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt

COPY . ./

FROM base as local

EXPOSE 3000

CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "3000"]
38 changes: 38 additions & 0 deletions hw2/hw/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
version: "3.8"

services:
shop-api:
build:
context: .
dockerfile: ./Dockerfile
target: local
restart: always
ports:
- "3000:3000"

prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./settings/prometheus/:/etc/prometheus/
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
- "--web.console.templates=/usr/share/prometheus/consoles"
- "--web.enable-lifecycle"
restart: always

grafana:
image: grafana/grafana:latest
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-storage:/var/lib/grafana
restart: always

volumes:
grafana-storage:
5 changes: 5 additions & 0 deletions hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0
sqlalchemy>=2.0.0
pydantic>=2.0.0

# Зависимости для тестирования
pytest>=7.4.0
pytest-asyncio>=0.21.0
httpx>=0.27.2
Faker>=37.8.0

# Мониторинг
prometheus_client>=0.20.0
9 changes: 9 additions & 0 deletions hw2/hw/settings/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global:
scrape_interval: 10s
evaluation_interval: 10s

scrape_configs:
- job_name: shop-api
metrics_path: /metrics
static_configs:
- targets: ["shop-api:3000"]
Loading