From 94c05fc66e753bb5aa8beca3fe019c39942dd8a0 Mon Sep 17 00:00:00 2001 From: bogdan01m Date: Sun, 28 Sep 2025 02:39:22 +0500 Subject: [PATCH 1/3] HW-1 Bogdan Minko --- hw1/app.py | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) diff --git a/hw1/app.py b/hw1/app.py index 6107b870..9723515f 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,4 +1,77 @@ from typing import Any, Awaitable, Callable +from dataclasses import dataclass +import math +import statistics +import json +from urllib.parse import parse_qs + +fibonacci_array = [0, 1] + +@dataclass +class fibonacci_model: + n: int + + def fibonacci(self) -> int: + assert self.n >= 0, "n shoud be greater or equal to 0" + + while len(fibonacci_array) <= self.n: + next_fib = fibonacci_array[-1] + fibonacci_array[-2] + fibonacci_array.append(next_fib) + + return fibonacci_array[self.n] + +@dataclass +class mean_model: + n: list[float] + + def mean(self) -> float: + result = statistics.mean(self.n) + return result + +@dataclass +class factorial_model: + n: int + + def factorial(self) -> int: + assert self.n >= 0, "n shoud be greater or equal to 0" + result = math.factorial(self.n) + return result + + +async def send_json_response(send, status_code, data): + """Отправляет ответ""" + response_body = json.dumps(data, ensure_ascii=False).encode('utf-8') + + await send({ + 'type': 'http.response.start', + 'status': status_code, + 'headers': [ + [b'content-type', b'application/json'], + [b'content-length', str(len(response_body)).encode()], + ], + }) + + await send({ + 'type': 'http.response.body', + 'body': response_body, + }) + + +async def send_error_response(send, status_code, error_message): + """Отправляет ответ с ошибкой""" + await send_json_response(send, status_code, {"error": error_message}) + + +async def get_request_body(receive): + """Получает тело запроса""" + body = b'' + while True: + message = await receive() + if message['type'] == 'http.request': + body += message.get('body', b'') + if not message.get('more_body', False): + break + return body async def application( @@ -12,8 +85,124 @@ 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 + + # Проверяем тип запроса + if scope['type'] != 'http': + await send_error_response(send, 422, "Unsupported request type") + return + + # Проверяем метод запроса + if scope["method"] != "GET": + await send_error_response(send, 404, "Method not allowed") + return + + # Получаем данные запроса + path = scope["path"] + query_string = scope["query_string"].decode("utf-8") + query_params = parse_qs(query_string) if query_string else {} + + try: + match path: + case p if p.startswith('/fibonacci/'): + try: + n = int(path.split('/fibonacci/')[-1]) + + fib_calc = fibonacci_model(n=n) + result = fib_calc.fibonacci() + + await send_json_response(send, 200, {"result": result}) + return + + except ValueError: + await send_error_response(send, 422, "Invalid number format") + return + except AssertionError as e: + await send_error_response(send, 400, str(e)) + return + + case '/factorial': + if 'n' not in query_params: + await send_error_response(send, 422, "Parameter 'n' is required") + return + + n_str = query_params['n'][0] + + + try: + n = int(n_str) + + if n < 0: + await send_error_response(send, 400, "n must be non-negative") + return + + fact_calc = factorial_model(n=n) + result = fact_calc.factorial() + + await send_json_response(send, 200, {"result": result}) + return + + except ValueError: + await send_error_response(send, 422, "Invalid number format") + return + + case '/mean': + try: + body = await get_request_body(receive) + + if not body: + await send_error_response(send, 422, "JSON body is required") + return + + try: + data = json.loads(body.decode('utf-8')) + except json.JSONDecodeError: + await send_error_response(send, 422, "Invalid JSON format") + return + + + if len(data) == 0: + await send_error_response(send, 400, "Numbers list cannot be empty") + return + + # # Проверяем, что все элементы - числа + numbers = [] + try: + numbers = [float(item) for item in data] + except (ValueError, TypeError): + await send_error_response(send, 422, "All elements must be numbers") + return + + mean_calc = mean_model(n=numbers) + result = mean_calc.mean() + + await send_json_response(send, 200, {"result": result}) + return + + except Exception as e: + await send_error_response(send, 422, f"Error processing request: {str(e)}") + return + + case _: + await send_error_response(send, 404, "Endpoint not found") + return + + except Exception as e: + await send_error_response(send, 500, f"Internal server error: {str(e)}") + if __name__ == "__main__": + + print("\n🚀 Запуск ASGI сервера...") import uvicorn - uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) + uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file From 3ca7656776edaf5786ff4fe223fd292cb63f09a8 Mon Sep 17 00:00:00 2001 From: bogdan01m Date: Sun, 28 Sep 2025 03:19:27 +0500 Subject: [PATCH 2/3] add abstract routers --- hw1/app.py | 251 ++++++++++++++++++++++++++--------------------------- 1 file changed, 122 insertions(+), 129 deletions(-) diff --git a/hw1/app.py b/hw1/app.py index 9723515f..94c4f420 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -7,54 +7,53 @@ fibonacci_array = [0, 1] + @dataclass -class fibonacci_model: +class int_model: n: int def fibonacci(self) -> int: - assert self.n >= 0, "n shoud be greater or equal to 0" - while len(fibonacci_array) <= self.n: next_fib = fibonacci_array[-1] + fibonacci_array[-2] fibonacci_array.append(next_fib) - + return fibonacci_array[self.n] + def factorial(self) -> int: + result = math.factorial(self.n) + return result + + @dataclass class mean_model: - n: list[float] + n: list[float] def mean(self) -> float: result = statistics.mean(self.n) - return result - -@dataclass -class factorial_model: - n: int - - def factorial(self) -> int: - assert self.n >= 0, "n shoud be greater or equal to 0" - result = math.factorial(self.n) - return result + return result async def send_json_response(send, status_code, data): """Отправляет ответ""" - response_body = json.dumps(data, ensure_ascii=False).encode('utf-8') - - await send({ - 'type': 'http.response.start', - 'status': status_code, - 'headers': [ - [b'content-type', b'application/json'], - [b'content-length', str(len(response_body)).encode()], - ], - }) - - await send({ - 'type': 'http.response.body', - 'body': response_body, - }) + response_body = json.dumps(data, ensure_ascii=False).encode("utf-8") + + await send( + { + "type": "http.response.start", + "status": status_code, + "headers": [ + [b"content-type", b"application/json"], + [b"content-length", str(len(response_body)).encode()], + ], + } + ) + + await send( + { + "type": "http.response.body", + "body": response_body, + } + ) async def send_error_response(send, status_code, error_message): @@ -64,16 +63,81 @@ async def send_error_response(send, status_code, error_message): async def get_request_body(receive): """Получает тело запроса""" - body = b'' + body = b"" while True: message = await receive() - if message['type'] == 'http.request': - body += message.get('body', b'') - if not message.get('more_body', False): + if message["type"] == "http.request": + body += message.get("body", b"") + if not message.get("more_body", False): break return body +async def route_fibonacci(send, path: str): + """GET /fibonacci/{n} - возвращает n-е число Фибоначчи""" + n_str = path.split("/fibonacci/")[-1] + + try: + n = int(n_str) + if n < 0: + await send_error_response(send, 400, "n should be greater or equal to 0") + return + result = int_model(n=n).fibonacci() + await send_json_response(send, 200, {"result": result}) + except ValueError: + await send_error_response(send, 422, "Invalid number format") + except AssertionError as e: + await send_error_response(send, 400, str(e)) + + +async def route_factorial(send, query_params: dict): + """GET /factorial?n=число - возвращает факториал числа""" + + if "n" not in query_params: + await send_error_response(send, 422, "Parameter 'n' is required") + return + + try: + n = int(query_params["n"][0]) + if n < 0: + await send_error_response(send, 400, "n should be greater or equal to 0") + return + result = int_model(n=n).factorial() + await send_json_response(send, 200, {"result": result}) + except ValueError: + await send_error_response(send, 422, "Invalid number format") + except AssertionError as e: + await send_error_response(send, 400, str(e)) + + +async def route_mean(send, receive): + """GET /mean с JSON body - возвращает среднее арифметическое""" + + body = await get_request_body(receive) + if not body: + await send_error_response(send, 422, "JSON body is required") + return + try: + data = json.loads(body.decode("utf-8")) + + if not isinstance(data, list): + await send_error_response(send, 422, "Expected a list of numbers") + return + + if len(data) == 0: + await send_error_response(send, 400, "Numbers list cannot be empty") + return + + numbers = [float(item) for item in data] + result = mean_model(n=numbers).mean() + await send_json_response(send, 200, {"result": result}) + + except json.JSONDecodeError: + await send_error_response(send, 422, "Invalid JSON format") + except (ValueError, TypeError): + await send_error_response(send, 422, "All elements must be numbers") + + async def application( scope: dict[str, Any], receive: Callable[[], Awaitable[dict[str, Any]]], @@ -85,124 +149,53 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - - if scope['type'] == 'lifespan': + + if scope["type"] == "lifespan": while True: message = await receive() - if message['type'] == 'lifespan.startup': + if message["type"] == "lifespan.startup": print("Application is starting up...") - await send({'type': 'lifespan.startup.complete'}) - elif message['type'] == 'lifespan.shutdown': + await send({"type": "lifespan.startup.complete"}) + elif message["type"] == "lifespan.shutdown": print("Application is shutting down...") - await send({'type': 'lifespan.shutdown.complete'}) + await send({"type": "lifespan.shutdown.complete"}) return - + # Проверяем тип запроса - if scope['type'] != 'http': + if scope["type"] != "http": await send_error_response(send, 422, "Unsupported request type") return - + # Проверяем метод запроса - if scope["method"] != "GET": + if scope["method"] != "GET": await send_error_response(send, 404, "Method not allowed") return - + # Получаем данные запроса path = scope["path"] query_string = scope["query_string"].decode("utf-8") query_params = parse_qs(query_string) if query_string else {} - + try: match path: - case p if p.startswith('/fibonacci/'): - try: - n = int(path.split('/fibonacci/')[-1]) - - fib_calc = fibonacci_model(n=n) - result = fib_calc.fibonacci() - - await send_json_response(send, 200, {"result": result}) - return - - except ValueError: - await send_error_response(send, 422, "Invalid number format") - return - except AssertionError as e: - await send_error_response(send, 400, str(e)) - return - - case '/factorial': - if 'n' not in query_params: - await send_error_response(send, 422, "Parameter 'n' is required") - return - - n_str = query_params['n'][0] - - - try: - n = int(n_str) - - if n < 0: - await send_error_response(send, 400, "n must be non-negative") - return - - fact_calc = factorial_model(n=n) - result = fact_calc.factorial() - - await send_json_response(send, 200, {"result": result}) - return - - except ValueError: - await send_error_response(send, 422, "Invalid number format") - return - - case '/mean': - try: - body = await get_request_body(receive) - - if not body: - await send_error_response(send, 422, "JSON body is required") - return - - try: - data = json.loads(body.decode('utf-8')) - except json.JSONDecodeError: - await send_error_response(send, 422, "Invalid JSON format") - return - - - if len(data) == 0: - await send_error_response(send, 400, "Numbers list cannot be empty") - return - - # # Проверяем, что все элементы - числа - numbers = [] - try: - numbers = [float(item) for item in data] - except (ValueError, TypeError): - await send_error_response(send, 422, "All elements must be numbers") - return - - mean_calc = mean_model(n=numbers) - result = mean_calc.mean() - - await send_json_response(send, 200, {"result": result}) - return - - except Exception as e: - await send_error_response(send, 422, f"Error processing request: {str(e)}") - return - + case p if p.startswith("/fibonacci/"): + await route_fibonacci(send, path) + + case "/factorial": + await route_factorial(send, query_params) + + case "/mean": + await route_mean(send, receive) + case _: await send_error_response(send, 404, "Endpoint not found") - return - + except Exception as e: await send_error_response(send, 500, f"Internal server error: {str(e)}") if __name__ == "__main__": - print("\n🚀 Запуск ASGI сервера...") import uvicorn - uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file + + uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True) From 4c864e6ebebaba8f936870d861e29018e3800c95 Mon Sep 17 00:00:00 2001 From: bogdan01m Date: Sun, 5 Oct 2025 20:53:31 +0500 Subject: [PATCH 3/3] add hw 2 --- hw2/hw/pyproject.toml | 14 ++ hw2/hw/shop_api/cart_routes.py | 42 ++++ hw2/hw/shop_api/contracts.py | 71 +++++++ hw2/hw/shop_api/item_routes.py | 53 +++++ hw2/hw/shop_api/main.py | 10 +- hw2/hw/shop_api/store/__init__.py | 0 hw2/hw/shop_api/store/storage.py | 177 ++++++++++++++++ hw2/hw/uv.lock | 338 ++++++++++++++++++++++++++++++ 8 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 hw2/hw/pyproject.toml create mode 100644 hw2/hw/shop_api/cart_routes.py create mode 100644 hw2/hw/shop_api/contracts.py create mode 100644 hw2/hw/shop_api/item_routes.py create mode 100644 hw2/hw/shop_api/store/__init__.py create mode 100644 hw2/hw/shop_api/store/storage.py create mode 100644 hw2/hw/uv.lock diff --git a/hw2/hw/pyproject.toml b/hw2/hw/pyproject.toml new file mode 100644 index 00000000..428b7d8d --- /dev/null +++ b/hw2/hw/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "hw" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "faker>=37.8.0", + "fastapi>=0.117.1", + "httpx>=0.27.2", + "pytest>=7.4.0", + "pytest-asyncio>=0.21.0", + "uvicorn>=0.24.0", +] diff --git a/hw2/hw/shop_api/cart_routes.py b/hw2/hw/shop_api/cart_routes.py new file mode 100644 index 00000000..d29bfd75 --- /dev/null +++ b/hw2/hw/shop_api/cart_routes.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, Response, HTTPException, Depends +from http import HTTPStatus +from .store.storage import local_storage +from .contracts import IdModel, CartResponseModel, ListQueryModel +cart_router = APIRouter(prefix="/cart") + +@cart_router.post("/", + status_code=HTTPStatus.CREATED) +async def post_cart(response: Response): + cart = local_storage.create_cart() + response.headers["location"] = f"/cart/{cart.id}" + return IdModel(id=cart.id) + + +@cart_router.post("/{cart_id}/add/{item_id}") +async def add_item_to_cart(cart_id: int, item_id: int): + success = local_storage.add_item_to_cart(cart_id, item_id) + if not success: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + return {"message": "Item added to cart"} + +@cart_router.get("/{cart_id}", + status_code=HTTPStatus.OK) +async def get_cart(cart_id: int) -> CartResponseModel: + try: + cart = local_storage.get_cart(id=cart_id) + return CartResponseModel.from_entity(cart) + except KeyError: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="cart not found") + + +@cart_router.get("/") +async def get_item_list(query: ListQueryModel = Depends()) -> list[CartResponseModel]: + carts = local_storage.get_carts( + offset=query.offset, + limit=query.limit, + min_price=query.min_price, + max_price=query.max_price, + min_quantity=query.min_quantity, + max_quantity=query.max_quantity + ) + return [CartResponseModel.from_entity(cart) for cart in carts] \ No newline at end of file diff --git a/hw2/hw/shop_api/contracts.py b/hw2/hw/shop_api/contracts.py new file mode 100644 index 00000000..6a21a14e --- /dev/null +++ b/hw2/hw/shop_api/contracts.py @@ -0,0 +1,71 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional, List +from .store.storage import CartData, ItemnInCartData, ItemData, ItemsData + +class IdModel(BaseModel): + """ + Model handles with id + """ + id: int = Field(description="Returns id") + +class ItemRequest(BaseModel): + name: str = Field(description="item name") + price: float = Field(gt=0, description="item price") + +class ItemPatchRequest(BaseModel): + model_config = ConfigDict(extra='forbid') + name: Optional[str] = None + price: Optional[float] = Field(None, gt=0.0) + +class ItemModel(BaseModel): + """ + Model defines items + """ + + id: int = Field(description="item id, int") + name: str = Field(description="item name, str") + price: float = Field(description="item price, float") + deleted: bool = Field(default=False, description="item available. bool") + @staticmethod + def from_entity(entity: ItemData) -> "ItemModel": + return ItemModel( + id=entity.id, + name=entity.name, + price=entity.price, + deleted=entity.deleted + ) + +class ListQueryModel(BaseModel): + offset: int = Field(0, ge=0, description="Number of items to skip") + limit: int = Field(10, gt=0, description="Maximum number of items to return") + min_price: Optional[float] = Field(None, ge=0, description="Minimum price filter") + max_price: Optional[float] = Field(None, ge=0, description="Maximum price filter") + show_deleted: bool = Field(False, description="Include deleted items") + min_quantity: Optional[int] = Field(None, ge=0, description="Minimum quantity filter") + max_quantity: Optional[int] = Field(None, ge=0, description="Maximum quantity filter") + +class CartItemModel(BaseModel): + """Model defines items in cart""" + id: int = Field(description="item id") + name: str = Field(description="item name") + quantity: int = Field(description="quantity in cart") + available: bool = Field(description="is item available (not deleted)") + +class CartResponseModel(BaseModel): + """Models defines Carts""" + id: int = Field(description="cart id") + items: List[CartItemModel] = Field(description="items in cart") + price: float = Field(description="total cart price") + + @staticmethod + def from_entity(entity: CartData) -> "CartResponseModel": + return CartResponseModel( + id=entity.id, + items=[CartItemModel( + id=item.id, + name=item.name, + quantity=item.quantity, + available=item.available + ) for item in entity.items], + price=entity.price + ) \ No newline at end of file diff --git a/hw2/hw/shop_api/item_routes.py b/hw2/hw/shop_api/item_routes.py new file mode 100644 index 00000000..6194e2a1 --- /dev/null +++ b/hw2/hw/shop_api/item_routes.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, HTTPException, Depends +from http import HTTPStatus +from .store.storage import local_storage +from .contracts import ItemPatchRequest, ItemRequest, ItemModel, ListQueryModel +item_router = APIRouter(prefix="/item") + +@item_router.post("/", + status_code=HTTPStatus.CREATED) +async def post_item(item_data: ItemRequest) -> ItemModel: + item = local_storage.add_item(name=item_data.name, price=item_data.price) + return ItemModel.from_entity(entity=item) + +@item_router.get("/{item_id}", + status_code=HTTPStatus.OK) +async def get_item(item_id: int) -> ItemModel: + try: + item = local_storage.get_item(id=item_id) + return ItemModel.from_entity(item) + except KeyError: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="item not found") + +@item_router.get("/") +async def get_item_list(query: ListQueryModel = Depends()) -> list[ItemModel]: + items = local_storage.get_items( + offset=query.offset, + limit=query.limit, + min_price=query.min_price, + max_price=query.max_price, + show_deleted=query.show_deleted + ) + return [ItemModel.from_entity(item) for item in items] + +@item_router.put("/{item_id}") +async def put_item(item_id: int, item_data:ItemRequest) -> ItemModel: + item = local_storage.put_item(item_id = item_id, + name = item_data.name, + price = item_data.price) + return ItemModel.from_entity(item) + +@item_router.patch("/{item_id}") +async def patch_item(item_id: int, item_data: ItemPatchRequest) -> ItemModel: + item = local_storage.patch_item(item_id=item_id, + name = item_data.name, + price = item_data.price) + if item.deleted: + raise HTTPException(status_code=HTTPStatus.NOT_MODIFIED) + return ItemModel.from_entity(item) + +@item_router.delete("/{item_id}") +async def patch_item(item_id: int) -> ItemModel: + item = local_storage.soft_delete_item(item_id=item_id) + return ItemModel.from_entity(item) + \ No newline at end of file diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..13e134a9 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,11 @@ from fastapi import FastAPI - +from .cart_routes import cart_router +from .item_routes import item_router app = FastAPI(title="Shop API") + +app.include_router(cart_router) +app.include_router(item_router) +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) + 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/storage.py b/hw2/hw/shop_api/store/storage.py new file mode 100644 index 00000000..09c0ac42 --- /dev/null +++ b/hw2/hw/shop_api/store/storage.py @@ -0,0 +1,177 @@ +from dataclasses import dataclass, field +from typing import List, Dict, Optional +import uuid + + +@dataclass(slots=True) +class ItemData: + """Represents an item in the shop with basic information.""" + + id: int + name: str + price: float + deleted: bool = False + + +@dataclass(slots=True) +class ItemsData: + """Container for multiple items data.""" + + items: List[ItemData] = field(default_factory=list) + + +@dataclass(slots=True) +class ItemnInCartData: + """Represents an item in a shopping cart with quantity and availability.""" + + id: int + name: str + quantity: int + available: bool + + +@dataclass(slots=True) +class CartData: + """Represents a shopping cart with items and total price.""" + + id: int + items: List[ItemnInCartData] = field(default_factory=list) + price: float = 0.0 + + +@dataclass(slots=True) +class IdDataGen: + """ID generator utility class.""" + + id: int + + def gen_id(): + """Generate a new unique ID using UUID4.""" + id = int(uuid.uuid4()) + return IdDataGen(id=id) + + +@dataclass(slots=True) +class Storage: + """Main storage class for managing items and carts.""" + + items: Dict[int, ItemData] = field(default_factory=dict) + carts: Dict[int, CartData] = field(default_factory=dict) + id_generator = IdDataGen + + def add_item(self, name: str, price: float) -> ItemData: + """Add a new item to storage and return it.""" + item_id = self.id_generator.gen_id().id + item = ItemData(id=item_id, name=name, price=price) + self.items[item_id] = item + return item + + def create_cart(self) -> CartData: + """Create a new empty cart and return it.""" + cart_id = self.id_generator.gen_id().id + cart = CartData(id=cart_id) + self.carts[cart_id] = cart + return cart + + def get_item(self, id,) -> ItemData: + """Get item by ID.""" + item = self.items[id] + if item.deleted: + raise KeyError(f"Item {id} not found") + return item + + def get_cart(self, id) -> CartData: + """Get cart by ID.""" + cart = self.carts[id] + return cart + + def get_items(self, + offset: int = 0, + limit: int = 10, + min_price: float = None, + max_price: float = None, + show_deleted: bool = False) -> list[ItemsData]: + """Get all items.""" + items = list(self.items.values()) + if not show_deleted: + items = [item for item in items if not item.deleted] + if min_price is not None: + items = [item for item in items if item.price >=min_price] + if max_price is not None: + items = [item for item in items if item.price <= max_price] + + return items[offset:offset+limit] + + def get_carts(self, + offset: int = 0, + limit: int = 10, + min_price: float = None, + max_price: float = None, + min_quantity: int = None, + max_quantity: int = None) -> list[CartData]: + carts = list(self.carts.values()) + + if min_price is not None: + carts = [cart for cart in carts if cart.price >= min_price] + if max_price is not None: + carts = [cart for cart in carts if cart.price <= max_price] + + if min_quantity is not None or max_quantity is not None: + def get_total_quantity(cart): + return sum(item.quantity for item in cart.items) + + if min_quantity is not None: + carts = [cart for cart in carts if get_total_quantity(cart) >= min_quantity] + if max_quantity is not None: + carts = [cart for cart in carts if get_total_quantity(cart) <= max_quantity] + + return carts[offset:offset+limit] + + + def add_item_to_cart(self, cart_id: int, item_id: int) -> bool: + """Add item to cart, updating quantity if item already exists. Returns True if successful.""" + if cart_id not in self.carts or item_id not in self.items: + return False + + cart = self.carts[cart_id] + item = self.items[item_id] + + for cart_item in cart.items: + if cart_item.id == item_id: + cart_item.quantity += 1 + break + else: + cart_item = ItemnInCartData( + id=item.id, name=item.name, quantity=1, available=not item.deleted + ) + cart.items.append(cart_item) + + cart.price = sum( + self.items[cart_item.id].price * cart_item.quantity + for cart_item in cart.items + if cart_item.available and cart_item.id in self.items + ) + + return True + + def put_item(self, item_id: int, name: str, price: float)-> ItemData: + item = self.items[item_id] + item.name = name + item.price = price + return item + + def patch_item(self, item_id: int, name: Optional[str], price: Optional[float])-> ItemData: + item = self.items[item_id] + if name is not None: + item.name = name + if price is not None: + item.price = price + return item + + def soft_delete_item(self, item_id:int): + item = self.items[item_id] + item.deleted = True + return item + +# Глобальный экземпляр storage +local_storage = Storage() \ No newline at end of file diff --git a/hw2/hw/uv.lock b/hw2/hw/uv.lock new file mode 100644 index 00000000..3948d2d1 --- /dev/null +++ b/hw2/hw/uv.lock @@ -0,0 +1,338 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "faker" +version = "37.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/da/1336008d39e5d4076dddb4e0f3a52ada41429274bf558a3cc28030d324a3/faker-37.8.0.tar.gz", hash = "sha256:090bb5abbec2b30949a95ce1ba6b20d1d0ed222883d63483a0d4be4a970d6fb8", size = 1912113, upload-time = "2025-09-15T20:24:13.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl", hash = "sha256:b08233118824423b5fc239f7dd51f145e7018082b4164f8da6a9994e1f1ae793", size = 1953940, upload-time = "2025-09-15T20:24:11.482Z" }, +] + +[[package]] +name = "fastapi" +version = "0.118.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/3c/2b9345a6504e4055eaa490e0b41c10e338ad61d9aeaae41d97807873cdf2/fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8", size = 310536, upload-time = "2025-09-29T03:37:23.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/54e2bdaad22ca91a59455251998d43094d5c3d3567c52c7c04774b3f43f2/fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855", size = 97694, upload-time = "2025-09-29T03:37:21.338Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hw" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "faker" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "faker", specifier = ">=37.8.0" }, + { name = "fastapi", specifier = ">=0.117.1" }, + { name = "httpx", specifier = ">=0.27.2" }, + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, + { name = "uvicorn", specifier = ">=0.24.0" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/54/ecab642b3bed45f7d5f59b38443dcb36ef50f85af192e6ece103dbfe9587/pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423", size = 788494, upload-time = "2025-10-04T10:40:41.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +]