diff --git a/hw1/README.md b/hw1/README.md index 517057e..7c3f914 100644 --- a/hw1/README.md +++ b/hw1/README.md @@ -2,19 +2,18 @@ ## πŸš€ Как Π½Π°Ρ‡Π°Ρ‚ΡŒ Ρ€Π°Π±ΠΎΡ‚Ρƒ +ВсС ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ Π½ΠΈΠΆΠ΅ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ ΠΈΠ· ΠΏΠ°ΠΏΠΊΠΈ `hw1`. ### 1. УстановитС зависимости ```bash pip install -r requirements.txt ``` -### 2. Π Π΅Π°Π»ΠΈΠ·ΡƒΠΉΡ‚Π΅ ASGI ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ -ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» `app.py` ΠΈ Π½Π°ΠΉΠ΄ΠΈΡ‚Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ `application` с ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅ΠΌ `TODO`. - -Π’Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ HTTP запросов Π²Π½ΡƒΡ‚Ρ€ΠΈ этой Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ. КакиС ΠΈΠΌΠ΅Π½Π½ΠΎ запросы Π²Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ? - -Π’ Π»Π΅ΠΊΡ†ΠΈΠΈ Π΅ΡΡ‚ΡŒ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ матСматичСского API с трСмя Ρ€ΡƒΡ‡ΠΊΠ°ΠΌΠΈ, ΠΈΡ… ΠΈ Π½Π°Π΄ΠΎ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ - +### 2. ASGI ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ +Π’ `app.py` Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π° функция `application`, ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°ΡŽΡ‰Π°Ρ HTTP-запросы +ΠΊ Ρ‚Ρ€Ρ‘ΠΌ эндпоинтам матСматичСского API: Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΈΠ°Π», числа Π€ΠΈΠ±ΠΎΠ½Π°Ρ‡Ρ‡ΠΈ ΠΈ срСднСС +арифмСтичСскоС. ВычислСния ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ² ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ ΡΡ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½ΡƒΡŽ +Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ Python. ### 3. ЗапуститС сСрвСр для тСстирования ```bash @@ -24,7 +23,34 @@ python app.py Π‘Π΅Ρ€Π²Π΅Ρ€ запустится Π½Π° `http://localhost:8000`. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΡ€ΠΎΡ‚Π΅ΡΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ эндпоинты: - http://localhost:8000/fibonacci/10 - http://localhost:8000/factorial?n=5 -- http://localhost:8000/mean?numbers=1,2,3 + +`GET /mean` ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ JSON-массив Π² **Ρ‚Π΅Π»Π΅ запроса**, ΠΊΠ°ΠΊ Π² `test_app.py`. +ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ `numbers` Π² URL Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ. ΠŸΡ€ΠΈΠΌΠ΅Ρ€ для PowerShell: + +```powershell +curl.exe -X GET http://localhost:8000/mean -H 'Content-Type: application/json' -d '[1,2,3]' +``` + +ΠžΡ‚Π²Π΅Ρ‚: `{"result": 2.0}`. Для curl: + +```bash +curl -X GET http://localhost:8000/mean -H 'Content-Type: application/json' -d '[1,2,3]' +``` + +### Π€ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ запросов ΠΈ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ + +| Запрос | Π’Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ | Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ | +| --- | --- | --- | +| `GET /factorial?n=5` | ΠΠ΅ΠΎΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ `n` | `{"result": 120}` | +| `GET /fibonacci/10` | ΠΠ΅ΠΎΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ Π² ΠΏΡƒΡ‚ΠΈ | `{"result": 55}` | +| `GET /mean` | НСпустой JSON-массив чисСл Π² Ρ‚Π΅Π»Π΅ | Π‘Ρ€Π΅Π΄Π½Π΅Π΅ арифмСтичСскоС | + +Для Π€ΠΈΠ±ΠΎΠ½Π°Ρ‡Ρ‡ΠΈ приняты `F(0) = 0`, `F(1) = 1`; `0! = 1`. +ΠžΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ `n`, пустой массив ΠΈ Π½Π΅ΠΊΠΎΠ½Π΅Ρ‡Π½Ρ‹Π΅ значСния `NaN`/`Infinity` +Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‚ `400`. ΠžΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹, Π½Π΅Π²Π΅Ρ€Π½Ρ‹ΠΉ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ Ρ†Π΅Π»ΠΎΠ³ΠΎ числа, +Π½Π΅Π²Π°Π»ΠΈΠ΄Π½Ρ‹ΠΉ JSON, значСния Π²Π½Π΅ массива, строки ΠΈ логичСскиС значСния Π²Π½ΡƒΡ‚Ρ€ΠΈ +массива Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‚ `422`. Для извСстного ΠΏΡƒΡ‚ΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ ΠΊΡ€ΠΎΠΌΠ΅ `GET` Ρ‚Π°ΠΊΠΆΠ΅ +Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‚ `422`, Π° нСизвСстныС ΠΏΡƒΡ‚ΠΈ β€” `404`. Ошибки ΠΈΠΌΠ΅ΡŽΡ‚ Π²ΠΈΠ΄ `{"error": "..."}`. ### 4. ЗапуститС тСсты локально, Ссли Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ```bash @@ -35,8 +61,8 @@ pytest test_app.py -v ### 1. ЗафиксируйтС измСнСния ```bash -git add app.py -git commit -m "Implement ASGI application with fibonacci, factorial, and mean endpoints" +git add app.py README.md +git commit -m "feat: Π΄ΠΎΠ±Π°Π²ΠΈΠ» матСматичСскоС ASGI API для Π΄Π· 1" ``` ### 2. ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ измСнСния Π² свой Ρ„ΠΎΡ€ΠΊ diff --git a/hw1/app.py b/hw1/app.py index 6107b87..8a1bab6 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,4 +1,92 @@ +import json +import math +import re +from http import HTTPStatus +from statistics import mean from typing import Any, Awaitable, Callable +from urllib.parse import parse_qs + +import uvicorn + + +class RequestError(ValueError): + def __init__(self, status: HTTPStatus, message: str): + super().__init__(message) + self.status = status + + +def parse_index(value: str) -> int: + if re.fullmatch(r"[+-]?[0-9]+", value.strip()) is None: + raise RequestError(HTTPStatus.UNPROCESSABLE_ENTITY, "n must be an integer") + try: + index = int(value) + except ValueError as error: + raise RequestError(HTTPStatus.BAD_REQUEST, "n is too large") from error + if index < 0: + raise RequestError(HTTPStatus.BAD_REQUEST, "n must be non-negative") + return index + + +def query_parameter(scope: dict[str, Any], name: str) -> str: + try: + query = parse_qs( + scope.get("query_string", b"").decode("utf-8"), + keep_blank_values=True, + errors="strict", + ) + except UnicodeError as error: + raise RequestError( + HTTPStatus.UNPROCESSABLE_ENTITY, "Invalid query encoding" + ) from error + values = query.get(name, []) + if len(values) != 1: + raise RequestError( + HTTPStatus.UNPROCESSABLE_ENTITY, f"Expected one {name} parameter" + ) + return values[0] + + +def fibonacci(index: int) -> int: + current, following = 0, 1 + for _ in range(index): + current, following = following, current + following + return current + + +async def read_body(receive: Callable[[], Awaitable[dict[str, Any]]]) -> bytes: + body = bytearray() + while True: + message = await receive() + if message["type"] == "http.disconnect": + raise ConnectionError("Client disconnected") + body.extend(message.get("body", b"")) + if not message.get("more_body", False): + return bytes(body) + + +def calculate_mean(body: bytes) -> float: + try: + numbers = json.loads(body) + except (ValueError, UnicodeError) as error: + raise RequestError( + HTTPStatus.UNPROCESSABLE_ENTITY, "Expected an array of numbers" + ) from error + if not isinstance(numbers, list) or any( + type(number) not in (int, float) for number in numbers + ): + raise RequestError( + HTTPStatus.UNPROCESSABLE_ENTITY, "Expected an array of numbers" + ) + if not numbers: + raise RequestError(HTTPStatus.BAD_REQUEST, "The array must not be empty") + if any( + isinstance(number, float) and not math.isfinite(number) for number in numbers + ): + raise RequestError(HTTPStatus.BAD_REQUEST, "Numbers must be finite") + try: + return float(mean(numbers)) + except OverflowError as error: + raise RequestError(HTTPStatus.BAD_REQUEST, "The mean is too large") from error async def application( @@ -6,14 +94,65 @@ async def application( receive: Callable[[], Awaitable[dict[str, Any]]], send: Callable[[dict[str, Any]], Awaitable[None]], ): - """ - Args: - scope: Π‘Π»ΠΎΠ²Π°Ρ€ΡŒ с ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠ΅ΠΉ ΠΎ запросС - receive: ΠšΠΎΡ€ΡƒΡ‚ΠΈΠ½Π° для получСния сообщСний ΠΎΡ‚ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° - send: ΠšΠΎΡ€ΡƒΡ‚ΠΈΠ½Π° для ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ сообщСний ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ - """ - # TODO: Π’Π°ΡˆΠ° рСализация здСсь + if scope["type"] == "lifespan": + 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 + + if scope["type"] != "http": + raise ValueError("Only HTTP and lifespan scopes are supported") + + path = scope["path"] + is_fibonacci = path.startswith("/fibonacci/") and path.count("/") == 2 + try: + if path not in ("/factorial", "/mean") and not is_fibonacci: + raise RequestError(HTTPStatus.NOT_FOUND, "Endpoint not found") + if scope["method"] != "GET": + raise RequestError( + HTTPStatus.UNPROCESSABLE_ENTITY, "Expected a GET request" + ) + + if path == "/factorial": + result = math.factorial(parse_index(query_parameter(scope, "n"))) + elif is_fibonacci: + result = fibonacci(parse_index(path.removeprefix("/fibonacci/"))) + else: + result = calculate_mean(await read_body(receive)) + + try: + body = json.dumps({"result": result}, allow_nan=False).encode("utf-8") + except (ValueError, OverflowError) as error: + raise RequestError( + HTTPStatus.BAD_REQUEST, "The result is too large" + ) from error + status = HTTPStatus.OK + except ConnectionError: + return + except RequestError as error: + status = error.status + body = json.dumps({"error": str(error)}).encode("utf-8") + + await send( + { + "type": "http.response.start", + "status": status, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode("ascii")), + ], + } + ) + await send( + { + "type": "http.response.body", + "body": b"" if scope["method"] == "HEAD" else body, + } + ) + if __name__ == "__main__": - import uvicorn uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True)