diff --git a/hw1/app.py b/hw1/app.py index 6107b870..55bb4104 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,5 +1,7 @@ from typing import Any, Awaitable, Callable - +import json +import math +from urllib.parse import parse_qs async def application( scope: dict[str, Any], @@ -12,8 +14,135 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + + if scope['type'] == 'lifespan': + while True: + message = await receive() + if message['type'] == 'lifespan.startup': + await send({'type': 'lifespan.startup.complete'}) + elif message['type'] == 'lifespan.shutdown': + await send({'type': 'lifespan.shutdown.complete'}) + break + return + + if scope['type'] != 'http': + return + + method = scope['method'] + path = scope['path'] + query_string = scope.get('query_string', b'').decode('utf-8') + query_params = parse_qs(query_string) + + async def send_response(status_code, body): + await send({ + 'type':'http.response.start', + 'status': status_code, + 'headers': [ + [b'content-type', b'application/json'], + ], + }) + await send({ + 'type': 'http.response.body', + 'body': json.dumps(body).encode('utf-8'), + }) + + async def receive_body(): + 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 body: + try: + return json.loads(body.decode('utf-8')) + except json.JSONDecodeError: + return None + return None + + if method != 'GET': + await send_response(404, {}) + return + + try: + if path == '/factorial': + if 'n' not in query_params or not query_params['n'][0]: + await send_response(422, {}) + return + + try: + n = int(query_params['n'][0]) + if n < 0: + await send_response(400, {}) + return + + result = math.factorial(n) + await send_response(200, {'result': result}) + return + except ValueError: + await send_response(422, {}) + return + + elif path.startswith('/fibonacci/'): + try: + n_str = path[11:] + if not n_str: + await send_response(422, {}) + return + n = int(n_str) + + if n < 0: + await send_response(400, {}) + return + if n == 0: + await send_response(200, {'result': 0}) + return + elif n == 1: + await send_response(200, {'result': 1}) + return + else: + a, b = 0, 1 + for _ in range(2, n + 1): + a, b = b, a + b + await send_response(200, {'result': b}) + return + + except Exception: + await send_response(422, {}) + return + + elif path == '/mean': + body = await receive_body() + + if body is None: + await send_response(422, {}) + return + + if not isinstance(body, list): + await send_response(400, {}) + return + + if len(body) == 0: + await send_response(400, {}) + return + try: + numbers = [float(x) for x in body] + mean_value = sum(numbers) / len(numbers) + await send_response(200, {'result': mean_value}) + return + except (ValueError, TypeError): + await send_response(400, {}) + return + + else: + await send_response(404, {}) + return + + except Exception: + await send_response(500, {}) + return if __name__ == "__main__": import uvicorn - uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) + uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/hw2/hw/.dockerignore b/hw2/hw/.dockerignore new file mode 100644 index 00000000..17080605 --- /dev/null +++ b/hw2/hw/.dockerignore @@ -0,0 +1,26 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.git +.gitignore +.env +.venv +env/ +venv/ +ENV/ +.pytest_cache +.coverage +htmlcov/ +*.log +.DS_Store +Thumbs.db +README.md +docker-compose.yml +Dockerfile \ No newline at end of file diff --git a/hw2/hw/Dockerfile b/hw2/hw/Dockerfile new file mode 100644 index 00000000..1904bc55 --- /dev/null +++ b/hw2/hw/Dockerfile @@ -0,0 +1,21 @@ + +FROM python:3.13-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +RUN pip install --no-cache-dir prometheus-client prometheus-fastapi-instrumentator + +COPY shop_api/ ./shop_api/ + +EXPOSE 8000 + + +ENV PYTHONUNBUFFERED=1 + +CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/hw2/hw/docker-compose.yml b/hw2/hw/docker-compose.yml new file mode 100644 index 00000000..9d30d877 --- /dev/null +++ b/hw2/hw/docker-compose.yml @@ -0,0 +1,65 @@ +version: '3.8' + +services: + shop-api: + build: + context: . + dockerfile: Dockerfile + container_name: shop-api + ports: + - "8000:8000" + networks: + - monitoring + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/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' + networks: + - monitoring + restart: unless-stopped + depends_on: + - shop-api + + grafana: + image: grafana/grafana:latest + container_name: grafana + ports: + - "3000:3000" + volumes: + - grafana-data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/var/lib/grafana/dashboards + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - monitoring + restart: unless-stopped + depends_on: + - prometheus + +networks: + monitoring: + driver: bridge + +volumes: + prometheus-data: + grafana-data: \ No newline at end of file diff --git a/hw2/hw/grafana/dashboards/shop-api-dashboard.json b/hw2/hw/grafana/dashboards/shop-api-dashboard.json new file mode 100644 index 00000000..07257adc --- /dev/null +++ b/hw2/hw/grafana/dashboards/shop-api-dashboard.json @@ -0,0 +1,549 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Requests/sec", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "expr": "rate(http_requests_total{job=\"shop-api\"}[1m])", + "refId": "A", + "legendFormat": "{{method}} {{handler}}" + } + ], + "title": "Request Rate (requests/sec)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 0.5 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "" + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job=\"shop-api\"}[5m]))", + "refId": "A" + } + ], + "title": "Response Time (95th percentile)", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + } + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": ["value", "percent"] + }, + "pieType": "donut", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "expr": "sum by (status) (http_requests_total{job=\"shop-api\"})", + "refId": "A", + "legendFormat": "{{status}}" + } + ], + "title": "HTTP Status Codes Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Requests", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": ["sum"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "expr": "sum by (handler) (increase(http_requests_total{job=\"shop-api\"}[5m]))", + "refId": "A", + "legendFormat": "{{handler}}" + } + ], + "title": "Requests by Endpoint (last 5 min)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "" + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "expr": "sum(increase(http_requests_total{job=\"shop-api\"}[1h]))", + "refId": "A" + } + ], + "title": "Total Requests (last hour)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.05 + }, + { + "color": "red", + "value": 0.1 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 16 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "mean" + ], + "fields": "" + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "expr": "rate(http_request_duration_seconds_sum{job=\"shop-api\"}[5m]) / rate(http_request_duration_seconds_count{job=\"shop-api\"}[5m])", + "refId": "A" + } + ], + "title": "Average Response Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 16 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "" + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "expr": "up{job=\"shop-api\"}", + "refId": "A" + } + ], + "title": "Service Status (1=UP, 0=DOWN)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 16 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "mean" + ], + "fields": "" + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "expr": "rate(http_request_size_bytes_sum{job=\"shop-api\"}[5m]) / rate(http_request_size_bytes_count{job=\"shop-api\"}[5m])", + "refId": "A" + } + ], + "title": "Average Request Size", + "type": "stat" + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": ["shop-api", "fastapi"], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Shop API Dashboard", + "uid": "shop-api-dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/hw2/hw/grafana/provisioning/dashboards/dashboard.yml b/hw2/hw/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 00000000..a8c80567 --- /dev/null +++ b/hw2/hw/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: 'Shop API Dashboards' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards \ No newline at end of file diff --git a/hw2/hw/grafana/provisioning/datasources/prometheus.yml b/hw2/hw/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 00000000..fb0e312f --- /dev/null +++ b/hw2/hw/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,13 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true + uid: prometheus + jsonData: + timeInterval: "5s" + httpMethod: POST \ 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..7aba99cf --- /dev/null +++ b/hw2/hw/prometheus/prometheus.yml @@ -0,0 +1,17 @@ + +global: + scrape_interval: 15s + evaluation_interval: 15s + external_labels: + monitor: 'shop-api-monitor' + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'shop-api' + scrape_interval: 5s + static_configs: + - targets: ['shop-api:8000'] + metrics_path: '/metrics' \ No newline at end of file diff --git a/hw2/hw/requirements.txt b/hw2/hw/requirements.txt index 207dcf5c..0328edee 100644 --- a/hw2/hw/requirements.txt +++ b/hw2/hw/requirements.txt @@ -1,9 +1,17 @@ + # Основные зависимости для ASGI приложения fastapi>=0.117.1 uvicorn>=0.24.0 + +websockets>=12.0 + +# Prometheus metrics +prometheus-client>=0.19.0 +prometheus-fastapi-instrumentator>=6.1.0 + # Зависимости для тестирования pytest>=7.4.0 pytest-asyncio>=0.21.0 httpx>=0.27.2 -Faker>=37.8.0 +Faker>=37.8.0 \ No newline at end of file diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..4544784f 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,274 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, Response, Query, WebSocket, WebSocketDisconnect +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from http import HTTPStatus +import random +from prometheus_fastapi_instrumentator import Instrumentator app = FastAPI(title="Shop API") + +instrumentator = Instrumentator() +instrumentator.instrument(app).expose(app) + +class ItemCreate(BaseModel): + name: str + price: float = Field(gt=0) + +class ItemUpdate(BaseModel): + name: str + price: float = Field(gt=0) + +class ItemPatch(BaseModel): + model_config = ConfigDict(extra='forbid') + + name: Optional[str] = None + price: Optional[float] = Field(None, gt=0) + +class Item(BaseModel): + id: int + name: str + price: float + deleted: bool = False + +class CartItem(BaseModel): + id: int + name: str + quantity: int + available: bool + +class Cart(BaseModel): + id: int + items: list[CartItem] + price: float + +class CartIdResponse(BaseModel): + id: int + +items_db: dict[int, Item] = {} +carts_db: dict[int, dict[int, int]] = {} +item_counter = 0 +cart_counter = 0 + +chat_rooms: dict[str, list[tuple[WebSocket, str]]] = {} + +def generate_username(): + adjectives = ["Happy", "Clever", "Brave", "Swift", "Strong", "Wise", "Cool", "Epic"] + nouns = ["Panda", "Tiger", "Eagle", "Dragon", "Phoenix", "Wolf", "Bear", "Fox"] + number = random.randint(100, 999) + return f"{random.choice(adjectives)}{random.choice(nouns)}{number}" + +@app.get("/health") +def health_check(): + return {"status": "healthy"} + + +@app.post("/item", status_code=HTTPStatus.CREATED, response_model=Item) +def create_item(item: ItemCreate): + global item_counter + item_counter += 1 + + new_item = Item( + id=item_counter, + name=item.name, + price=item.price, + deleted=False + ) + items_db[item_counter] = new_item + return new_item + + +@app.get("/item/{item_id}", response_model=Item) +def get_item(item_id: int): + if item_id not in items_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + item = items_db[item_id] + if item.deleted: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + return item + + +@app.get("/item", response_model=list[Item]) +def get_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 = False +): + filtered_items = [] + + for item in items_db.values(): + if not show_deleted and item.deleted: + continue + + if min_price is not None and item.price < min_price: + continue + if max_price is not None and item.price > max_price: + continue + + filtered_items.append(item) + + return filtered_items[offset:offset + limit] + + +@app.put("/item/{item_id}", response_model=Item) +def update_item(item_id: int, item: ItemUpdate): + if item_id not in items_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + existing_item = items_db[item_id] + existing_item.name = item.name + existing_item.price = item.price + + return existing_item + + +@app.patch("/item/{item_id}", response_model=Item) +def patch_item(item_id: int, item: ItemPatch): + if item_id not in items_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + existing_item = items_db[item_id] + + if existing_item.deleted: + return Response(status_code=HTTPStatus.NOT_MODIFIED) + + if item.name is not None: + existing_item.name = item.name + if item.price is not None: + existing_item.price = item.price + + return existing_item + + +@app.delete("/item/{item_id}") +def delete_item(item_id: int): + if item_id not in items_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + items_db[item_id].deleted = True + return Response(status_code=HTTPStatus.OK) + + +@app.post("/cart", status_code=HTTPStatus.CREATED, response_model=CartIdResponse) +def create_cart(response: Response): + global cart_counter + cart_counter += 1 + + carts_db[cart_counter] = {} + response.headers["location"] = f"/cart/{cart_counter}" + + return CartIdResponse(id=cart_counter) + + +@app.get("/cart/{cart_id}", response_model=Cart) +def get_cart(cart_id: int): + if cart_id not in carts_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Cart not found") + + cart_items_data = carts_db[cart_id] + cart_items = [] + total_price = 0.0 + + for item_id, quantity in cart_items_data.items(): + if item_id in items_db: + item = items_db[item_id] + cart_items.append(CartItem( + id=item.id, + name=item.name, + quantity=quantity, + available=not item.deleted + )) + if not item.deleted: + total_price += item.price * quantity + + return Cart( + id=cart_id, + items=cart_items, + price=total_price + ) + + +@app.get("/cart", response_model=list[Cart]) +def get_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) +): + filtered_carts = [] + + for cart_id in carts_db.keys(): + cart = get_cart(cart_id) + + + total_quantity = sum(item.quantity for item in cart.items) + + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + + if min_quantity is not None and total_quantity < min_quantity: + continue + if max_quantity is not None and total_quantity > max_quantity: + continue + + filtered_carts.append(cart) + + return filtered_carts[offset:offset + limit] + + +@app.post("/cart/{cart_id}/add/{item_id}") +def add_item_to_cart(cart_id: int, item_id: int): + if cart_id not in carts_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Cart not found") + + if item_id not in items_db: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + if item_id in carts_db[cart_id]: + carts_db[cart_id][item_id] += 1 + else: + carts_db[cart_id][item_id] = 1 + + return Response(status_code=HTTPStatus.OK) + +@app.websocket("/chat/{chat_name}") +async def chat_endpoint(websocket: WebSocket, chat_name: str): + await websocket.accept() + + + username = generate_username() + + + if chat_name not in chat_rooms: + chat_rooms[chat_name] = [] + + + chat_rooms[chat_name].append((websocket, username)) + + try: + while True: + message = await websocket.receive_text() + + formatted_message = f"{username} :: {message}" + + for ws, user in chat_rooms[chat_name]: + try: + await ws.send_text(formatted_message) + except: + pass + + except WebSocketDisconnect: + chat_rooms[chat_name] = [ + (ws, user) for ws, user in chat_rooms[chat_name] + if ws != websocket + ] + + if not chat_rooms[chat_name]: + del chat_rooms[chat_name] \ No newline at end of file diff --git a/hw2/test_chat.html b/hw2/test_chat.html new file mode 100644 index 00000000..8aeab5db --- /dev/null +++ b/hw2/test_chat.html @@ -0,0 +1,35 @@ + + +
+