From 36a77ebc27618b42858e5f3de890cc3e7201d2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 28 Sep 2025 23:13:41 +0300 Subject: [PATCH 1/8] Implement ASGI application with fibonacci, factorial, and mean endpoints --- hw1/app.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/hw1/app.py b/hw1/app.py index 6107b870..00f79ffe 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,5 +1,84 @@ from typing import Any, Awaitable, Callable +import math +import json +from urllib.parse import parse_qs +def fibonacci(n): + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a + +def mean(nums): + return sum(nums) / len(nums) + +async def send_response(send, status, data): + body = json.dumps(data).encode() + await send({ + "type": "http.response.start", + "status": status, + "headers": [(b"content-type", b"application/json")], + }) + await send({"type": "http.response.body", "body": body}) + +async def factorial_endpoint(send, query_string): + q = parse_qs(query_string) + nums = q.get("n") + if not nums: + await send_response(send, 422, {}) + return + + try: + n = int(nums[0]) + except ValueError: + await send_response(send, 422, {}) + return + + if n < 0: + await send_response(send, 400, {}) + return + + await send_response(send, 200, {"result": math.factorial(n)}) + +async def fibonacci_endpoint(send, n_str): + try: + n = int(n_str) + except ValueError: + await send_response(send, 422, {}) + return + + if n < 0: + await send_response(send, 400, {"error": "n must be >= 0"}) + return + await send_response(send, 200, {"result": fibonacci(n)}) + +async def mean_endpoint(send, receive): + body = b"" + while True: + message = await receive() + if message["type"] == "http.request": + body += message.get("body", b"") + if not message.get("more_body", False): + break + if not body: + await send_response(send, 422, {}) + return + try: + data = json.loads(body.decode()) + except (json.JSONDecodeError, UnicodeDecodeError): + await send_response(send, 422, {}) + return + if not isinstance(data, list): + await send_response(send, 422, {}) + return + if len(data) == 0: + await send_response(send, 400, {}) + return + try: + await send_response(send, 200, {"result": mean(data)}) + except (TypeError, ValueError): + await send_response(send, 422, {}) + return async def application( scope: dict[str, Any], @@ -13,6 +92,35 @@ async def application( send: Корутина для отправки сообщений клиенту """ # TODO: Ваша реализация здесь + + if scope['type'] == 'lifespan': + while True: + message = await receive() + if message['type'] == 'lifespan.startup': + await send({'type': 'lifespan.startup.complete'}) + elif message['type'] == 'lifespan.shutdown': + await send({'type': 'lifespan.shutdown.complete'}) + break + return + if scope['type'] != 'http': + return + method = scope['method'] + path = scope['path'] + query_string = scope.get('query_string', b'').decode() + + if method != 'GET': + await send_response(send, 404, {"error": "Not found"}) + return + + if path == '/factorial': + await factorial_endpoint(send, query_string) + elif path.startswith('/fibonacci/'): + n_str = path[len('/fibonacci/'):] + await fibonacci_endpoint(send, n_str) + elif path == '/mean': + await mean_endpoint(send, receive) + else: + await send_response(send, 404, {"error": "Not found"}) if __name__ == "__main__": import uvicorn From 9b96c350850d8df7d623f2a82cc5e777237dce22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 5 Oct 2025 23:32:15 +0300 Subject: [PATCH 2/8] hw2 --- hw2/hw/shop_api/main.py | 189 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..23fc04f4 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,190 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, Query, Path, Response +from pydantic import BaseModel, Field +from typing import Optional, List app = FastAPI(title="Shop API") + +ITEMS = {} # товары{"id","name","price","deleted"} +CARTS = {} # корзина{item_id: quantity} +_item_id = 1 +_cart_id = 1 + +class ItemCreate(BaseModel): + name: str = Field(..., min_length=1) + price: float = Field(..., ge=0.0) + +class ItemPut(BaseModel): + name: str = Field(..., min_length=1) + price: float = Field(..., ge=0.0) + +class ItemPatch(BaseModel): + name: Optional[str] = Field(None, min_length=1) + price: Optional[float] = Field(None, ge=0.0) + class Config: + extra = "forbid" + +class ItemOut(BaseModel): + id: int + name: str + price: float + deleted: bool = False + +class CartItemOut(BaseModel): + id: int + name: str + quantity: int + available: bool + +class CartOut(BaseModel): + id: int + items: List[CartItemOut] + price: float + +# utils +def get_item_or_404(item_id: int, allow_deleted: bool = False): + """Возвращает товар по id (или 404, если не найден)""" + it = ITEMS.get(item_id) + if not it: + raise HTTPException(404, "Item not found") + if it["deleted"] and not allow_deleted: + raise HTTPException(404, "Item not found") + return it + +def get_cart_or_404(cart_id: int): + """Возвращает корзину по id (или 404, если не найдена)""" + cart = CARTS.get(cart_id) + if cart is None: + raise HTTPException(404, "Cart not found") + return cart + +def cart_snapshot(cart_id: int): + """Формирует текущее состояние корзины с товарами и общей ценой""" + cart = get_cart_or_404(cart_id) + items_out = [] + total = 0.0 + for iid, qty in cart.items(): + it = ITEMS.get(iid) + if not it: + continue + items_out.append(CartItemOut( + id=it["id"], + name=it["name"], + quantity=qty, + available=not it["deleted"], + )) + total += float(it["price"]) * qty # по текущей цене + return CartOut(id=cart_id, items=items_out, price=total) + +# cart +@app.post("/cart", status_code=201) +def create_cart(response: Response): + """Создаёт новую корзину и возвращает её id""" + global _cart_id + cid = _cart_id + _cart_id += 1 + CARTS[cid] = {} + response.headers["Location"] = f"/cart/{cid}" + return {"id": cid} + +@app.get("/cart/{cart_id}", response_model=CartOut) +def get_cart(cart_id: int = Path(..., ge=1)): + """Возвращает корзину по id""" + return cart_snapshot(cart_id) + +@app.get("/cart", response_model=List[CartOut]) +def list_carts( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0.0), + max_price: Optional[float] = Query(None, ge=0.0), + min_quantity: Optional[int] = Query(None, ge=0), + max_quantity: Optional[int] = Query(None, ge=0), +): + """Возвращает список корзин с фильтрами и пагинацией""" + snaps = [] + for cid in sorted(CARTS.keys()): + snap = cart_snapshot(cid) + qty = sum(ci.quantity for ci in snap.items) + if min_quantity is not None and qty < min_quantity: + continue + if max_quantity is not None and qty > max_quantity: + continue + if min_price is not None and snap.price < min_price: + continue + if max_price is not None and snap.price > max_price: + continue + snaps.append(snap) + return snaps[offset: offset + limit] + +@app.post("/cart/{cart_id}/add/{item_id}", response_model=CartOut) +def add_to_cart(cart_id: int = Path(..., ge=1), item_id: int = Path(..., ge=1)): + """Добавляет товар в корзину/увеличивает кол-во, если уже есть""" + cart = get_cart_or_404(cart_id) + it = get_item_or_404(item_id, allow_deleted=True) + if it["deleted"]: + raise HTTPException(400, "Cannot add a deleted item to cart") + cart[item_id] = cart.get(item_id, 0) + 1 + return cart_snapshot(cart_id) + +# --- item --- +@app.post("/item", response_model=ItemOut, status_code=201) +def create_item(body: ItemCreate): + """Создаёт новый товар""" + global _item_id + iid = _item_id + _item_id += 1 + ITEMS[iid] = {"id": iid, "name": body.name, "price": float(body.price), "deleted": False} + return ItemOut(**ITEMS[iid]) + +@app.get("/item/{item_id}", response_model=ItemOut) +def get_item(item_id: int = Path(..., ge=1)): + """Возвращает товар по id (404, если удалён)""" + return ItemOut(**get_item_or_404(item_id)) + +@app.get("/item", response_model=List[ItemOut]) +def list_items( + offset: int = Query(0, ge=0), + limit: int = Query(10, gt=0), + min_price: Optional[float] = Query(None, ge=0.0), + max_price: Optional[float] = Query(None, ge=0.0), + show_deleted: bool = Query(False), +): + """Возвращает список товаров с фильтрацией и пагинацией""" + out = [] + for iid in sorted(ITEMS.keys()): + it = ITEMS[iid] + if not show_deleted and it["deleted"]: + continue + if min_price is not None and float(it["price"]) < min_price: + continue + if max_price is not None and float(it["price"]) > max_price: + continue + out.append(ItemOut(**it)) + return out[offset: offset + limit] + +@app.put("/item/{item_id}", response_model=ItemOut) +def put_item(item_id: int = Path(..., ge=1), body: ItemPut = ...): + """Полностью обновляет товар (name/price)""" + it = get_item_or_404(item_id) + it["name"] = body.name + it["price"] = float(body.price) + return ItemOut(**it) + +@app.patch("/item/{item_id}", response_model=ItemOut) +def patch_item(item_id: int = Path(..., ge=1), body: ItemPatch = ...): + """Частично обновляет товар. Если удалён — 304""" + it = get_item_or_404(item_id, allow_deleted=True) + if it["deleted"]: + return Response(status_code=304) + if body.name is not None: + it["name"] = body.name + if body.price is not None: + it["price"] = float(body.price) + return ItemOut(**it) + +@app.delete("/item/{item_id}") +def delete_item(item_id: int = Path(..., ge=1)): + """Помечает товар как удалённый (200 OK)""" + it = get_item_or_404(item_id, allow_deleted=True) + it["deleted"] = True + return {} \ No newline at end of file From 5cc2155e387d50c74f44851c1edd921df38a38d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 12 Oct 2025 23:45:08 +0300 Subject: [PATCH 3/8] hw3 --- hw2/hw/Dockerfile | 23 +++++++++++++++++++++++ hw2/hw/docker-compose.yml | 31 +++++++++++++++++++++++++++++++ hw2/hw/prometheus/prometheus.yml | 10 ++++++++++ hw2/hw/requirements.txt | 1 + hw2/hw/shop_api/main.py | 2 ++ 5 files changed, 67 insertions(+) create mode 100644 hw2/hw/Dockerfile create mode 100644 hw2/hw/docker-compose.yml create mode 100644 hw2/hw/prometheus/prometheus.yml diff --git a/hw2/hw/Dockerfile b/hw2/hw/Dockerfile new file mode 100644 index 00000000..27b0b636 --- /dev/null +++ b/hw2/hw/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12 AS base + +ARG PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PIP_NO_CACHE_DIR=on \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=500 + +RUN apt-get update && apt-get install -y gcc +RUN python -m pip install --upgrade pip + +WORKDIR $APP_ROOT/src +COPY . ./ + +ENV VIRTUAL_ENV=$APP_ROOT/src/.venv \ + PATH=$APP_ROOT/src/.venv/bin:$PATH + +RUN pip install -r requirements.txt + +FROM base as local + +CMD ["uvicorn", "shop_api.main:app", "--port", "8080", "--host", "0.0.0.0"] diff --git a/hw2/hw/docker-compose.yml b/hw2/hw/docker-compose.yml new file mode 100644 index 00000000..900e804f --- /dev/null +++ b/hw2/hw/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3" + +services: + + local: + build: + context: . + dockerfile: ./Dockerfile + target: local + restart: always + ports: + - 8080:8080 + + grafana: + image: grafana/grafana:latest + ports: + - 3000:3000 + restart: always + + prometheus: + image: prom/prometheus + volumes: + - ./prometheus/:/etc/prometheus/ + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/usr/share/prometheus/console_libraries" + - "--web.console.templates=/usr/share/prometheus/consoles" + ports: + - 9090:9090 + restart: always \ No newline at end of file diff --git a/hw2/hw/prometheus/prometheus.yml b/hw2/hw/prometheus/prometheus.yml new file mode 100644 index 00000000..6bdf88e7 --- /dev/null +++ b/hw2/hw/prometheus/prometheus.yml @@ -0,0 +1,10 @@ +global: + scrape_interval: 10s + evaluation_interval: 10s + +scrape_configs: + - job_name: demo-service-local + metrics_path: /metrics + static_configs: + - targets: + - local:8080 diff --git a/hw2/hw/requirements.txt b/hw2/hw/requirements.txt index 207dcf5c..bfebfeda 100644 --- a/hw2/hw/requirements.txt +++ b/hw2/hw/requirements.txt @@ -1,6 +1,7 @@ # Основные зависимости для ASGI приложения fastapi>=0.117.1 uvicorn>=0.24.0 +prometheus-fastapi-instrumentator # Зависимости для тестирования pytest>=7.4.0 diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index 23fc04f4..7a86c9b7 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,8 +1,10 @@ from fastapi import FastAPI, HTTPException, Query, Path, Response from pydantic import BaseModel, Field from typing import Optional, List +from prometheus_fastapi_instrumentator import Instrumentator app = FastAPI(title="Shop API") +Instrumentator().instrument(app).expose(app) ITEMS = {} # товары{"id","name","price","deleted"} CARTS = {} # корзина{item_id: quantity} From 4916113dac50bbb824ec88ac3c740fde5714a691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Mon, 13 Oct 2025 00:25:06 +0300 Subject: [PATCH 4/8] add dashboard json --- hw2/hw/grafana/dashboard.json | 258 ++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 hw2/hw/grafana/dashboard.json diff --git a/hw2/hw/grafana/dashboard.json b/hw2/hw/grafana/dashboard.json new file mode 100644 index 00000000..564e79e8 --- /dev/null +++ b/hw2/hw/grafana/dashboard.json @@ -0,0 +1,258 @@ +{ + "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": "cf0uyho4gzn5sd" + }, + "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 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{instance=\"local:8080\", job=\"demo-service-local\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "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": [ + { + "editorMode": "builder", + "expr": "rate(process_cpu_seconds_total[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "cpu usage, %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cf0uyho4gzn5sd" + }, + "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": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cf0uyho4gzn5sd" + }, + "editorMode": "builder", + "expr": "sum(rate(http_requests_total[1m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "incoming http requests (1m rate)", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "hw3", + "uid": "ad6jrql", + "version": 7 +} \ No newline at end of file From 633fec346a147ef5567d07046a987d33bd06638c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 26 Oct 2025 23:38:27 +0300 Subject: [PATCH 5/8] hw5 --- .github/workflows/hw4_5-tests.yml | 57 +++++++++++++++++++++++++++++++ hw2/hw/requirements.txt | 4 +++ hw2/hw/tests/test.py | 29 ++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 .github/workflows/hw4_5-tests.yml create mode 100644 hw2/hw/tests/test.py diff --git a/.github/workflows/hw4_5-tests.yml b/.github/workflows/hw4_5-tests.yml new file mode 100644 index 00000000..e8a271c1 --- /dev/null +++ b/.github/workflows/hw4_5-tests.yml @@ -0,0 +1,57 @@ +name: Run tests HW2 + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: hw2_db + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U user -d hw2_db" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + env: + DATABASE_URL: postgresql+psycopg2://user:password@localhost:5432/hw2_db + PYTHONPATH: ./hw2/hw + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r hw2/hw/requirements.txt + pip install pytest pytest-cov faker fastapi psycopg2-binary + + - name: Wait for PostgreSQL + run: | + until pg_isready -h localhost -p 5432 -U user; do + echo "Waiting for Postgres..." + sleep 2 + done + + - name: Run tests with coverage + working-directory: hw2/hw + run: | + pytest --cov=shop_api --cov-report=term-missing --cov-fail-under=95 diff --git a/hw2/hw/requirements.txt b/hw2/hw/requirements.txt index 9df2dff0..8c2b9d75 100644 --- a/hw2/hw/requirements.txt +++ b/hw2/hw/requirements.txt @@ -3,6 +3,10 @@ fastapi>=0.117.1 uvicorn>=0.24.0 prometheus-fastapi-instrumentator +# Тесты +pytest>=8.2.0 +pytest-cov>=4.1.0 + # БД SQLAlchemy>=2.0.30 psycopg2-binary>=2.9.9 diff --git a/hw2/hw/tests/test.py b/hw2/hw/tests/test.py new file mode 100644 index 00000000..9161e490 --- /dev/null +++ b/hw2/hw/tests/test.py @@ -0,0 +1,29 @@ +from http import HTTPStatus +from fastapi.testclient import TestClient +from shop_api.main import app + +client = TestClient(app) + + +def test_get_item_not_found(): + """Проверяет 404 при запросе несуществующего товара""" + resp = client.get("/item/999999") + assert resp.status_code == HTTPStatus.NOT_FOUND + + +def test_get_cart_not_found(): + """Проверяет 404 при запросе несуществующей корзины""" + resp = client.get("/cart/999999") + assert resp.status_code == HTTPStatus.NOT_FOUND + + +def test_add_deleted_item_to_cart_returns_400(): + """Проверяет 400 при добавлении удалённого товара в корзину""" + # создаём корзину + cart_id = client.post("/cart").json()["id"] + # создаём и удаляем товар + item_id = client.post("/item", json={"name": "X", "price": 1.0}).json()["id"] + client.delete(f"/item/{item_id}") + # пытаемся добавить удалённый товар + resp = client.post(f"/cart/{cart_id}/add/{item_id}") + assert resp.status_code == HTTPStatus.BAD_REQUEST From c3470de0eb1f46d27dc56e9342ffebc0a5bf7958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 26 Oct 2025 23:40:21 +0300 Subject: [PATCH 6/8] fix --- .github/workflows/hw4_5-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hw4_5-tests.yml b/.github/workflows/hw4_5-tests.yml index e8a271c1..06546ed0 100644 --- a/.github/workflows/hw4_5-tests.yml +++ b/.github/workflows/hw4_5-tests.yml @@ -1,4 +1,4 @@ -name: Run tests HW2 +name: Run tests HW4 and HW5 on: push: From 2e0806d622cacaf31b8fbd40435d2e659925625d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 26 Oct 2025 23:47:05 +0300 Subject: [PATCH 7/8] small fix --- .github/workflows/hw4_5-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hw4_5-tests.yml b/.github/workflows/hw4_5-tests.yml index 06546ed0..d6562581 100644 --- a/.github/workflows/hw4_5-tests.yml +++ b/.github/workflows/hw4_5-tests.yml @@ -42,7 +42,6 @@ jobs: run: | python -m pip install --upgrade pip pip install -r hw2/hw/requirements.txt - pip install pytest pytest-cov faker fastapi psycopg2-binary - name: Wait for PostgreSQL run: | @@ -53,5 +52,8 @@ jobs: - name: Run tests with coverage working-directory: hw2/hw + env: + PYTHONPATH: . + DATABASE_URL: postgresql+psycopg2://user:password@localhost:5432/hw2_db run: | - pytest --cov=shop_api --cov-report=term-missing --cov-fail-under=95 + pytest --cov=shop_api --cov-report=term-missing --cov-fail-under=98 From 4c2df09f458795a5a2a4c1993808c25711e0ec30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8B=D1=81=D0=BA=D0=B8=D0=BD=D0=B0=20=D0=94=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0?= Date: Sun, 26 Oct 2025 23:49:25 +0300 Subject: [PATCH 8/8] update requirements --- .github/workflows/hw4_5-tests.yml | 4 ---- hw2/hw/requirements.txt | 9 +++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/hw4_5-tests.yml b/.github/workflows/hw4_5-tests.yml index d6562581..9d875bf7 100644 --- a/.github/workflows/hw4_5-tests.yml +++ b/.github/workflows/hw4_5-tests.yml @@ -25,10 +25,6 @@ jobs: --health-timeout=5s --health-retries=10 - env: - DATABASE_URL: postgresql+psycopg2://user:password@localhost:5432/hw2_db - PYTHONPATH: ./hw2/hw - steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/hw2/hw/requirements.txt b/hw2/hw/requirements.txt index 8c2b9d75..634b12b1 100644 --- a/hw2/hw/requirements.txt +++ b/hw2/hw/requirements.txt @@ -6,13 +6,10 @@ prometheus-fastapi-instrumentator # Тесты pytest>=8.2.0 pytest-cov>=4.1.0 +pytest-asyncio>=0.21.0 +httpx>=0.27.2 +Faker>=37.8.0 # БД SQLAlchemy>=2.0.30 psycopg2-binary>=2.9.9 - -# Зависимости для тестирования -pytest>=7.4.0 -pytest-asyncio>=0.21.0 -httpx>=0.27.2 -Faker>=37.8.0