diff --git a/hw1/app.py b/hw1/app.py index 6107b87..de4b62e 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -12,7 +12,93 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + if scope["type"] == "lifespan": + while True: + message = await receive() + await send({"type": f"{message['type']}.complete"}) + + path = scope["path"].rstrip("/") + status = 404 + result = None + + if scope["method"] == "GET": + if path == "/factorial": + kvs = scope["query_string"].decode() + n = None + for kv in kvs.split("&"): + if "=" in kv: + k, v = kv.split("=", 1) + if k == "n": + n = v + status, result = compute_number(n, factorial) + elif path.startswith("/fibonacci/"): + status, result = compute_number(path.removeprefix("/fibonacci/"), fibonacci) + elif path == "/mean": + raw_bytes = await read_bytes(receive) + status, result = mean(raw_bytes) + + await answer(send, status, result) + + +def compute_number(raw: str | None, func: Callable[[int], int]) -> tuple[int, int | None]: + try: + n = int(raw) + except (TypeError, ValueError): + return 422, None + if n < 0: + return 400, None + return 200, func(n) + + +def factorial(n: int) -> int: + ans = 1 + for i in range(n): + ans *= (i + 1) + return ans + + +def fibonacci(n: int) -> int: + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a + + +def mean(body: bytes) -> tuple[int, float | None]: + try: + text = body.decode().strip() + numbers = [float(item) for item in text[1:-1].split(",") if item.strip()] + except ValueError: + return 422, None + if not numbers: + return 400, None + res = sum(numbers) / len(numbers) + return 200, res + + +async def read_bytes(receive: Callable[[], Awaitable[dict[str, Any]]]) -> bytes: + body = b"" + has_more = True + while has_more: + message = await receive() + body += message.get("body", b"") + has_more = message.get("more_body", False) + return body + + +async def answer(send, status, result): + await send( + { + "type": "http.response.start", + "status": status, + "headers": [[b"content-type", b"application/json"]], + } + ) + body = b"" + if status == 200: + body = f'{{"result": {result}}}'.encode() + await send({"type": "http.response.body", "body": body}) + if __name__ == "__main__": import uvicorn diff --git a/hw2/grpc_example/ping_pb2.py b/hw2/grpc_example/ping_pb2.py new file mode 100644 index 0000000..6abb411 --- /dev/null +++ b/hw2/grpc_example/ping_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: ping.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'ping.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nping.proto\x12\x07\x65xample\"\x1e\n\x0bPingRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x1f\n\x0cPongResponse\x12\x0f\n\x07message\x18\x01 \x01(\t2}\n\x07\x45xample\x12\x33\n\x04Ping\x12\x14.example.PingRequest\x1a\x15.example.PongResponse\x12=\n\nPingStream\x12\x14.example.PingRequest\x1a\x15.example.PongResponse(\x01\x30\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ping_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_PINGREQUEST']._serialized_start=23 + _globals['_PINGREQUEST']._serialized_end=53 + _globals['_PONGRESPONSE']._serialized_start=55 + _globals['_PONGRESPONSE']._serialized_end=86 + _globals['_EXAMPLE']._serialized_start=88 + _globals['_EXAMPLE']._serialized_end=213 +# @@protoc_insertion_point(module_scope) diff --git a/hw2/grpc_example/ping_pb2.pyi b/hw2/grpc_example/ping_pb2.pyi new file mode 100644 index 0000000..4d38c1b --- /dev/null +++ b/hw2/grpc_example/ping_pb2.pyi @@ -0,0 +1,17 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class PingRequest(_message.Message): + __slots__ = ("message",) + MESSAGE_FIELD_NUMBER: _ClassVar[int] + message: str + def __init__(self, message: _Optional[str] = ...) -> None: ... + +class PongResponse(_message.Message): + __slots__ = ("message",) + MESSAGE_FIELD_NUMBER: _ClassVar[int] + message: str + def __init__(self, message: _Optional[str] = ...) -> None: ... diff --git a/hw2/grpc_example/ping_pb2_grpc.py b/hw2/grpc_example/ping_pb2_grpc.py new file mode 100644 index 0000000..ca56a55 --- /dev/null +++ b/hw2/grpc_example/ping_pb2_grpc.py @@ -0,0 +1,140 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import ping_pb2 as ping__pb2 + +GRPC_GENERATED_VERSION = '1.84.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in ping_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class ExampleStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Ping = channel.unary_unary( + '/example.Example/Ping', + request_serializer=ping__pb2.PingRequest.SerializeToString, + response_deserializer=ping__pb2.PongResponse.FromString, + _registered_method=True) + self.PingStream = channel.stream_stream( + '/example.Example/PingStream', + request_serializer=ping__pb2.PingRequest.SerializeToString, + response_deserializer=ping__pb2.PongResponse.FromString, + _registered_method=True) + + +class ExampleServicer: + """Missing associated documentation comment in .proto file.""" + + def Ping(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def PingStream(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ExampleServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Ping': grpc.unary_unary_rpc_method_handler( + servicer.Ping, + request_deserializer=ping__pb2.PingRequest.FromString, + response_serializer=ping__pb2.PongResponse.SerializeToString, + ), + 'PingStream': grpc.stream_stream_rpc_method_handler( + servicer.PingStream, + request_deserializer=ping__pb2.PingRequest.FromString, + response_serializer=ping__pb2.PongResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'example.Example', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('example.Example', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Example: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Ping(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/example.Example/Ping', + ping__pb2.PingRequest.SerializeToString, + ping__pb2.PongResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def PingStream(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/example.Example/PingStream', + ping__pb2.PingRequest.SerializeToString, + ping__pb2.PongResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/hw2/hw/shop_api/api/__init__.py b/hw2/hw/shop_api/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hw2/hw/shop_api/api/carts/__init__.py b/hw2/hw/shop_api/api/carts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hw2/hw/shop_api/api/carts/contracts.py b/hw2/hw/shop_api/api/carts/contracts.py new file mode 100644 index 0000000..b9db88d --- /dev/null +++ b/hw2/hw/shop_api/api/carts/contracts.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel +from ...store import queries +from ...store.models import Cart + + +class CartItemResponse(BaseModel): + id: int + name: str + quantity: int + available: bool + + +class CartResponse(BaseModel): + id: int + items: list[CartItemResponse] + price: float + + @staticmethod + def from_entity(cart: Cart): + items = [] + for item_id, quantity in cart.item_id_to_count.items(): + item = queries.get_item(item_id) + items.append( + CartItemResponse( + id=item.id, + name=item.name, + quantity=quantity, + available=not item.deleted, + ) + ) + return CartResponse( + id=cart.id, + items=items, + price=queries.cart_price(cart), + ) diff --git a/hw2/hw/shop_api/api/carts/routes.py b/hw2/hw/shop_api/api/carts/routes.py new file mode 100644 index 0000000..9c77582 --- /dev/null +++ b/hw2/hw/shop_api/api/carts/routes.py @@ -0,0 +1,41 @@ +from http import HTTPStatus +from typing import Annotated +from fastapi import APIRouter, Query, Response +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveInt +from .contracts import CartResponse +from ...store import queries + + +router = APIRouter(prefix="/cart") + + +@router.post("", status_code=HTTPStatus.CREATED) +async def post_cart(response: Response) -> dict[str, int]: + cart = queries.add_cart() + response.headers["location"] = f"/cart/{cart.id}" + return {"id": cart.id} + + +@router.get("/{id}") +async def get_cart(id: int) -> CartResponse: + cart = queries.get_cart(id) + return CartResponse.from_entity(cart) + + +@router.get("") +async def get_carts( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat | None, Query()] = None, + max_price: Annotated[NonNegativeFloat | None, Query()] = None, + min_quantity: Annotated[NonNegativeInt | None, Query()] = None, + max_quantity: Annotated[NonNegativeInt | None, Query()] = None, +) -> list[CartResponse]: + carts = queries.get_carts(offset, limit, min_price, max_price, min_quantity, max_quantity) + return [CartResponse.from_entity(cart) for cart in carts] + + +@router.post("/{cart_id}/add/{item_id}") +async def add_item_to_cart(cart_id: int, item_id: int) -> CartResponse: + cart = queries.add_item_to_cart(cart_id, item_id) + return CartResponse.from_entity(cart) diff --git a/hw2/hw/shop_api/api/items/__init__.py b/hw2/hw/shop_api/api/items/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hw2/hw/shop_api/api/items/contracts.py b/hw2/hw/shop_api/api/items/contracts.py new file mode 100644 index 0000000..0a19823 --- /dev/null +++ b/hw2/hw/shop_api/api/items/contracts.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel, ConfigDict, NonNegativeFloat +from ...store.models import Item + + +class ItemRequest(BaseModel): + name: str + price: NonNegativeFloat + + +class ItemPatchRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + name: str | None = None + price: NonNegativeFloat | None = None + + +class ItemResponse(BaseModel): + id: int + name: str + price: float + deleted: bool + + @staticmethod + def from_entity(item: Item): + return ItemResponse( + id=item.id, + name=item.name, + price=item.price, + deleted=item.deleted, + ) diff --git a/hw2/hw/shop_api/api/items/routes.py b/hw2/hw/shop_api/api/items/routes.py new file mode 100644 index 0000000..031dd01 --- /dev/null +++ b/hw2/hw/shop_api/api/items/routes.py @@ -0,0 +1,55 @@ +from http import HTTPStatus +from typing import Annotated +from fastapi import APIRouter, HTTPException, Query, Response +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveInt +from .contracts import ItemPatchRequest, ItemRequest, ItemResponse +from ...store import queries + + +router = APIRouter(prefix="/item") + + +@router.post("", status_code=HTTPStatus.CREATED) +async def post_item(body: ItemRequest) -> ItemResponse: + item = queries.add_item(body.name, body.price) + return ItemResponse.from_entity(item) + + +@router.get("/{id}") +async def get_item(id: int) -> ItemResponse: + item = queries.get_item(id) + if item.deleted: + raise HTTPException(HTTPStatus.NOT_FOUND) + return ItemResponse.from_entity(item) + + +@router.get("") +async def get_items( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat | None, Query()] = None, + max_price: Annotated[NonNegativeFloat | None, Query()] = None, + show_deleted: bool = False, +) -> list[ItemResponse]: + items = queries.get_items(offset, limit, min_price, max_price, show_deleted) + return [ItemResponse.from_entity(item) for item in items] + + +@router.put("/{id}") +async def put_item(id: int, body: ItemRequest) -> ItemResponse: + item = queries.update_item(id, body.name, body.price) + return ItemResponse.from_entity(item) + + +@router.patch("/{id}") +async def patch_item(id: int, body: ItemPatchRequest) -> ItemResponse: + item = queries.get_item(id) + if item.deleted: + raise HTTPException(HTTPStatus.NOT_MODIFIED) + return ItemResponse.from_entity(queries.patch_item(id, body.name, body.price)) + + +@router.delete("/{id}") +async def delete_item(id: int) -> Response: + queries.delete_item(id) + return Response() diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c6..a66e235 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,7 @@ from fastapi import FastAPI +from .api.carts.routes import router as carts_router +from .api.items.routes import router as items_router app = FastAPI(title="Shop API") +app.include_router(carts_router) +app.include_router(items_router) diff --git a/hw2/hw/shop_api/store/__init__.py b/hw2/hw/shop_api/store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hw2/hw/shop_api/store/models.py b/hw2/hw/shop_api/store/models.py new file mode 100644 index 0000000..8553da3 --- /dev/null +++ b/hw2/hw/shop_api/store/models.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + id: int + name: str + price: float + deleted: bool = False + + +@dataclass +class Cart: + id: int + item_id_to_count: dict[int, int] = field(default_factory=dict) diff --git a/hw2/hw/shop_api/store/queries.py b/hw2/hw/shop_api/store/queries.py new file mode 100644 index 0000000..d0bbc6e --- /dev/null +++ b/hw2/hw/shop_api/store/queries.py @@ -0,0 +1,111 @@ +from itertools import count +from .models import Cart, Item + + +_item_ids_counter = count(1) +_cart_ids_counter = count(1) +_id_to_item: dict[int, Item] = {} +_id_to_cart: dict[int, Cart] = {} + + +def add_item(name: str, price: float) -> Item: + item = Item(id=next(_item_ids_counter), name=name, price=price) + _id_to_item[item.id] = item + return item + + +def get_item(id: int) -> Item | None: + return _id_to_item.get(id) + + +def get_items( + offset: int, + limit: int, + min_price: float | None, + max_price: float | None, + show_deleted: bool, +) -> list[Item]: + result = [ + item + for item in _id_to_item.values() + if (show_deleted or not item.deleted) + and (min_price is None or item.price >= min_price) + and (max_price is None or item.price <= max_price) + ] + return result[offset : offset + limit] + + +def update_item(id: int, name: str, price: float) -> Item | None: + item = _id_to_item.get(id) + if item is None or item.deleted: + return None + item.name = name + item.price = price + return item + + +def patch_item(id: int, name: str | None, price: float | None) -> Item | None: + item = _id_to_item.get(id) + if item is None or item.deleted: + return None + if name is not None: + item.name = name + if price is not None: + item.price = price + return item + + +def delete_item(id: int) -> None: + item = _id_to_item.get(id) + if item is not None: + item.deleted = True + + +def add_cart() -> Cart: + cart = Cart(id=next(_cart_ids_counter)) + _id_to_cart[cart.id] = cart + return cart + + +def get_cart(id: int) -> Cart | None: + return _id_to_cart.get(id) + + +def get_carts( + offset: int, + limit: int, + min_price: float | None, + max_price: float | None, + min_quantity: int | None, + max_quantity: int | None, +) -> list[Cart]: + result = [ + cart + for cart in _id_to_cart.values() + if (min_price is None or cart_price(cart) >= min_price) + and (max_price is None or cart_price(cart) <= max_price) + and (min_quantity is None or cart_quantity(cart) >= min_quantity) + and (max_quantity is None or cart_quantity(cart) <= max_quantity) + ] + return result[offset : offset + limit] + + +def add_item_to_cart(cart_id: int, item_id: int) -> Cart | None: + cart = _id_to_cart.get(cart_id) + item = _id_to_item.get(item_id) + if cart is None or item is None or item.deleted: + return None + cart.item_id_to_count[item_id] = cart.item_id_to_count.get(item_id, 0) + 1 + return cart + + +def cart_price(cart: Cart) -> float: + return sum( + _id_to_item[item_id].price * quantity + for item_id, quantity in cart.item_id_to_count.items() + if not _id_to_item[item_id].deleted + ) + + +def cart_quantity(cart: Cart) -> int: + return sum(cart.item_id_to_count.values())