From 9e8f3eda48939b300c344f598f290078e363a109 Mon Sep 17 00:00:00 2001 From: Ivanov_Daniil Date: Sat, 27 Sep 2025 19:34:55 +0300 Subject: [PATCH 1/4] Implement ASGI application with fibonacci, factorial, and mean endpoints --- hw1/app.py | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 5 deletions(-) diff --git a/hw1/app.py b/hw1/app.py index 6107b870..4fba3f08 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,10 +1,12 @@ from typing import Any, Awaitable, Callable +import json +import math async def application( - scope: dict[str, Any], - receive: Callable[[], Awaitable[dict[str, Any]]], - send: Callable[[dict[str, Any]], Awaitable[None]], + scope: dict[str, Any], + receive: Callable[[], Awaitable[dict[str, Any]]], + send: Callable[[dict[str, Any]], Awaitable[None]], ): """ Args: @@ -12,8 +14,169 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + # Проверяем, что это HTTP запрос + if scope["type"] != "http": + return + + # Получаем метод и путь + method = scope["method"] + path = scope["path"] + + # Обрабатываем только GET запросы + if method != "GET": + await send_error(send, 405, "Method Not Allowed") + return + + # Разбираем путь и параметры + if path.startswith("/fibonacci/"): + await handle_fibonacci(scope, send) + elif path == "/factorial": + await handle_factorial(scope, send) + elif path == "/mean": + await handle_mean(scope, send) + else: + await send_error(send, 404, "Not Found") + + +async def handle_fibonacci(scope: dict[str, Any], send: Callable): + """Обработка /fibonacci/{n}""" + try: + # Извлекаем n из пути + path = scope["path"] + n_str = path.split("/fibonacci/")[1] + n = int(n_str) + + if n < 0: + await send_error(send, 400, "n must be non-negative") + return + + # Вычисляем n-е число Фибоначчи + result = fibonacci(n) + await send_response(send, 200, {"result": result}) + + except (ValueError, IndexError): + await send_error(send, 400, "Invalid parameter n") + + +async def handle_factorial(scope: dict[str, Any], send: Callable): + """Обработка /factorial?n=5""" + try: + # Извлекаем параметр n из query string + query_string = scope.get("query_string", b"").decode() + params = parse_query_string(query_string) + n_str = params.get("n", [""])[0] + + if not n_str: + await send_error(send, 400, "Missing parameter n") + return + + n = int(n_str) + + if n < 0: + await send_error(send, 400, "n must be non-negative") + return + + # Вычисляем факториал + result = math.factorial(n) + await send_response(send, 200, {"result": result}) + + except ValueError: + await send_error(send, 400, "Invalid parameter n") + + +async def handle_mean(scope: dict[str, Any], send: Callable): + """Обработка /mean?numbers=1,2,3""" + try: + # Извлекаем параметр numbers из query string + query_string = scope.get("query_string", b"").decode() + params = parse_query_string(query_string) + numbers_str = params.get("numbers", [""])[0] + + if not numbers_str: + await send_error(send, 400, "Missing parameter numbers") + return + + # Парсим числа + numbers = [float(x) for x in numbers_str.split(",")] + + if not numbers: + await send_error(send, 400, "Empty numbers list") + return + + # Вычисляем среднее арифметическое + result = sum(numbers) / len(numbers) + await send_response(send, 200, {"result": result}) + + except (ValueError, ZeroDivisionError): + await send_error(send, 400, "Invalid numbers format") + + +def fibonacci(n: int) -> int: + """Вычисляет n-е число Фибоначчи""" + if n == 0: + return 0 + elif n == 1: + return 1 + + a, b = 0, 1 + for _ in range(2, n + 1): + a, b = b, a + b + return b + + +def parse_query_string(query_string: str) -> dict: + """Парсит query string в словарь параметров""" + params = {} + if not query_string: + return params + + for pair in query_string.split("&"): + if "=" in pair: + key, value = pair.split("=", 1) + if key not in params: + params[key] = [] + params[key].append(value) + + return params + + +async def send_response(send: Callable, status: int, data: dict): + """Отправляет успешный JSON ответ""" + body = json.dumps(data).encode("utf-8") + + 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 send_error(send: Callable, status: int, message: str): + """Отправляет ошибку""" + body = json.dumps({"error": message}).encode("utf-8") + + await send({ + "type": "http.response.start", + "status": status, + "headers": [ + [b"content-type", b"application/json"], + ], + }) + + await send({ + "type": "http.response.body", + "body": body, + }) + 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 From 3f8030c3683ff86a0ded9286cd30df23ef6df88e Mon Sep 17 00:00:00 2001 From: Ivanov_Daniil Date: Sat, 27 Sep 2025 19:42:46 +0300 Subject: [PATCH 2/4] Implement ASGI application with fibonacci, factorial, and mean endpoints --- hw1/app.py | 225 +++++++++++++++++++++-------------------------------- 1 file changed, 87 insertions(+), 138 deletions(-) diff --git a/hw1/app.py b/hw1/app.py index 4fba3f08..dfc47805 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,12 +1,13 @@ from typing import Any, Awaitable, Callable import json -import math +from math import prod +from urllib.parse import parse_qs async def application( - scope: dict[str, Any], - receive: Callable[[], Awaitable[dict[str, Any]]], - send: Callable[[dict[str, Any]], Awaitable[None]], + scope: dict[str, Any], + receive: Callable[[], Awaitable[dict[str, Any]]], + send: Callable[[dict[str, Any]], Awaitable[None]], ): """ Args: @@ -14,169 +15,117 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # Проверяем, что это HTTP запрос - if scope["type"] != "http": - return - - # Получаем метод и путь - method = scope["method"] - path = scope["path"] - # Обрабатываем только GET запросы - if method != "GET": - await send_error(send, 405, "Method Not Allowed") + if scope.get("type") != "http": + await _send_json(send, {"detail": "unsupported scope type"}, status=422) return - # Разбираем путь и параметры - if path.startswith("/fibonacci/"): - await handle_fibonacci(scope, send) - elif path == "/factorial": - await handle_factorial(scope, send) - elif path == "/mean": - await handle_mean(scope, send) - else: - await send_error(send, 404, "Not Found") - - -async def handle_fibonacci(scope: dict[str, Any], send: Callable): - """Обработка /fibonacci/{n}""" - try: - # Извлекаем n из пути - path = scope["path"] - n_str = path.split("/fibonacci/")[1] - n = int(n_str) + method = scope.get("method", "GET").upper() + path: str = scope.get("path", "/") + query = parse_qs((scope.get("query_string") or b"").decode( + "utf-8"), keep_blank_values=True) + if path.startswith("/fibonacci/"): + n_str = path[len("/fibonacci/"):].strip("/") + if n_str == "": + await _send_json(send, {"detail": "n is required"}, status=422) + return + try: + n = int(n_str) + except ValueError: + await _send_json(send, {"detail": "n must be an integer"}, status=422) + return if n < 0: - await send_error(send, 400, "n must be non-negative") + await _send_json(send, {"detail": "n must be non-negative"}, status=400) return - # Вычисляем n-е число Фибоначчи - result = fibonacci(n) - await send_response(send, 200, {"result": result}) - - except (ValueError, IndexError): - await send_error(send, 400, "Invalid parameter n") - - -async def handle_factorial(scope: dict[str, Any], send: Callable): - """Обработка /factorial?n=5""" - try: - # Извлекаем параметр n из query string - query_string = scope.get("query_string", b"").decode() - params = parse_query_string(query_string) - n_str = params.get("n", [""])[0] + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + await _send_json(send, {"result": a}, status=200) + return - if not n_str: - await send_error(send, 400, "Missing parameter n") + if path == "/factorial": + if method != "GET": + await _send_json(send, {"detail": "not found"}, status=404) return - n = int(n_str) - + raw = query.get("n", [None])[0] + if raw is None or raw == "": + await _send_json(send, {"detail": "n is required"}, status=422) + return + try: + n = int(raw) + except ValueError: + await _send_json(send, {"detail": "n must be an integer"}, status=422) + return if n < 0: - await send_error(send, 400, "n must be non-negative") + await _send_json(send, {"detail": "n must be non-negative"}, status=400) return - # Вычисляем факториал - result = math.factorial(n) - await send_response(send, 200, {"result": result}) - - except ValueError: - await send_error(send, 400, "Invalid parameter n") - - -async def handle_mean(scope: dict[str, Any], send: Callable): - """Обработка /mean?numbers=1,2,3""" - try: - # Извлекаем параметр numbers из query string - query_string = scope.get("query_string", b"").decode() - params = parse_query_string(query_string) - numbers_str = params.get("numbers", [""])[0] + result = 1 if n == 0 else prod(range(1, n + 1)) + await _send_json(send, {"result": result}, status=200) + return - if not numbers_str: - await send_error(send, 400, "Missing parameter numbers") + if path == "/mean": + if method != "GET": + await _send_json(send, {"detail": "not found"}, status=404) return - # Парсим числа - numbers = [float(x) for x in numbers_str.split(",")] - - if not numbers: - await send_error(send, 400, "Empty numbers list") + body_bytes = await _read_body(receive) + if not body_bytes: + await _send_json(send, {"detail": "json body required (array of numbers)"}, status=422) return - # Вычисляем среднее арифметическое - result = sum(numbers) / len(numbers) - await send_response(send, 200, {"result": result}) - - except (ValueError, ZeroDivisionError): - await send_error(send, 400, "Invalid numbers format") - - -def fibonacci(n: int) -> int: - """Вычисляет n-е число Фибоначчи""" - if n == 0: - return 0 - elif n == 1: - return 1 - - a, b = 0, 1 - for _ in range(2, n + 1): - a, b = b, a + b - return b - - -def parse_query_string(query_string: str) -> dict: - """Парсит query string в словарь параметров""" - params = {} - if not query_string: - return params - - for pair in query_string.split("&"): - if "=" in pair: - key, value = pair.split("=", 1) - if key not in params: - params[key] = [] - params[key].append(value) + try: + data = json.loads(body_bytes.decode("utf-8")) + except json.JSONDecodeError: + await _send_json(send, {"detail": "invalid JSON"}, status=422) + return - return params + if not isinstance(data, list): + await _send_json(send, {"detail": "body must be a JSON array"}, status=422) + return + if len(data) == 0: + await _send_json(send, {"detail": "array must not be empty"}, status=400) + return -async def send_response(send: Callable, status: int, data: dict): - """Отправляет успешный JSON ответ""" - body = json.dumps(data).encode("utf-8") + try: + nums = [float(x) for x in data] + except (TypeError, ValueError): + await _send_json(send, {"detail": "array must contain only numbers"}, status=422) + return - await send({ - "type": "http.response.start", - "status": status, - "headers": [ - [b"content-type", b"application/json"], - ], - }) + mean_value = sum(nums) / len(nums) + await _send_json(send, {"result": mean_value}, status=200) + return - await send({ - "type": "http.response.body", - "body": body, - }) + await _send_json(send, {"detail": "not found"}, status=404) -async def send_error(send: Callable, status: int, message: str): - """Отправляет ошибку""" - body = json.dumps({"error": message}).encode("utf-8") +async def _read_body(receive: Callable[[], Awaitable[dict[str, Any]]]) -> bytes: + body = b"" + more = True + while more: + message = await receive() + if message["type"] != "http.request": + break + body += message.get("body", b"") + more = message.get("more_body", False) + return body - 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 _send_json(send: Callable[[dict[str, Any]], Awaitable[None]], payload: dict, status: int = 200): + body = json.dumps(payload).encode("utf-8") + headers = [ + (b"content-type", b"application/json; charset=utf-8"), + (b"content-length", str(len(body)).encode("ascii")), + ] + await send({"type": "http.response.start", "status": status, "headers": headers}) + await send({"type": "http.response.body", "body": body}) if __name__ == "__main__": import uvicorn - uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file From 742a9d81d2f6489bdaa48a8609518bde1169053e Mon Sep 17 00:00:00 2001 From: Ivanov_Daniil Date: Sun, 5 Oct 2025 20:44:27 +0300 Subject: [PATCH 3/4] version_1 --- hw2/hw/shop_api/main.py | 314 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 313 insertions(+), 1 deletion(-) diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..294e58a6 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,315 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Response, Query, HTTPException +from typing import Optional +import uvicorn +import json +import os +from http import HTTPStatus app = FastAPI(title="Shop API") + +carts_file = "carts.json" +items_file = "items.json" + +def load_json(file_path): + if not os.path.exists(file_path): + with open(file_path, 'w', encoding='utf-8') as f: + json.dump({}, f) + with open(file_path, 'r', encoding='utf-8') as f: + carts = json.load(f) + return carts + +def save_json(json_file, file_path): + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(json_file, f, indent=4) + + +### CART +@app.post("/cart") +def create_cart(): + + carts = load_json(carts_file) + if carts: + cart_id = max(carts.keys()) + else: + cart_id = 1 + carts[cart_id] = {'id': cart_id, 'items': [], 'price': 0.0 } + + save_json(carts, carts_file) + + response = Response( + content=json.dumps(carts[cart_id]), + status_code=HTTPStatus.CREATED, + media_type="application/json", + headers={} + ) + response.headers["Location"] = f"/cart/{cart_id}" + return response + +@app.get("/cart/{cart_id}") +def get_cart(cart_id: int): + carts = load_json(carts_file) + return carts[str(cart_id)] + +@app.get("/cart") +def get_carts( + offset: int = Query(0, ge=0, description="Смещение по списку"), + limit: int = Query(10, gt=0, description="Ограничение на количество"), + min_price: Optional[float] = Query(None, ge=0, description="Минимальная цена"), + max_price: Optional[float] = Query(None, ge=0, description="Максимальная цена"), + min_quantity: Optional[int] = Query(None, ge=0, description="Минимальное количество товаров"), + max_quantity: Optional[int] = Query(None, ge=0, description="Максимальное количество товаров") +): + carts = load_json(carts_file) + cart_list = list(carts.values()) + + filtered_carts = [] + + for cart in cart_list: + # Фильтр по цене + price_ok = True + if min_price is not None and cart['price'] < min_price: + price_ok = False + if max_price is not None and cart['price'] > max_price: + price_ok = False + if not price_ok: + continue + + filtered_carts.append(cart) + + # Теперь применяем фильтры min_quantity/max_quantity ко всему результату + # Эти фильтры работают на уровне всего набора данных + + if min_quantity is not None or max_quantity is not None: + # Вычисляем общее количество товаров во всех отфильтрованных корзинах + total_quantity_all_carts = sum( + item['quantity'] + for cart in filtered_carts + for item in cart['items'] + ) + + # Проверяем условия для всего набора + if min_quantity is not None and total_quantity_all_carts < min_quantity: + # Если общее количество меньше min_quantity, возвращаем пустой список + filtered_carts = [] + elif max_quantity is not None and total_quantity_all_carts > max_quantity: + # Если общее количество больше max_quantity, возвращаем пустой список + filtered_carts = [] + + # Сортируем по ID + filtered_carts.sort(key=lambda x: x['id']) + + # Пагинация + start_idx = offset + end_idx = offset + limit + result = filtered_carts[start_idx:end_idx] + + return result + +@app.post("/cart/{cart_id}/add/{item_id}") +def add_to_cart(cart_id: int, item_id: int): + carts = load_json(carts_file) + items = load_json(items_file) + item = items[str(item_id)] + cart = carts[str(cart_id)] + + # Проверяем, есть ли товар в корзине + item_found = False + for cart_item in cart['items']: + if cart_item['id'] == item_id: + # Если товар уже есть, увеличиваем quantity на 1 + cart_item['quantity'] += 1 + item_found = True + break + + # Если товара нет в корзине, добавляем его + if not item_found: + cart['items'].append(item) + + # Пересчитываем общую стоимость корзины + total_price = 0.0 + for cart_item in cart['items']: + total_price += cart_item['price'] * cart_item['quantity'] + cart['price'] = total_price + + save_json(carts, carts_file) + return carts[str(cart_id)] + +### ITEM + +@app.post("/item") +def create_item(item: dict): + items = load_json(items_file) + if items: + item_id = str(max(items.keys())) + else: + item_id = str(1) + item['id'] = int(item_id) + item['quantity'] = 1 + items[item_id] = item + + + save_json(items, items_file) + + response = Response( + content=json.dumps(items[item_id]), + status_code=HTTPStatus.CREATED, + media_type="application/json", + headers={} + ) + return response + + +@app.get("/item/{item_id}") +def get_item(item_id: int): + items = load_json(items_file) + + if str(item_id) not in items: + raise HTTPException(status_code=404, detail="Item not found") + + item = items[str(item_id)] + + # Для удаленных товаров возвращаем 404 + if item.get('deleted', False): + raise HTTPException(status_code=404, detail="Item not found") + + return item + + +@app.get("/item") +def get_items( + offset: int = Query(0, ge=0, description="Смещение по списку"), + limit: int = Query(10, gt=0, description="Ограничение на количество"), + min_price: Optional[float] = Query(None, ge=0, description="Минимальная цена"), + max_price: Optional[float] = Query(None, ge=0, description="Максимальная цена"), + show_deleted: bool = Query(False, description="Показывать удаленные товары") +): + items = load_json(items_file) + item_list = list(items.values()) + + filtered_items = [] + + for item in item_list: + # Фильтр по статусу удаления + if not show_deleted and item.get('deleted', False): + continue + + # Фильтр по цене + price_ok = True + if min_price is not None and item['price'] < min_price: + price_ok = False + if max_price is not None and item['price'] > max_price: + price_ok = False + if not price_ok: + continue + + filtered_items.append(item) + + # Сортируем по ID (или можно по другому полю) + filtered_items.sort(key=lambda x: x['id']) + + # Пагинация + start_idx = offset + end_idx = offset + limit + result = filtered_items[start_idx:end_idx] + + return result + + +@app.put("/item/{item_id}") +def update_item(item_id: int, item_data: dict): + items = load_json(items_file) + + # Проверяем существование товара + if str(item_id) not in items: + raise HTTPException(status_code=404, detail="Item not found") + + item = items[str(item_id)] + + # Проверяем, что товар не удален + if item.get('deleted', False): + raise HTTPException(status_code=404, detail="Item not found") + + # Проверяем обязательные поля + if 'name' not in item_data or 'price' not in item_data: + raise HTTPException( + status_code=422, + detail="Name and price are required for PUT" + ) + + # Полная замена товара (сохраняем только id и переданные поля) + updated_item = { + 'id': item_id, + 'name': item_data['name'], + 'price': item_data['price'], + 'quantity': 1 + } + + items[str(item_id)] = updated_item + save_json(items, items_file) + + return updated_item + + +@app.patch("/item/{item_id}") +def patch_item(item_id: int, item_data: dict): + items = load_json(items_file) + + if str(item_id) not in items: + raise HTTPException(status_code=404, detail="Item not found") + + item = items[str(item_id)] + + # Для удаленных товаров возвращаем 304 + if item.get('deleted', False): + raise HTTPException(status_code=304, detail="Item is deleted") + + # Проверяем на лишние поля + allowed_fields = {'name', 'price'} + extra_fields = set(item_data.keys()) - allowed_fields + if extra_fields: + raise HTTPException( + status_code=422, + detail=f"Extra fields not allowed: {extra_fields}" + ) + + # Запрещаем менять deleted + if 'deleted' in item_data: + raise HTTPException(status_code=422, detail="Cannot change deleted status") + + # Если тело пустое - возвращаем текущий товар без изменений + if not item_data: + return item + + # Частичное обновление + if 'name' in item_data: + item['name'] = item_data['name'] + + if 'price' in item_data: + if item_data['price'] < 0: + raise HTTPException(status_code=422, detail="Price cannot be negative") + item['price'] = item_data['price'] + + save_json(items, items_file) + return item + + +@app.delete("/item/{item_id}") +def delete_item(item_id: int): + items = load_json(items_file) + + if str(item_id) not in items: + return HTTPStatus.NOT_FOUND + + item = items[str(item_id)] + + # Если уже удален - все равно успех + if not item.get('deleted', False): + item['deleted'] = True + save_json(items, items_file) + + # Возвращаем просто сообщение, а не весь item + return HTTPStatus.OK + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file From 1b85bb91b5407532f00c1d71040e70d052253a13 Mon Sep 17 00:00:00 2001 From: Ivanov_Daniil Date: Sun, 12 Oct 2025 13:42:23 +0300 Subject: [PATCH 4/4] add prometheus and grafana --- hw2/hw/Dockerfile | 23 + hw2/hw/docker-compose.yml | 31 ++ hw2/hw/ping.py | 51 ++ hw2/hw/requirements.txt | 1 + hw2/hw/settings/grafana-dashboard.json | 607 ++++++++++++++++++++++ hw2/hw/settings/prometheus/prometheus.yml | 10 + hw2/hw/shop_api/main.py | 2 + 7 files changed, 725 insertions(+) create mode 100644 hw2/hw/Dockerfile create mode 100644 hw2/hw/docker-compose.yml create mode 100644 hw2/hw/ping.py create mode 100644 hw2/hw/settings/grafana-dashboard.json create mode 100644 hw2/hw/settings/prometheus/prometheus.yml diff --git a/hw2/hw/Dockerfile b/hw2/hw/Dockerfile new file mode 100644 index 00000000..43812477 --- /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"] \ 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..290b1a30 --- /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: + - ./settings/prometheus/:/etc/prometheus/ + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/usr/share/prometheus/console_libraries" + - "--web.console.templates=/usr/share/prometheus/consoles" + ports: + - 9090:9090 + restart: always \ No newline at end of file diff --git a/hw2/hw/ping.py b/hw2/hw/ping.py new file mode 100644 index 00000000..67a8d1d5 --- /dev/null +++ b/hw2/hw/ping.py @@ -0,0 +1,51 @@ +import time + +import requests + +BASE = 'http://localhost:8080' + + +def run_tests(): + # создаем товар + r = requests.post(f'{BASE}/item', json={'name': 'Test Item', 'price': 10.5}) + print('POST /item', r.status_code, r.json()) + item_id = r.json()['id'] + + # получаем товар + r = requests.get(f'{BASE}/item/{item_id}') + print('GET /item/{id}', r.status_code, r.json()) + + # обновляем товар + r = requests.patch(f'{BASE}/item/{item_id}', json={'price': 12.0}) + print('PATCH /item/{id}', r.status_code, r.json()) + + # список товаров + r = requests.get(f'{BASE}/item') + print('GET /item', r.status_code, len(r.json())) + + # создаем корзину + r = requests.post(f'{BASE}/cart') + print('POST /cart', r.status_code, r.json()) + cart_id = r.json()['id'] + + # получаем корзину + r = requests.get(f'{BASE}/cart/{cart_id}') + print('GET /cart/{id}', r.status_code, r.json()) + + # добавляем товар в корзину + r = requests.post(f'{BASE}/cart/{cart_id}/add/{item_id}') + print('POST /cart/{cart_id}/add/{item_id}', r.status_code, r.json()) + + # список корзин + r = requests.get(f'{BASE}/cart') + print('GET /cart', r.status_code, len(r.json())) + + +if __name__ == '__main__': + while True: + try: + run_tests() + except Exception as e: + print('Error:', e) + + time.sleep(1) \ No newline at end of file diff --git a/hw2/hw/requirements.txt b/hw2/hw/requirements.txt index 207dcf5c..679714b9 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==7.1.0 # для prometheus + grafana (hm3) # Зависимости для тестирования pytest>=7.4.0 diff --git a/hw2/hw/settings/grafana-dashboard.json b/hw2/hw/settings/grafana-dashboard.json new file mode 100644 index 00000000..de97d72d --- /dev/null +++ b/hw2/hw/settings/grafana-dashboard.json @@ -0,0 +1,607 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "df01l3rlsp728b" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "4xx" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "HTTP 500" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#bf1b00", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 0, + "y": 0 + }, + "id": 13, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "$$hashKey": "object:140", + "datasource": { + "type": "prometheus", + "uid": "e4584a9f-5364-4b3d-a851-7abbc5250820" + }, + "editorMode": "code", + "expr": "sum by (status) (rate(http_requests_total[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ status }}", + "range": true, + "refId": "A" + } + ], + "title": "Request per minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df01l3rlsp728b" + }, + "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": "smooth", + "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": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 14, + "x": 5, + "y": 0 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "$$hashKey": "object:146", + "datasource": { + "type": "prometheus", + "uid": "e4584a9f-5364-4b3d-a851-7abbc5250820" + }, + "editorMode": "code", + "expr": "http_request_duration_seconds_sum{handler!=\"none\"} / http_request_duration_seconds_count", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ handler }}", + "range": true, + "refId": "A" + } + ], + "title": "Average response time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df01l3rlsp728b" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "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": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 19, + "y": 0 + }, + "id": 9, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "$$hashKey": "object:638", + "datasource": { + "type": "prometheus", + "uid": "e4584a9f-5364-4b3d-a851-7abbc5250820" + }, + "editorMode": "code", + "expr": "rate(process_cpu_seconds_total{}[30s])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "cpu", + "range": true, + "refId": "A" + } + ], + "title": "CPU usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df01l3rlsp728b" + }, + "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": 19, + "x": 0, + "y": 7 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "$$hashKey": "object:214", + "datasource": { + "type": "prometheus", + "uid": "e4584a9f-5364-4b3d-a851-7abbc5250820" + }, + "editorMode": "code", + "expr": "increase(http_requests_total{}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ method }} {{ handler }}", + "range": true, + "refId": "A" + } + ], + "title": "Total requests per minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df01l3rlsp728b" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "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": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 19, + "y": 7 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "$$hashKey": "object:638", + "datasource": { + "type": "prometheus", + "uid": "e4584a9f-5364-4b3d-a851-7abbc5250820" + }, + "editorMode": "code", + "expr": "process_resident_memory_bytes{}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem", + "range": true, + "refId": "A" + } + ], + "title": "Memory usage", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "auto", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [] + }, + "timezone": "", + "title": "FastAPI Dashboard", + "uid": "_eX4mpl3", + "version": 2 +} \ No newline at end of file diff --git a/hw2/hw/settings/prometheus/prometheus.yml b/hw2/hw/settings/prometheus/prometheus.yml new file mode 100644 index 00000000..275d8544 --- /dev/null +++ b/hw2/hw/settings/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 \ No newline at end of file diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index 294e58a6..67d48b73 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -4,8 +4,10 @@ import json import os from http import HTTPStatus +from prometheus_fastapi_instrumentator import Instrumentator app = FastAPI(title="Shop API") +Instrumentator().instrument(app).expose(app) carts_file = "carts.json" items_file = "items.json"