From 083d3753e941028ce964184aee83046ef1ec9be0 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sun, 21 Sep 2025 19:41:29 +0300 Subject: [PATCH 1/7] Implement ASGI application with fibonacci, factorial, and mean endpoints --- hw1/app.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/hw1/app.py b/hw1/app.py index 6107b870..edf6d119 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,6 +1,81 @@ +import json from typing import Any, Awaitable, Callable +def get_fibonacci_n(path: str) -> int: + _, n = path.rsplit("/", 1) + return int(n) + + +def fibonacci(n: int) -> int: + if n < 0: + raise ValueError("n should be non-negative") + if n < 2: + return n + a, b = 0, 1 + for _ in range(n - 2): + a, b = a + b, a + return b + + +def mean(numbers: list[int]) -> float: + print(numbers, "numbers") + if not numbers: + raise ValueError("Empty list") + return sum(numbers) / len(numbers) + + +def factorial(n: int) -> int: + if n < 0: + raise ValueError("n should be non-negative") + res = 1 + mul = 1 + for _ in range(n): + res *= mul + mul += 1 + return res + + +def parse_query_params(query_params: bytes) -> dict[bytes, bytes]: + if not query_params: + return {} + query_groups = [query_group.split(b"=") for query_group in query_params.split(b"&")] + return {query_group[0]: query_group[1] for query_group in query_groups} + + +async def _send_response(send, message: str, status: int) -> None: + await send({ + "type": "http.response.start", + "status": status, + "headers": [(b"content-type", b"text/plain")], + }) + await send({ + "type": "http.response.body", + "body": message.encode(), + }) + + +async def _send_result(send, result) -> None: + await send({ + "type": "http.response.start", + "status": 200, + "headers": [(b"content-type", b"application/json")], + }) + await send({ + "type": "http.response.body", + "body": json.dumps({"result": result}).encode(), + }) + + +async def get_json_body(receive): + res = [] + while True: + message = await receive() + res.append(message["body"]) + if not message.get("more_body", False): + return json.loads(b"".join(res)) + + async def application( scope: dict[str, Any], receive: Callable[[], Awaitable[dict[str, Any]]], @@ -12,7 +87,60 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + if scope['type'] == 'lifespan': + while True: + message = await receive() + if message['type'] == 'lifespan.startup': + print("Application is starting up...") + await send({'type': 'lifespan.startup.complete'}) + elif message['type'] == 'lifespan.shutdown': + print("Application is shutting down...") + await send({'type': 'lifespan.shutdown.complete'}) + return + + send_response = lambda *args, **kwargs: _send_response(send, *args, **kwargs) + send_result = lambda *args, **kwargs: _send_result(send, *args, **kwargs) + path = scope["path"] + method_name = path.split("/")[1] + if method_name not in ("fibonacci", "mean", "factorial"): + return await send_response("Wrong method", 404) + elif method_name == "fibonacci": + try: + n = get_fibonacci_n(path) + except ValueError: + return await send_response("Could not parse n", 422) + try: + res = fibonacci(n) + except ValueError: + return await send_response("Could not calculate fibonacci", 400) + return await send_result(res) + query_params = parse_query_params(scope["query_string"]) + if method_name == "mean": + numbers = await get_json_body(receive) + if not numbers: + if numbers is None: + return await send_response("empty body", 422) + if numbers is None: + return await send_response("empty list", 400) + try: + res = mean(numbers) + except ValueError: + return await send_response("Empty list", 400) + return await send_result(res) + elif method_name == "factorial": + if b"n" not in query_params: + return await send_response("n required for factorial", 422) + try: + n = int(query_params[b"n"]) + except ValueError: + return await send_response("Could not parse n", 422) + try: + res = factorial(n) + except ValueError: + return await send_response("Could not calculate factorial", 400) + return await send_result(res) + assert 0 + if __name__ == "__main__": import uvicorn From 57145331e0c4e5556b4ce1c9fb8efcfc0c3fd619 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sun, 21 Sep 2025 19:48:30 +0300 Subject: [PATCH 2/7] print --- hw1/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/hw1/app.py b/hw1/app.py index edf6d119..d3c3b929 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -19,7 +19,6 @@ def fibonacci(n: int) -> int: def mean(numbers: list[int]) -> float: - print(numbers, "numbers") if not numbers: raise ValueError("Empty list") return sum(numbers) / len(numbers) @@ -91,10 +90,8 @@ async def application( while True: message = await receive() if message['type'] == 'lifespan.startup': - print("Application is starting up...") await send({'type': 'lifespan.startup.complete'}) elif message['type'] == 'lifespan.shutdown': - print("Application is shutting down...") await send({'type': 'lifespan.shutdown.complete'}) return From 8b26505467bb6f157f249554edf6c2c881080548 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sun, 21 Sep 2025 23:09:20 +0300 Subject: [PATCH 3/7] trigger ci From 4d3d6bf4a859e0a3a0602a99d808af10f9cf69e5 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sun, 21 Sep 2025 23:10:13 +0300 Subject: [PATCH 4/7] trigger ci From 98e680cf3b86f32b8adfd32d2b9ebccc6eabc1f1 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sun, 21 Sep 2025 23:10:43 +0300 Subject: [PATCH 5/7] trigger ci --- .github/workflows/hw1-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hw1-tests.yml b/.github/workflows/hw1-tests.yml index 95fe89f7..4c0271ba 100644 --- a/.github/workflows/hw1-tests.yml +++ b/.github/workflows/hw1-tests.yml @@ -7,7 +7,7 @@ on: paths: [ 'hw1/**' ] push: branches: [ main ] - paths: [ 'hw1/**' ] + # paths: [ 'hw1/**' ] jobs: test-hw1: From 53806fbd552aaf9e49333b0809ebc01a7959ef82 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sun, 21 Sep 2025 23:11:33 +0300 Subject: [PATCH 6/7] revert --- .github/workflows/hw1-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hw1-tests.yml b/.github/workflows/hw1-tests.yml index 4c0271ba..95fe89f7 100644 --- a/.github/workflows/hw1-tests.yml +++ b/.github/workflows/hw1-tests.yml @@ -7,7 +7,7 @@ on: paths: [ 'hw1/**' ] push: branches: [ main ] - # paths: [ 'hw1/**' ] + paths: [ 'hw1/**' ] jobs: test-hw1: From f5dcbadd5e4658e81db9648d99aeef0036956ad1 Mon Sep 17 00:00:00 2001 From: Tialo Date: Sat, 4 Oct 2025 17:53:35 +0300 Subject: [PATCH 7/7] hw2 --- hw2/hw/shop_api/api/__init__.py | 4 ++ hw2/hw/shop_api/api/cart/__init__.py | 1 + hw2/hw/shop_api/api/cart/routes.py | 54 ++++++++++++++++++ hw2/hw/shop_api/api/item/__init__.py | 0 hw2/hw/shop_api/api/item/contracts.py | 20 +++++++ hw2/hw/shop_api/api/item/routes.py | 68 +++++++++++++++++++++++ hw2/hw/shop_api/main.py | 5 ++ hw2/hw/shop_api/store/__init__.py | 0 hw2/hw/shop_api/store/models.py | 21 +++++++ hw2/hw/shop_api/store/queries.py | 80 +++++++++++++++++++++++++++ 10 files changed, 253 insertions(+) create mode 100644 hw2/hw/shop_api/api/__init__.py create mode 100644 hw2/hw/shop_api/api/cart/__init__.py create mode 100644 hw2/hw/shop_api/api/cart/routes.py create mode 100644 hw2/hw/shop_api/api/item/__init__.py create mode 100644 hw2/hw/shop_api/api/item/contracts.py create mode 100644 hw2/hw/shop_api/api/item/routes.py create mode 100644 hw2/hw/shop_api/store/__init__.py create mode 100644 hw2/hw/shop_api/store/models.py create mode 100644 hw2/hw/shop_api/store/queries.py diff --git a/hw2/hw/shop_api/api/__init__.py b/hw2/hw/shop_api/api/__init__.py new file mode 100644 index 00000000..3f93c8dc --- /dev/null +++ b/hw2/hw/shop_api/api/__init__.py @@ -0,0 +1,4 @@ +from shop_api.api.cart.routes import router as cart_router +from shop_api.api.item.routes import router as item_router + +__all__ = ["cart_router", "item_router"] diff --git a/hw2/hw/shop_api/api/cart/__init__.py b/hw2/hw/shop_api/api/cart/__init__.py new file mode 100644 index 00000000..fa47ebc6 --- /dev/null +++ b/hw2/hw/shop_api/api/cart/__init__.py @@ -0,0 +1 @@ +from shop_api.api.cart import routes diff --git a/hw2/hw/shop_api/api/cart/routes.py b/hw2/hw/shop_api/api/cart/routes.py new file mode 100644 index 00000000..ebbaad9b --- /dev/null +++ b/hw2/hw/shop_api/api/cart/routes.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import APIRouter, Query, Response, HTTPException, status +from pydantic import NonNegativeInt, PositiveInt + +from shop_api.store.queries import get_carts as _get_carts +from shop_api.store.queries import get_items as _get_items +from shop_api.store.queries import create_cart as _create_cart +from shop_api.store.queries import add_item_to_cart as _add_item_to_cart +from shop_api.store.models import Cart + + +router = APIRouter(prefix="/cart") + + +@router.post("/", status_code=status.HTTP_201_CREATED) +async def create_cart(response: Response) -> Cart: + cart = _create_cart() + response.headers["Location"] = f"/cart/{cart.id}" + return cart + + +@router.get("/") +async def get_carts( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[float | None, Query(gt=0)] = None, + max_price: Annotated[float | None, Query(gt=0)] = None, + min_quantity: Annotated[int | None, Query(gt=0)] = None, + max_quantity: Annotated[int | None, Query(ge=0)] = None, +) -> list[Cart]: + return _get_carts( + offset, limit, min_price, max_price, min_quantity, max_quantity + ) + + +@router.get("/{id}") +async def get_cart(id: int) -> Cart: + carts = _get_carts(offset=id, limit=1) + if not carts: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return carts[0] + + +@router.post("/{cart_id}/add/{item_id}", status_code=status.HTTP_201_CREATED) +async def add_item_to_cart(cart_id: int, item_id: int) -> Cart: + cart = _get_carts(offset=cart_id, limit=1) + if not cart: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cart not found") + item = _get_items(offset=item_id, limit=1) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found") + _add_item_to_cart(cart[0], item[0]) + return cart[0] diff --git a/hw2/hw/shop_api/api/item/__init__.py b/hw2/hw/shop_api/api/item/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw2/hw/shop_api/api/item/contracts.py b/hw2/hw/shop_api/api/item/contracts.py new file mode 100644 index 00000000..f06e9a33 --- /dev/null +++ b/hw2/hw/shop_api/api/item/contracts.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + + +class ItemPostRequest(BaseModel): + name: str + price: float + + +class ItemPutRequest(BaseModel): + name: str + price: float + + +class ItemPatchRequest(BaseModel): + name: str | None = None + price: float | None = None + + model_config = { + "extra": "forbid", + } \ No newline at end of file diff --git a/hw2/hw/shop_api/api/item/routes.py b/hw2/hw/shop_api/api/item/routes.py new file mode 100644 index 00000000..95182e63 --- /dev/null +++ b/hw2/hw/shop_api/api/item/routes.py @@ -0,0 +1,68 @@ +from typing import Annotated + +from fastapi import APIRouter, Query, Response, HTTPException, status +from pydantic import NonNegativeInt, PositiveInt + +from shop_api.store.queries import get_items as _get_items +from shop_api.store.queries import create_item as _create_item +from shop_api.store.queries import replace_item as _replace_item +from shop_api.store.queries import patch_item as _patch_item +from shop_api.store.models import Item +from shop_api.api.item.contracts import ItemPostRequest, ItemPutRequest, ItemPatchRequest + + +router = APIRouter(prefix="/item") + + +@router.post("/", status_code=status.HTTP_201_CREATED) +async def create_item(request: ItemPostRequest, response: Response) -> Item: + item = _create_item(request.name, request.price) + response.headers["Location"] = f"/item/{item.id}" + return item + + +@router.get("/") +async def get_items( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[float | None, Query(gt=0)] = None, + max_price: Annotated[float | None, Query(gt=0)] = None, + show_deleted: Annotated[bool, Query()] = False, +) -> list[Item]: + return _get_items( + offset, limit, min_price, max_price, show_deleted + ) + + +@router.get("/{id}") +async def get_item(id: int) -> Item: + items = _get_items(offset=id, limit=1) + if not items: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return items[0] + + +@router.put("/{id}") +async def put_item(id: int, request: ItemPutRequest) -> Item: + items = _get_items(offset=id, limit=1) + if not items: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + _replace_item(items[0].id, request.name, request.price) + return items[0] + + +@router.patch("/{id}") +async def patch_item(id: int, request: ItemPatchRequest) -> Item: + items = _get_items(offset=id, limit=1) + if not items: + raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED) + _patch_item(items[0].id, request.name, request.price) + return items[0] + + +@router.delete("/{id}") +async def delete_item(id: int) -> None: + items = _get_items(offset=id, limit=1) + if not items: + return + items[0].deleted = True diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..1f1f5435 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,8 @@ from fastapi import FastAPI +from shop_api.api import cart_router, item_router + app = FastAPI(title="Shop API") + +app.include_router(cart_router) +app.include_router(item_router) diff --git a/hw2/hw/shop_api/store/__init__.py b/hw2/hw/shop_api/store/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw2/hw/shop_api/store/models.py b/hw2/hw/shop_api/store/models.py new file mode 100644 index 00000000..44445bb4 --- /dev/null +++ b/hw2/hw/shop_api/store/models.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + + +class Item(BaseModel): + id: int + name: str + price: float + deleted: bool = False + + +class CartItem(BaseModel): + id: int + name: str + quantity: int + available: bool + + +class Cart(BaseModel): + id: int + items: list[CartItem] = [] + price: float = 0.0 diff --git a/hw2/hw/shop_api/store/queries.py b/hw2/hw/shop_api/store/queries.py new file mode 100644 index 00000000..d5d77a79 --- /dev/null +++ b/hw2/hw/shop_api/store/queries.py @@ -0,0 +1,80 @@ +from shop_api.store.models import Cart, Item, CartItem + +_carts: list[Cart] = [] +_items: list[Item] = [] + + +def create_cart() -> Cart: + cart = Cart(id=len(_carts)) + _carts.append(cart) + return cart + + +def get_carts( + offset: int = 0, + limit: int = 10, + min_price: float | None = None, + max_price: float | None = None, + min_quantity: int | None = None, + max_quantity: int | None = None, +): + min_price = min_price or -float("inf") + max_price = max_price or float("inf") + min_quantity = min_quantity or -float("inf") + max_quantity = float('inf') if max_quantity is None else max_quantity + return [ + _carts[i] + for i in range(offset, min(offset + limit, len(_carts))) + if (min_price <= _carts[i].price <= max_price) + and (min_quantity <= len(_carts[i].items) <= max_quantity) + ] + + +def get_items( + offset: int = 0, + limit: int = 10, + min_price: float | None = None, + max_price: float | None = None, + show_deleted: bool = False, +) -> list[Item]: + min_price = min_price or -float("inf") + max_price = max_price or float("inf") + return [ + _items[i] + for i in range(offset, min(offset + limit, len(_items))) + if (show_deleted or not _items[i].deleted) + and min_price <= _items[i].price <= max_price + ] + + +def add_item_to_cart(cart: Cart, item: Item) -> None: + for cart_item in cart.items: + if cart_item.id == item.id: + cart_item.quantity += 1 + cart.price += item.price + return + cart.items.append(CartItem(id=item.id, name=item.name, quantity=1, available=not item.deleted)) + cart.price += item.price + + +def create_item(name: str, price: float) -> Item: + item = Item(id=len(_items), name=name, price=price, deleted=False) + _items.append(item) + return item + + +def replace_item(item_id: int, name: str, price: float) -> None: + _items[item_id].name = name + _items[item_id].price = price + _items[item_id].deleted = False + + +def patch_item(item_id: int, name: str | None, price: float | None) -> None: + if name is not None: + _items[item_id].name = name + if price is not None: + _items[item_id].price = price + + +def delete_item(item_id: int) -> None: + _items[item_id].deleted = True