Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions hw1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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. Отправьте изменения в свой форк
Expand Down
155 changes: 147 additions & 8 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,158 @@
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лучше вынести обработку ошибки реквеста за логику парсера, парсеру логичнее возвращать либо значение либо рейзить ошибку

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(
scope: dict[str, Any],
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)
Loading