From 77a5066babef9dab8aadf74f1cfb72cadae6fc02 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Mon, 22 Sep 2025 18:17:17 +0300 Subject: [PATCH 01/19] Implement ASGI application with fibonacci, factorial, and mean endpoints --- hw1/app.py | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/hw1/app.py b/hw1/app.py index 6107b870..53145660 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,5 +1,139 @@ +import math +import json +from http import HTTPStatus from typing import Any, Awaitable, Callable +async def read_body(receive): + body = b'' + more_body = True + while more_body: + message = await receive() + body += message.get('body', b'') + more_body = message.get('more_body', False) + return body + + +def parse_query_string(scope: dict[str, Any]): + result = {} + query_string = scope.get('query_string').decode() + if len(query_string) == 0: + return result + for entry in query_string.split('&'): + key, value = entry.split('=') + result[key] = value + return result + + +def not_found_response(value = ""): + return { + 'value': str(value), + 'code': HTTPStatus.NOT_FOUND + } + +def bad_request_response(value = ""): + return { + 'value': str(value), + 'code': HTTPStatus.BAD_REQUEST + } + +def unprocessable_content_response(value = ""): + return { + 'value': str(value), + 'code': HTTPStatus.UNPROCESSABLE_CONTENT + } + +def ok_response(value = ""): + return { + 'value': str(value), + 'code': HTTPStatus.OK + } + + +def get_factorial(query_params: dict[str, Any], path_parameters: list[str], body: bytes) -> dict[str, Any]: + n = int(query_params.get('n')) + if n < 0: + return bad_request_response("Invalid value for n, must be a non-negative") + result = math.factorial(n) + return ok_response(result) + + +def get_fibonacci(query_params: dict[str, Any], path_parameters: list[str], body: bytes) -> dict[str, Any]: + n = int(path_parameters[0]) + if n < 0: + return bad_request_response("Invalid value for n, must be a non-negative") + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return ok_response(a) + + +def get_mean(query_params: dict[str, Any], path_parameters: list[str], body: bytes) -> dict[str, Any]: + data = json.loads(body.decode()) + if len(data) == 0: + return bad_request_response("Invalid value for body, must be non-empty array of floats") + result = sum(data) / len(data) + return ok_response(result) + + +def route_request(scope: dict[str, Any]) -> Callable: + method = scope.get('method') + path = scope.get('path') + + function = None + routing_params = [] + + if method == "GET": + if path.startswith("/factorial"): + function = get_factorial + routing_params = path[10:].split("/") + elif path.startswith("/fibonacci"): + function = get_fibonacci + routing_params = path[11:].split("/") + elif path.startswith("/mean"): + function = get_mean + routing_params = path[6:].split("/") + + return [function, routing_params] + + +async def handle_http(scope: dict[str, Any], receive: Callable, send: Callable): + + function, path_parameters = route_request(scope) + if function is None: + response = not_found_response() + + else: + try: + body = await read_body(receive) + query_params = parse_query_string(scope) + response = function(query_params, path_parameters, body) + except Exception: + response = unprocessable_content_response() + + code = response['code'] + body = json.dumps({"result": response['value']}) + await send({ + 'type': 'http.response.start', + 'status': code, + 'headers': [ + [b'content-type', b'text/plain'], + ] + }) + await send({ + 'type': 'http.response.body', + 'body': body.encode() + }) + + +async def handle_lifespan(scope: dict[str, Any], receive: Callable, send: Callable): + 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'}) + return + async def application( scope: dict[str, Any], @@ -12,7 +146,12 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + scope_type = scope['type'] + if scope_type == 'lifespan': + await handle_lifespan(scope, receive, send) + elif scope_type == 'http': + await handle_http(scope, receive, send) + if __name__ == "__main__": import uvicorn From e7ecc6e08b586e3ac2e6005218c25d8669d586b6 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Mon, 22 Sep 2025 18:24:38 +0300 Subject: [PATCH 02/19] 'UNPROCESSABLE_CONTENT' fixed to 'UNPROCESSABLE_ENTITY' --- hw1/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hw1/app.py b/hw1/app.py index 53145660..648cf5b5 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -36,10 +36,10 @@ def bad_request_response(value = ""): 'code': HTTPStatus.BAD_REQUEST } -def unprocessable_content_response(value = ""): +def unprocessable_entity_response(value = ""): return { 'value': str(value), - 'code': HTTPStatus.UNPROCESSABLE_CONTENT + 'code': HTTPStatus.UNPROCESSABLE_ENTITY } def ok_response(value = ""): @@ -92,7 +92,7 @@ def route_request(scope: dict[str, Any]) -> Callable: elif path.startswith("/mean"): function = get_mean routing_params = path[6:].split("/") - + return [function, routing_params] @@ -108,7 +108,7 @@ async def handle_http(scope: dict[str, Any], receive: Callable, send: Callable): query_params = parse_query_string(scope) response = function(query_params, path_parameters, body) except Exception: - response = unprocessable_content_response() + response = unprocessable_entity_response() code = response['code'] body = json.dumps({"result": response['value']}) From 8787ebcf7aedce964443f435b1bfbfeca4dd371c Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 5 Oct 2025 17:47:02 +0300 Subject: [PATCH 03/19] HW2 implemented --- hw2/hw/shop_api/main.py | 182 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..024454d6 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,183 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Response, HTTPException, status +from pydantic import BaseModel, conint, confloat, Extra app = FastAPI(title="Shop API") +cart_id_counter = 0 +item_id_counter = 0 + +carts = {} +items = {} + +posint = conint(gt=0) +uint = conint(ge=0) +ufloat = confloat(ge=0) + +class ModifiedItem(BaseModel): + name: str | None = None + price: float | None = None + model_config = { + "extra": "forbid" + } + +class Item(BaseModel): + id: uint = 0 + name: str + price: float + deleted: bool = False + +class CartItem(BaseModel): + id: uint = 0 + name: str + price: float + quantity: int = 0 + deleted: bool = False + +class Cart(BaseModel): + id: int + price: float = 0.0 + items: list[CartItem] = [] + + +""" +cart +""" + +# POST cart - создание, работает как RPC, не принимает тело, возвращает идентификатор +@app.post("/cart", status_code=status.HTTP_201_CREATED) +async def create_cart(response: Response): + global cart_id_counter + cart_id_counter += 1 + new_cart = Cart( + id=cart_id_counter + ) + carts[new_cart.id] = new_cart + response.headers["Location"] = f"/cart/{new_cart.id}" + return {"id": new_cart.id} + + +# GET /cart/{id} - получение корзины по id +@app.get("/cart/{cart_id}", status_code=status.HTTP_200_OK) +async def get_cart(cart_id: int): + return carts[cart_id] + + +# GET /cart - получение списка корзин с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# min_quantity - неотрицательное целое число, минимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +# max_quantity - неотрицательное целое число, максимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +@app.get("/cart", status_code=status.HTTP_200_OK) +async def get_cart_list(offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + min_quantity: uint = None, max_quantity: uint = None): + filtered_carts = [] + for cart in list(carts.values())[offset:]: + if len(filtered_carts) == limit: + break + + min_price_ok = cart.price >= min_price if min_price else True + max_price_ok = cart.price <= max_price if max_price else True + min_quantity_ok = sum(item.quantity for item in cart.items) >= min_quantity if not min_quantity is None else True + max_quantity_ok = sum(item.quantity for item in cart.items) <= max_quantity if not max_quantity is None else True + + if min_price_ok and max_price_ok and min_quantity_ok and max_quantity_ok: + filtered_carts.append(cart) + return filtered_carts + + +# POST /cart/{cart_id}/add/{item_id} - добавление в корзину с cart_id предмета с item_id, +# если товар уже есть, то увеличивается его количество +@app.post("/cart/{cart_id}/add/{item_id}", status_code=status.HTTP_200_OK) +async def add_to_cart(cart_id: int, item_id: int): + cart = carts[cart_id] + item = items[item_id] + + item_exists = False + for i in range(len(cart.items)): + if cart.items[i].id == item_id: + cart.items[i].quantity += 1 + item_exists = True + break + + if not item_exists: + new_item = CartItem( + **item.model_dump(), + quantity=1 + ) + cart.items.append(new_item) + + cart.price += item.price + + +""" +item +""" + +# POST /item - добавление нового товара +@app.post("/item", status_code=status.HTTP_201_CREATED) +async def create_item(item: Item): + global item_id_counter + item_id_counter += 1 + new_item = Item(id=item_id_counter, name=item.name, price=item.price) + items[new_item.id] = new_item + return new_item + +# GET /item/{id} - получение товара по id +@app.get("/item/{item_id}", status_code=status.HTTP_200_OK) +async def get_item(item_id: int): + if items[item_id].deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + return items[item_id] + + +# GET /item - получение списка товаров с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена (опционально, если нет, не учитывает в фильтре) +# show_deleted - булевая переменная, показывать ли удаленные товары (по умолчанию False) +@app.get("/item", status_code=status.HTTP_200_OK) +async def get_item_list(offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + show_deleted: bool = False): + filtered_items = [] + for item in list(items.values())[offset:]: + if len(filtered_items) == limit: + break + + min_price_ok = item.price >= min_price if min_price else True + max_price_ok = item.price <= max_price if max_price else True + show_deleted_ok = (not item.deleted) or show_deleted + + if min_price_ok and max_price_ok and show_deleted_ok: + filtered_items.append(item) + + return filtered_items + +# PUT /item/{id} - замена товара по id (создание запрещено, только замена существующего) +@app.put("/item/{item_id}", status_code=status.HTTP_200_OK) +async def put_item(item_id: int, new_item: Item): + new_item.id = item_id + items[item_id] = new_item + return new_item + +# PATCH /item/{id} - частичное обновление товара по id (разрешено менять все поля, кроме deleted) +@app.patch("/item/{item_id}", status_code=status.HTTP_200_OK) +async def patch_item(item_id: int, new_item: ModifiedItem): + item = items[item_id] + if item.deleted: + raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED) + if new_item.name and item.name != new_item.name: + items[item_id].name = new_item.name + if new_item.price and item.price != new_item.price: + items[item_id].price = new_item.price + return items[item_id] + +# DELETE /item/{id} - удаление товара по id (товар помечается как удаленный) +@app.delete("/item/{item_id}", status_code=status.HTTP_200_OK) +async def delete_item(item_id: int): + items[item_id].deleted = True \ No newline at end of file From 707a0dba630cbcbb458205d8ba93c59a3bd9cb15 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk <79217493+EkatherinaS@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:54:56 +0300 Subject: [PATCH 04/19] Remove unused import --- hw2/hw/shop_api/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index 024454d6..64a81a9a 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI, Response, HTTPException, status -from pydantic import BaseModel, conint, confloat, Extra +from pydantic import BaseModel, conint, confloat app = FastAPI(title="Shop API") cart_id_counter = 0 @@ -180,4 +180,4 @@ async def patch_item(item_id: int, new_item: ModifiedItem): # DELETE /item/{id} - удаление товара по id (товар помечается как удаленный) @app.delete("/item/{item_id}", status_code=status.HTTP_200_OK) async def delete_item(item_id: int): - items[item_id].deleted = True \ No newline at end of file + items[item_id].deleted = True From 5242b427405fbd947525f3d7c403abdfd2ac181a Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sat, 11 Oct 2025 18:58:21 +0300 Subject: [PATCH 05/19] hw3 implemented --- hw3/Dockerfile | 23 ++ hw3/README.md | 13 + ...creenshot 2025-10-11 grafana-dashboard.png | Bin 0 -> 136508 bytes hw3/__init__.py | 0 hw3/docker-compose.yml | 31 ++ hw3/grafana-dashboard.json | 290 ++++++++++++++++++ hw3/requirements.txt | 4 + hw3/settings/prometheus/prometheus.yml | 10 + hw3/shop_api/__init__.py | 0 hw3/shop_api/main.py | 186 +++++++++++ 10 files changed, 557 insertions(+) create mode 100644 hw3/Dockerfile create mode 100644 hw3/README.md create mode 100644 hw3/Screenshot 2025-10-11 grafana-dashboard.png create mode 100644 hw3/__init__.py create mode 100644 hw3/docker-compose.yml create mode 100644 hw3/grafana-dashboard.json create mode 100644 hw3/requirements.txt create mode 100644 hw3/settings/prometheus/prometheus.yml create mode 100644 hw3/shop_api/__init__.py create mode 100644 hw3/shop_api/main.py diff --git a/hw3/Dockerfile b/hw3/Dockerfile new file mode 100644 index 00000000..27b0b636 --- /dev/null +++ b/hw3/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/hw3/README.md b/hw3/README.md new file mode 100644 index 00000000..aad28c54 --- /dev/null +++ b/hw3/README.md @@ -0,0 +1,13 @@ +# ДЗ + +## Настроить сборку образов Docker и мониторинг с помощью Prometheus и Grafana + +Интегрировать Docker с Prometheus и Grafana в любой уже написанный в ДЗ сервис (по аналогии с тем, как в репе) + +По сути, если вы выполнили вторую домашку, то теперь для неё надо написать Dockerfile и настроить мониторинг. Если вторую домашку вы не делали, то можно взять сервис из [rest_example](../hw2/rest_example/main.py) + +Сдача через PR, так же нужно: + +1) Dockerfile для сборки сервиса +2) docker-compose.yml для локального разворачивания в Docker +3) Приложить скрин с парой Дашбордов в Grafana diff --git a/hw3/Screenshot 2025-10-11 grafana-dashboard.png b/hw3/Screenshot 2025-10-11 grafana-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..6be865ead8f791fae0be27be21fc12694ebce36b GIT binary patch literal 136508 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%0AZ@#y;*243+ip6hS#VkjO+>M-+F~%AW$i zF1Z_sRB6;X}XOEmz{?%lY&?r^vedF&{V2>_iVLn7JQHc+>*iy9%~}nyA54 zi&>(eE(hlmmRcUHX$rsV{uAv{lq-VkxpK6dT~(i+`T&f|a?pyY|Fz$T#1qG3dg?%v z?Vr*A9EjHazQV>+$FUooPqwM4mbif-POGWl$?Og+rGY}=iXJ!+ECHBhXK$|#&x^LZzFAa5e*3JlCmgJ-i->0WcaW zf38@Xxa+?yVwz_bh;h(PvBLczL2xDCUNHCi$bt>Ndxl5HlW3GGv2qKo-jVx?_srV3 z*BS#KRE;k;^rA0oViXFRKQ$P03tKdJR2gcqhL03~8eqj3v!~H<4`47#vi3u7pC`hY zkVJ-%&d9tjt^&#OF99H+oEKjjC(w=4m3XE`K)aW9YILre2YaFE-|j@8G?_OjC5x<| z()(;E{v%qqs3a)5_p@C;w_mpSsGr=YyY(f1^OSQMA-nwdQ-A$eu(ptu!knxVh;W~| zbh>ZNGpJQS2bw+dC$3A>=r+6P3aFnykHr*Bw1D62bYMS{rBD**Bb1Y@eqf;F@)~)M z`4vQxUm~Z4X1{V}hwSCNM@~`;88+@4YwK{ZEXtKFeUW&B4ZW9cL!Ce;bn{X$`A^^4 zddN3PJ*jGcukTdUnJD2YS%rIjsL+jq+n6CJkDv38Wk9=_3{*Gx?6h~r6&3)u)yFd)kLLn)oFw*=3G?~!P;3_oz;)iwJK~tF;3vL z(1!JyL78EpWE5x0B`iJHr}d`O>MnxoGfEXlz@sIJvVQXc+g)pMp^qE6oowW{wTm4r z&)*MIGXbBPBGp_7Zk*q*AW*8=JfA~~Gbk|{9sM%a`ye9wa=&xTpj1NLIZ_KC{QXTr z3|u{Zs0L=s=NHhrj47XAc`UxWa?s~&sm*|Nr~L)?wrFd|8CT=V_AyBQkMLFJ@% zt{0|!bWDvX(jAWlL>=RTo!}2)qHEdQY6W4!RP^iB|1da8TWoH(KnFHB)jDag{Rvj} z^WnV*w7qCp*%ts!a;bLy^LY#E&d+mc;$}(dV~_D#vloZ~jhLiN?Bbvb^p8g0y2iD$n~3 zv3rAPZpfDcc3B2M3LuuQW)#7Vkvffh#s6a;o(CWKpQZWdclpZyH`?Y#fF%{?^n-Uq z1u`KlE%C=Xgitm53<`A8L zU&8;#!2@@*tIuNa$S2&ps7(l#kq;(xaFxr*Ka)j*mfU5SnLXoKD#*>*+`D2v;jvO}FI=kp2m1XGndBG55p zKzW0oAd(*cUz+yA+W~av^J6~)G4X)Za>Hd10?GU3WV(4^plw)5%Vbj)u!_NYOcbR}6@#akJN&yc8>adVfXSpd3Tj

|3w{%ebDUgsCooW!;rJw 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�?z4|4O02aF&>I$%8tH1qw|mg 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⪼a0V`J0uc`fgNM$o4N{ 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 D86nmA literal 0 HcmV?d00001 diff --git a/hw3/__init__.py b/hw3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/docker-compose.yml b/hw3/docker-compose.yml new file mode 100644 index 00000000..91b5555c --- /dev/null +++ b/hw3/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 diff --git a/hw3/grafana-dashboard.json b/hw3/grafana-dashboard.json new file mode 100644 index 00000000..44fd2094 --- /dev/null +++ b/hw3/grafana-dashboard.json @@ -0,0 +1,290 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "Prometheus", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "HW3 Application Metrics", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "bf0qoawhp68zkb" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 10, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "editorMode": "code", + "expr": "(time() - process_start_time_seconds) / 3600", + "legendFormat": "uptime", + "range": true, + "refId": "A" + } + ], + "title": "Uptime (hours)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "bf0qoawhp68zkb" + }, + "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": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 13, + "x": 10, + "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": "sum(rate(http_requests_total[5m])) by (method, status)", + "legendFormat": "{{method}} {{status}}", + "refId": "A" + } + ], + "title": "Request Rate (RPS)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "bf0qoawhp68zkb" + }, + "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": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 0, + "y": 3 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "expr": "rate(process_cpu_seconds_total[5m])", + "legendFormat": "CPU", + "refId": "A" + } + ], + "title": "Process CPU Usage (seconds)", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [ + "webapp", + "python", + "prometheus" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "HW3 Application Metrics", + "uid": "859be40b-c262-476d-b206-8d3613d07056", + "version": 2 +} \ No newline at end of file diff --git a/hw3/requirements.txt b/hw3/requirements.txt new file mode 100644 index 00000000..101a06b5 --- /dev/null +++ b/hw3/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.117.1 +uvicorn>=0.24.0 +prometheus-fastapi-instrumentator + diff --git a/hw3/settings/prometheus/prometheus.yml b/hw3/settings/prometheus/prometheus.yml new file mode 100644 index 00000000..7fa1951b --- /dev/null +++ b/hw3/settings/prometheus/prometheus.yml @@ -0,0 +1,10 @@ +global: + scrape_interval: 10s + evaluation_interval: 10s + +scrape_configs: + - job_name: shop-api-local + metrics_path: /metrics + static_configs: + - targets: + - local:8080 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..092921f1 --- /dev/null +++ b/hw3/shop_api/main.py @@ -0,0 +1,186 @@ +from fastapi import FastAPI, Response, HTTPException, status +from pydantic import BaseModel, conint, confloat +from prometheus_fastapi_instrumentator import Instrumentator + +app = FastAPI(title="Shop API") +Instrumentator().instrument(app).expose(app) + +cart_id_counter = 0 +item_id_counter = 0 + +carts = {} +items = {} + +posint = conint(gt=0) +uint = conint(ge=0) +ufloat = confloat(ge=0) + +class ModifiedItem(BaseModel): + name: str | None = None + price: float | None = None + model_config = { + "extra": "forbid" + } + +class Item(BaseModel): + id: uint = 0 + name: str + price: float + deleted: bool = False + +class CartItem(BaseModel): + id: uint = 0 + name: str + price: float + quantity: int = 0 + deleted: bool = False + +class Cart(BaseModel): + id: int + price: float = 0.0 + items: list[CartItem] = [] + + +""" +cart +""" + +# POST cart - создание, работает как RPC, не принимает тело, возвращает идентификатор +@app.post("/cart", status_code=status.HTTP_201_CREATED) +async def create_cart(response: Response): + global cart_id_counter + cart_id_counter += 1 + new_cart = Cart( + id=cart_id_counter + ) + carts[new_cart.id] = new_cart + response.headers["Location"] = f"/cart/{new_cart.id}" + return {"id": new_cart.id} + + +# GET /cart/{id} - получение корзины по id +@app.get("/cart/{cart_id}", status_code=status.HTTP_200_OK) +async def get_cart(cart_id: int): + return carts[cart_id] + + +# GET /cart - получение списка корзин с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# min_quantity - неотрицательное целое число, минимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +# max_quantity - неотрицательное целое число, максимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +@app.get("/cart", status_code=status.HTTP_200_OK) +async def get_cart_list(offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + min_quantity: uint = None, max_quantity: uint = None): + filtered_carts = [] + for cart in list(carts.values())[offset:]: + if len(filtered_carts) == limit: + break + + min_price_ok = cart.price >= min_price if min_price else True + max_price_ok = cart.price <= max_price if max_price else True + min_quantity_ok = sum(item.quantity for item in cart.items) >= min_quantity if not min_quantity is None else True + max_quantity_ok = sum(item.quantity for item in cart.items) <= max_quantity if not max_quantity is None else True + + if min_price_ok and max_price_ok and min_quantity_ok and max_quantity_ok: + filtered_carts.append(cart) + return filtered_carts + + +# POST /cart/{cart_id}/add/{item_id} - добавление в корзину с cart_id предмета с item_id, +# если товар уже есть, то увеличивается его количество +@app.post("/cart/{cart_id}/add/{item_id}", status_code=status.HTTP_200_OK) +async def add_to_cart(cart_id: int, item_id: int): + cart = carts[cart_id] + item = items[item_id] + + item_exists = False + for i in range(len(cart.items)): + if cart.items[i].id == item_id: + cart.items[i].quantity += 1 + item_exists = True + break + + if not item_exists: + new_item = CartItem( + **item.model_dump(), + quantity=1 + ) + cart.items.append(new_item) + + cart.price += item.price + + +""" +item +""" + +# POST /item - добавление нового товара +@app.post("/item", status_code=status.HTTP_201_CREATED) +async def create_item(item: Item): + global item_id_counter + item_id_counter += 1 + new_item = Item(id=item_id_counter, name=item.name, price=item.price) + items[new_item.id] = new_item + return new_item + +# GET /item/{id} - получение товара по id +@app.get("/item/{item_id}", status_code=status.HTTP_200_OK) +async def get_item(item_id: int): + if items[item_id].deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + return items[item_id] + + +# GET /item - получение списка товаров с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена (опционально, если нет, не учитывает в фильтре) +# show_deleted - булевая переменная, показывать ли удаленные товары (по умолчанию False) +@app.get("/item", status_code=status.HTTP_200_OK) +async def get_item_list(offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + show_deleted: bool = False): + filtered_items = [] + for item in list(items.values())[offset:]: + if len(filtered_items) == limit: + break + + min_price_ok = item.price >= min_price if min_price else True + max_price_ok = item.price <= max_price if max_price else True + show_deleted_ok = (not item.deleted) or show_deleted + + if min_price_ok and max_price_ok and show_deleted_ok: + filtered_items.append(item) + + return filtered_items + +# PUT /item/{id} - замена товара по id (создание запрещено, только замена существующего) +@app.put("/item/{item_id}", status_code=status.HTTP_200_OK) +async def put_item(item_id: int, new_item: Item): + new_item.id = item_id + items[item_id] = new_item + return new_item + +# PATCH /item/{id} - частичное обновление товара по id (разрешено менять все поля, кроме deleted) +@app.patch("/item/{item_id}", status_code=status.HTTP_200_OK) +async def patch_item(item_id: int, new_item: ModifiedItem): + item = items[item_id] + if item.deleted: + raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED) + if new_item.name and item.name != new_item.name: + items[item_id].name = new_item.name + if new_item.price and item.price != new_item.price: + items[item_id].price = new_item.price + return items[item_id] + +# DELETE /item/{id} - удаление товара по id (товар помечается как удаленный) +@app.delete("/item/{item_id}", status_code=status.HTTP_200_OK) +async def delete_item(item_id: int): + items[item_id].deleted = True \ No newline at end of file From 1ccf2870edc267c636ce51b7d992b767c052452f Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 18:51:23 +0300 Subject: [PATCH 06/19] hw4 implemented --- hw4/Dockerfile | 23 +++ hw4/README.md | 16 ++ hw4/__init__.py | 0 hw4/db_transaction_problems.py | 120 ++++++++++++++ hw4/docker-compose.yml | 44 +++++ hw4/requirements.txt | 5 + hw4/shop_api/__init__.py | 0 hw4/shop_api/main.py | 282 +++++++++++++++++++++++++++++++++ 8 files changed, 490 insertions(+) create mode 100644 hw4/Dockerfile create mode 100644 hw4/README.md create mode 100644 hw4/__init__.py create mode 100644 hw4/db_transaction_problems.py create mode 100644 hw4/docker-compose.yml create mode 100644 hw4/requirements.txt create mode 100644 hw4/shop_api/__init__.py create mode 100644 hw4/shop_api/main.py diff --git a/hw4/Dockerfile b/hw4/Dockerfile new file mode 100644 index 00000000..27b0b636 --- /dev/null +++ b/hw4/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/hw4/README.md b/hw4/README.md new file mode 100644 index 00000000..93638951 --- /dev/null +++ b/hw4/README.md @@ -0,0 +1,16 @@ +## ДЗ + +За каждый пункт - 1 балл + +Внедрить во вторую домашку хранение данных в БД, для этого надо: +1) Добавить БД в docket-compose.yml (если БД - это отдельный сервис, если хотите использовать sqlite, то можно скипнуть этот шаг) +2) Переписать код на взаимодействие с вашей БД (если вы еще этого не сделали, если вы уже написали код с БД, подзравляю, вам остался только 3 пункт) +3) В свободной форме, напишите скрипты, которые просимулируют разные "проблемы" которые могут возникнуть в транзакциях (dirty read, not-repeatable read, serialize) и настраивая уровне изоляции покажите, что они действительно решаются (через SQLAlchemy например), то есть: +- показать dirty read при read uncommited +- показать что нет dirty read при read commited +- показать non-repeatable read при read commited +- показать что нет non-repeatable read при repeatable read +- показать phantom reads при repeatable read +- показать что нет phantom reads при serializable + +*Тут зависит от того какую БД вы выбрали, разные БД могут поддерживать разные уровни изоляции \ 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/db_transaction_problems.py b/hw4/db_transaction_problems.py new file mode 100644 index 00000000..8dfaf50a --- /dev/null +++ b/hw4/db_transaction_problems.py @@ -0,0 +1,120 @@ +import threading +import time + +from sqlalchemy.orm import Session +from shop_api.main import ItemOrm, CartItemOrm, engine + +def get_session_with_isolation(level: str) -> Session: + conn = engine.connect() + conn = conn.execution_options(isolation_level=level) + session = Session(bind=conn) + return session + +def clear_items(): + session = get_session_with_isolation("READ COMMITTED") + session.query(CartItemOrm).delete() + session.query(ItemOrm).delete() + session.commit() + session.close() + +def dirty_read_example(): + print("RUN DIRTY READ") + clear_items() + + session1 = get_session_with_isolation("READ UNCOMMITTED") + session2 = get_session_with_isolation("READ UNCOMMITTED") + + session1.connection() + session2.connection() + + def t1(): + item = ItemOrm(name="DirtyItem", price=123) + session1.add(item) + session1.flush() + print("T1: Inserted, not committed") + time.sleep(3) + session1.rollback() + + def t2(): + time.sleep(1) + items = session2.query(ItemOrm).filter_by(name="DirtyItem").all() + print(f"T2: Read items: {items}") + + thread1 = threading.Thread(target=t1) + thread2 = threading.Thread(target=t2) + thread1.start() + thread2.start() + thread1.join() + thread2.join() + + +def non_repeatable_read(): + print("\nRUN REPEATABLE READ") + clear_items() + + session1 = get_session_with_isolation("READ COMMITTED") + session2 = get_session_with_isolation("READ COMMITTED") + + session1.connection() + session2.connection() + + item = ItemOrm(name="NonRepeat", price=10) + session1.add(item) + session1.commit() + + def t1(): + i1 = session1.query(ItemOrm).filter_by(name="NonRepeat").first() + print(f"T1 first read: {i1.price}") + time.sleep(3) + i2 = session1.query(ItemOrm).filter_by(name="NonRepeat").first() + print(f"T1 second read: {i2.price}") + + def t2(): + time.sleep(1) + i = session2.query(ItemOrm).filter_by(name="NonRepeat").first() + i.price = 20 + session2.commit() + print("T2: Updated price to 20") + + thread1 = threading.Thread(target=t1) + thread2 = threading.Thread(target=t2) + thread1.start() + thread2.start() + thread1.join() + thread2.join() + +def phantom_read_example(): + print("\nRUN PHANTOM READ") + clear_items() + + session1 = get_session_with_isolation("REPEATABLE READ") + session2 = get_session_with_isolation("REPEATABLE READ") + + session1.connection() + session2.connection() + + def t1(): + items1 = session1.query(ItemOrm).all() + print(f"T1 first read: {[i.name for i in items1]}") + time.sleep(3) + items2 = session1.query(ItemOrm).all() + print(f"T1 second read: {[i.name for i in items2]}") + + def t2(): + time.sleep(1) + item = ItemOrm(name="Phantom", price=50) + session2.add(item) + session2.commit() + print("T2: Added Phantom") + + thread1 = threading.Thread(target=t1) + thread2 = threading.Thread(target=t2) + thread1.start() + thread2.start() + thread1.join() + thread2.join() + +if __name__ == "__main__": + dirty_read_example() + non_repeatable_read() + phantom_read_example() diff --git a/hw4/docker-compose.yml b/hw4/docker-compose.yml new file mode 100644 index 00000000..947dd220 --- /dev/null +++ b/hw4/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3" + +services: + + postgres: + image: postgres:15 + container_name: hw4_postgres + environment: + POSTGRES_DB: hw4_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + local: + container_name: hw4_local + depends_on: + postgres: + condition: service_healthy + build: + context: . + dockerfile: ./Dockerfile + target: local + restart: always + ports: + - 8080:8080 + + transactions: + container_name: hw4_transactions + build: . + command: python -u db_transaction_problems.py + depends_on: + postgres: + condition: service_healthy + +volumes: + postgres_data: \ No newline at end of file diff --git a/hw4/requirements.txt b/hw4/requirements.txt new file mode 100644 index 00000000..2f5464a9 --- /dev/null +++ b/hw4/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.117.1 +uvicorn>=0.24.0 +sqlalchemy==2.0.25 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 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/main.py b/hw4/shop_api/main.py new file mode 100644 index 00000000..8e636142 --- /dev/null +++ b/hw4/shop_api/main.py @@ -0,0 +1,282 @@ +from fastapi import FastAPI, Response, HTTPException, status +from pydantic import BaseModel, conint, confloat + +from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey +from sqlalchemy.orm import relationship, Session +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from fastapi import Depends + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +DB_HOST = "postgres" +DB_PORT = "5432" +DB_NAME = "hw4_db" +DB_USER = "postgres" +DB_PASSWORD = "password" +DATABASE_URL = ( + f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" +) +engine = create_engine( + DATABASE_URL, + echo=False +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +class ItemOrm(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) + +class CartOrm(Base): + __tablename__ = "carts" + id = Column(Integer, primary_key=True, index=True) + price = Column(Float, default=0.0) + items = relationship("CartItemOrm", back_populates="cart") + +class CartItemOrm(Base): + __tablename__ = "cart_items" + id = Column(Integer, primary_key=True) + cart_id = Column(Integer, ForeignKey("carts.id")) + item_id = Column(Integer, ForeignKey("items.id")) + name = Column(String) + price = Column(Float) + quantity = Column(Integer, default=1) + cart = relationship("CartOrm", back_populates="items") + +Base.metadata.create_all(bind=engine) +app = FastAPI(title="Shop API") + +posint = conint(gt=0) +uint = conint(ge=0) +ufloat = confloat(ge=0) + +class ModifiedItem(BaseModel): + name: str | None = None + price: float | None = None + model_config = { + "extra": "forbid" + } + +class Item(BaseModel): + id: uint = 0 + name: str + price: float + deleted: bool = False + +class CartItem(BaseModel): + id: uint = 0 + name: str + price: float + quantity: int = 0 + deleted: bool = False + +class Cart(BaseModel): + id: int + price: float = 0.0 + items: list[CartItem] = [] + + +""" +cart +""" + +# POST cart - создание, работает как RPC, не принимает тело, возвращает идентификатор +@app.post("/cart", status_code=status.HTTP_201_CREATED) +async def create_cart(response: Response, db: Session = Depends(get_db)): + new_cart = CartOrm() + db.add(new_cart) + db.commit() + db.refresh(new_cart) + response.headers["Location"] = f"/cart/{new_cart.id}" + return {"id": new_cart.id} + + +# GET /cart/{id} - получение корзины по id +@app.get("/cart/{cart_id}", status_code=status.HTTP_200_OK) +async def get_cart(cart_id: int, db: Session = Depends(get_db)): + cart = db.query(CartOrm).filter(CartOrm.id == cart_id).first() + if not cart: + raise HTTPException(status_code=404) + return { + "id": cart.id, + "price": cart.price, + "items": [ + { + "id": i.item_id, + "name": i.name, + "price": i.price, + "quantity": i.quantity, + } + for i in cart.items + ] + } + + +# GET /cart - получение списка корзин с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# min_quantity - неотрицательное целое число, минимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +# max_quantity - неотрицательное целое число, максимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +@app.get("/cart", status_code=status.HTTP_200_OK) +async def get_cart_list( + offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + min_quantity: uint = None, max_quantity: uint = None, + db: Session = Depends(get_db) +): + query = db.query(CartOrm) + + if min_price is not None: + query = query.filter(CartOrm.price >= min_price) + if max_price is not None: + query = query.filter(CartOrm.price <= max_price) + + carts = query.offset(offset).limit(limit).all() + + result = [] + for cart in carts: + total_quantity = sum(i.quantity for i in cart.items) + if min_quantity is not None and total_quantity < min_quantity: + continue + if max_quantity is not None and total_quantity > max_quantity: + continue + + result.append({ + "id": cart.id, + "price": cart.price, + "items": [ + { + "id": i.item_id, + "name": i.name, + "price": i.price, + "quantity": i.quantity + } + for i in cart.items + ], + }) + + return result + + +# POST /cart/{cart_id}/add/{item_id} - добавление в корзину с cart_id предмета с item_id, +# если товар уже есть, то увеличивается его количество +@app.post("/cart/{cart_id}/add/{item_id}", status_code=status.HTTP_200_OK) +async def add_to_cart(cart_id: int, item_id: int, db: Session = Depends(get_db)): + cart = db.query(CartOrm).filter(CartOrm.id == cart_id).first() + item = db.query(ItemOrm).filter(ItemOrm.id == item_id, ItemOrm.deleted == False).first() + if not cart or not item: + raise HTTPException(status_code=404) + cart_item = db.query(CartItemOrm).filter_by(cart_id=cart.id, item_id=item.id).first() + if cart_item: + cart_item.quantity += 1 + else: + cart_item = CartItemOrm( + cart_id=cart.id, + item_id=item.id, + name=item.name, + price=item.price, + quantity=1 + ) + db.add(cart_item) + cart.price += item.price + db.commit() + db.refresh(cart) + + +""" +item +""" + +# POST /item - добавление нового товара +@app.post("/item", status_code=status.HTTP_201_CREATED) +async def create_item(item: Item, db: Session = Depends(get_db)): + db_item = ItemOrm(name=item.name, price=item.price) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + + +# GET /item/{id} - получение товара по id +@app.get("/item/{item_id}", status_code=status.HTTP_200_OK) +async def get_item(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item or db_item.deleted: + raise HTTPException(status_code=404) + return db_item + + +# GET /item - получение списка товаров с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена (опционально, если нет, не учитывает в фильтре) +# show_deleted - булевая переменная, показывать ли удаленные товары (по умолчанию False) +@app.get("/item", status_code=status.HTTP_200_OK) +async def get_item_list( + offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + show_deleted: bool = False, + db: Session = Depends(get_db) +): + query = db.query(ItemOrm) + if not show_deleted: + query = query.filter(ItemOrm.deleted == False) + if min_price is not None: + query = query.filter(ItemOrm.price >= min_price) + if max_price is not None: + query = query.filter(ItemOrm.price <= max_price) + return query.offset(offset).limit(limit).all() + + +# PUT /item/{id} - замена товара по id (создание запрещено, только замена существующего) +@app.put("/item/{item_id}", status_code=status.HTTP_200_OK) +async def put_item(item_id: int, item: Item, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404, detail="Item not found") + db_item.name = item.name + db_item.price = item.price + db.commit() + db.refresh(db_item) + return db_item + + +# PATCH /item/{id} - частичное обновление товара по id (разрешено менять все поля, кроме deleted) +@app.patch("/item/{item_id}", status_code=status.HTTP_200_OK) +async def patch_item(item_id: int, item: ModifiedItem, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + if db_item.deleted: + raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED) + + if item.name is not None and db_item.name != item.name: + db_item.name = item.name + if item.price is not None and db_item.price != item.price: + db_item.price = item.price + db.commit() + db.refresh(db_item) + return db_item + + +# DELETE /item/{id} - удаление товара по id (товар помечается как удаленный) +@app.delete("/item/{item_id}", status_code=status.HTTP_200_OK) +async def delete_item(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404) + db_item.deleted = True + db.commit() \ No newline at end of file From c731175fc523e4088e9cd50dec2c72a25d1b4fde Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 18:54:41 +0300 Subject: [PATCH 07/19] hw5 implemented --- .github/workflows/hw5-tests.yml | 53 ++++++ .gitignore | 1 + hw5/Dockerfile | 23 +++ hw5/README.md | 5 + hw5/__init__.py | 0 hw5/docker-compose.yml | 36 ++++ hw5/requirements.txt | 13 ++ hw5/shop_api/__init__.py | 0 hw5/shop_api/main.py | 282 ++++++++++++++++++++++++++++++++ hw5/tests/__init__.py | 0 hw5/tests/cart_tests.py | 4 + hw5/tests/conftest.py | 21 +++ hw5/tests/item_tests.py | 7 + 13 files changed, 445 insertions(+) create mode 100644 .github/workflows/hw5-tests.yml create mode 100644 hw5/Dockerfile create mode 100644 hw5/README.md create mode 100644 hw5/__init__.py create mode 100644 hw5/docker-compose.yml 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/tests/__init__.py create mode 100644 hw5/tests/cart_tests.py create mode 100644 hw5/tests/conftest.py create mode 100644 hw5/tests/item_tests.py diff --git a/.github/workflows/hw5-tests.yml b/.github/workflows/hw5-tests.yml new file mode 100644 index 00000000..62b08c58 --- /dev/null +++ b/.github/workflows/hw5-tests.yml @@ -0,0 +1,53 @@ +name: "HW5 Tests" + +on: + pull_request: + branches: [ main, hw5 ] + paths: [ 'hw5/**' ] + push: + branches: [ main, hw5 ] + paths: [ 'hw5/**' ] + +jobs: + test-hw5: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13"] + + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: hw5_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + working-directory: hw5 + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + working-directory: hw5 + env: + PYTHONPATH: ${{ github.workspace }}/hw5 + run: | + pytest -vv --cov=hw5/ ./hw5/tests diff --git a/.gitignore b/.gitignore index 852216e6..ec584d56 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,4 @@ dmypy.json # macOS .DS_Store +/hw4/settings/ diff --git a/hw5/Dockerfile b/hw5/Dockerfile new file mode 100644 index 00000000..27b0b636 --- /dev/null +++ b/hw5/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/hw5/README.md b/hw5/README.md new file mode 100644 index 00000000..33e79328 --- /dev/null +++ b/hw5/README.md @@ -0,0 +1,5 @@ +# ДЗ + +1) Добиться 95% покрытия тестами вашей второй домашки - 1 балл + +2) Настроить автозапуск этих тестов в CI, если вы подключали сторонюю БД, то можно посмотреть вот [сюда](https://dev.to/kashifsoofi/integration-test-postgres-using-github-actions-3lln), чтобы поддержать тесты с ней в CI. По итогу у вас должен получится зеленый пайплайн - оценивается в еще 2 балла. diff --git a/hw5/__init__.py b/hw5/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/docker-compose.yml b/hw5/docker-compose.yml new file mode 100644 index 00000000..dda45d26 --- /dev/null +++ b/hw5/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3" + +services: + + postgres: + image: postgres:15 + container_name: hw5_postgres + environment: + POSTGRES_DB: hw5_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + local: + container_name: hw5_local + depends_on: + postgres: + condition: service_healthy + build: + context: . + dockerfile: ./Dockerfile + target: local + restart: always + ports: + - 8080:8080 + +volumes: + postgres_data: \ No newline at end of file diff --git a/hw5/requirements.txt b/hw5/requirements.txt new file mode 100644 index 00000000..d7ccda4c --- /dev/null +++ b/hw5/requirements.txt @@ -0,0 +1,13 @@ +fastapi>=0.117.1 +uvicorn>=0.24.0 +sqlalchemy==2.0.25 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 + +pytest-cov +pytest-mock +pytest +pytest-asyncio +responses +faker +httpx 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..8e636142 --- /dev/null +++ b/hw5/shop_api/main.py @@ -0,0 +1,282 @@ +from fastapi import FastAPI, Response, HTTPException, status +from pydantic import BaseModel, conint, confloat + +from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey +from sqlalchemy.orm import relationship, Session +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from fastapi import Depends + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +DB_HOST = "postgres" +DB_PORT = "5432" +DB_NAME = "hw4_db" +DB_USER = "postgres" +DB_PASSWORD = "password" +DATABASE_URL = ( + f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" +) +engine = create_engine( + DATABASE_URL, + echo=False +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +class ItemOrm(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) + +class CartOrm(Base): + __tablename__ = "carts" + id = Column(Integer, primary_key=True, index=True) + price = Column(Float, default=0.0) + items = relationship("CartItemOrm", back_populates="cart") + +class CartItemOrm(Base): + __tablename__ = "cart_items" + id = Column(Integer, primary_key=True) + cart_id = Column(Integer, ForeignKey("carts.id")) + item_id = Column(Integer, ForeignKey("items.id")) + name = Column(String) + price = Column(Float) + quantity = Column(Integer, default=1) + cart = relationship("CartOrm", back_populates="items") + +Base.metadata.create_all(bind=engine) +app = FastAPI(title="Shop API") + +posint = conint(gt=0) +uint = conint(ge=0) +ufloat = confloat(ge=0) + +class ModifiedItem(BaseModel): + name: str | None = None + price: float | None = None + model_config = { + "extra": "forbid" + } + +class Item(BaseModel): + id: uint = 0 + name: str + price: float + deleted: bool = False + +class CartItem(BaseModel): + id: uint = 0 + name: str + price: float + quantity: int = 0 + deleted: bool = False + +class Cart(BaseModel): + id: int + price: float = 0.0 + items: list[CartItem] = [] + + +""" +cart +""" + +# POST cart - создание, работает как RPC, не принимает тело, возвращает идентификатор +@app.post("/cart", status_code=status.HTTP_201_CREATED) +async def create_cart(response: Response, db: Session = Depends(get_db)): + new_cart = CartOrm() + db.add(new_cart) + db.commit() + db.refresh(new_cart) + response.headers["Location"] = f"/cart/{new_cart.id}" + return {"id": new_cart.id} + + +# GET /cart/{id} - получение корзины по id +@app.get("/cart/{cart_id}", status_code=status.HTTP_200_OK) +async def get_cart(cart_id: int, db: Session = Depends(get_db)): + cart = db.query(CartOrm).filter(CartOrm.id == cart_id).first() + if not cart: + raise HTTPException(status_code=404) + return { + "id": cart.id, + "price": cart.price, + "items": [ + { + "id": i.item_id, + "name": i.name, + "price": i.price, + "quantity": i.quantity, + } + for i in cart.items + ] + } + + +# GET /cart - получение списка корзин с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена включительно (опционально, если нет, не учитывает в фильтре) +# min_quantity - неотрицательное целое число, минимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +# max_quantity - неотрицательное целое число, максимальное общее число товаров включительно (опционально, если нет, не учитывается в фильтре) +@app.get("/cart", status_code=status.HTTP_200_OK) +async def get_cart_list( + offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + min_quantity: uint = None, max_quantity: uint = None, + db: Session = Depends(get_db) +): + query = db.query(CartOrm) + + if min_price is not None: + query = query.filter(CartOrm.price >= min_price) + if max_price is not None: + query = query.filter(CartOrm.price <= max_price) + + carts = query.offset(offset).limit(limit).all() + + result = [] + for cart in carts: + total_quantity = sum(i.quantity for i in cart.items) + if min_quantity is not None and total_quantity < min_quantity: + continue + if max_quantity is not None and total_quantity > max_quantity: + continue + + result.append({ + "id": cart.id, + "price": cart.price, + "items": [ + { + "id": i.item_id, + "name": i.name, + "price": i.price, + "quantity": i.quantity + } + for i in cart.items + ], + }) + + return result + + +# POST /cart/{cart_id}/add/{item_id} - добавление в корзину с cart_id предмета с item_id, +# если товар уже есть, то увеличивается его количество +@app.post("/cart/{cart_id}/add/{item_id}", status_code=status.HTTP_200_OK) +async def add_to_cart(cart_id: int, item_id: int, db: Session = Depends(get_db)): + cart = db.query(CartOrm).filter(CartOrm.id == cart_id).first() + item = db.query(ItemOrm).filter(ItemOrm.id == item_id, ItemOrm.deleted == False).first() + if not cart or not item: + raise HTTPException(status_code=404) + cart_item = db.query(CartItemOrm).filter_by(cart_id=cart.id, item_id=item.id).first() + if cart_item: + cart_item.quantity += 1 + else: + cart_item = CartItemOrm( + cart_id=cart.id, + item_id=item.id, + name=item.name, + price=item.price, + quantity=1 + ) + db.add(cart_item) + cart.price += item.price + db.commit() + db.refresh(cart) + + +""" +item +""" + +# POST /item - добавление нового товара +@app.post("/item", status_code=status.HTTP_201_CREATED) +async def create_item(item: Item, db: Session = Depends(get_db)): + db_item = ItemOrm(name=item.name, price=item.price) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + + +# GET /item/{id} - получение товара по id +@app.get("/item/{item_id}", status_code=status.HTTP_200_OK) +async def get_item(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item or db_item.deleted: + raise HTTPException(status_code=404) + return db_item + + +# GET /item - получение списка товаров с query-параметрами +# offset - неотрицательное целое число, смещение по списку (опционально, по-умолчанию 0) +# limit - положительное целое число, ограничение на количество (опционально, по-умолчанию 10) +# min_price - число с плавающей запятой, минимальная цена (опционально, если нет, не учитывает в фильтре) +# max_price - число с плавающей запятой, максимальная цена (опционально, если нет, не учитывает в фильтре) +# show_deleted - булевая переменная, показывать ли удаленные товары (по умолчанию False) +@app.get("/item", status_code=status.HTTP_200_OK) +async def get_item_list( + offset: uint = 0, limit: posint = 10, + min_price: ufloat = None, max_price: ufloat = None, + show_deleted: bool = False, + db: Session = Depends(get_db) +): + query = db.query(ItemOrm) + if not show_deleted: + query = query.filter(ItemOrm.deleted == False) + if min_price is not None: + query = query.filter(ItemOrm.price >= min_price) + if max_price is not None: + query = query.filter(ItemOrm.price <= max_price) + return query.offset(offset).limit(limit).all() + + +# PUT /item/{id} - замена товара по id (создание запрещено, только замена существующего) +@app.put("/item/{item_id}", status_code=status.HTTP_200_OK) +async def put_item(item_id: int, item: Item, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404, detail="Item not found") + db_item.name = item.name + db_item.price = item.price + db.commit() + db.refresh(db_item) + return db_item + + +# PATCH /item/{id} - частичное обновление товара по id (разрешено менять все поля, кроме deleted) +@app.patch("/item/{item_id}", status_code=status.HTTP_200_OK) +async def patch_item(item_id: int, item: ModifiedItem, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + if db_item.deleted: + raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED) + + if item.name is not None and db_item.name != item.name: + db_item.name = item.name + if item.price is not None and db_item.price != item.price: + db_item.price = item.price + db.commit() + db.refresh(db_item) + return db_item + + +# DELETE /item/{id} - удаление товара по id (товар помечается как удаленный) +@app.delete("/item/{item_id}", status_code=status.HTTP_200_OK) +async def delete_item(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(ItemOrm).filter(ItemOrm.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404) + db_item.deleted = True + db.commit() \ No newline at end of file diff --git a/hw5/tests/__init__.py b/hw5/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw5/tests/cart_tests.py b/hw5/tests/cart_tests.py new file mode 100644 index 00000000..f87b91cb --- /dev/null +++ b/hw5/tests/cart_tests.py @@ -0,0 +1,4 @@ +def test_create_cart(client): + res = client.post("/cart") + assert res.status_code == 201 + assert res.json()["id"] == 1 \ No newline at end of file diff --git a/hw5/tests/conftest.py b/hw5/tests/conftest.py new file mode 100644 index 00000000..f7d1a10f --- /dev/null +++ b/hw5/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from hw5.shop_api.main import app, Base, get_db + +DATABASE_URL = "postgresql://postgres:password@postgres:5432/hw5_db" + +engine = create_engine(DATABASE_URL) +TestingSessionLocal = sessionmaker(bind=engine) + +@pytest.fixture(autouse=True) +def clean_db(): + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) + yield + +@pytest.fixture() +def client(): + return TestClient(app) diff --git a/hw5/tests/item_tests.py b/hw5/tests/item_tests.py new file mode 100644 index 00000000..958be2f9 --- /dev/null +++ b/hw5/tests/item_tests.py @@ -0,0 +1,7 @@ +def test_create_item(client): + res = client.post("/item", json={"name": "item1", "price": 1.0}) + assert res.status_code == 201 + body = res.json() + assert body["id"] == 1 + assert body["name"] == "item1" + assert body["price"] == 1.0 From 3806afd4b50418957eb9d5fcbbcd2ecf72fae9a8 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:05:14 +0300 Subject: [PATCH 08/19] db fixes --- .github/workflows/hw5-tests.yml | 4 ++-- hw5/tests/conftest.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/hw5-tests.yml b/.github/workflows/hw5-tests.yml index 62b08c58..6aba8452 100644 --- a/.github/workflows/hw5-tests.yml +++ b/.github/workflows/hw5-tests.yml @@ -2,10 +2,10 @@ name: "HW5 Tests" on: pull_request: - branches: [ main, hw5 ] + branches: [ main ] paths: [ 'hw5/**' ] push: - branches: [ main, hw5 ] + branches: [ main ] paths: [ 'hw5/**' ] jobs: diff --git a/hw5/tests/conftest.py b/hw5/tests/conftest.py index f7d1a10f..844cdb23 100644 --- a/hw5/tests/conftest.py +++ b/hw5/tests/conftest.py @@ -3,9 +3,9 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from hw5.shop_api.main import app, Base, get_db +from hw5.shop_api.main import app, Base -DATABASE_URL = "postgresql://postgres:password@postgres:5432/hw5_db" +DATABASE_URL = "postgresql://postgres:password@localhost:5432/hw5_db" engine = create_engine(DATABASE_URL) TestingSessionLocal = sessionmaker(bind=engine) From 90d7d9b48daaf6d9fe91761d0746f713c51f2958 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:08:31 +0300 Subject: [PATCH 09/19] db fixes --- hw5/shop_api/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hw5/shop_api/main.py b/hw5/shop_api/main.py index 8e636142..fc956f97 100644 --- a/hw5/shop_api/main.py +++ b/hw5/shop_api/main.py @@ -14,9 +14,9 @@ def get_db(): finally: db.close() -DB_HOST = "postgres" +DB_HOST = "localhost" DB_PORT = "5432" -DB_NAME = "hw4_db" +DB_NAME = "hw5_db" DB_USER = "postgres" DB_PASSWORD = "password" DATABASE_URL = ( From f613c76c9c3698b72dd8493f2f157689e42a3324 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:13:20 +0300 Subject: [PATCH 10/19] db fixes --- .github/workflows/hw5-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hw5-tests.yml b/.github/workflows/hw5-tests.yml index 6aba8452..309e5f9b 100644 --- a/.github/workflows/hw5-tests.yml +++ b/.github/workflows/hw5-tests.yml @@ -50,4 +50,4 @@ jobs: env: PYTHONPATH: ${{ github.workspace }}/hw5 run: | - pytest -vv --cov=hw5/ ./hw5/tests + pytest -vv --cov=hw5/ ./tests From fd6cfab29d272afab2a02518d15636dac2e7423c Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:16:45 +0300 Subject: [PATCH 11/19] test files names fixed --- hw5/tests/{cart_tests.py => test_carts.py} | 0 hw5/tests/{item_tests.py => test_items.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename hw5/tests/{cart_tests.py => test_carts.py} (100%) rename hw5/tests/{item_tests.py => test_items.py} (100%) diff --git a/hw5/tests/cart_tests.py b/hw5/tests/test_carts.py similarity index 100% rename from hw5/tests/cart_tests.py rename to hw5/tests/test_carts.py diff --git a/hw5/tests/item_tests.py b/hw5/tests/test_items.py similarity index 100% rename from hw5/tests/item_tests.py rename to hw5/tests/test_items.py From 041272587162e4cec32525e08adc3fc18b7c0553 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:20:00 +0300 Subject: [PATCH 12/19] yml fixed --- .github/workflows/hw5-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hw5-tests.yml b/.github/workflows/hw5-tests.yml index 309e5f9b..a9e23073 100644 --- a/.github/workflows/hw5-tests.yml +++ b/.github/workflows/hw5-tests.yml @@ -50,4 +50,4 @@ jobs: env: PYTHONPATH: ${{ github.workspace }}/hw5 run: | - pytest -vv --cov=hw5/ ./tests + pytest -vv --cov=shop_api ./tests From a8429162090938f468d8105dfe456a94dc7dc33b Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:28:39 +0300 Subject: [PATCH 13/19] requirements.txt updated --- hw5/requirements.txt | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/hw5/requirements.txt b/hw5/requirements.txt index d7ccda4c..e4a8d43e 100644 --- a/hw5/requirements.txt +++ b/hw5/requirements.txt @@ -1,13 +1,13 @@ -fastapi>=0.117.1 -uvicorn>=0.24.0 -sqlalchemy==2.0.25 -psycopg2-binary==2.9.9 -python-dotenv==1.0.0 +fastapi==0.120.0 +uvicorn==0.38.0 +sqlalchemy==2.0.44 +psycopg2-binary==2.9.11 +python-dotenv==1.2.1 -pytest-cov -pytest-mock -pytest -pytest-asyncio -responses -faker -httpx +pytest==8.4.2 +pytest-cov==7.0.0 +pytest-mock==3.15.1 +pytest-asyncio==1.0.0 +responses==0.22.0 +Faker==19.0.1 +httpx==0.24.0 From 58a2f70a102a0d68e9313c62a6759f0a5804b1d2 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:31:12 +0300 Subject: [PATCH 14/19] requirements.txt updated --- hw5/requirements.txt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hw5/requirements.txt b/hw5/requirements.txt index e4a8d43e..6cfd288f 100644 --- a/hw5/requirements.txt +++ b/hw5/requirements.txt @@ -8,6 +8,10 @@ pytest==8.4.2 pytest-cov==7.0.0 pytest-mock==3.15.1 pytest-asyncio==1.0.0 -responses==0.22.0 -Faker==19.0.1 -httpx==0.24.0 + +responses +faker +httpx + + + From 67d6ab9d842a0e606bfba641aa6ff07f1c327950 Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:34:22 +0300 Subject: [PATCH 15/19] tests added --- hw5/tests/test_carts.py | 40 +++++++++++++++++++++++++++++++++++++++- hw5/tests/test_items.py | 22 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/hw5/tests/test_carts.py b/hw5/tests/test_carts.py index f87b91cb..59b64cbf 100644 --- a/hw5/tests/test_carts.py +++ b/hw5/tests/test_carts.py @@ -1,4 +1,42 @@ def test_create_cart(client): res = client.post("/cart") assert res.status_code == 201 - assert res.json()["id"] == 1 \ No newline at end of file + assert res.json()["id"] == 1 + + +def test_add_to_cart(client): + res_item = client.post("/item", json={"name": "item1", "price": 1.0}) + assert res_item.status_code == 201 + item_id = res_item.json()["id"] + + res_cart = client.post("/cart") + assert res_cart.status_code == 201 + cart_id = res_cart.json()["id"] + + res = client.post(f"/cart/{cart_id}/add/{item_id}") + assert res.status_code == 200 + + res_cart = client.get(f"/cart/{cart_id}") + assert res_cart.status_code == 200 + cart = res_cart.json() + + assert cart["id"] == cart_id + assert len(cart["items"]) == 1 + assert cart["items"][0]["name"] == "item1" + assert cart["items"][0]["quantity"] == 1 + assert cart["price"] == 1.0 + + +def test_cart_list_filtering(client): + res_item = client.post("/item", json={"name": "item1", "price": 1.0}) + item_id = res_item.json()["id"] + + res_cart = client.post("/cart") + cart_id = res_cart.json()["id"] + client.post(f"/cart/{cart_id}/add/{item_id}") + + res = client.get("/cart?min_price=0.5&max_price=2.0") + assert res.status_code == 200 + carts = res.json() + assert len(carts) >= 1 + assert carts[0]["id"] == cart_id \ No newline at end of file diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py index 958be2f9..0c6e700e 100644 --- a/hw5/tests/test_items.py +++ b/hw5/tests/test_items.py @@ -5,3 +5,25 @@ def test_create_item(client): assert body["id"] == 1 assert body["name"] == "item1" assert body["price"] == 1.0 + + +def test_get_item(client): + client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.get("/item/1") + assert res.status_code == 200 + assert res.json()["name"] == "item1" + + +def test_patch_item(client): + client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.patch("/item/1", json={"price": 2.0}) + assert res.status_code == 200 + assert res.json()["price"] == 2.0 + + +def test_delete_item(client): + client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.delete("/item/1") + assert res.status_code == 200 + res2 = client.get("/item/1") + assert res2.status_code == 404 From 8a7e3a91c7c83949ae9d23c1b6af2c4e034f763e Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 19:51:48 +0300 Subject: [PATCH 16/19] tests added --- hw5/tests/test_carts.py | 50 +++++++++++++++++++++++++++++------------ hw5/tests/test_items.py | 31 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/hw5/tests/test_carts.py b/hw5/tests/test_carts.py index 59b64cbf..97a47116 100644 --- a/hw5/tests/test_carts.py +++ b/hw5/tests/test_carts.py @@ -4,30 +4,29 @@ def test_create_cart(client): assert res.json()["id"] == 1 -def test_add_to_cart(client): - res_item = client.post("/item", json={"name": "item1", "price": 1.0}) - assert res_item.status_code == 201 - item_id = res_item.json()["id"] - +def test_get_cart(client): res_cart = client.post("/cart") - assert res_cart.status_code == 201 cart_id = res_cart.json()["id"] + assert res_cart.status_code == 201 - res = client.post(f"/cart/{cart_id}/add/{item_id}") - assert res.status_code == 200 + res_item = client.post("/item", json={"name": "item1", "price": 1.0}) + item_id = res_item.json()["id"] + assert res_item.status_code == 201 - res_cart = client.get(f"/cart/{cart_id}") - assert res_cart.status_code == 200 - cart = res_cart.json() + res_add = client.post(f"/cart/{cart_id}/add/{item_id}") + assert res_add.status_code == 200 + res_get = client.get(f"/cart/{cart_id}") + assert res_get.status_code == 200 + cart = res_get.json() assert cart["id"] == cart_id + assert cart["price"] == 1.0 assert len(cart["items"]) == 1 assert cart["items"][0]["name"] == "item1" assert cart["items"][0]["quantity"] == 1 - assert cart["price"] == 1.0 -def test_cart_list_filtering(client): +def test_get_cart_list(client): res_item = client.post("/item", json={"name": "item1", "price": 1.0}) item_id = res_item.json()["id"] @@ -39,4 +38,27 @@ def test_cart_list_filtering(client): assert res.status_code == 200 carts = res.json() assert len(carts) >= 1 - assert carts[0]["id"] == cart_id \ No newline at end of file + assert carts[0]["id"] == cart_id + + +def test_add_to_cart(client): + res_item = client.post("/item", json={"name": "item1", "price": 1.0}) + assert res_item.status_code == 201 + item_id = res_item.json()["id"] + + res_cart = client.post("/cart") + assert res_cart.status_code == 201 + cart_id = res_cart.json()["id"] + + res = client.post(f"/cart/{cart_id}/add/{item_id}") + assert res.status_code == 200 + + res_cart = client.get(f"/cart/{cart_id}") + assert res_cart.status_code == 200 + cart = res_cart.json() + + assert cart["id"] == cart_id + assert len(cart["items"]) == 1 + assert cart["items"][0]["name"] == "item1" + assert cart["items"][0]["quantity"] == 1 + assert cart["price"] == 1.0 diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py index 0c6e700e..d2698db1 100644 --- a/hw5/tests/test_items.py +++ b/hw5/tests/test_items.py @@ -14,6 +14,37 @@ def test_get_item(client): assert res.json()["name"] == "item1" +def test_get_item_list(client): + for i in range(1, 4): + res = client.post("/item", json={"name": f"item{i}", "price": float(i)}) + assert res.status_code == 201 + + res_list = client.get("/item") + assert res_list.status_code == 200 + items = res_list.json() + for i in range(1, 4): + assert any(item["name"] == f"item{i}" and item["price"] == float(i) for item in items) + + +def test_put_item(client): + res_create = client.post("/item", json={"name": "item1", "price": 1.0}) + item_id = res_create.json()["id"] + assert res_create.status_code == 201 + + res_put = client.put(f"/item/{item_id}", json={"name": "item2", "price": 2.0}) + assert res_put.status_code == 200 + item = res_put.json() + assert item["id"] == item_id + assert item["name"] == "item2" + assert item["price"] == 2.0 + + res_get = client.get(f"/item/{item_id}") + assert res_get.status_code == 200 + item_get = res_get.json() + assert item_get["name"] == "item2" + assert item_get["price"] == 2.0 + + def test_patch_item(client): client.post("/item", json={"name": "item1", "price": 1.0}) res = client.patch("/item/1", json={"price": 2.0}) From 93273aa6b3a713b695c9cd8b1fa924ca25b80b4b Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 20:08:14 +0300 Subject: [PATCH 17/19] tests added --- hw5/tests/test_carts.py | 33 +++++++++++++++++++++ hw5/tests/test_items.py | 65 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/hw5/tests/test_carts.py b/hw5/tests/test_carts.py index 97a47116..9c56f086 100644 --- a/hw5/tests/test_carts.py +++ b/hw5/tests/test_carts.py @@ -26,6 +26,11 @@ def test_get_cart(client): assert cart["items"][0]["quantity"] == 1 +def test_get_cart_fail(client): + res_get = client.get(f"/cart/{1}") + assert res_get.status_code == 404 + + def test_get_cart_list(client): res_item = client.post("/item", json={"name": "item1", "price": 1.0}) item_id = res_item.json()["id"] @@ -40,6 +45,18 @@ def test_get_cart_list(client): assert len(carts) >= 1 assert carts[0]["id"] == cart_id + res = client.get("/cart?min_price=0.5") + assert res.status_code == 200 + carts = res.json() + assert len(carts) >= 1 + assert carts[0]["id"] == cart_id + + res = client.get("/cart?max_price=2.0") + assert res.status_code == 200 + carts = res.json() + assert len(carts) >= 1 + assert carts[0]["id"] == cart_id + def test_add_to_cart(client): res_item = client.post("/item", json={"name": "item1", "price": 1.0}) @@ -62,3 +79,19 @@ def test_add_to_cart(client): assert cart["items"][0]["name"] == "item1" assert cart["items"][0]["quantity"] == 1 assert cart["price"] == 1.0 + + +def test_add_to_cart_fail(client): + res_item = client.post("/item", json={"name": "item1", "price": 1.0}) + assert res_item.status_code == 201 + item_id = res_item.json()["id"] + + res_cart = client.post("/cart") + assert res_cart.status_code == 201 + cart_id = res_cart.json()["id"] + + res = client.post(f"/cart/{cart_id}/add/{item_id + 1}") + assert res.status_code == 404 + + res = client.post(f"/cart/{cart_id + 1}/add/{item_id}") + assert res.status_code == 404 diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py index d2698db1..40a5ea61 100644 --- a/hw5/tests/test_items.py +++ b/hw5/tests/test_items.py @@ -14,16 +14,48 @@ def test_get_item(client): assert res.json()["name"] == "item1" +def test_get_item_fail(client): + res = client.get("/item/1") + assert res.status_code == 404 + + def test_get_item_list(client): for i in range(1, 4): res = client.post("/item", json={"name": f"item{i}", "price": float(i)}) assert res.status_code == 201 + res_delete = client.delete("/item/2") + assert res_delete.status_code == 200 + res_list = client.get("/item") assert res_list.status_code == 200 items = res_list.json() - for i in range(1, 4): - assert any(item["name"] == f"item{i}" and item["price"] == float(i) for item in items) + assert len(items) == 2 + assert all(item["id"] != 2 for item in items) + + res_all = client.get("/item?show_deleted=true") + assert res_all.status_code == 200 + all_items = res_all.json() + assert len(all_items) == 3 + assert any(item["id"] == 2 for item in all_items) + + res_min = client.get("/item?min_price=2.0") + assert res_min.status_code == 200 + min_items = res_min.json() + assert len(min_items) == 2 + assert all(item["price"] >= 2.0 for item in min_items) + + res_max = client.get("/item?max_price=2.0") + assert res_max.status_code == 200 + max_items = res_max.json() + assert len(max_items) == 2 + assert all(item["price"] <= 2.0 for item in max_items) + + res_range = client.get("/item?min_price=1.0&max_price=2.0") + assert res_range.status_code == 200 + range_items = res_range.json() + assert len(range_items) == 2 + assert all(1.0 <= item["price"] <= 2.0 for item in range_items) def test_put_item(client): @@ -45,16 +77,35 @@ def test_put_item(client): assert item_get["price"] == 2.0 +def test_put_item_fail(client): + res_create = client.post("/item", json={"name": "item1", "price": 1.0}) + item_id = res_create.json()["id"] + assert res_create.status_code == 201 + + res_put = client.put(f"/item/{item_id + 1}", json={"name": "item2", "price": 2.0}) + assert res_put.status_code == 404 + + def test_patch_item(client): - client.post("/item", json={"name": "item1", "price": 1.0}) - res = client.patch("/item/1", json={"price": 2.0}) + res_create = client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.patch(f"/item/{res_create}", json={"price": 2.0}) assert res.status_code == 200 assert res.json()["price"] == 2.0 +def test_patch_item_fail(client): + res_create = client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.patch(f"/item/{res_create + 1}", json={"price": 2.0}) + assert res.status_code == 404 + + client.delete(f"/item/{res_create}") + res = client.patch(f"/item/{res_create}", json={"price": 2.0}) + assert res.status_code == 304 + + def test_delete_item(client): - client.post("/item", json={"name": "item1", "price": 1.0}) - res = client.delete("/item/1") + res_create = client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.delete(f"/item/{res_create}") assert res.status_code == 200 - res2 = client.get("/item/1") + res2 = client.get(f"/item/{res_create}") assert res2.status_code == 404 From ddaf76af24a43cb2b2fe75109c6a90fe9bdc33eb Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 20:12:53 +0300 Subject: [PATCH 18/19] tests fixed --- hw5/tests/test_items.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py index 40a5ea61..8869d146 100644 --- a/hw5/tests/test_items.py +++ b/hw5/tests/test_items.py @@ -42,19 +42,19 @@ def test_get_item_list(client): res_min = client.get("/item?min_price=2.0") assert res_min.status_code == 200 min_items = res_min.json() - assert len(min_items) == 2 + assert len(min_items) == 1 assert all(item["price"] >= 2.0 for item in min_items) res_max = client.get("/item?max_price=2.0") assert res_max.status_code == 200 max_items = res_max.json() - assert len(max_items) == 2 + assert len(max_items) == 1 assert all(item["price"] <= 2.0 for item in max_items) res_range = client.get("/item?min_price=1.0&max_price=2.0") assert res_range.status_code == 200 range_items = res_range.json() - assert len(range_items) == 2 + assert len(range_items) == 1 assert all(1.0 <= item["price"] <= 2.0 for item in range_items) @@ -82,7 +82,8 @@ def test_put_item_fail(client): item_id = res_create.json()["id"] assert res_create.status_code == 201 - res_put = client.put(f"/item/{item_id + 1}", json={"name": "item2", "price": 2.0}) + next_id = item_id + 1 + res_put = client.put(f"/item/{next_id}", json={"name": "item2", "price": 2.0}) assert res_put.status_code == 404 @@ -95,7 +96,8 @@ def test_patch_item(client): def test_patch_item_fail(client): res_create = client.post("/item", json={"name": "item1", "price": 1.0}) - res = client.patch(f"/item/{res_create + 1}", json={"price": 2.0}) + next_id = res_create + 1 + res = client.patch(f"/item/{next_id}", json={"price": 2.0}) assert res.status_code == 404 client.delete(f"/item/{res_create}") From ddd3d290c04dafbff5d6ee993e52b217e1d3adef Mon Sep 17 00:00:00 2001 From: Ekaterina Semchuk Date: Sun, 26 Oct 2025 20:16:09 +0300 Subject: [PATCH 19/19] tests fixed --- hw5/tests/test_items.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/hw5/tests/test_items.py b/hw5/tests/test_items.py index 8869d146..191f81d7 100644 --- a/hw5/tests/test_items.py +++ b/hw5/tests/test_items.py @@ -88,26 +88,25 @@ def test_put_item_fail(client): def test_patch_item(client): - res_create = client.post("/item", json={"name": "item1", "price": 1.0}) - res = client.patch(f"/item/{res_create}", json={"price": 2.0}) + client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.patch(f"/item/1", json={"price": 2.0}) assert res.status_code == 200 assert res.json()["price"] == 2.0 def test_patch_item_fail(client): - res_create = client.post("/item", json={"name": "item1", "price": 1.0}) - next_id = res_create + 1 - res = client.patch(f"/item/{next_id}", json={"price": 2.0}) + client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.patch(f"/item/2", json={"price": 2.0}) assert res.status_code == 404 - client.delete(f"/item/{res_create}") - res = client.patch(f"/item/{res_create}", json={"price": 2.0}) + client.delete(f"/item/1") + res = client.patch(f"/item/1", json={"price": 2.0}) assert res.status_code == 304 def test_delete_item(client): - res_create = client.post("/item", json={"name": "item1", "price": 1.0}) - res = client.delete(f"/item/{res_create}") + client.post("/item", json={"name": "item1", "price": 1.0}) + res = client.delete(f"/item/1") assert res.status_code == 200 - res2 = client.get(f"/item/{res_create}") + res2 = client.get(f"/item/1") assert res2.status_code == 404