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
121 changes: 120 additions & 1 deletion hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
from typing import Any, Awaitable, Callable
import json
import math
from urllib.parse import parse_qs


async def _send_json(send: Callable[[dict[str, Any]], Awaitable[None]], status: int, payload: dict[str, Any] | None = None) -> None:
body_bytes = json.dumps(payload or {}).encode()
await send(
{
"type": "http.response.start",
"status": status,
"headers": [
(b"content-type", b"application/json; charset=utf-8"),
],
}
)
await send({"type": "http.response.body", "body": body_bytes})


def _parse_int(value: str | None) -> tuple[bool, int | None]:
if value is None:
return False, None
try:
return True, int(value)
except Exception:
return False, None


def _fibonacci(n: int) -> int:
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return b


async def application(
Expand All @@ -12,8 +45,94 @@ async def application(
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь
if scope.get("type") != "http":
await _send_json(send, 404, {"detail": "Not Found"})
return

method: str = scope.get("method", "GET")
path: str = scope.get("path", "")

if method != "GET":
await _send_json(send, 404, {"detail": "Not Found"})
return

# /factorial?n=<int>
if path == "/factorial":
query_raw: bytes = scope.get("query_string", b"")
query_params = parse_qs(query_raw.decode())
values = query_params.get("n")
if not values or len(values) != 1:
await _send_json(send, 422, {"detail": "Invalid query params"})
return
ok, n = _parse_int(values[0])
if not ok:
await _send_json(send, 422, {"detail": "Parameter n must be integer"})
return
if n is None or n < 0:
await _send_json(send, 400, {"detail": "Invalid value for n, must be non-negative"})
return
result = math.factorial(n)
await _send_json(send, 200, {"result": result})
return

# /fibonacci/{n}
if path.startswith("/fibonacci/") and path.count("/") == 2:
n_str = path.split("/")[-1]
ok, n = _parse_int(n_str)
if not ok:
await _send_json(send, 422, {"detail": "Path parameter n must be integer"})
return
if n is None or n < 0:
await _send_json(send, 400, {"detail": "Invalid value for n, must be non-negative"})
return
result = _fibonacci(n)
await _send_json(send, 200, {"result": result})
return

if path == "/mean":
body = b""
while True:
message = await receive()
body += message.get("body", b"")
if not message.get("more_body", False):
break

if not body:
await _send_json(send, 422, {"detail": "Request body required"})
return

try:
data = json.loads(body)
except Exception:
await _send_json(send, 422, {"detail": "Invalid JSON"})
return

if not isinstance(data, list):
await _send_json(send, 422, {"detail": "Body must be a JSON array"})
return

if len(data) == 0:
await _send_json(send, 400, {"detail": "Array must be non-empty"})
return

def _is_number(x: Any) -> bool:
# bool is a subclass of int -> exclude
return isinstance(x, (int, float)) and not isinstance(x, bool)

if not all(_is_number(x) for x in data):
await _send_json(send, 422, {"detail": "Array must contain only numbers"})
return

nums = [float(x) for x in data]
mean_value = sum(nums) / len(nums)
await _send_json(send, 200, {"result": mean_value})
return

# default 404
await _send_json(send, 404, {"detail": "Not Found"})


if __name__ == "__main__":
import uvicorn

uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True)
19 changes: 19 additions & 0 deletions hw2/hw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY shop_api/ ./shop_api/

RUN adduser --disabled-password --gecos '' appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=2)"

CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
80 changes: 80 additions & 0 deletions hw2/hw/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
services:
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: shop
MYSQL_USER: shop
MYSQL_PASSWORD: shop
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -proot || exit 1"]
interval: 10s
timeout: 5s
retries: 10
volumes:
- mysql_data:/var/lib/mysql
networks:
- monitoring

shop-api:
build: .
ports:
- "8000:8000"
environment:
- PYTHONUNBUFFERED=1
- DATABASE_URL=mysql+pymysql://shop:shop@db:3306/shop
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- monitoring

prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
networks:
- monitoring

grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
depends_on:
- prometheus
networks:
- monitoring

networks:
monitoring:
driver: bridge

volumes:
prometheus_data:
grafana_data:
mysql_data:
12 changes: 12 additions & 0 deletions hw2/hw/grafana/provisioning/dashboards/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: 1

providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards
Loading