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
55 changes: 55 additions & 0 deletions .github/workflows/hw5-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

name: HW5 Tests

on:
push:
branches: [ main ]
paths: [ 'hw2/hw/**' ]
pull_request:
branches: [ main ]
paths: [ 'hw2/hw/**' ]

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

services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: shop_db
POSTGRES_USER: shop_user
POSTGRES_PASSWORD: shop_password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

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: Run tests with coverage
working-directory: hw2/hw
env:
DATABASE_URL: postgresql://shop_user:shop_password@localhost:5432/shop_db
PYTHONPATH: ${{ github.workspace }}/hw2/hw
run: |
pytest test_homework2.py --cov=shop_api --cov-report=term -v
26 changes: 26 additions & 0 deletions 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 postgresql-client && apt-get install -y gcc
RUN python -m pip install --upgrade pip

WORKDIR $APP_ROOT/src
COPY . ./

ENV VIRTUAL_ENV=$APP_ROOT/src/.venv \
PATH=$APP_ROOT/src/.venv/bin:$PATH

RUN pip install -r hw2/hw/requirements.txt

FROM base as local

EXPOSE 8080
EXPOSE 5432

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

local:
build:
context: .
dockerfile: ./Dockerfile
target: local
restart: always
ports:
- 8080:8080
environment:
- DATABASE_URL=postgresql://shop_user:shop_password@postgres:5432/shop_db
depends_on:
postgres:
condition: service_healthy

postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: shop_db
POSTGRES_USER: shop_user
POSTGRES_PASSWORD: shop_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- .hw2/hw/migrations/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

grafana:
image: grafana/grafana:latest
ports:
- 3000:3000
restart: always

prometheus:
image: prom/prometheus
volumes:
- ./hw2/hw/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

volumes:
prometheus-data:
postgres_data:
193 changes: 191 additions & 2 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import math
from http import HTTPStatus
from typing import Any, Awaitable, Callable


async def application(
scope: dict[str, Any],
receive: Callable[[], Awaitable[dict[str, Any]]],
Expand All @@ -12,7 +14,194 @@ async def application(
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь

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

if scope['type'] == 'http':
path = scope['path'].split("/")[1:]

match path[0]:
case "fibonacci":
await fibonacci(
scope = scope,
send = send,
)

case "factorial":
await factorial(
scope = scope,
send = send,
)

case "mean":
await mean(
receive=receive,
send = send,
)

case _:
await send_response(
send = send,
data = {"error": "Not available"},
status = HTTPStatus.NOT_FOUND,
)

async def fibonacci(
scope: dict[str, Any],
send: Callable[[dict[str, Any]], Awaitable[None]],
):
path = scope['path'].split("/")[1:]

try:
value = int(path[1])
except ValueError:
return await send_response(
send = send,
data = { "error": "path parameter is not an integer" },
status = HTTPStatus.UNPROCESSABLE_ENTITY,
)

if value < 0:
return await send_response(
send = send,
data = { "error": "path parameter can't be negative" },
status = HTTPStatus.BAD_REQUEST,
)
else:
return await send_response(
send = send,
data = { "result": _fibonacci(value) },
status = HTTPStatus.OK
)

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

async def factorial(
scope: dict[str, Any],
send: Callable[[dict[str, Any]], Awaitable[None]],
):
query_params: dict[str, Any] = await read_query_params(scope)

if "n" not in query_params:
return await send_response(
send = send,
data = { "error": "no query param with name \"n\""},
status = HTTPStatus.UNPROCESSABLE_ENTITY,
)

try:
n = int(query_params['n'])
except:
return await send_response(
send = send,
data = { "error": "invalid value of param \"n\""},
status = HTTPStatus.UNPROCESSABLE_ENTITY,
)

try:
return await send_response(
send = send,
data = { "result": math.factorial(n) },
status = HTTPStatus.OK,
)
except ValueError:
return await send_response(
send = send,
data = { "error": "value of \"n\" is negative"},
status = HTTPStatus.BAD_REQUEST,
)

async def mean(
receive: Callable[[], Awaitable[dict[str, Any]]],
send: Callable[[dict[str, Any]], Awaitable[None]],
):
try:
body = await read_body(receive=receive)
numbers_data = json.loads(body)
except json.JSONDecodeError:
return await send_response(
send = send,
data = {"error": "Invalid JSON"},
status = HTTPStatus.UNPROCESSABLE_ENTITY,
)

if not isinstance(numbers_data, list):
return await send_response(
send = send,
data = { "error": "numbers is not a list"},
status = HTTPStatus.UNPROCESSABLE_ENTITY,
)
elif len(numbers_data) == 0:
return await send_response(
send = send,
data = { "error": "numbers is empty"},
status = HTTPStatus.BAD_REQUEST,
)
else:
mean = sum(numbers_data) / len(numbers_data)
return await send_response(
send = send,
data = { "result": mean },
status = HTTPStatus.OK
)



async def read_body(
receive: Callable[[], Awaitable[dict[str, Any]]],
):
body = b''
more_body = True

while more_body:
message = await receive()
body += message.get('body', b'')
more_body = message.get('more_body', False)

return body.decode('utf-8')

async def read_query_params(
scope: dict[str, Any],
) -> dict[str, Any]:
query_string: str | None = scope.get("query_string", b"").decode()
params: dict[str, Any] = {}
if query_string:
for param in query_string.split("&"):
if "=" in param:
key, value = param.split("=", 1)
params[key] = value

return params

async def send_response(
send: Callable[[dict[str, Any]], Awaitable[None]],
data: dict[str, Any],
status: HTTPStatus,
):
await send({
"type": "http.response.start",
"status": status,
"headers": [[b"content-type", b"application/json"]]
})
await send({
"type": "http.response.body",
"body": json.dumps(data).encode(),
})
return


if __name__ == "__main__":
import uvicorn
Expand Down
Empty file added hw2/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions hw2/hw/migrations/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
DROP TABLE IF EXISTS cart_items CASCADE;
DROP TABLE IF EXISTS carts CASCADE;
DROP TABLE IF EXISTS items CASCADE;

CREATE TABLE items (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
deleted BOOLEAN DEFAULT FALSE
);

CREATE TABLE carts (
id SERIAL PRIMARY KEY
);

CREATE TABLE cart_items (
id SERIAL PRIMARY KEY,
cart_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0)
);
8 changes: 8 additions & 0 deletions hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0
sqlalchemy==2.0.25
psycopg2-binary==2.9.9
python-dotenv==1.0.0

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

prometheus-fastapi-instrumentator
10 changes: 10 additions & 0 deletions hw2/hw/settings/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global:
scrape_interval: 10s
evaluation_interval: 10s

scrape_configs:
- job_name: demo-service-local
metrics_path: /metrics
static_configs:
- targets:
- local:8080
Loading
Loading