From 635fd454a6dd3fa44550b5d80f13c888a65f3459 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Fri, 26 Sep 2025 14:46:49 +0300 Subject: [PATCH 01/14] Implement application logic for factorial, fibonacci, and mean endpoints; add JSON response handling --- hw1/app.py | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++- hw1/utils.py | 19 +++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 hw1/utils.py diff --git a/hw1/app.py b/hw1/app.py index 6107b870..0322e89c 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,5 +1,8 @@ from typing import Any, Awaitable, Callable +from urllib.parse import parse_qs +import json +from utils import fibonacci, factorial, mean async def application( scope: dict[str, Any], @@ -12,7 +15,92 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + if scope.get("type") == "lifespan": + while True: + message = await receive() + if message.get("type") == "lifespan.startup": + await send({"type": "lifespan.startup.complete"}) + elif message.get("type") == "lifespan.shutdown": + await send({"type": "lifespan.shutdown.complete"}) + return + + path = scope["path"] + query = parse_qs(scope["query_string"].decode()) + method = scope.get("method", "GET") + + def json_response(data: dict, status_code: int = 200): + return json.dumps(data).encode(), b"application/json", status_code + + if path == "/factorial" and method == "GET": + n_raw = query.get("n", [None])[0] + if not n_raw: + body, content_type, status = json_response({"detail": "Missing or empty 'n' parameter"}, 422) + else: + try: + n = int(n_raw) + except Exception: + body, content_type, status = json_response({"detail": "'n' must be an integer"}, 422) + else: + if n < 0: + body, content_type, status = json_response({"detail": "'n' must be >= 0"}, 400) + else: + body, content_type, status = json_response({"result": factorial(n)}) + + elif path.startswith("/fibonacci") and method == "GET": + parts = path.split("/") + n_raw = parts[2] if len(parts) > 2 else "" + if not n_raw: + body, content_type, status = json_response({"detail": "Missing or empty n parameter"}, 422) + else: + try: + n = int(n_raw) + except Exception: + body, content_type, status = json_response({"detail": "n must be integer"}, 422) + else: + if n < 0: + body, content_type, status = json_response({"detail": "n must be >= 0"}, 400) + else: + body, content_type, status = json_response({"result": fibonacci(n)}) + + elif path == "/mean" and method == "GET": + raw_body = scope.get("body", b"") + while True: + message = await receive() + if message["type"] == "http.request": + raw_body += message.get("body", b"") + if not message.get("more_body", False): + break + try: + body_data = json.loads(raw_body.decode()) if raw_body else None + except Exception: + body_data = None + + if body_data is None: + body, content_type, status = json_response({"detail": "Missing or invalid body"}, 422) + elif not isinstance(body_data, list) or not body_data: + body, content_type, status = json_response({"detail": "Body must be non-empty list"}, 400) + else: + try: + numbers = [float(x) for x in body_data] + except Exception: + body, content_type, status = json_response({"detail": "All elements must be numbers"}, 400) + else: + body, content_type, status = json_response({"result": mean(",".join(str(x) for x in numbers))}) + + else: + body, content_type, status = json_response({"detail": "Not Found"}, 404) + + await send({ + "type": "http.response.start", + "status": status, + "headers": [[b"content-type", content_type]] + }) + + await send({ + "type": "http.response.body", + "body": body + }) + if __name__ == "__main__": import uvicorn diff --git a/hw1/utils.py b/hw1/utils.py new file mode 100644 index 00000000..de73f266 --- /dev/null +++ b/hw1/utils.py @@ -0,0 +1,19 @@ +def fibonacci(n): + n = int(n) + if n <= 0: + return [] + seq = [0, 1] + for _ in range(2, n): + seq.append(seq[-1] + seq[-2]) + return seq[:n] + +def factorial(n): + n = int(n) + result = 1 + for i in range(2, n+1): + result *= i + return result + +def mean(numbers): + numbers = [float(x) for x in numbers.split(",")] + return sum(numbers) / len(numbers) \ No newline at end of file From 8e8e92c74ca8ee9cc520fa7e773b8c1ef37e9439 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sat, 4 Oct 2025 16:29:51 +0300 Subject: [PATCH 02/14] HW2 ready --- hw2/hw/shop_api/main.py | 5 ++ hw2/hw/shop_api/models/__init__.py | 0 hw2/hw/shop_api/routers/__init__.py | 0 hw2/hw/shop_api/routers/carts.py | 58 +++++++++++++++++++++ hw2/hw/shop_api/routers/items.py | 81 +++++++++++++++++++++++++++++ hw2/hw/shop_api/schemas/__init__.py | 0 hw2/hw/shop_api/schemas/cart.py | 14 +++++ hw2/hw/shop_api/schemas/item.py | 18 +++++++ hw2/hw/shop_api/storage/__init__.py | 0 hw2/hw/shop_api/storage/memory.py | 9 ++++ hw2/hw/shop_api/utils/cart_utils.py | 20 +++++++ 11 files changed, 205 insertions(+) create mode 100644 hw2/hw/shop_api/models/__init__.py create mode 100644 hw2/hw/shop_api/routers/__init__.py create mode 100644 hw2/hw/shop_api/routers/carts.py create mode 100644 hw2/hw/shop_api/routers/items.py create mode 100644 hw2/hw/shop_api/schemas/__init__.py create mode 100644 hw2/hw/shop_api/schemas/cart.py create mode 100644 hw2/hw/shop_api/schemas/item.py create mode 100644 hw2/hw/shop_api/storage/__init__.py create mode 100644 hw2/hw/shop_api/storage/memory.py create mode 100644 hw2/hw/shop_api/utils/cart_utils.py diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..d3b9a9aa 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,8 @@ from fastapi import FastAPI +from shop_api.routers import items, carts + app = FastAPI(title="Shop API") + +app.include_router(items.router) +app.include_router(carts.router) \ No newline at end of file diff --git a/hw2/hw/shop_api/models/__init__.py b/hw2/hw/shop_api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw2/hw/shop_api/routers/__init__.py b/hw2/hw/shop_api/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw2/hw/shop_api/routers/carts.py b/hw2/hw/shop_api/routers/carts.py new file mode 100644 index 00000000..39a179be --- /dev/null +++ b/hw2/hw/shop_api/routers/carts.py @@ -0,0 +1,58 @@ +from fastapi import APIRouter, HTTPException, Query, Path, Response +from shop_api.schemas.cart import Cart +from shop_api.utils.cart_utils import compute_cart +from shop_api.storage.memory import _carts, _items, _lock, _next_cart_id +from typing import Optional, List + + +router = APIRouter(prefix="/cart", tags=["carts"]) + +@router.post("/", status_code=201) +def create_cart(response: Response): + global _next_cart_id + with _lock: + cid = _next_cart_id + _next_cart_id += 1 + _carts[cid] = {} + response.headers["Location"] = f"/cart/{cid}" + return {"id": cid} + +@router.get("/{id}", response_model=Cart) +def get_cart(id: int = Path(..., gt=0)): + try: + return compute_cart(id) + except KeyError: + raise HTTPException(status_code=404, detail="cart not found") + +@router.get("/", response_model=List[Cart]) +def list_carts( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + min_quantity: Optional[int] = Query(None, ge=0), + max_quantity: Optional[int] = Query(None, ge=0), +): + carts = [] + for cid in sorted(_carts.keys()): + cart = compute_cart(cid) + total_qty = sum(i.quantity for i in cart.items) + if min_quantity is not None and total_qty < min_quantity: + continue + if max_quantity is not None and total_qty > max_quantity: + continue + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + carts.append(cart) + return carts[offset: offset + limit] + +@router.post("/{cart_id}/add/{item_id}", response_model=Cart) +def add_item(cart_id: int, item_id: int): + if cart_id not in _carts: + raise HTTPException(status_code=404, detail="cart not found") + if item_id not in _items: + raise HTTPException(status_code=404, detail="item not found") + _carts[cart_id][item_id] = _carts[cart_id].get(item_id, 0) + 1 + return compute_cart(cart_id) diff --git a/hw2/hw/shop_api/routers/items.py b/hw2/hw/shop_api/routers/items.py new file mode 100644 index 00000000..3942bd8b --- /dev/null +++ b/hw2/hw/shop_api/routers/items.py @@ -0,0 +1,81 @@ +from fastapi import APIRouter, HTTPException, Query, Response +from shop_api.schemas.item import Item, ItemCreate +from shop_api.storage.memory import _items, _lock, _next_item_id +from typing import Optional, List + + +router = APIRouter(prefix="/item", tags=["items"]) + +@router.post("/", response_model=Item, status_code=201) +def create_item(item: ItemCreate): + global _next_item_id + with _lock: + iid = _next_item_id + _next_item_id += 1 + new_item = Item(id=iid, name=item.name, price=item.price) + _items[iid] = new_item + return new_item + +@router.get("/{id}", response_model=Item) +def get_item(id: int): + item = _items.get(id) + if not item or item.deleted: + raise HTTPException(status_code=404, detail="item not found") + return item + +@router.get("/", response_model=List[Item]) +def list_items( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + show_deleted: bool = Query(False), +): + items = [] + for it in _items.values(): + if not show_deleted and it.deleted: + continue + if min_price is not None and it.price < min_price: + continue + if max_price is not None and it.price > max_price: + continue + items.append(it) + return items[offset: offset + limit] + +@router.put("/{id}", response_model=Item) +def replace_item(id: int, item: ItemCreate): + if id not in _items: + raise HTTPException(status_code=404, detail="item not found") + existing = _items[id] + existing.name = item.name + existing.price = item.price + _items[id] = existing + return existing + +@router.patch("/{id}", response_model=Item) +def patch_item(id: int, patch: dict): + if id not in _items: + raise HTTPException(status_code=404, detail="item not found") + item = _items[id] + if item.deleted: + return Response(status_code=304) + allowed_keys = {"name", "price"} + if not set(patch.keys()).issubset(allowed_keys): + raise HTTPException(status_code=422) + if "price" in patch: + price = patch["price"] + if price is not None and price < 0: + raise HTTPException(status_code=422) + item.price = price + if "name" in patch: + item.name = patch["name"] + _items[id] = item + return item + +@router.delete("/{id}") +def delete_item(id: int): + item = _items.get(id) + if not item: + return {"status": "ok"} + item.deleted = True + return {"status": "ok"} \ No newline at end of file diff --git a/hw2/hw/shop_api/schemas/__init__.py b/hw2/hw/shop_api/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw2/hw/shop_api/schemas/cart.py b/hw2/hw/shop_api/schemas/cart.py new file mode 100644 index 00000000..4e3ed90c --- /dev/null +++ b/hw2/hw/shop_api/schemas/cart.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel +from typing import List + + +class CartItem(BaseModel): + id: int + name: str + quantity: int + available: bool + +class Cart(BaseModel): + id: int + items: List[CartItem] + price: float \ No newline at end of file diff --git a/hw2/hw/shop_api/schemas/item.py b/hw2/hw/shop_api/schemas/item.py new file mode 100644 index 00000000..53021cde --- /dev/null +++ b/hw2/hw/shop_api/schemas/item.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class ItemBase(BaseModel): + name: str + price: float = Field(..., ge=0) + +class ItemCreate(ItemBase): + pass + +class Item(ItemBase): + id: int + deleted: bool = False + +class ItemPatch(BaseModel): + name: Optional[str] + price: Optional[float] \ No newline at end of file diff --git a/hw2/hw/shop_api/storage/__init__.py b/hw2/hw/shop_api/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw2/hw/shop_api/storage/memory.py b/hw2/hw/shop_api/storage/memory.py new file mode 100644 index 00000000..e769f81d --- /dev/null +++ b/hw2/hw/shop_api/storage/memory.py @@ -0,0 +1,9 @@ +from typing import Dict +from threading import Lock +from shop_api.schemas.item import Item + +_items: Dict[int, Item] = {} +_carts: Dict[int, Dict[int, int]] = {} +_next_item_id = 1 +_next_cart_id = 1 +_lock = Lock() diff --git a/hw2/hw/shop_api/utils/cart_utils.py b/hw2/hw/shop_api/utils/cart_utils.py new file mode 100644 index 00000000..b0c65c2e --- /dev/null +++ b/hw2/hw/shop_api/utils/cart_utils.py @@ -0,0 +1,20 @@ +from shop_api.schemas.cart import Cart, CartItem +from shop_api.storage.memory import _carts, _items + + +def compute_cart(cart_id: int) -> Cart: + if cart_id not in _carts: + raise KeyError + bag = _carts[cart_id] + items_out = [] + total = 0.0 + for iid, qty in bag.items(): + item = _items.get(iid) + if item is None: + name, available = "", False + else: + name, available = item.name, not item.deleted + items_out.append(CartItem(id=iid, name=name, quantity=qty, available=available)) + if item and not item.deleted: + total += item.price * qty + return Cart(id=cart_id, items=items_out, price=total) \ No newline at end of file From 22f54832f22e2c29ab265cb8afffd76318539393 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sat, 4 Oct 2025 16:55:45 +0300 Subject: [PATCH 03/14] Additional exercise --- hw2/hw/shop_api/main.py | 5 +++-- hw2/hw/shop_api/routers/chat.py | 30 ++++++++++++++++++++++++++++++ hw2/hw/websocket_test.py | 13 +++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 hw2/hw/shop_api/routers/chat.py create mode 100644 hw2/hw/websocket_test.py diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index d3b9a9aa..952e76fc 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,8 +1,9 @@ from fastapi import FastAPI -from shop_api.routers import items, carts +from shop_api.routers import items, carts, chat app = FastAPI(title="Shop API") app.include_router(items.router) -app.include_router(carts.router) \ No newline at end of file +app.include_router(carts.router) +app.include_router(chat.router) \ No newline at end of file diff --git a/hw2/hw/shop_api/routers/chat.py b/hw2/hw/shop_api/routers/chat.py new file mode 100644 index 00000000..0dcf6da9 --- /dev/null +++ b/hw2/hw/shop_api/routers/chat.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from collections import defaultdict +import random +import string +from typing import Dict, List + +router = APIRouter(prefix="/chat", tags=["chat"]) + +chat_rooms: Dict[str, List[WebSocket]] = defaultdict(list) +usernames: Dict[WebSocket, str] = {} + +def random_username() -> str: + return ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + +@router.websocket("/{chat_name}") +async def websocket_chat(websocket: WebSocket, chat_name: str): + await websocket.accept() + username = random_username() + usernames[websocket] = username + chat_rooms[chat_name].append(websocket) + try: + while True: + data = await websocket.receive_text() + message = f"{username} :: {data}" + for ws in chat_rooms[chat_name]: + if ws != websocket: + await ws.send_text(message) + except WebSocketDisconnect: + chat_rooms[chat_name].remove(websocket) + del usernames[websocket] diff --git a/hw2/hw/websocket_test.py b/hw2/hw/websocket_test.py new file mode 100644 index 00000000..d2d81fd2 --- /dev/null +++ b/hw2/hw/websocket_test.py @@ -0,0 +1,13 @@ +from fastapi.testclient import TestClient +from shop_api.main import app + +client = TestClient(app) + + +def test_chat_broadcast(): + with client.websocket_connect("/chat/testroom") as ws1, client.websocket_connect("/chat/testroom") as ws2: + ws1.send_text("Hello world!") + message = ws2.receive_text() + + assert "Hello world!" in message + assert "::" in message \ No newline at end of file From c33346d9ecd891f333e1cc16a7b4546c2485a4e6 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sat, 4 Oct 2025 17:15:10 +0300 Subject: [PATCH 04/14] Issolation room test --- hw2/hw/websocket_test.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/hw2/hw/websocket_test.py b/hw2/hw/websocket_test.py index d2d81fd2..2f14a519 100644 --- a/hw2/hw/websocket_test.py +++ b/hw2/hw/websocket_test.py @@ -1,13 +1,40 @@ from fastapi.testclient import TestClient from shop_api.main import app +import threading client = TestClient(app) def test_chat_broadcast(): - with client.websocket_connect("/chat/testroom") as ws1, client.websocket_connect("/chat/testroom") as ws2: + with ( + client.websocket_connect("/chat/testroom") as ws1, + client.websocket_connect("/chat/testroom") as ws2 + ): ws1.send_text("Hello world!") message = ws2.receive_text() assert "Hello world!" in message - assert "::" in message \ No newline at end of file + assert "::" in message + + +def test_chat_isolation_between_rooms(): + with ( + client.websocket_connect("/chat/testroom") as ws1, + client.websocket_connect("/chat/anotherroom") as ws2 + ): + ws1.send_text("Message to room1") + + received = [] + + def try_receive(): + try: + msg = ws2.receive_text() + received.append(msg) + except Exception: + pass + + t = threading.Thread(target=try_receive, daemon=True) + t.start() + t.join(timeout=0.5) + + assert not received From b317e873dd5b31e555cea0499c6aa0b4cd3c5d3f Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 12 Oct 2025 16:53:06 +0300 Subject: [PATCH 05/14] HW3 ready --- hw3/Dockerfile | 13 + hw3/docker-compose.yml | 37 ++ .../provisioning/dashboards/dashboard.yml | 11 + .../shop_api_business_dashboard.json | 334 ++++++++++++++ .../dashboards/shop_api_dashboard.json | 408 ++++++++++++++++++ .../provisioning/datasources/datasource.yml | 11 + hw3/prometheus/prometheus.yml | 8 + hw3/requirements.txt | 4 + hw3/shop_api/__init__.py | 0 hw3/shop_api/main.py | 11 + hw3/shop_api/metrics.py | 8 + hw3/shop_api/models/__init__.py | 0 hw3/shop_api/routers/__init__.py | 0 hw3/shop_api/routers/carts.py | 60 +++ hw3/shop_api/routers/chat.py | 39 ++ hw3/shop_api/routers/items.py | 83 ++++ hw3/shop_api/schemas/__init__.py | 0 hw3/shop_api/schemas/cart.py | 14 + hw3/shop_api/schemas/item.py | 18 + hw3/shop_api/storage/__init__.py | 0 hw3/shop_api/storage/memory.py | 9 + hw3/shop_api/utils/cart_utils.py | 20 + hw3/test.py | 102 +++++ 23 files changed, 1190 insertions(+) create mode 100644 hw3/Dockerfile create mode 100644 hw3/docker-compose.yml create mode 100644 hw3/grafana/provisioning/dashboards/dashboard.yml create mode 100644 hw3/grafana/provisioning/dashboards/shop_api_business_dashboard.json create mode 100644 hw3/grafana/provisioning/dashboards/shop_api_dashboard.json create mode 100644 hw3/grafana/provisioning/datasources/datasource.yml create mode 100644 hw3/prometheus/prometheus.yml create mode 100644 hw3/requirements.txt create mode 100644 hw3/shop_api/__init__.py create mode 100644 hw3/shop_api/main.py create mode 100644 hw3/shop_api/metrics.py create mode 100644 hw3/shop_api/models/__init__.py create mode 100644 hw3/shop_api/routers/__init__.py create mode 100644 hw3/shop_api/routers/carts.py create mode 100644 hw3/shop_api/routers/chat.py create mode 100644 hw3/shop_api/routers/items.py create mode 100644 hw3/shop_api/schemas/__init__.py create mode 100644 hw3/shop_api/schemas/cart.py create mode 100644 hw3/shop_api/schemas/item.py create mode 100644 hw3/shop_api/storage/__init__.py create mode 100644 hw3/shop_api/storage/memory.py create mode 100644 hw3/shop_api/utils/cart_utils.py create mode 100644 hw3/test.py diff --git a/hw3/Dockerfile b/hw3/Dockerfile new file mode 100644 index 00000000..f08e675f --- /dev/null +++ b/hw3/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/hw3/docker-compose.yml b/hw3/docker-compose.yml new file mode 100644 index 00000000..695d209d --- /dev/null +++ b/hw3/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3.9" + +services: + app: + build: . + container_name: shop_api + ports: + - "8000:8000" + networks: + - shopnet + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + networks: + - shopnet + + grafana: + image: grafana/grafana:latest + container_name: grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_SECURITY_ADMIN_USER=admin + ports: + - "3000:3000" + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning + networks: + - shopnet + +networks: + shopnet: + driver: bridge diff --git a/hw3/grafana/provisioning/dashboards/dashboard.yml b/hw3/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 00000000..472a2279 --- /dev/null +++ b/hw3/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "Shop API Technical" + folder: "Tech" + options: + path: /etc/grafana/provisioning/dashboards + - name: "Shop API Business" + folder: "Business" + options: + path: /etc/grafana/provisioning/dashboards diff --git a/hw3/grafana/provisioning/dashboards/shop_api_business_dashboard.json b/hw3/grafana/provisioning/dashboards/shop_api_business_dashboard.json new file mode 100644 index 00000000..794d4643 --- /dev/null +++ b/hw3/grafana/provisioning/dashboards/shop_api_business_dashboard.json @@ -0,0 +1,334 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 15, + "x": 0, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "editorMode": "code", + "expr": "rate(shop_carts_created_total[1m])", + "legendFormat": "Carts per minute", + "range": true, + "refId": "A" + } + ], + "title": "Carts Created per Minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 15, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "count(shop_ws_connections)", + "refId": "A" + } + ], + "title": "Active Chat Rooms", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 15, + "x": 0, + "y": 6 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "sum(rate(shop_ws_messages_total[1m])) by (chat_name)", + "legendFormat": "{{chat_name}}", + "refId": "A" + } + ], + "title": "Chat Messages per Minute", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 15, + "y": 6 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "sum(shop_items_total)", + "refId": "A" + } + ], + "title": "Total Items in Stock", + "type": "stat" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "timepicker": {}, + "timezone": "browser", + "title": "Business Metrics", + "uid": "440d983f-0b3d-4579-b85d-3ee2dad1babd", + "version": 2 +} \ No newline at end of file diff --git a/hw3/grafana/provisioning/dashboards/shop_api_dashboard.json b/hw3/grafana/provisioning/dashboards/shop_api_dashboard.json new file mode 100644 index 00000000..5779d8e0 --- /dev/null +++ b/hw3/grafana/provisioning/dashboards/shop_api_dashboard.json @@ -0,0 +1,408 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(http_requests_total[1m])) by (method, handler)", + "legendFormat": "{{method}} {{handler}}", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Requests per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p95", + "refId": "A" + } + ], + "title": "Request Duration (p95)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "shop_ws_connections", + "legendFormat": "{{chat_name}}", + "refId": "A" + } + ], + "title": "Active WebSocket Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "increase(shop_ws_messages_total[5m])", + "legendFormat": "{{chat_name}}", + "refId": "A" + } + ], + "title": "WebSocket Messages Sent", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "timepicker": {}, + "timezone": "browser", + "title": "Shop API Dashboard", + "uid": "ecd408fb-c57c-4ba2-8452-260e76f954ed", + "version": 3 +} \ No newline at end of file diff --git a/hw3/grafana/provisioning/datasources/datasource.yml b/hw3/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 00000000..7285dbe4 --- /dev/null +++ b/hw3/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + orgId: 1 + uid: prometheus_ds + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/hw3/prometheus/prometheus.yml b/hw3/prometheus/prometheus.yml new file mode 100644 index 00000000..046265e3 --- /dev/null +++ b/hw3/prometheus/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: 'shop_api' + static_configs: + - targets: ['app:8000'] + metrics_path: /metrics diff --git a/hw3/requirements.txt b/hw3/requirements.txt new file mode 100644 index 00000000..415c999e --- /dev/null +++ b/hw3/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn[standard] +prometheus-fastapi-instrumentator +pydantic \ No newline at end of file diff --git a/hw3/shop_api/__init__.py b/hw3/shop_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/shop_api/main.py b/hw3/shop_api/main.py new file mode 100644 index 00000000..030e07d8 --- /dev/null +++ b/hw3/shop_api/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from shop_api.routers import items, carts, chat +from prometheus_fastapi_instrumentator import Instrumentator + + +app = FastAPI(title="Shop API") +Instrumentator().instrument(app).expose(app) + +app.include_router(items.router) +app.include_router(carts.router) +app.include_router(chat.router) \ No newline at end of file diff --git a/hw3/shop_api/metrics.py b/hw3/shop_api/metrics.py new file mode 100644 index 00000000..0b18aff2 --- /dev/null +++ b/hw3/shop_api/metrics.py @@ -0,0 +1,8 @@ +from prometheus_client import Counter, Gauge + +carts_created_counter = Counter( + "shop_carts_created_total", "Total number of carts created" +) +items_in_stock_gauge = Gauge( + "shop_items_total", "Current number of non-deleted items in stock" +) diff --git a/hw3/shop_api/models/__init__.py b/hw3/shop_api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/shop_api/routers/__init__.py b/hw3/shop_api/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/shop_api/routers/carts.py b/hw3/shop_api/routers/carts.py new file mode 100644 index 00000000..37076862 --- /dev/null +++ b/hw3/shop_api/routers/carts.py @@ -0,0 +1,60 @@ +from fastapi import APIRouter, HTTPException, Query, Path, Response +from shop_api.schemas.cart import Cart +from shop_api.utils.cart_utils import compute_cart +from shop_api.metrics import carts_created_counter +from shop_api.storage.memory import _carts, _items, _lock, _next_cart_id +from typing import Optional, List + + +router = APIRouter(prefix="/cart", tags=["carts"]) + +@router.post("/", status_code=201) +def create_cart(response: Response): + global _next_cart_id + with _lock: + cid = _next_cart_id + _next_cart_id += 1 + _carts[cid] = {} + response.headers["Location"] = f"/cart/{cid}" + carts_created_counter.inc() + return {"id": cid} + +@router.get("/{id}", response_model=Cart) +def get_cart(id: int = Path(..., gt=0)): + try: + return compute_cart(id) + except KeyError: + raise HTTPException(status_code=404, detail="cart not found") + +@router.get("/", response_model=List[Cart]) +def list_carts( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + min_quantity: Optional[int] = Query(None, ge=0), + max_quantity: Optional[int] = Query(None, ge=0), +): + carts = [] + for cid in sorted(_carts.keys()): + cart = compute_cart(cid) + total_qty = sum(i.quantity for i in cart.items) + if min_quantity is not None and total_qty < min_quantity: + continue + if max_quantity is not None and total_qty > max_quantity: + continue + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + carts.append(cart) + return carts[offset: offset + limit] + +@router.post("/{cart_id}/add/{item_id}", response_model=Cart) +def add_item(cart_id: int, item_id: int): + if cart_id not in _carts: + raise HTTPException(status_code=404, detail="cart not found") + if item_id not in _items: + raise HTTPException(status_code=404, detail="item not found") + _carts[cart_id][item_id] = _carts[cart_id].get(item_id, 0) + 1 + return compute_cart(cart_id) diff --git a/hw3/shop_api/routers/chat.py b/hw3/shop_api/routers/chat.py new file mode 100644 index 00000000..ef026015 --- /dev/null +++ b/hw3/shop_api/routers/chat.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from collections import defaultdict +import random +import string +from typing import Dict, List +from prometheus_client import Counter, Gauge + +ws_connections = Gauge("shop_ws_connections", "Active WebSocket connections", ["chat_name"]) +ws_messages_total = Counter("shop_ws_messages_total", "Total WebSocket messages sent", ["chat_name"]) + +router = APIRouter(prefix="/chat", tags=["chat"]) + +chat_rooms: Dict[str, List[WebSocket]] = defaultdict(list) +usernames: Dict[WebSocket, str] = {} + +def random_username() -> str: + return ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + +@router.websocket("/{chat_name}") +async def websocket_chat(websocket: WebSocket, chat_name: str): + await websocket.accept() + username = random_username() + usernames[websocket] = username + chat_rooms[chat_name].append(websocket) + ws_connections.labels(chat_name=chat_name).inc() # <--- увеличиваем число соединений + + try: + while True: + data = await websocket.receive_text() + message = f"{username} :: {data}" + for ws in chat_rooms[chat_name]: + if ws != websocket: + await ws.send_text(message) + ws_messages_total.labels(chat_name=chat_name).inc() # <--- увеличиваем счетчик сообщений + except WebSocketDisconnect: + chat_rooms[chat_name].remove(websocket) + del usernames[websocket] + ws_connections.labels(chat_name=chat_name).dec() # <--- уменьшаем число соединений + diff --git a/hw3/shop_api/routers/items.py b/hw3/shop_api/routers/items.py new file mode 100644 index 00000000..e432edb8 --- /dev/null +++ b/hw3/shop_api/routers/items.py @@ -0,0 +1,83 @@ +from fastapi import APIRouter, HTTPException, Query, Response +from shop_api.schemas.item import Item, ItemCreate +from shop_api.storage.memory import _items, _lock, _next_item_id +from typing import Optional, List +from shop_api.metrics import items_in_stock_gauge + +router = APIRouter(prefix="/item", tags=["items"]) + +@router.post("/", response_model=Item, status_code=201) +def create_item(item: ItemCreate): + global _next_item_id + with _lock: + iid = _next_item_id + _next_item_id += 1 + new_item = Item(id=iid, name=item.name, price=item.price) + _items[iid] = new_item + items_in_stock_gauge.inc() + return new_item + +@router.get("/{id}", response_model=Item) +def get_item(id: int): + item = _items.get(id) + if not item or item.deleted: + raise HTTPException(status_code=404, detail="item not found") + return item + +@router.get("/", response_model=List[Item]) +def list_items( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + show_deleted: bool = Query(False), +): + items = [] + for it in _items.values(): + if not show_deleted and it.deleted: + continue + if min_price is not None and it.price < min_price: + continue + if max_price is not None and it.price > max_price: + continue + items.append(it) + return items[offset: offset + limit] + +@router.put("/{id}", response_model=Item) +def replace_item(id: int, item: ItemCreate): + if id not in _items: + raise HTTPException(status_code=404, detail="item not found") + existing = _items[id] + existing.name = item.name + existing.price = item.price + _items[id] = existing + return existing + +@router.patch("/{id}", response_model=Item) +def patch_item(id: int, patch: dict): + if id not in _items: + raise HTTPException(status_code=404, detail="item not found") + item = _items[id] + if item.deleted: + return Response(status_code=304) + allowed_keys = {"name", "price"} + if not set(patch.keys()).issubset(allowed_keys): + raise HTTPException(status_code=422) + if "price" in patch: + price = patch["price"] + if price is not None and price < 0: + raise HTTPException(status_code=422) + item.price = price + if "name" in patch: + item.name = patch["name"] + _items[id] = item + return item + +@router.delete("/{id}") +def delete_item(id: int): + item = _items.get(id) + if not item: + raise HTTPException(status_code=404, detail="Item not found") + if not item.deleted: + item.deleted = True + items_in_stock_gauge.dec() \ No newline at end of file diff --git a/hw3/shop_api/schemas/__init__.py b/hw3/shop_api/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/shop_api/schemas/cart.py b/hw3/shop_api/schemas/cart.py new file mode 100644 index 00000000..4e3ed90c --- /dev/null +++ b/hw3/shop_api/schemas/cart.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel +from typing import List + + +class CartItem(BaseModel): + id: int + name: str + quantity: int + available: bool + +class Cart(BaseModel): + id: int + items: List[CartItem] + price: float \ No newline at end of file diff --git a/hw3/shop_api/schemas/item.py b/hw3/shop_api/schemas/item.py new file mode 100644 index 00000000..53021cde --- /dev/null +++ b/hw3/shop_api/schemas/item.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class ItemBase(BaseModel): + name: str + price: float = Field(..., ge=0) + +class ItemCreate(ItemBase): + pass + +class Item(ItemBase): + id: int + deleted: bool = False + +class ItemPatch(BaseModel): + name: Optional[str] + price: Optional[float] \ No newline at end of file diff --git a/hw3/shop_api/storage/__init__.py b/hw3/shop_api/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/shop_api/storage/memory.py b/hw3/shop_api/storage/memory.py new file mode 100644 index 00000000..e769f81d --- /dev/null +++ b/hw3/shop_api/storage/memory.py @@ -0,0 +1,9 @@ +from typing import Dict +from threading import Lock +from shop_api.schemas.item import Item + +_items: Dict[int, Item] = {} +_carts: Dict[int, Dict[int, int]] = {} +_next_item_id = 1 +_next_cart_id = 1 +_lock = Lock() diff --git a/hw3/shop_api/utils/cart_utils.py b/hw3/shop_api/utils/cart_utils.py new file mode 100644 index 00000000..b0c65c2e --- /dev/null +++ b/hw3/shop_api/utils/cart_utils.py @@ -0,0 +1,20 @@ +from shop_api.schemas.cart import Cart, CartItem +from shop_api.storage.memory import _carts, _items + + +def compute_cart(cart_id: int) -> Cart: + if cart_id not in _carts: + raise KeyError + bag = _carts[cart_id] + items_out = [] + total = 0.0 + for iid, qty in bag.items(): + item = _items.get(iid) + if item is None: + name, available = "", False + else: + name, available = item.name, not item.deleted + items_out.append(CartItem(id=iid, name=name, quantity=qty, available=available)) + if item and not item.deleted: + total += item.price * qty + return Cart(id=cart_id, items=items_out, price=total) \ No newline at end of file diff --git a/hw3/test.py b/hw3/test.py new file mode 100644 index 00000000..a05b0cca --- /dev/null +++ b/hw3/test.py @@ -0,0 +1,102 @@ +import asyncio +import random +import string +import threading +import time +import requests +import websockets +from http import HTTPStatus + +API_URL = "http://localhost:8000" +WS_URL = "ws://localhost:8000/chat" + +def random_name(length=8): + return ''.join(random.choices(string.ascii_lowercase, k=length)) + +def random_price(): + return round(random.uniform(5, 500), 2) + + +def simulate_http_load(): + while True: + try: + item = {"name": f"Item {random_name()}", "price": random_price()} + start = time.perf_counter() + r = requests.post(f"{API_URL}/item", json=item) + latency = time.perf_counter() - start + + if r.status_code == HTTPStatus.CREATED: + item_id = r.json()["id"] + requests.get(f"{API_URL}/item/{item_id}") + requests.put( + f"{API_URL}/item/{item_id}", + json={"name": item["name"], "price": item["price"] + 1}, + ) + if random.random() < 0.2: + requests.delete(f"{API_URL}/item/{item_id}") + else: + requests.get(f"{API_URL}/item/99999999") + + cart_response = requests.post(f"{API_URL}/cart") + if cart_response.status_code == HTTPStatus.CREATED: + cart_id = cart_response.json()["id"] + if r.status_code == HTTPStatus.CREATED: + requests.post(f"{API_URL}/cart/{cart_id}/add/{r.json()['id']}") + requests.get(f"{API_URL}/cart/{cart_id}") + else: + requests.get(f"{API_URL}/cart/-1") + + requests.get(f"{API_URL}/item", params={ + "min_price": random.uniform(1, 50), + "max_price": random.uniform(100, 500), + "offset": random.randint(0, 5), + "limit": random.randint(1, 10), + }) + + requests.get(f"{API_URL}/cart", params={ + "min_price": random.uniform(1, 50), + "max_price": random.uniform(100, 500), + "offset": random.randint(0, 5), + "limit": random.randint(1, 10), + }) + + asyncio.run(asyncio.sleep(random.uniform(0.05, 0.2))) + + except Exception as e: + print(f"[HTTP Error] {e}") + + +async def simulate_websocket_load(chat_name: str): + uri = f"{WS_URL}/{chat_name}" + try: + async with websockets.connect(uri) as ws: + for _ in range(random.randint(5, 15)): + msg = f"Hello from {random_name()}!" + await ws.send(msg) + try: + await asyncio.wait_for(ws.recv(), timeout=1) + except asyncio.TimeoutError: + pass + await asyncio.sleep(random.uniform(0.2, 1.0)) + except Exception as e: + print(f"[WS Error] {e}") + + +def run_websocket_load(): + asyncio.run(simulate_websocket_load(random.choice(["general", "orders", "support", "promo"]))) + + +if __name__ == "__main__": + print("Started") + + for _ in range(8): + threading.Thread(target=simulate_http_load, daemon=True).start() + + for _ in range(4): + threading.Thread(target=run_websocket_load, daemon=True).start() + + try: + while True: + time.sleep(2) + except KeyboardInterrupt: + print("Stopped") From 5e2d5da390ad85981126c2891c7288e5eee0892c Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 12 Oct 2025 16:55:14 +0300 Subject: [PATCH 06/14] Dashboard pics --- hw3/pics/bussiness_dashboard.jpg | Bin 0 -> 95594 bytes hw3/pics/tech_dashboard.jpg | Bin 0 -> 66265 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 hw3/pics/bussiness_dashboard.jpg create mode 100644 hw3/pics/tech_dashboard.jpg diff --git a/hw3/pics/bussiness_dashboard.jpg b/hw3/pics/bussiness_dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0abdb880da472c6c54c79c850a29725250c3c03b GIT binary patch literal 95594 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 K?lJxG>VE)>@xL7a literal 0 HcmV?d00001 diff --git a/hw3/pics/tech_dashboard.jpg b/hw3/pics/tech_dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..937ebdf03308a1135b353051eac319165c019856 GIT binary patch literal 66265 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 JUjbid{txy4?v4Ne literal 0 HcmV?d00001 From 9b868a9776f6f688973e5d533caf3ada328f0e21 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 12 Oct 2025 16:59:10 +0300 Subject: [PATCH 07/14] HW3 readme updated --- hw3/README.md | 9 +++++++++ ...ssiness_dashboard.jpg => business_dashboard.jpg} | Bin 2 files changed, 9 insertions(+) create mode 100644 hw3/README.md rename hw3/pics/{bussiness_dashboard.jpg => business_dashboard.jpg} (100%) diff --git a/hw3/README.md b/hw3/README.md new file mode 100644 index 00000000..cad0c96f --- /dev/null +++ b/hw3/README.md @@ -0,0 +1,9 @@ +### Технический мониторинг +![Tech Dashboard](./pics/tech_dashboard.jpg) + +### Бизнес-метрики +![Business Dashboard](./pics/business_dashboard.jpg) + +### Запуск: +```bash +docker-compose up --build \ No newline at end of file diff --git a/hw3/pics/bussiness_dashboard.jpg b/hw3/pics/business_dashboard.jpg similarity index 100% rename from hw3/pics/bussiness_dashboard.jpg rename to hw3/pics/business_dashboard.jpg From 9bf084714a8acf202d5bbbeb98fc3a2b8cdd4e0a Mon Sep 17 00:00:00 2001 From: Roman Kharkovskoy <90906464+RomanKharkovskoy@users.noreply.github.com> Date: Sun, 12 Oct 2025 17:00:18 +0300 Subject: [PATCH 08/14] Update README.md --- hw3/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hw3/README.md b/hw3/README.md index cad0c96f..33cd5544 100644 --- a/hw3/README.md +++ b/hw3/README.md @@ -1,9 +1,8 @@ -### Технический мониторинг +### Grafana + Prometheus ![Tech Dashboard](./pics/tech_dashboard.jpg) -### Бизнес-метрики ![Business Dashboard](./pics/business_dashboard.jpg) ### Запуск: ```bash -docker-compose up --build \ No newline at end of file +docker-compose up --build From a67472c1b1deefb57ab054f6c7799490584bd759 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 26 Oct 2025 14:12:43 +0300 Subject: [PATCH 09/14] hw4 --- hw4/Dockerfile | 13 + hw4/README.md | 9 + hw4/__init__.py | 0 hw4/docker-compose.yml | 51 +++ .../provisioning/dashboards/dashboard.yml | 11 + .../shop_api_business_dashboard.json | 334 ++++++++++++++ .../dashboards/shop_api_dashboard.json | 408 ++++++++++++++++++ .../provisioning/datasources/datasource.yml | 11 + hw4/pics/business_dashboard.jpg | Bin 0 -> 95594 bytes hw4/pics/tech_dashboard.jpg | Bin 0 -> 66265 bytes hw4/prometheus/prometheus.yml | 8 + hw4/requirements.txt | 6 + hw4/shop_api/__init__.py | 0 hw4/shop_api/database.py | 17 + hw4/shop_api/main.py | 13 + hw4/shop_api/metrics.py | 8 + hw4/shop_api/models/__init__.py | 0 hw4/shop_api/models/cart.py | 22 + hw4/shop_api/models/item.py | 10 + hw4/shop_api/routers/__init__.py | 0 hw4/shop_api/routers/carts.py | 80 ++++ hw4/shop_api/routers/chat.py | 39 ++ hw4/shop_api/routers/items.py | 63 +++ hw4/shop_api/schemas/__init__.py | 0 hw4/shop_api/schemas/cart.py | 16 + hw4/shop_api/schemas/item.py | 18 + hw4/shop_api/storage/__init__.py | 0 hw4/shop_api/storage/memory.py | 9 + hw4/shop_api/utils/cart_utils.py | 20 + hw4/test.py | 102 +++++ hw4/test_db.py | 13 + hw4/tx_demos/README.md | 44 ++ hw4/tx_demos/__init__.py | 1 + hw4/tx_demos/db_setup.py | 79 ++++ hw4/tx_demos/demo_dirty_read.py | 49 +++ hw4/tx_demos/demo_non_repeatable_read.py | 60 +++ hw4/tx_demos/demo_phantom_read.py | 65 +++ 37 files changed, 1579 insertions(+) create mode 100644 hw4/Dockerfile create mode 100644 hw4/README.md create mode 100644 hw4/__init__.py create mode 100644 hw4/docker-compose.yml create mode 100644 hw4/grafana/provisioning/dashboards/dashboard.yml create mode 100644 hw4/grafana/provisioning/dashboards/shop_api_business_dashboard.json create mode 100644 hw4/grafana/provisioning/dashboards/shop_api_dashboard.json create mode 100644 hw4/grafana/provisioning/datasources/datasource.yml create mode 100644 hw4/pics/business_dashboard.jpg create mode 100644 hw4/pics/tech_dashboard.jpg create mode 100644 hw4/prometheus/prometheus.yml create mode 100644 hw4/requirements.txt create mode 100644 hw4/shop_api/__init__.py create mode 100644 hw4/shop_api/database.py create mode 100644 hw4/shop_api/main.py create mode 100644 hw4/shop_api/metrics.py create mode 100644 hw4/shop_api/models/__init__.py create mode 100644 hw4/shop_api/models/cart.py create mode 100644 hw4/shop_api/models/item.py create mode 100644 hw4/shop_api/routers/__init__.py create mode 100644 hw4/shop_api/routers/carts.py create mode 100644 hw4/shop_api/routers/chat.py create mode 100644 hw4/shop_api/routers/items.py create mode 100644 hw4/shop_api/schemas/__init__.py create mode 100644 hw4/shop_api/schemas/cart.py create mode 100644 hw4/shop_api/schemas/item.py create mode 100644 hw4/shop_api/storage/__init__.py create mode 100644 hw4/shop_api/storage/memory.py create mode 100644 hw4/shop_api/utils/cart_utils.py create mode 100644 hw4/test.py create mode 100644 hw4/test_db.py create mode 100644 hw4/tx_demos/README.md create mode 100644 hw4/tx_demos/__init__.py create mode 100644 hw4/tx_demos/db_setup.py create mode 100644 hw4/tx_demos/demo_dirty_read.py create mode 100644 hw4/tx_demos/demo_non_repeatable_read.py create mode 100644 hw4/tx_demos/demo_phantom_read.py diff --git a/hw4/Dockerfile b/hw4/Dockerfile new file mode 100644 index 00000000..f08e675f --- /dev/null +++ b/hw4/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/hw4/README.md b/hw4/README.md new file mode 100644 index 00000000..cad0c96f --- /dev/null +++ b/hw4/README.md @@ -0,0 +1,9 @@ +### Технический мониторинг +![Tech Dashboard](./pics/tech_dashboard.jpg) + +### Бизнес-метрики +![Business Dashboard](./pics/business_dashboard.jpg) + +### Запуск: +```bash +docker-compose up --build \ No newline at end of file diff --git a/hw4/__init__.py b/hw4/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/docker-compose.yml b/hw4/docker-compose.yml new file mode 100644 index 00000000..01a84a56 --- /dev/null +++ b/hw4/docker-compose.yml @@ -0,0 +1,51 @@ +services: + db: + image: postgres:15 + environment: + POSTGRES_USER: shop_user + POSTGRES_PASSWORD: shop_pass + POSTGRES_DB: shop_db + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U shop_user -d shop_db"] + interval: 2s + timeout: 2s + retries: 10 + + shop-api: + build: . + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgresql+asyncpg://shop_user:shop_pass@db:5432/shop_db + ports: + - "8000:8000" + + prometheus: + image: prom/prometheus:latest + volumes: + - ./prometheus:/etc/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + ports: + - "9090:9090" + + grafana: + image: grafana/grafana:latest + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + ports: + - "3000:3000" + depends_on: + - prometheus + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + +volumes: + db_data: + grafana_data: diff --git a/hw4/grafana/provisioning/dashboards/dashboard.yml b/hw4/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 00000000..472a2279 --- /dev/null +++ b/hw4/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "Shop API Technical" + folder: "Tech" + options: + path: /etc/grafana/provisioning/dashboards + - name: "Shop API Business" + folder: "Business" + options: + path: /etc/grafana/provisioning/dashboards diff --git a/hw4/grafana/provisioning/dashboards/shop_api_business_dashboard.json b/hw4/grafana/provisioning/dashboards/shop_api_business_dashboard.json new file mode 100644 index 00000000..794d4643 --- /dev/null +++ b/hw4/grafana/provisioning/dashboards/shop_api_business_dashboard.json @@ -0,0 +1,334 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 15, + "x": 0, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "editorMode": "code", + "expr": "rate(shop_carts_created_total[1m])", + "legendFormat": "Carts per minute", + "range": true, + "refId": "A" + } + ], + "title": "Carts Created per Minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 15, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "count(shop_ws_connections)", + "refId": "A" + } + ], + "title": "Active Chat Rooms", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 15, + "x": 0, + "y": 6 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "sum(rate(shop_ws_messages_total[1m])) by (chat_name)", + "legendFormat": "{{chat_name}}", + "refId": "A" + } + ], + "title": "Chat Messages per Minute", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 15, + "y": 6 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "sum(shop_items_total)", + "refId": "A" + } + ], + "title": "Total Items in Stock", + "type": "stat" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "timepicker": {}, + "timezone": "browser", + "title": "Business Metrics", + "uid": "440d983f-0b3d-4579-b85d-3ee2dad1babd", + "version": 2 +} \ No newline at end of file diff --git a/hw4/grafana/provisioning/dashboards/shop_api_dashboard.json b/hw4/grafana/provisioning/dashboards/shop_api_dashboard.json new file mode 100644 index 00000000..5779d8e0 --- /dev/null +++ b/hw4/grafana/provisioning/dashboards/shop_api_dashboard.json @@ -0,0 +1,408 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(http_requests_total[1m])) by (method, handler)", + "legendFormat": "{{method}} {{handler}}", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Requests per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p95", + "refId": "A" + } + ], + "title": "Request Duration (p95)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "shop_ws_connections", + "legendFormat": "{{chat_name}}", + "refId": "A" + } + ], + "title": "Active WebSocket Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "increase(shop_ws_messages_total[5m])", + "legendFormat": "{{chat_name}}", + "refId": "A" + } + ], + "title": "WebSocket Messages Sent", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "timepicker": {}, + "timezone": "browser", + "title": "Shop API Dashboard", + "uid": "ecd408fb-c57c-4ba2-8452-260e76f954ed", + "version": 3 +} \ No newline at end of file diff --git a/hw4/grafana/provisioning/datasources/datasource.yml b/hw4/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 00000000..7285dbe4 --- /dev/null +++ b/hw4/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + orgId: 1 + uid: prometheus_ds + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/hw4/pics/business_dashboard.jpg b/hw4/pics/business_dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0abdb880da472c6c54c79c850a29725250c3c03b GIT binary patch literal 95594 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 K?lJxG>VE)>@xL7a literal 0 HcmV?d00001 diff --git a/hw4/pics/tech_dashboard.jpg b/hw4/pics/tech_dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..937ebdf03308a1135b353051eac319165c019856 GIT binary patch literal 66265 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 JUjbid{txy4?v4Ne literal 0 HcmV?d00001 diff --git a/hw4/prometheus/prometheus.yml b/hw4/prometheus/prometheus.yml new file mode 100644 index 00000000..046265e3 --- /dev/null +++ b/hw4/prometheus/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: 'shop_api' + static_configs: + - targets: ['app:8000'] + metrics_path: /metrics diff --git a/hw4/requirements.txt b/hw4/requirements.txt new file mode 100644 index 00000000..76867af0 --- /dev/null +++ b/hw4/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +prometheus-fastapi-instrumentator +pydantic +sqlalchemy +asyncpg \ No newline at end of file diff --git a/hw4/shop_api/__init__.py b/hw4/shop_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/shop_api/database.py b/hw4/shop_api/database.py new file mode 100644 index 00000000..3319d7b3 --- /dev/null +++ b/hw4/shop_api/database.py @@ -0,0 +1,17 @@ +import os +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker, declarative_base + +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgresql+asyncpg://shop_user:shop_pass@db:5432/shop_db" +) + +engine = create_async_engine(DATABASE_URL, echo=True, future=True) +async_session_maker = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) +Base = declarative_base() + + +async def get_session() -> AsyncSession: + async with async_session_maker() as session: + yield session diff --git a/hw4/shop_api/main.py b/hw4/shop_api/main.py new file mode 100644 index 00000000..1d866fb9 --- /dev/null +++ b/hw4/shop_api/main.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI +from shop_api.routers import items, carts +from shop_api.database import Base, engine + +app = FastAPI(title="Shop API") + +@app.on_event("startup") +async def on_startup(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + +app.include_router(items.router) +app.include_router(carts.router) diff --git a/hw4/shop_api/metrics.py b/hw4/shop_api/metrics.py new file mode 100644 index 00000000..0b18aff2 --- /dev/null +++ b/hw4/shop_api/metrics.py @@ -0,0 +1,8 @@ +from prometheus_client import Counter, Gauge + +carts_created_counter = Counter( + "shop_carts_created_total", "Total number of carts created" +) +items_in_stock_gauge = Gauge( + "shop_items_total", "Current number of non-deleted items in stock" +) diff --git a/hw4/shop_api/models/__init__.py b/hw4/shop_api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/shop_api/models/cart.py b/hw4/shop_api/models/cart.py new file mode 100644 index 00000000..2695bee4 --- /dev/null +++ b/hw4/shop_api/models/cart.py @@ -0,0 +1,22 @@ +from sqlalchemy import Column, Integer, Float, ForeignKey +from sqlalchemy.orm import relationship +from shop_api.database import Base + + +class Cart(Base): + __tablename__ = "carts" + + id = Column(Integer, primary_key=True, index=True) + price = Column(Float, default=0.0) + items = relationship("CartItem", back_populates="cart", cascade="all, delete-orphan") + + +class CartItem(Base): + __tablename__ = "cart_items" + + id = Column(Integer, primary_key=True, index=True) + cart_id = Column(Integer, ForeignKey("carts.id")) + item_id = Column(Integer, ForeignKey("items.id")) + quantity = Column(Integer, default=1) + + cart = relationship("Cart", back_populates="items") \ No newline at end of file diff --git a/hw4/shop_api/models/item.py b/hw4/shop_api/models/item.py new file mode 100644 index 00000000..184b6617 --- /dev/null +++ b/hw4/shop_api/models/item.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String, Float, Boolean +from shop_api.database import Base + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + price = Column(Float, nullable=False) + deleted = Column(Boolean, default=False) \ No newline at end of file diff --git a/hw4/shop_api/routers/__init__.py b/hw4/shop_api/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/shop_api/routers/carts.py b/hw4/shop_api/routers/carts.py new file mode 100644 index 00000000..38b0444f --- /dev/null +++ b/hw4/shop_api/routers/carts.py @@ -0,0 +1,80 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func +from typing import List, Optional + +from shop_api.database import get_session +from shop_api.models.cart import Cart, CartItem +from shop_api.models.item import Item +from shop_api.schemas.cart import CartOut, CartItemOut +from shop_api.metrics import carts_created_counter + +router = APIRouter(prefix="/cart", tags=["cart"]) + + +@router.post("/", status_code=201) +async def create_cart(session: AsyncSession = Depends(get_session)): + cart = Cart() + session.add(cart) + await session.commit() + await session.refresh(cart) + + carts_created_counter.inc() + return {"id": cart.id} + + +@router.post("/{cart_id}/add/{item_id}", status_code=200) +async def add_item_to_cart(cart_id: int, item_id: int, session: AsyncSession = Depends(get_session)): + cart = await session.get(Cart, cart_id) + if not cart: + raise HTTPException(status_code=404, detail="Cart not found") + + item = await session.get(Item, item_id) + if not item or item.deleted: + raise HTTPException(status_code=404, detail="Item not found") + + result = await session.execute( + select(CartItem).where(CartItem.cart_id == cart_id, CartItem.item_id == item_id) + ) + cart_item = result.scalar_one_or_none() + + if cart_item: + cart_item.quantity += 1 + else: + cart_item = CartItem(cart_id=cart_id, item_id=item_id, quantity=1) + session.add(cart_item) + + await session.commit() + return {"status": "ok"} + + +@router.get("/{cart_id}", response_model=CartOut) +async def get_cart(cart_id: int, session: AsyncSession = Depends(get_session)): + cart = await session.get(Cart, cart_id) + if not cart: + raise HTTPException(status_code=404, detail="Cart not found") + + result = await session.execute( + select(CartItem, Item) + .join(Item, CartItem.item_id == Item.id) + .where(CartItem.cart_id == cart_id) + ) + + cart_items = [] + total_price = 0.0 + for cart_item, item in result.all(): + cart_items.append( + CartItemOut( + id=item.id, + name=item.name, + quantity=cart_item.quantity, + available=not item.deleted, + ) + ) + if not item.deleted: + total_price += item.price * cart_item.quantity + + cart.price = total_price + await session.commit() + + return CartOut(id=cart.id, items=cart_items, price=total_price) diff --git a/hw4/shop_api/routers/chat.py b/hw4/shop_api/routers/chat.py new file mode 100644 index 00000000..ef026015 --- /dev/null +++ b/hw4/shop_api/routers/chat.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from collections import defaultdict +import random +import string +from typing import Dict, List +from prometheus_client import Counter, Gauge + +ws_connections = Gauge("shop_ws_connections", "Active WebSocket connections", ["chat_name"]) +ws_messages_total = Counter("shop_ws_messages_total", "Total WebSocket messages sent", ["chat_name"]) + +router = APIRouter(prefix="/chat", tags=["chat"]) + +chat_rooms: Dict[str, List[WebSocket]] = defaultdict(list) +usernames: Dict[WebSocket, str] = {} + +def random_username() -> str: + return ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + +@router.websocket("/{chat_name}") +async def websocket_chat(websocket: WebSocket, chat_name: str): + await websocket.accept() + username = random_username() + usernames[websocket] = username + chat_rooms[chat_name].append(websocket) + ws_connections.labels(chat_name=chat_name).inc() # <--- увеличиваем число соединений + + try: + while True: + data = await websocket.receive_text() + message = f"{username} :: {data}" + for ws in chat_rooms[chat_name]: + if ws != websocket: + await ws.send_text(message) + ws_messages_total.labels(chat_name=chat_name).inc() # <--- увеличиваем счетчик сообщений + except WebSocketDisconnect: + chat_rooms[chat_name].remove(websocket) + del usernames[websocket] + ws_connections.labels(chat_name=chat_name).dec() # <--- уменьшаем число соединений + diff --git a/hw4/shop_api/routers/items.py b/hw4/shop_api/routers/items.py new file mode 100644 index 00000000..2c3def81 --- /dev/null +++ b/hw4/shop_api/routers/items.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from typing import List, Optional + +from shop_api.database import get_session +from shop_api.models.item import Item +from shop_api.schemas.item import ItemCreate, ItemOut +from shop_api.metrics import items_in_stock_gauge + +router = APIRouter(prefix="/item", tags=["item"]) + + +@router.post("/", response_model=ItemOut, status_code=201) +async def create_item(item: ItemCreate, session: AsyncSession = Depends(get_session)): + new_item = Item(**item.dict()) + session.add(new_item) + await session.commit() + await session.refresh(new_item) + + items_in_stock_gauge.inc() + return new_item + + +@router.get("/{item_id}", response_model=ItemOut) +async def get_item(item_id: int, session: AsyncSession = Depends(get_session)): + item = await session.get(Item, item_id) + if not item or item.deleted: + raise HTTPException(status_code=404, detail="Item not found") + return item + + +@router.get("/", response_model=List[ItemOut]) +async def list_items( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + show_deleted: bool = Query(False), + session: AsyncSession = Depends(get_session), +): + query = select(Item) + if not show_deleted: + query = query.where(Item.deleted.is_(False)) + if min_price is not None: + query = query.where(Item.price >= min_price) + if max_price is not None: + query = query.where(Item.price <= max_price) + + result = await session.execute(query.offset(offset).limit(limit)) + return result.scalars().all() + + +@router.delete("/{item_id}", status_code=200) +async def delete_item(item_id: int, session: AsyncSession = Depends(get_session)): + item = await session.get(Item, item_id) + if not item: + raise HTTPException(status_code=404, detail="Item not found") + if not item.deleted: + item.deleted = True + await session.commit() + items_in_stock_gauge.dec() + return {"status": "ok"} diff --git a/hw4/shop_api/schemas/__init__.py b/hw4/shop_api/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/shop_api/schemas/cart.py b/hw4/shop_api/schemas/cart.py new file mode 100644 index 00000000..c1ae093f --- /dev/null +++ b/hw4/shop_api/schemas/cart.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel +from typing import List + +class CartItemOut(BaseModel): + id: int + name: str + quantity: int + available: bool + +class CartOut(BaseModel): + id: int + items: List[CartItemOut] + price: float + + class Config: + orm_mode = True diff --git a/hw4/shop_api/schemas/item.py b/hw4/shop_api/schemas/item.py new file mode 100644 index 00000000..9093763d --- /dev/null +++ b/hw4/shop_api/schemas/item.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +class ItemBase(BaseModel): + name: str + price: float + +class ItemCreate(ItemBase): + pass + +class ItemUpdate(ItemBase): + deleted: bool = False + +class ItemOut(ItemBase): + id: int + deleted: bool + + class Config: + orm_mode = True diff --git a/hw4/shop_api/storage/__init__.py b/hw4/shop_api/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/shop_api/storage/memory.py b/hw4/shop_api/storage/memory.py new file mode 100644 index 00000000..e769f81d --- /dev/null +++ b/hw4/shop_api/storage/memory.py @@ -0,0 +1,9 @@ +from typing import Dict +from threading import Lock +from shop_api.schemas.item import Item + +_items: Dict[int, Item] = {} +_carts: Dict[int, Dict[int, int]] = {} +_next_item_id = 1 +_next_cart_id = 1 +_lock = Lock() diff --git a/hw4/shop_api/utils/cart_utils.py b/hw4/shop_api/utils/cart_utils.py new file mode 100644 index 00000000..b0c65c2e --- /dev/null +++ b/hw4/shop_api/utils/cart_utils.py @@ -0,0 +1,20 @@ +from shop_api.schemas.cart import Cart, CartItem +from shop_api.storage.memory import _carts, _items + + +def compute_cart(cart_id: int) -> Cart: + if cart_id not in _carts: + raise KeyError + bag = _carts[cart_id] + items_out = [] + total = 0.0 + for iid, qty in bag.items(): + item = _items.get(iid) + if item is None: + name, available = "", False + else: + name, available = item.name, not item.deleted + items_out.append(CartItem(id=iid, name=name, quantity=qty, available=available)) + if item and not item.deleted: + total += item.price * qty + return Cart(id=cart_id, items=items_out, price=total) \ No newline at end of file diff --git a/hw4/test.py b/hw4/test.py new file mode 100644 index 00000000..a05b0cca --- /dev/null +++ b/hw4/test.py @@ -0,0 +1,102 @@ +import asyncio +import random +import string +import threading +import time +import requests +import websockets +from http import HTTPStatus + +API_URL = "http://localhost:8000" +WS_URL = "ws://localhost:8000/chat" + +def random_name(length=8): + return ''.join(random.choices(string.ascii_lowercase, k=length)) + +def random_price(): + return round(random.uniform(5, 500), 2) + + +def simulate_http_load(): + while True: + try: + item = {"name": f"Item {random_name()}", "price": random_price()} + start = time.perf_counter() + r = requests.post(f"{API_URL}/item", json=item) + latency = time.perf_counter() - start + + if r.status_code == HTTPStatus.CREATED: + item_id = r.json()["id"] + requests.get(f"{API_URL}/item/{item_id}") + requests.put( + f"{API_URL}/item/{item_id}", + json={"name": item["name"], "price": item["price"] + 1}, + ) + if random.random() < 0.2: + requests.delete(f"{API_URL}/item/{item_id}") + else: + requests.get(f"{API_URL}/item/99999999") + + cart_response = requests.post(f"{API_URL}/cart") + if cart_response.status_code == HTTPStatus.CREATED: + cart_id = cart_response.json()["id"] + if r.status_code == HTTPStatus.CREATED: + requests.post(f"{API_URL}/cart/{cart_id}/add/{r.json()['id']}") + requests.get(f"{API_URL}/cart/{cart_id}") + else: + requests.get(f"{API_URL}/cart/-1") + + requests.get(f"{API_URL}/item", params={ + "min_price": random.uniform(1, 50), + "max_price": random.uniform(100, 500), + "offset": random.randint(0, 5), + "limit": random.randint(1, 10), + }) + + requests.get(f"{API_URL}/cart", params={ + "min_price": random.uniform(1, 50), + "max_price": random.uniform(100, 500), + "offset": random.randint(0, 5), + "limit": random.randint(1, 10), + }) + + asyncio.run(asyncio.sleep(random.uniform(0.05, 0.2))) + + except Exception as e: + print(f"[HTTP Error] {e}") + + +async def simulate_websocket_load(chat_name: str): + uri = f"{WS_URL}/{chat_name}" + try: + async with websockets.connect(uri) as ws: + for _ in range(random.randint(5, 15)): + msg = f"Hello from {random_name()}!" + await ws.send(msg) + try: + await asyncio.wait_for(ws.recv(), timeout=1) + except asyncio.TimeoutError: + pass + await asyncio.sleep(random.uniform(0.2, 1.0)) + except Exception as e: + print(f"[WS Error] {e}") + + +def run_websocket_load(): + asyncio.run(simulate_websocket_load(random.choice(["general", "orders", "support", "promo"]))) + + +if __name__ == "__main__": + print("Started") + + for _ in range(8): + threading.Thread(target=simulate_http_load, daemon=True).start() + + for _ in range(4): + threading.Thread(target=run_websocket_load, daemon=True).start() + + try: + while True: + time.sleep(2) + except KeyboardInterrupt: + print("Stopped") diff --git a/hw4/test_db.py b/hw4/test_db.py new file mode 100644 index 00000000..9138df32 --- /dev/null +++ b/hw4/test_db.py @@ -0,0 +1,13 @@ +import asyncio +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy import text + +DATABASE_URL = "postgresql+asyncpg://shop_user:shop_pass@localhost:5432/shop_db" +engine = create_async_engine(DATABASE_URL) + +async def test_db(): + async with engine.begin() as conn: + result = await conn.execute(text("SELECT 1;")) + print(result.fetchall()) + +asyncio.run(test_db()) diff --git a/hw4/tx_demos/README.md b/hw4/tx_demos/README.md new file mode 100644 index 00000000..b761192c --- /dev/null +++ b/hw4/tx_demos/README.md @@ -0,0 +1,44 @@ +# HW4 + +## Запуск проекта + +```bash +docker compose up -d +``` + +## Структура базы данных + +Таблицы: + +- carts (хранит корзины пользователей): id, price +- items (хранит товары): id, name, price, deleted +- cart_items (связывает корзины и товары многие ко многим): id, cart_id, item_id. quantity + +## Демонстрации транзакционных аномалий (dirty read, non-repeatable read, phantom) + +### Dirty read + +```bash +python tx_demos/demo_dirty_read.py +``` + +В PostgreSQL не возникает даже при READ UNCOMMITTED, так как этот уровень фактически работает как READ COMMITTED. \ +Невозможно прочитать неподтверждённые изменения другой транзакции. + +### Non-repeatable read + +```bash +python tx_demos/non_repeatable_read.py +``` + +Возможно при READ COMMITTED, потому что между двумя чтениями другая транзакция может изменить данные и зафиксировать изменения. \ +Данные, считанные повторно, могут измениться. + +### Phantom read + +```bash +python tx_demos/demo_phantom_read.py +``` + +Возможно при REPEATABLE READ, но предотвращается при SERIALIZABLE. \ +Между двумя одинаковыми запросами может появиться (или исчезнуть) новая строка, удовлетворяющая условию выборки. \ No newline at end of file diff --git a/hw4/tx_demos/__init__.py b/hw4/tx_demos/__init__.py new file mode 100644 index 00000000..8538f779 --- /dev/null +++ b/hw4/tx_demos/__init__.py @@ -0,0 +1 @@ +# package marker for tx_demos diff --git a/hw4/tx_demos/db_setup.py b/hw4/tx_demos/db_setup.py new file mode 100644 index 00000000..19840159 --- /dev/null +++ b/hw4/tx_demos/db_setup.py @@ -0,0 +1,79 @@ +import asyncio +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine + +DATABASE_URL = "postgresql+asyncpg://shop_user:shop_pass@localhost:5432/shop_db" +engine = create_async_engine(DATABASE_URL) + + +async def create_tables(): + async with engine.begin() as conn: + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS carts ( + id SERIAL PRIMARY KEY, + price FLOAT DEFAULT 0.0 + ); + """ + ) + ) + + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS items ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + price FLOAT NOT NULL, + deleted BOOLEAN DEFAULT FALSE + ); + """ + ) + ) + + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS cart_items ( + id SERIAL PRIMARY KEY, + cart_id INTEGER REFERENCES carts(id) ON DELETE CASCADE, + item_id INTEGER REFERENCES items(id), + quantity INTEGER DEFAULT 1 + ); + """ + ) + ) + + +async def seed_default(): + async with engine.begin() as conn: + await conn.execute( + text( + """ + INSERT INTO carts (price) VALUES (100.0) + ON CONFLICT DO NOTHING; + """ + ) + ) + + await conn.execute( + text( + """ + INSERT INTO items (name, price, deleted) + VALUES ('Item A', 10.0, FALSE), + ('Item B', 20.0, FALSE) + ON CONFLICT DO NOTHING; + """ + ) + ) + + +async def prepare(): + await create_tables() + await seed_default() + + +if __name__ == "__main__": + asyncio.run(prepare()) + print("Done.") diff --git a/hw4/tx_demos/demo_dirty_read.py b/hw4/tx_demos/demo_dirty_read.py new file mode 100644 index 00000000..0fd32410 --- /dev/null +++ b/hw4/tx_demos/demo_dirty_read.py @@ -0,0 +1,49 @@ +import asyncio +from sqlalchemy import text +from tx_demos.db_setup import prepare, engine + + +async def dirty_read_demo(isolation_level: str): + async with engine.connect() as conn1: + trans1 = await conn1.begin() + try: + await conn1.execute(text(f"SET TRANSACTION ISOLATION LEVEL {isolation_level}")) + print(f"[T1] Установлен уровень изоляции: {isolation_level}. Обновляю carts.price -> 999 (без фиксации)") + await conn1.execute(text("UPDATE carts SET price = 999 WHERE id = 1")) + + async with engine.connect() as conn2: + trans2 = await conn2.begin() + try: + await conn2.execute(text(f"SET TRANSACTION ISOLATION LEVEL {isolation_level}")) + print("[T2] Выполняю чтение значения price для корзины с id = 1") + res = await conn2.execute(text("SELECT price FROM carts WHERE id = 1")) + row = res.first() + print(f"[T2] Получено значение price: {row[0] if row else 'нет данных'}") + await trans2.commit() + print("[T2] Транзакция зафиксирована") + except Exception as e: + await trans2.rollback() + print(f"[T2] Ошибка: {e}. Транзакция отменена") + + print("[T1] Отменяю изменения, фиксации не будет") + await trans1.rollback() + except Exception as e: + print(f"[T1] Ошибка: {e}. Транзакция отменена") + await trans1.rollback() + + +async def main(): + print("Подготовка базы данных: создание таблиц и тестовых данных") + await prepare() + + print("\n=== Демонстрация dirty read (READ UNCOMMITTED) ===") + await dirty_read_demo("READ UNCOMMITTED") + + print("\n=== Демонстрация без dirty read (READ COMMITTED) ===") + await dirty_read_demo("READ COMMITTED") + + print("\nЗавершено") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/hw4/tx_demos/demo_non_repeatable_read.py b/hw4/tx_demos/demo_non_repeatable_read.py new file mode 100644 index 00000000..9041497b --- /dev/null +++ b/hw4/tx_demos/demo_non_repeatable_read.py @@ -0,0 +1,60 @@ +import asyncio +from sqlalchemy import text +from tx_demos.db_setup import prepare, engine + + +async def non_repeatable_demo(isolation_level: str): + async with engine.connect() as conn1: + trans1 = await conn1.begin() + try: + await conn1.execute(text(f"SET TRANSACTION ISOLATION LEVEL {isolation_level}")) + print(f"\n=== Уровень изоляции: {isolation_level} ===") + print("[T1] Первое чтение значения price из таблицы carts") + + res1 = await conn1.execute(text("SELECT price FROM carts WHERE id = 1")) + r1 = res1.first()[0] + print(f"[T1] Результат первого чтения: {r1}") + + async with engine.connect() as conn2: + trans2 = await conn2.begin() + try: + print("[T2] Обновляю carts.price -> 200 и фиксирую изменения") + await conn2.execute(text("UPDATE carts SET price = 200 WHERE id = 1")) + await trans2.commit() + print("[T2] Транзакция зафиксирована") + except Exception as e: + await trans2.rollback() + print(f"[T2] Ошибка: {e}. Транзакция отменена") + + print("[T1] Второе чтение значения price после фиксации изменений во второй транзакции") + res2 = await conn1.execute(text("SELECT price FROM carts WHERE id = 1")) + r2 = res2.first()[0] + print(f"[T1] Результат второго чтения: {r2}") + + if r1 != r2: + print("[T1] Обнаружено non repeatable read (значение изменилось между запросами)") + else: + print("[T1] Значение не изменилось. Non repeatable read не произошло") + + await trans1.commit() + print("[T1] Транзакция зафиксирована") + except Exception as e: + await trans1.rollback() + print(f"[T1] Ошибка: {e}. Транзакция отменена") + + +async def main_async(): + print("Подготовка базы данных: создание таблиц и тестовых данных") + await prepare() + + print("\n--- Демонстрация non repeatable read (READ COMMITTED) ---") + await non_repeatable_demo("READ COMMITTED") + + print("\n--- Демонстрация предотвращения non repeatable read (REPEATABLE READ) ---") + await non_repeatable_demo("REPEATABLE READ") + + print("\nЗавершено") + + +if __name__ == "__main__": + asyncio.run(main_async()) diff --git a/hw4/tx_demos/demo_phantom_read.py b/hw4/tx_demos/demo_phantom_read.py new file mode 100644 index 00000000..a55f1497 --- /dev/null +++ b/hw4/tx_demos/demo_phantom_read.py @@ -0,0 +1,65 @@ +import asyncio +from sqlalchemy import text +from tx_demos.db_setup import prepare, engine + + +async def phantom_demo(isolation_level: str): + async with engine.connect() as conn1: + trans1 = await conn1.begin() + try: + await conn1.execute(text(f"SET TRANSACTION ISOLATION LEVEL {isolation_level}")) + print(f"\n=== Уровень изоляции: {isolation_level} ===") + + print("[T1] Выполняю первый подсчет записей, где deleted = FALSE") + res1 = await conn1.execute(text("SELECT COUNT(*) FROM items WHERE deleted = FALSE")) + c1 = res1.first()[0] + print(f"[T1] Количество записей при первом чтении: {c1}") + + async with engine.connect() as conn2: + trans2 = await conn2.begin() + try: + print("[T2] Добавляю новую запись (deleted = FALSE) и фиксирую изменения") + await conn2.execute( + text("INSERT INTO items (name, price, deleted) VALUES ('PhantomItem', 50.0, FALSE)") + ) + await trans2.commit() + print("[T2] Транзакция зафиксирована") + except Exception as e: + await trans2.rollback() + print(f"[T2] Ошибка: {e}. Транзакция отменена") + + print("[T1] Повторяю подсчет записей, где deleted = FALSE, после фиксации второй транзакции") + res2 = await conn1.execute(text("SELECT COUNT(*) FROM items WHERE deleted = FALSE")) + c2 = res2.first()[0] + print(f"[T1] Количество записей при втором чтении: {c2}") + + if c1 != c2: + print("[T1] Обнаружено phantom read (в результате появилась новая запись)") + else: + print("[T1] Phantom read не зафиксировано, результаты совпадают") + + await trans1.commit() + print("[T1] Транзакция зафиксирована") + except Exception as e: + await trans1.rollback() + print(f"[T1] Ошибка: {e}. Транзакция отменена") + + +async def main(): + print("Подготовка базы данных: создание таблиц и начальных данных") + await prepare() + + print("\n--- Демонстрация phantom read (READ COMMITTED) ---") + await phantom_demo("READ COMMITTED") + + print("\n--- Демонстрация предотвращения phantom read (REPEATABLE READ) ---") + await phantom_demo("REPEATABLE READ") + + print("\n--- Полная изоляция (SERIALIZABLE) ---") + await phantom_demo("SERIALIZABLE") + + print("\nЗавершено") + + +if __name__ == "__main__": + asyncio.run(main()) From a3d56a92a207bc3a75a46dce9d1e1cfeb8a64bbb Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 26 Oct 2025 17:23:36 +0300 Subject: [PATCH 10/14] hw5 --- .github/workflows/hw5-ci.yml | 54 +++++++++++ hw5/__init__.py | 0 hw5/requirements.txt | 7 ++ hw5/shop_api/__init__.py | 0 hw5/shop_api/main.py | 9 ++ hw5/shop_api/models/__init__.py | 0 hw5/shop_api/routers/__init__.py | 0 hw5/shop_api/routers/carts.py | 58 ++++++++++++ hw5/shop_api/routers/chat.py | 30 +++++++ hw5/shop_api/routers/items.py | 81 +++++++++++++++++ hw5/shop_api/schemas/__init__.py | 0 hw5/shop_api/schemas/cart.py | 14 +++ hw5/shop_api/schemas/item.py | 18 ++++ hw5/shop_api/storage/__init__.py | 0 hw5/shop_api/storage/memory.py | 9 ++ hw5/shop_api/utils/cart_utils.py | 20 +++++ hw5/tests/test_carts.py | 130 +++++++++++++++++++++++++++ hw5/tests/test_chat_ws.py | 63 +++++++++++++ hw5/tests/test_fastapi.py | 20 +++++ hw5/tests/test_items.py | 150 +++++++++++++++++++++++++++++++ 20 files changed, 663 insertions(+) create mode 100644 .github/workflows/hw5-ci.yml create mode 100644 hw5/__init__.py create mode 100644 hw5/requirements.txt create mode 100644 hw5/shop_api/__init__.py create mode 100644 hw5/shop_api/main.py create mode 100644 hw5/shop_api/models/__init__.py create mode 100644 hw5/shop_api/routers/__init__.py create mode 100644 hw5/shop_api/routers/carts.py create mode 100644 hw5/shop_api/routers/chat.py create mode 100644 hw5/shop_api/routers/items.py create mode 100644 hw5/shop_api/schemas/__init__.py create mode 100644 hw5/shop_api/schemas/cart.py create mode 100644 hw5/shop_api/schemas/item.py create mode 100644 hw5/shop_api/storage/__init__.py create mode 100644 hw5/shop_api/storage/memory.py create mode 100644 hw5/shop_api/utils/cart_utils.py create mode 100644 hw5/tests/test_carts.py create mode 100644 hw5/tests/test_chat_ws.py create mode 100644 hw5/tests/test_fastapi.py create mode 100644 hw5/tests/test_items.py diff --git a/.github/workflows/hw5-ci.yml b/.github/workflows/hw5-ci.yml new file mode 100644 index 00000000..fcf0f486 --- /dev/null +++ b/.github/workflows/hw5-ci.yml @@ -0,0 +1,54 @@ +name: Python CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.11, 3.13] + + defaults: + run: + working-directory: hw5 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-cov fastapi uvicorn httpx + + - name: Run tests with coverage + run: | + pytest --cov=shop_api \ + --cov-report=term-missing \ + --cov-report=xml \ + --cov-report=html \ + tests/ + + - name: Upload coverage XML + uses: actions/upload-artifact@v3 + with: + name: coverage-xml + path: coverage.xml + + - name: Upload coverage HTML + uses: actions/upload-artifact@v3 + with: + name: coverage-html + path: htmlcov/ diff --git a/hw5/__init__.py b/hw5/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/requirements.txt b/hw5/requirements.txt new file mode 100644 index 00000000..49106c6e --- /dev/null +++ b/hw5/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.117.1 +uvicorn>=0.24.0 + +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +httpx>=0.27.2 +Faker>=37.8.0 diff --git a/hw5/shop_api/__init__.py b/hw5/shop_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/shop_api/main.py b/hw5/shop_api/main.py new file mode 100644 index 00000000..952e76fc --- /dev/null +++ b/hw5/shop_api/main.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from shop_api.routers import items, carts, chat + + +app = FastAPI(title="Shop API") + +app.include_router(items.router) +app.include_router(carts.router) +app.include_router(chat.router) \ No newline at end of file diff --git a/hw5/shop_api/models/__init__.py b/hw5/shop_api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/shop_api/routers/__init__.py b/hw5/shop_api/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/shop_api/routers/carts.py b/hw5/shop_api/routers/carts.py new file mode 100644 index 00000000..39a179be --- /dev/null +++ b/hw5/shop_api/routers/carts.py @@ -0,0 +1,58 @@ +from fastapi import APIRouter, HTTPException, Query, Path, Response +from shop_api.schemas.cart import Cart +from shop_api.utils.cart_utils import compute_cart +from shop_api.storage.memory import _carts, _items, _lock, _next_cart_id +from typing import Optional, List + + +router = APIRouter(prefix="/cart", tags=["carts"]) + +@router.post("/", status_code=201) +def create_cart(response: Response): + global _next_cart_id + with _lock: + cid = _next_cart_id + _next_cart_id += 1 + _carts[cid] = {} + response.headers["Location"] = f"/cart/{cid}" + return {"id": cid} + +@router.get("/{id}", response_model=Cart) +def get_cart(id: int = Path(..., gt=0)): + try: + return compute_cart(id) + except KeyError: + raise HTTPException(status_code=404, detail="cart not found") + +@router.get("/", response_model=List[Cart]) +def list_carts( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + min_quantity: Optional[int] = Query(None, ge=0), + max_quantity: Optional[int] = Query(None, ge=0), +): + carts = [] + for cid in sorted(_carts.keys()): + cart = compute_cart(cid) + total_qty = sum(i.quantity for i in cart.items) + if min_quantity is not None and total_qty < min_quantity: + continue + if max_quantity is not None and total_qty > max_quantity: + continue + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + carts.append(cart) + return carts[offset: offset + limit] + +@router.post("/{cart_id}/add/{item_id}", response_model=Cart) +def add_item(cart_id: int, item_id: int): + if cart_id not in _carts: + raise HTTPException(status_code=404, detail="cart not found") + if item_id not in _items: + raise HTTPException(status_code=404, detail="item not found") + _carts[cart_id][item_id] = _carts[cart_id].get(item_id, 0) + 1 + return compute_cart(cart_id) diff --git a/hw5/shop_api/routers/chat.py b/hw5/shop_api/routers/chat.py new file mode 100644 index 00000000..0dcf6da9 --- /dev/null +++ b/hw5/shop_api/routers/chat.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from collections import defaultdict +import random +import string +from typing import Dict, List + +router = APIRouter(prefix="/chat", tags=["chat"]) + +chat_rooms: Dict[str, List[WebSocket]] = defaultdict(list) +usernames: Dict[WebSocket, str] = {} + +def random_username() -> str: + return ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + +@router.websocket("/{chat_name}") +async def websocket_chat(websocket: WebSocket, chat_name: str): + await websocket.accept() + username = random_username() + usernames[websocket] = username + chat_rooms[chat_name].append(websocket) + try: + while True: + data = await websocket.receive_text() + message = f"{username} :: {data}" + for ws in chat_rooms[chat_name]: + if ws != websocket: + await ws.send_text(message) + except WebSocketDisconnect: + chat_rooms[chat_name].remove(websocket) + del usernames[websocket] diff --git a/hw5/shop_api/routers/items.py b/hw5/shop_api/routers/items.py new file mode 100644 index 00000000..3942bd8b --- /dev/null +++ b/hw5/shop_api/routers/items.py @@ -0,0 +1,81 @@ +from fastapi import APIRouter, HTTPException, Query, Response +from shop_api.schemas.item import Item, ItemCreate +from shop_api.storage.memory import _items, _lock, _next_item_id +from typing import Optional, List + + +router = APIRouter(prefix="/item", tags=["items"]) + +@router.post("/", response_model=Item, status_code=201) +def create_item(item: ItemCreate): + global _next_item_id + with _lock: + iid = _next_item_id + _next_item_id += 1 + new_item = Item(id=iid, name=item.name, price=item.price) + _items[iid] = new_item + return new_item + +@router.get("/{id}", response_model=Item) +def get_item(id: int): + item = _items.get(id) + if not item or item.deleted: + raise HTTPException(status_code=404, detail="item not found") + return item + +@router.get("/", response_model=List[Item]) +def list_items( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0), + max_price: Optional[float] = Query(None, ge=0), + show_deleted: bool = Query(False), +): + items = [] + for it in _items.values(): + if not show_deleted and it.deleted: + continue + if min_price is not None and it.price < min_price: + continue + if max_price is not None and it.price > max_price: + continue + items.append(it) + return items[offset: offset + limit] + +@router.put("/{id}", response_model=Item) +def replace_item(id: int, item: ItemCreate): + if id not in _items: + raise HTTPException(status_code=404, detail="item not found") + existing = _items[id] + existing.name = item.name + existing.price = item.price + _items[id] = existing + return existing + +@router.patch("/{id}", response_model=Item) +def patch_item(id: int, patch: dict): + if id not in _items: + raise HTTPException(status_code=404, detail="item not found") + item = _items[id] + if item.deleted: + return Response(status_code=304) + allowed_keys = {"name", "price"} + if not set(patch.keys()).issubset(allowed_keys): + raise HTTPException(status_code=422) + if "price" in patch: + price = patch["price"] + if price is not None and price < 0: + raise HTTPException(status_code=422) + item.price = price + if "name" in patch: + item.name = patch["name"] + _items[id] = item + return item + +@router.delete("/{id}") +def delete_item(id: int): + item = _items.get(id) + if not item: + return {"status": "ok"} + item.deleted = True + return {"status": "ok"} \ No newline at end of file diff --git a/hw5/shop_api/schemas/__init__.py b/hw5/shop_api/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/shop_api/schemas/cart.py b/hw5/shop_api/schemas/cart.py new file mode 100644 index 00000000..4e3ed90c --- /dev/null +++ b/hw5/shop_api/schemas/cart.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel +from typing import List + + +class CartItem(BaseModel): + id: int + name: str + quantity: int + available: bool + +class Cart(BaseModel): + id: int + items: List[CartItem] + price: float \ No newline at end of file diff --git a/hw5/shop_api/schemas/item.py b/hw5/shop_api/schemas/item.py new file mode 100644 index 00000000..53021cde --- /dev/null +++ b/hw5/shop_api/schemas/item.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class ItemBase(BaseModel): + name: str + price: float = Field(..., ge=0) + +class ItemCreate(ItemBase): + pass + +class Item(ItemBase): + id: int + deleted: bool = False + +class ItemPatch(BaseModel): + name: Optional[str] + price: Optional[float] \ No newline at end of file diff --git a/hw5/shop_api/storage/__init__.py b/hw5/shop_api/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/shop_api/storage/memory.py b/hw5/shop_api/storage/memory.py new file mode 100644 index 00000000..e769f81d --- /dev/null +++ b/hw5/shop_api/storage/memory.py @@ -0,0 +1,9 @@ +from typing import Dict +from threading import Lock +from shop_api.schemas.item import Item + +_items: Dict[int, Item] = {} +_carts: Dict[int, Dict[int, int]] = {} +_next_item_id = 1 +_next_cart_id = 1 +_lock = Lock() diff --git a/hw5/shop_api/utils/cart_utils.py b/hw5/shop_api/utils/cart_utils.py new file mode 100644 index 00000000..b0c65c2e --- /dev/null +++ b/hw5/shop_api/utils/cart_utils.py @@ -0,0 +1,20 @@ +from shop_api.schemas.cart import Cart, CartItem +from shop_api.storage.memory import _carts, _items + + +def compute_cart(cart_id: int) -> Cart: + if cart_id not in _carts: + raise KeyError + bag = _carts[cart_id] + items_out = [] + total = 0.0 + for iid, qty in bag.items(): + item = _items.get(iid) + if item is None: + name, available = "", False + else: + name, available = item.name, not item.deleted + items_out.append(CartItem(id=iid, name=name, quantity=qty, available=available)) + if item and not item.deleted: + total += item.price * qty + return Cart(id=cart_id, items=items_out, price=total) \ No newline at end of file diff --git a/hw5/tests/test_carts.py b/hw5/tests/test_carts.py new file mode 100644 index 00000000..7ff7d932 --- /dev/null +++ b/hw5/tests/test_carts.py @@ -0,0 +1,130 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from shop_api.routers import carts +from shop_api.storage.memory import _carts, _items, _next_cart_id, _lock +from shop_api.schemas.cart import Cart, CartItem + +app = FastAPI() +app.include_router(carts.router) + +client = TestClient(app) + + +@pytest.fixture(autouse=True) +def reset_memory(): + global _carts, _items, _next_cart_id, _lock + _carts.clear() + _items.clear() + _next_cart_id = 1 + _lock = _lock.__class__() + _items[1] = type("Item", (), {"name": "Item1", "price": 10.0, "deleted": False})() + yield + _carts.clear() + _items.clear() + _next_cart_id = 1 + + +def mock_compute_cart(cart_id: int): + if cart_id not in _carts: + raise KeyError + bag = _carts[cart_id] + items_out = [] + total = 0.0 + for iid, qty in bag.items(): + item = _items[iid] + items_out.append(CartItem( + id=iid, + name=item.name, + quantity=qty, + available=not item.deleted + )) + if not item.deleted: + total += item.price * qty + return Cart(id=cart_id, items=items_out, price=total) + + +def test_create_cart(): + response = client.post("/cart/") + assert response.status_code == 201 + data = response.json() + assert "id" in data + assert response.headers["Location"] == f"/cart/{data['id']}" + assert data["id"] in _carts + + +def test_get_cart_success(monkeypatch): + _carts[1] = {} + monkeypatch.setattr("shop_api.utils.cart_utils.compute_cart", mock_compute_cart) + response = client.get("/cart/1") + assert response.status_code == 200 + data = response.json() + assert data["id"] == 1 + assert isinstance(data["items"], list) + assert data["price"] == 0.0 + + +def test_get_cart_not_found(): + response = client.get("/cart/999") + assert response.status_code == 404 + assert response.json() == {"detail": "cart not found"} + + +def test_list_carts_filters(monkeypatch): + _carts[1] = {1: 2} + _carts[2] = {1: 5} + + monkeypatch.setattr("shop_api.utils.cart_utils.compute_cart", mock_compute_cart) + + resp = client.get("/cart/") + assert resp.status_code == 200 + assert len(resp.json()) == 2 + + resp = client.get("/cart/?min_price=30") + data = resp.json() + assert len(data) == 1 + assert data[0]["id"] == 2 + + resp = client.get("/cart/?max_price=20") + data = resp.json() + assert len(data) == 1 + assert data[0]["id"] == 1 + + resp = client.get("/cart/?min_quantity=3") + data = resp.json() + assert len(data) == 1 + assert data[0]["id"] == 2 + + resp = client.get("/cart/?max_quantity=3") + data = resp.json() + assert len(data) == 1 + assert data[0]["id"] == 1 + + resp = client.get("/cart/?offset=1&limit=1") + data = resp.json() + assert len(data) == 1 + assert data[0]["id"] == 2 + + +def test_add_item_success(monkeypatch): + _carts[1] = {} + monkeypatch.setattr("shop_api.utils.cart_utils.compute_cart", mock_compute_cart) + response = client.post("/cart/1/add/1") + assert response.status_code == 200 + data = response.json() + assert data["id"] == 1 + assert data["items"][0]["quantity"] == 1 + assert _carts[1][1] == 1 + + +def test_add_item_cart_not_found(): + response = client.post("/cart/999/add/1") + assert response.status_code == 404 + assert response.json() == {"detail": "cart not found"} + + +def test_add_item_item_not_found(): + _carts[1] = {} + response = client.post("/cart/1/add/999") + assert response.status_code == 404 + assert response.json() == {"detail": "item not found"} diff --git a/hw5/tests/test_chat_ws.py b/hw5/tests/test_chat_ws.py new file mode 100644 index 00000000..3fa82f4a --- /dev/null +++ b/hw5/tests/test_chat_ws.py @@ -0,0 +1,63 @@ +import pytest +from fastapi import FastAPI, WebSocketDisconnect +from fastapi.testclient import TestClient +from shop_api.routers import chat + +app = FastAPI() +app.include_router(chat.router) +client = TestClient(app) + + +@pytest.fixture(autouse=True) +def reset_chat_rooms(): + chat.chat_rooms.clear() + chat.usernames.clear() + yield + chat.chat_rooms.clear() + chat.usernames.clear() + + +def test_random_username_length(): + username = chat.random_username() + assert len(username) == 8 + assert username.isalnum() + + +def test_websocket_connect_and_disconnect(): + with client.websocket_connect("/chat/room1") as ws: + ws.send_text("test") + assert chat.chat_rooms.get("room1") == [] + assert len(chat.usernames) == 0 + + +def test_websocket_send_receive_between_two_clients(): + with client.websocket_connect("/chat/roomX") as ws1, \ + client.websocket_connect("/chat/roomX") as ws2: + + ws1.send_text("Hello") + msg = ws2.receive_text() + assert " :: Hello" in msg + + ws2.send_text("Hi there") + msg2 = ws1.receive_text() + assert " :: Hi there" in msg2 + + assert chat.chat_rooms.get("roomX") == [] + assert len(chat.usernames) == 0 + + +def test_websocket_multiple_messages_and_disconnect(): + with client.websocket_connect("/chat/roomY") as ws1, \ + client.websocket_connect("/chat/roomY") as ws2: + + ws1.send_text("First") + ws2.send_text("Second") + + msg2 = ws2.receive_text() + assert " :: First" in msg2 + + msg1 = ws1.receive_text() + assert " :: Second" in msg1 + + assert chat.chat_rooms.get("roomY") == [] + assert len(chat.usernames) == 0 diff --git a/hw5/tests/test_fastapi.py b/hw5/tests/test_fastapi.py new file mode 100644 index 00000000..aa0d0148 --- /dev/null +++ b/hw5/tests/test_fastapi.py @@ -0,0 +1,20 @@ +from fastapi.testclient import TestClient +from shop_api import main + +client = TestClient(main.app) + + +def test_app_exists(): + assert main.app is not None + + +def test_routers_included(): + route_paths = [route.path for route in main.app.routes] + assert "/item/" in route_paths or any(p.startswith("/item") for p in route_paths) + assert "/cart/" in route_paths or any(p.startswith("/cart") for p in route_paths) + assert "/chat/" in route_paths or any(p.startswith("/chat") for p in route_paths) + + +def test_main_app_root_status(): + response = client.get("/") + assert response.status_code in (404, 200) diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py new file mode 100644 index 00000000..e86ad656 --- /dev/null +++ b/hw5/tests/test_items.py @@ -0,0 +1,150 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from shop_api.routers import items +from shop_api.storage.memory import _items, _next_item_id, _lock + +app = FastAPI() +app.include_router(items.router) + +client = TestClient(app) + + +@pytest.fixture(autouse=True) +def reset_memory(): + global _items, _next_item_id, _lock + _items.clear() + _next_item_id = 1 + _lock = _lock.__class__() + yield + _items.clear() + _next_item_id = 1 + + +def test_create_item(): + payload = {"name": "TestItem", "price": 100.0} + resp = client.post("/item/", json=payload) + assert resp.status_code == 201 + data = resp.json() + assert data["id"] == 1 + assert data["name"] == "TestItem" + assert data["price"] == 100.0 + assert 1 in _items + + +def test_get_item_success(): + _items[1] = type("Item", (), {"id": 1, "name": "Item1", "price": 10.0, "deleted": False})() + resp = client.get("/item/1") + assert resp.status_code == 200 + data = resp.json() + assert data["id"] == 1 + assert data["name"] == "Item1" + + +def test_get_item_not_found(): + resp = client.get("/item/999") + assert resp.status_code == 404 + assert resp.json() == {"detail": "item not found"} + + # помеченный как deleted + _items[1] = type("Item", (), {"id": 1, "name": "Item1", "price": 10.0, "deleted": True})() + resp = client.get("/item/1") + assert resp.status_code == 404 + + +def test_list_items_filters(): + # создаем 3 элемента + _items[1] = type("Item", (), {"id": 1, "name": "A", "price": 10, "deleted": False})() + _items[2] = type("Item", (), {"id": 2, "name": "B", "price": 20, "deleted": False})() + _items[3] = type("Item", (), {"id": 3, "name": "C", "price": 30, "deleted": True})() + + # без фильтров, show_deleted=False + resp = client.get("/item/") + data = resp.json() + assert len(data) == 2 # не учитываем deleted + + # show_deleted=True + resp = client.get("/item/?show_deleted=true") + data = resp.json() + assert len(data) == 3 + + # min_price + resp = client.get("/item/?min_price=15") + data = resp.json() + assert all(d["price"] >= 15 for d in data) + + # max_price + resp = client.get("/item/?max_price=15") + data = resp.json() + assert all(d["price"] <= 15 for d in data) + + # offset & limit + resp = client.get("/item/?offset=1&limit=1") + data = resp.json() + assert len(data) == 1 + + +def test_replace_item_success(): + _items[1] = type("Item", (), {"id": 1, "name": "Old", "price": 10.0, "deleted": False})() + payload = {"name": "NewName", "price": 99.0} + resp = client.put("/item/1", json=payload) + assert resp.status_code == 200 + data = resp.json() + assert data["name"] == "NewName" + assert data["price"] == 99.0 + + +def test_replace_item_not_found(): + payload = {"name": "X", "price": 1.0} + resp = client.put("/item/999", json=payload) + assert resp.status_code == 404 + + +def test_patch_item_success(): + _items[1] = type("Item", (), {"id": 1, "name": "Old", "price": 10.0, "deleted": False})() + resp = client.patch("/item/1", json={"name": "New"}) + assert resp.status_code == 200 + assert _items[1].name == "New" + + resp = client.patch("/item/1", json={"price": 50}) + assert resp.status_code == 200 + assert _items[1].price == 50 + + resp = client.patch("/item/1", json={"name": "Final", "price": 100}) + assert resp.status_code == 200 + assert _items[1].name == "Final" + assert _items[1].price == 100 + + +def test_patch_item_not_found(): + resp = client.patch("/item/999", json={"name": "X"}) + assert resp.status_code == 404 + + +def test_patch_item_deleted(): + _items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": True})() + resp = client.patch("/item/1", json={"name": "Y"}) + assert resp.status_code == 304 + + +def test_patch_item_invalid_field(): + _items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": False})() + resp = client.patch("/item/1", json={"invalid": 123}) + assert resp.status_code == 422 + + resp = client.patch("/item/1", json={"price": -10}) + assert resp.status_code == 422 + + +def test_delete_item(): + # элемент существует + _items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": False})() + resp = client.delete("/item/1") + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert _items[1].deleted is True + + # элемент не существует + resp = client.delete("/item/999") + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} From 44c8edb610572672fa826a1175673ad238f0aaf3 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 26 Oct 2025 17:25:04 +0300 Subject: [PATCH 11/14] ci v3 updated to v4 --- .github/workflows/hw5-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hw5-ci.yml b/.github/workflows/hw5-ci.yml index fcf0f486..aa1df20e 100644 --- a/.github/workflows/hw5-ci.yml +++ b/.github/workflows/hw5-ci.yml @@ -42,13 +42,13 @@ jobs: tests/ - name: Upload coverage XML - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-xml path: coverage.xml - name: Upload coverage HTML - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-html path: htmlcov/ From 5a3c2cec690876a07a80add84059a7d602be7383 Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 26 Oct 2025 17:26:35 +0300 Subject: [PATCH 12/14] added pythonpath shop-api --- .github/workflows/hw5-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/hw5-ci.yml b/.github/workflows/hw5-ci.yml index aa1df20e..03fc8f77 100644 --- a/.github/workflows/hw5-ci.yml +++ b/.github/workflows/hw5-ci.yml @@ -33,6 +33,9 @@ jobs: pip install -r requirements.txt pip install pytest pytest-cov fastapi uvicorn httpx + - name: Set PYTHONPATH + run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV + - name: Run tests with coverage run: | pytest --cov=shop_api \ From d6bec8adaa2e1507c875a2264b2041c11420187c Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 26 Oct 2025 17:29:34 +0300 Subject: [PATCH 13/14] fixture added --- hw5/tests/conftest.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 hw5/tests/conftest.py diff --git a/hw5/tests/conftest.py b/hw5/tests/conftest.py new file mode 100644 index 00000000..7188b0e2 --- /dev/null +++ b/hw5/tests/conftest.py @@ -0,0 +1,19 @@ +import pytest +from shop_api.storage import memory + +class DummyLock: + def __enter__(self): + return self + def __exit__(self, exc_type, exc_val, exc_tb): + pass + +@pytest.fixture(autouse=True) +def patch_lock(monkeypatch): + monkeypatch.setattr(memory, "_lock", DummyLock()) + +@pytest.fixture(autouse=True) +def reset_memory(): + memory._carts.clear() + memory._items.clear() + memory._next_cart_id = 1 + memory._next_item_id = 1 From 7a4efcb27f0839db72a15b44075308209daa847c Mon Sep 17 00:00:00 2001 From: RomanKharkovskoy Date: Sun, 26 Oct 2025 17:36:42 +0300 Subject: [PATCH 14/14] dummylock update --- hw5/tests/conftest.py | 19 ------------ hw5/tests/test_carts.py | 53 +++++++++++++++++++-------------- hw5/tests/test_items.py | 66 ++++++++++++++++++----------------------- 3 files changed, 59 insertions(+), 79 deletions(-) delete mode 100644 hw5/tests/conftest.py diff --git a/hw5/tests/conftest.py b/hw5/tests/conftest.py deleted file mode 100644 index 7188b0e2..00000000 --- a/hw5/tests/conftest.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest -from shop_api.storage import memory - -class DummyLock: - def __enter__(self): - return self - def __exit__(self, exc_type, exc_val, exc_tb): - pass - -@pytest.fixture(autouse=True) -def patch_lock(monkeypatch): - monkeypatch.setattr(memory, "_lock", DummyLock()) - -@pytest.fixture(autouse=True) -def reset_memory(): - memory._carts.clear() - memory._items.clear() - memory._next_cart_id = 1 - memory._next_item_id = 1 diff --git a/hw5/tests/test_carts.py b/hw5/tests/test_carts.py index 7ff7d932..c6358576 100644 --- a/hw5/tests/test_carts.py +++ b/hw5/tests/test_carts.py @@ -2,37 +2,45 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from shop_api.routers import carts -from shop_api.storage.memory import _carts, _items, _next_cart_id, _lock +from shop_api.storage import memory from shop_api.schemas.cart import Cart, CartItem app = FastAPI() app.include_router(carts.router) - client = TestClient(app) @pytest.fixture(autouse=True) -def reset_memory(): - global _carts, _items, _next_cart_id, _lock - _carts.clear() - _items.clear() - _next_cart_id = 1 - _lock = _lock.__class__() - _items[1] = type("Item", (), {"name": "Item1", "price": 10.0, "deleted": False})() +def setup_memory(monkeypatch): + class DummyLock: + def __enter__(self): + return self + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + monkeypatch.setattr(memory, "_lock", DummyLock()) + + memory._carts.clear() + memory._items.clear() + memory._next_cart_id = 1 + memory._next_item_id = 1 + + memory._items[1] = type("Item", (), {"name": "Item1", "price": 10.0, "deleted": False})() yield - _carts.clear() - _items.clear() - _next_cart_id = 1 + memory._carts.clear() + memory._items.clear() + memory._next_cart_id = 1 + memory._next_item_id = 1 def mock_compute_cart(cart_id: int): - if cart_id not in _carts: + if cart_id not in memory._carts: raise KeyError - bag = _carts[cart_id] + bag = memory._carts[cart_id] items_out = [] total = 0.0 for iid, qty in bag.items(): - item = _items[iid] + item = memory._items[iid] items_out.append(CartItem( id=iid, name=item.name, @@ -50,11 +58,11 @@ def test_create_cart(): data = response.json() assert "id" in data assert response.headers["Location"] == f"/cart/{data['id']}" - assert data["id"] in _carts + assert data["id"] in memory._carts def test_get_cart_success(monkeypatch): - _carts[1] = {} + memory._carts[1] = {} monkeypatch.setattr("shop_api.utils.cart_utils.compute_cart", mock_compute_cart) response = client.get("/cart/1") assert response.status_code == 200 @@ -71,9 +79,8 @@ def test_get_cart_not_found(): def test_list_carts_filters(monkeypatch): - _carts[1] = {1: 2} - _carts[2] = {1: 5} - + memory._carts[1] = {1: 2} + memory._carts[2] = {1: 5} monkeypatch.setattr("shop_api.utils.cart_utils.compute_cart", mock_compute_cart) resp = client.get("/cart/") @@ -107,14 +114,14 @@ def test_list_carts_filters(monkeypatch): def test_add_item_success(monkeypatch): - _carts[1] = {} + memory._carts[1] = {} monkeypatch.setattr("shop_api.utils.cart_utils.compute_cart", mock_compute_cart) response = client.post("/cart/1/add/1") assert response.status_code == 200 data = response.json() assert data["id"] == 1 assert data["items"][0]["quantity"] == 1 - assert _carts[1][1] == 1 + assert memory._carts[1][1] == 1 def test_add_item_cart_not_found(): @@ -124,7 +131,7 @@ def test_add_item_cart_not_found(): def test_add_item_item_not_found(): - _carts[1] = {} + memory._carts[1] = {} response = client.post("/cart/1/add/999") assert response.status_code == 404 assert response.json() == {"detail": "item not found"} diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py index e86ad656..2893873d 100644 --- a/hw5/tests/test_items.py +++ b/hw5/tests/test_items.py @@ -2,23 +2,24 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from shop_api.routers import items -from shop_api.storage.memory import _items, _next_item_id, _lock +from shop_api.storage import memory app = FastAPI() app.include_router(items.router) - client = TestClient(app) - @pytest.fixture(autouse=True) -def reset_memory(): - global _items, _next_item_id, _lock - _items.clear() - _next_item_id = 1 - _lock = _lock.__class__() - yield - _items.clear() - _next_item_id = 1 +def setup_memory(monkeypatch): + class DummyLock: + def __enter__(self): + return self + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + monkeypatch.setattr(memory, "_lock", DummyLock()) + + memory._items.clear() + memory._next_item_id = 1 def test_create_item(): @@ -29,11 +30,11 @@ def test_create_item(): assert data["id"] == 1 assert data["name"] == "TestItem" assert data["price"] == 100.0 - assert 1 in _items + assert 1 in memory._items def test_get_item_success(): - _items[1] = type("Item", (), {"id": 1, "name": "Item1", "price": 10.0, "deleted": False})() + memory._items[1] = type("Item", (), {"id": 1, "name": "Item1", "price": 10.0, "deleted": False})() resp = client.get("/item/1") assert resp.status_code == 200 data = resp.json() @@ -46,46 +47,39 @@ def test_get_item_not_found(): assert resp.status_code == 404 assert resp.json() == {"detail": "item not found"} - # помеченный как deleted - _items[1] = type("Item", (), {"id": 1, "name": "Item1", "price": 10.0, "deleted": True})() + memory._items[1] = type("Item", (), {"id": 1, "name": "Item1", "price": 10.0, "deleted": True})() resp = client.get("/item/1") assert resp.status_code == 404 def test_list_items_filters(): - # создаем 3 элемента - _items[1] = type("Item", (), {"id": 1, "name": "A", "price": 10, "deleted": False})() - _items[2] = type("Item", (), {"id": 2, "name": "B", "price": 20, "deleted": False})() - _items[3] = type("Item", (), {"id": 3, "name": "C", "price": 30, "deleted": True})() + memory._items[1] = type("Item", (), {"id": 1, "name": "A", "price": 10, "deleted": False})() + memory._items[2] = type("Item", (), {"id": 2, "name": "B", "price": 20, "deleted": False})() + memory._items[3] = type("Item", (), {"id": 3, "name": "C", "price": 30, "deleted": True})() - # без фильтров, show_deleted=False resp = client.get("/item/") data = resp.json() - assert len(data) == 2 # не учитываем deleted + assert len(data) == 2 - # show_deleted=True resp = client.get("/item/?show_deleted=true") data = resp.json() assert len(data) == 3 - # min_price resp = client.get("/item/?min_price=15") data = resp.json() assert all(d["price"] >= 15 for d in data) - # max_price resp = client.get("/item/?max_price=15") data = resp.json() assert all(d["price"] <= 15 for d in data) - # offset & limit resp = client.get("/item/?offset=1&limit=1") data = resp.json() assert len(data) == 1 def test_replace_item_success(): - _items[1] = type("Item", (), {"id": 1, "name": "Old", "price": 10.0, "deleted": False})() + memory._items[1] = type("Item", (), {"id": 1, "name": "Old", "price": 10.0, "deleted": False})() payload = {"name": "NewName", "price": 99.0} resp = client.put("/item/1", json=payload) assert resp.status_code == 200 @@ -101,19 +95,19 @@ def test_replace_item_not_found(): def test_patch_item_success(): - _items[1] = type("Item", (), {"id": 1, "name": "Old", "price": 10.0, "deleted": False})() + memory._items[1] = type("Item", (), {"id": 1, "name": "Old", "price": 10.0, "deleted": False})() resp = client.patch("/item/1", json={"name": "New"}) assert resp.status_code == 200 - assert _items[1].name == "New" + assert memory._items[1].name == "New" resp = client.patch("/item/1", json={"price": 50}) assert resp.status_code == 200 - assert _items[1].price == 50 + assert memory._items[1].price == 50 resp = client.patch("/item/1", json={"name": "Final", "price": 100}) assert resp.status_code == 200 - assert _items[1].name == "Final" - assert _items[1].price == 100 + assert memory._items[1].name == "Final" + assert memory._items[1].price == 100 def test_patch_item_not_found(): @@ -122,13 +116,13 @@ def test_patch_item_not_found(): def test_patch_item_deleted(): - _items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": True})() + memory._items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": True})() resp = client.patch("/item/1", json={"name": "Y"}) assert resp.status_code == 304 def test_patch_item_invalid_field(): - _items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": False})() + memory._items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": False})() resp = client.patch("/item/1", json={"invalid": 123}) assert resp.status_code == 422 @@ -137,14 +131,12 @@ def test_patch_item_invalid_field(): def test_delete_item(): - # элемент существует - _items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": False})() + memory._items[1] = type("Item", (), {"id": 1, "name": "X", "price": 10.0, "deleted": False})() resp = client.delete("/item/1") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} - assert _items[1].deleted is True + assert memory._items[1].deleted is True - # элемент не существует resp = client.delete("/item/999") assert resp.status_code == 200 assert resp.json() == {"status": "ok"}