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
29 changes: 26 additions & 3 deletions .github/workflows/hw2-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,48 @@ jobs:
strategy:
matrix:
python-version: ["3.12", "3.13"]


services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: shop_db
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
working-directory: hw2/hw
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run database migrations
working-directory: hw2/hw/shop_api
env:
DATABASE_URL: postgresql+asyncpg://admin:admin@localhost:5432/shop_db
run: |
alembic upgrade head

- name: Run tests
working-directory: hw2/hw
env:
PYTHONPATH: ${{ github.workspace }}/hw2/hw
DATABASE_URL: postgresql+asyncpg://admin:admin@localhost:5432/shop_db
run: |
pytest test_homework2.py -v
60 changes: 60 additions & 0 deletions .github/workflows/hw5-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: HW5 Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.13'

- name: Install dependencies
run: |
cd hw2/hw
pip install -r requirements.txt

- name: Run database migrations
env:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/test_db
run: |
cd hw2/hw/shop_api
alembic upgrade head

- name: Run tests with coverage
env:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/test_db
run: |
cd hw2/hw
pytest --cov=shop_api --cov-report=term-missing --cov-report=xml --cov-fail-under=95

- name: Upload coverage to Codecov (optional)
uses: codecov/codecov-action@v3
with:
file: ./hw2/hw/coverage.xml
flags: hw5
name: hw5-coverage
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,7 @@ dmypy.json

# macOS
.DS_Store

# Custom
explain.md
test_analysis.md
223 changes: 222 additions & 1 deletion hw1/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,229 @@ async def application(
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь

def get_params(query_string: str) -> dict[str, str]:
"""Парсит query_string в словарь параметров вида {"key": "value"}"""

query_params = {}
for pair in query_string.split("&"):
if "=" in pair:
key, value = pair.split("=", 1)
query_params[key] = value
return query_params

async def send_response(status: int, message: str) -> None:
"""Отправляет ответ клиенту"""
await send(
{
"type": "http.response.start",
"status": status,
"headers": [
[b"content-type", b"application/json"],
],
}
)
await send(
{
"type": "http.response.body",
"body": message.encode(),
}
)

if scope["type"] == "http":

path = scope["path"]
query_string = scope.get("query_string", b"").decode()

# Реализация API для расчета чисел Фибоначчи
if path.startswith("/fibonacci/"):

path_parsed = path.split("/")
n_value = path_parsed[2]

# Проверка наличия параметра для расчета
if not n_value:
message = (
'{"error": "Number parameter \\"/fibonacci/{n}\\" is required"}'
)
await send_response(422, message)
return

# Попытка преобразовать параметр в число
try:
n = int(n_value)
except ValueError:
message = f'{{"error": "Parameter \\"n\\" must be an integer, got \\"{n_value}\\""}}'
await send_response(422, message)
return

# Проверка на отрицательное число
if n < 0:
message = (
f'{{"error": "Parameter \\"n\\" cannot be negative, got {n}"}}'
)
await send_response(400, message)
return

# Расчет числа Фибоначчи
else:

a, b = 0, 1
for _ in range(n):
a, b = b, a + b

factorial_number = a
result = f'{{"result": {factorial_number}}}'

await send_response(200, result)
return

# Реализация API для расчета факториала
elif path.startswith("/factorial"):

query_params = get_params(query_string)

# Проверка наличия параметра n
if "n" not in query_params:
message = '{"error": "Parameter \\"n\\" is required"}'
await send_response(422, message)
return

n_value = query_params["n"]

# Проверка, что параметр не пустой
if not n_value:
message = '{"error": "Parameter \\"n\\" cannot be empty"}'
await send_response(422, message)
return

# Попытка преобразовать параметр в число
try:
n = int(n_value)
except ValueError:
message = f'{{"error": "Parameter \\"n\\" must be an integer, got \\"{n_value}\\""}}'
await send_response(422, message)
return

# Проверка на отрицательное число
if n < 0:
message = (
f'{{"error": "Parameter \\"n\\" cannot be negative, got {n}"}}'
)
await send_response(400, message)
return

# Расчет факториала
else:

factorial_number = 1
for i in range(1, n + 1):
factorial_number *= i

result = f'{{"result": {factorial_number}}}'
await send_response(200, result)
return

# Реализация API для расчета среднего
elif path.startswith("/mean"):

query_params = get_params(query_string)

# Проверка наличия параметра numbers
try:
numbers_list = []

# Получение данных из тела запроса
body = b""
more_body = True

while more_body:
message = await receive()
body += message.get("body", b"")
more_body = message.get("more_body", False)

if body:

body_str = body.decode().strip()

# Проверка, что данные не пустые
if body_str == "[]":
message = '{"error": "Parameter \\"numbers\\" cannot be empty"}'
await send_response(400, message)
return

# Обработка JSON из тела запроса
if body_str.startswith("[") and body_str.endswith("]"):
numbers_str = body_str[1:-1].strip()
if numbers_str:
for num_str in numbers_str.split(","):
num_clean = num_str.strip()
if num_clean:
numbers_list.append(float(num_clean))

# Проверка query_params, если данных нет в теле запроса
else:

# Проверка наличия параметра numbers
if "numbers" not in query_params:
message = '{"error": "Parameter \\"numbers\\" is required"}'
await send_response(422, message)
return

numbers_value = query_params["numbers"]

# Проверка, что параметр не пустой
if not numbers_value:
message = '{"error": "Parameter \\"numbers\\" cannot be empty"}'
await send_response(400, message)
return

# Попытка преобразования параметра в список чисел
try:
numbers_list = numbers_value.replace("%20", "").split(",")
numbers_list = [int(float(number)) for number in numbers_list]
except ValueError:
message = f'{{"error": "Parameter \\"numbers\\" must be a list of integers, got \\"{numbers_value}\\""}}'
await send_response(422, message)
return

mean_number = sum(numbers_list) / len(numbers_list)
result = f'{{"result": {mean_number}}}'
await send_response(200, result)
return

except Exception:
message = '{"error": "Invalid request"}'
await send_response(422, message)
return

# Обработка запроса favicon.ico
elif path.startswith("/favicon.ico"):
message = '{"error": "No Content favicon"}'
await send_response(204, message)

# Возвращение ошибки 404, если путь не соответствует ни одному из ожидаемых
else:
message = '{"error": "Not found"}'
await send_response(404, message)
return

# Обработка запросов жизненного цикла (startup/shutdown)
elif 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
else:
message = '{"error": "Unsupported request type"}'
await send_response(422, message)
return


if __name__ == "__main__":
import uvicorn

uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True)
21 changes: 21 additions & 0 deletions hw2/hw/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[run]
source = shop_api
omit =
*/migrations/*
*/tests/*
*/conftest.py
*/__pycache__/*
venv/*
.venv/*
*/transaction_scripts/*
*/not_pytests/*
*/old_*.py
*/alembic/*

[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
Loading