Skip to content
Closed
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
39 changes: 39 additions & 0 deletions .github/workflows/hw5-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: "HW5 Tests"

# Запускаем тесты при изменении файлов в hw2/hw/
on:
pull_request:
branches: [ main ]
paths: [ 'hw2/hw/**' ]
push:
branches: [ main ]
paths: [ 'hw2/hw/**' ]

jobs:
test-hw2:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.13"]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
working-directory: hw2/hw
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Tests coverage report
working-directory: hw2/hw
env:
PYTHONPATH: ${{ github.workspace }}/hw2/hw
run: |
pytest --cov
129 changes: 124 additions & 5 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,138 @@
from typing import Any, Awaitable, Callable
from http import HTTPStatus
from typing import (
Any,
Awaitable,
Callable,
)
import json as jsonlib
from urllib.parse import parse_qs


async def application(
scope: dict[str, Any],
receive: Callable[[], Awaitable[dict[str, Any]]],
send: Callable[[dict[str, Any]], Awaitable[None]],
scope: dict[str, Any],
receive: Callable[[], Awaitable[dict[str, Any]]],
send: Callable[[dict[str, Any]], Awaitable[None]],
):
"""
Args:
scope: Словарь с информацией о запросе
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь
# assert scope['type'] == 'http'
method: str = scope.get("method", "GET")
path: str = scope.get("path", "/")
query_string: bytes = scope.get("query_string", b"")

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

# Route handling
if method == "GET" and path == "/factorial":
params = parse_qs(query_string.decode("utf-8")) if query_string else {}
raw_n = params.get("n", [None])[0]
if raw_n is None or raw_n == "":
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
try:
n = int(raw_n)
except (TypeError, ValueError):
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
if n < 0:
await send_json(HTTPStatus.BAD_REQUEST)
return
result = 1
for i in range(2, n + 1):
result *= i
await send_json(HTTPStatus.OK, {"result": result})
return

if method == "GET" and path.startswith("/fibonacci"):
parts = path.split("/")
raw_n = parts[2] if len(parts) > 2 and parts[2] != "" else None
if raw_n is None:
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
try:
n = int(raw_n)
except (TypeError, ValueError):
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
if n < 0:
await send_json(HTTPStatus.BAD_REQUEST)
return
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
await send_json(HTTPStatus.OK, {"result": a})
return

if method == "GET" and path == "/mean":
body = b""
more_body = True
while more_body:
message = await receive()
msg_type = message.get("type")
if msg_type == "http.disconnect":
break
if msg_type != "http.request":
continue
body += message.get("body", b"")
more_body = message.get("more_body", False)
if body:
try:
data = jsonlib.loads(body.decode("utf-8"))
except jsonlib.JSONDecodeError:
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
if not isinstance(data, list):
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
if len(data) == 0:
await send_json(HTTPStatus.BAD_REQUEST)
return
try:
numbers = [float(x) for x in data]
except (TypeError, ValueError):
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
mean_value = sum(numbers) / len(numbers)
await send_json(HTTPStatus.OK, {"result": mean_value})
return

params = parse_qs(query_string.decode("utf-8")) if query_string else {}
numbers_param = params.get("numbers", [None])[0]
if numbers_param is None:
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
items = [p for p in numbers_param.split(',') if p != ""]
if len(items) == 0:
await send_json(HTTPStatus.BAD_REQUEST)
return
try:
numbers = [float(x) for x in items]
except (TypeError, ValueError):
await send_json(HTTPStatus.UNPROCESSABLE_ENTITY)
return
mean_value = sum(numbers) / len(numbers)
await send_json(HTTPStatus.OK, {"result": mean_value})
return

await send_json(HTTPStatus.NOT_FOUND)


if __name__ == "__main__":
import uvicorn

uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True)
26 changes: 26 additions & 0 deletions hw2/hw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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 ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Keep source under /app/hw so the package name `hw` is importable
RUN mkdir -p /app/hw
COPY . /app/hw

# Ensure both top-level `hw` and `shop_api` are importable
ENV PYTHONPATH=/app:/app/hw

FROM base as local
EXPOSE 8080
CMD ["uvicorn", "shop_api.main:app", "--port", "8080", "--host", "0.0.0.0"]
36 changes: 36 additions & 0 deletions hw2/hw/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
services:

local:
build:
context: .
dockerfile: ./Dockerfile
target: local
restart: always
ports:
- 8080:8080

grafana:
depends_on:
- prometheus
volumes:
- ./settings/grafana/provisioning/:/etc/grafana/provisioning/
- ./settings/grafana/dashboards/:/var/lib/grafana/dashboards/
image: grafana/grafana:latest
ports:
- 3000:3000
restart: always

prometheus:
image: prom/prometheus
depends_on:
- local
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"
ports:
- 9090:9090
restart: always
3 changes: 3 additions & 0 deletions hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0
prometheus_fastapi_instrumentator
sqlalchemy>=2.0.36,<3
pytest-cov

# Зависимости для тестирования
pytest>=7.4.0
Expand Down
Loading