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
155 changes: 154 additions & 1 deletion hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,57 @@
from typing import Any, Awaitable, Callable
from typing import Any, Awaitable, Callable, List
from http import HTTPStatus
from json import dumps, loads
from json.decoder import JSONDecodeError
from math import factorial

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

def mean(numbers: List[float]) -> float:
return sum(numbers) / len(numbers)

async def return_ok(send: Callable[[dict[str, Any]], Awaitable[None]],
payload_bytes: bytes) -> None:
#code 200
await send({
"type": "http.response.start",
"status": HTTPStatus.OK,
"headers": [(b"content-type", b"application/json")],
})
await send({"type": "http.response.body", "body": payload_bytes})

async def return_bad_request(send: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
#code 400
await send({
"type": "http.response.start",
"status": HTTPStatus.BAD_REQUEST,
"headers": [(b"content-type", b"text/plain")],
})
await send({"type": "http.response.body", "body": b"Bad Request"})

async def return_not_found(send: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
#code 404
await send({
"type": "http.response.start",
"status": HTTPStatus.NOT_FOUND,
"headers": [(b"content-type", b"text/plain")],
})
await send({"type": "http.response.body", "body": b"Not Found"})

async def return_unprocessable_entity(send: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
#code 422
await send({
"type": "http.response.start",
"status": HTTPStatus.UNPROCESSABLE_ENTITY,
"headers": [(b"content-type", b"text/plain")],
})
await send({"type": "http.response.body", "body": b"Unprocessable Entity"})


async def application(
Expand All @@ -14,6 +67,106 @@ async def application(
"""
# TODO: Ваша реализация здесь

scope_type = scope.get("type")


if scope_type == "lifespan":
while True:
event = await receive()
if event["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})
elif event["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
return

if scope_type != "http":
return

method = scope.get("method", "GET")
path = scope.get("path", "/")


if method == "GET":
if "fibonacci" in path:
parts = path.split("/")
if len(parts) == 3 and parts[:2] == ["", "fibonacci"]:
try:
n = int(parts[2])
except ValueError:
await return_unprocessable_entity(send)
return

if n < 0:
await return_bad_request(send)
return

result = fibonacci(n)
payload = dumps({"result": result}).encode()

await return_ok(send, payload)
return

if path == "/factorial":
query_string = scope["query_string"].decode()
parts = query_string.split("=")
if len(parts) == 2 and parts[0] == "n":
try:
n = int(parts[1])
except ValueError:
await return_unprocessable_entity(send)
return

if n < 0:
await return_bad_request(send)
return

result = factorial(n)
payload = dumps({"result": result}).encode()

await return_ok(send, payload)
return

await return_unprocessable_entity(send)
return

if path == "/mean":
body = b""
while True:
event = await receive()
if event["type"] == "http.request":
body += event.get("body", b"")
if not event.get("more_body", False):
break

try:
body = loads(body.decode())
except JSONDecodeError:
await return_unprocessable_entity(send)
return

if not isinstance(body, list):
await return_unprocessable_entity(send)
return

if len(body) == 0:
await return_bad_request(send)
return

if all(map(lambda x: isinstance(x, int) or isinstance(x, float), body)):
numbers = body
result = mean(numbers)
payload = dumps({"result": result}).encode()
await return_ok(send, payload)
return

await return_unprocessable_entity(send)
return

await return_not_found(send)
return


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

WORKDIR /app

COPY requirements.txt .

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

COPY . .

EXPOSE 8000

# Run the app with uvicorn
CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
Binary file added hw2/hw/dashboard1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hw2/hw/dashboard2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions hw2/hw/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
version: "3"

services:

local:
build:
context: .
dockerfile: ./Dockerfile
restart: always
ports:
- 8000:8000
depends_on:
- db

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

prometheus:
image: prom/prometheus
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

db:
image: postgres:15
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: shopdb
ports:
- 5432:5432
volumes:
- postgres_data:/var/lib/postgresql/data
- ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d shopdb -h localhost"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
23 changes: 23 additions & 0 deletions hw2/hw/migrations/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- Очистка старых таблиц
DROP TABLE IF EXISTS items_in_carts CASCADE;
DROP TABLE IF EXISTS carts CASCADE;
DROP TABLE IF EXISTS items CASCADE;

-- Создание таблиц заново
CREATE TABLE items (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(12,2) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE TABLE carts (
id SERIAL PRIMARY KEY
);

CREATE TABLE items_in_carts (
cart_id INTEGER NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
item_id INTEGER NOT NULL REFERENCES items(id),
quantity INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (cart_id, item_id)
);
34 changes: 34 additions & 0 deletions hw2/hw/random_requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import requests
import time
import json
import random

URL = "http://localhost:8000/" # Change this


r = requests.post(f"{URL}/item", json={"name": 'testing', 'price': 100})
id = json.loads(r.text)['id']

def send_requests():
N = random.randint(1, 10)
INTERVAL = 10 / N
for i in range(N):
try:
r = requests.get(f"{URL}/item/{id}")
print(f"[{i+1}/{N}] Status: {r.status_code}")
if random.random() < 0.5:
r = requests.get(f"{URL}/item/{id+1}")
print(f"[{i + 1}/{N}] Status: {r.status_code}")
if random.random() < 0.5:
r = requests.post(f"{URL}/abc")
print(f"[{i + 1}/{N}] Status: {r.status_code}")
except Exception as e:
print(f"[{i+1}/{N}] Error: {e}")
time.sleep(INTERVAL)

if __name__ == "__main__":
while True:
print("\n--- Sending batch of GET requests ---")
send_requests()
print("Waiting for next 10 seconds...")
time.sleep(10)
6 changes: 6 additions & 0 deletions hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0
prometheus-fastapi-instrumentator

sqlalchemy==2.0.25
psycopg2-binary==2.9.9
asyncpg
alembic

# Зависимости для тестирования
pytest>=7.4.0
Expand Down
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:8000
34 changes: 34 additions & 0 deletions hw2/hw/shop_api/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import ForeignKey, Numeric, Integer, Boolean, Text, UniqueConstraint
import os

DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://user:pass@db:5432/shopdb")

engine = create_async_engine(DATABASE_URL, echo=False)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)

class Base(DeclarativeBase):
pass

class Item(Base):
__tablename__ = "items"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(Text, nullable=False)
price: Mapped[float] = mapped_column(Numeric(12,2), nullable=False)
deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

class Cart(Base):
__tablename__ = "carts"

id: Mapped[int] = mapped_column(primary_key=True)

class ItemInCart(Base):
__tablename__ = "items_in_carts"

__table_args__ = (UniqueConstraint("cart_id", "item_id", name="uq_cart_item"),)

cart_id: Mapped[int] = mapped_column(ForeignKey("carts.id", ondelete="CASCADE"), primary_key=True)
item_id: Mapped[int] = mapped_column(ForeignKey("items.id"), primary_key=True)
quantity: Mapped[int] = mapped_column(Integer, nullable=False)
Loading
Loading