Skip to content
Closed
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
129 changes: 129 additions & 0 deletions .github/workflows/hw2-comprehensive-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: HW2 Comprehensive Tests

on:
push:
branches: [ main, dev ]
paths:
- 'hw2/hw/**'
- '.github/workflows/hw2-comprehensive-tests.yml'
pull_request:
branches: [ main ]
paths:
- 'hw2/hw/**'
- '.github/workflows/hw2-comprehensive-tests.yml'

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: shop_db_test
POSTGRES_USER: shop_user
POSTGRES_PASSWORD: shop_password
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
uses: actions/setup-python@v5
with:
python-version: '3.9'
cache: 'pip'

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

- name: Wait for PostgreSQL
run: |
until pg_isready -h localhost -p 5432 -U shop_user; do
echo "Waiting for postgres..."
sleep 2
done

- name: Run tests with coverage
working-directory: ./hw2/hw
env:
DATABASE_URL: postgresql+asyncpg://shop_user:shop_password@localhost:5432/shop_db_test
run: |
pytest test_homework2_all.py -v --cov=shop_api --cov-report=xml --cov-report=term

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./hw2/hw/coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false

- name: Check coverage threshold
working-directory: ./hw2/hw
run: |
coverage report --fail-under=95

lint:
runs-on: ubuntu-latest

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

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9'

- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install flake8 black isort

- name: Run flake8
working-directory: ./hw2/hw
run: |
flake8 shop_api --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 shop_api --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Check formatting with black
working-directory: ./hw2/hw
run: |
black --check shop_api || true

- name: Check imports with isort
working-directory: ./hw2/hw
run: |
isort --check-only shop_api || true

docker-build:
runs-on: ubuntu-latest

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

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
working-directory: ./hw2/hw
run: |
docker build -t shop-api:test .

- name: Test Docker Compose
working-directory: ./hw2/hw
run: |
docker compose config
135 changes: 132 additions & 3 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Any, Awaitable, Callable

import json
import math
from urllib.parse import parse_qs

async def application(
scope: dict[str, Any],
Expand All @@ -12,8 +14,135 @@ async def application(
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'})
break
return

if scope['type'] != 'http':
return

method = scope['method']
path = scope['path']
query_string = scope.get('query_string', b'').decode('utf-8')
query_params = parse_qs(query_string)

async def send_response(status_code, body):
await send({
'type':'http.response.start',
'status': status_code,
'headers': [
[b'content-type', b'application/json'],
],
})
await send({
'type': 'http.response.body',
'body': json.dumps(body).encode('utf-8'),
})

async def receive_body():
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
if body:
try:
return json.loads(body.decode('utf-8'))
except json.JSONDecodeError:
return None
return None

if method != 'GET':
await send_response(404, {})
return

try:
if path == '/factorial':
if 'n' not in query_params or not query_params['n'][0]:
await send_response(422, {})
return

try:
n = int(query_params['n'][0])
if n < 0:
await send_response(400, {})
return

result = math.factorial(n)
await send_response(200, {'result': result})
return
except ValueError:
await send_response(422, {})
return

elif path.startswith('/fibonacci/'):
try:
n_str = path[11:]
if not n_str:
await send_response(422, {})
return
n = int(n_str)

if n < 0:
await send_response(400, {})
return
if n == 0:
await send_response(200, {'result': 0})
return
elif n == 1:
await send_response(200, {'result': 1})
return
else:
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
await send_response(200, {'result': b})
return

except Exception:
await send_response(422, {})
return

elif path == '/mean':
body = await receive_body()

if body is None:
await send_response(422, {})
return

if not isinstance(body, list):
await send_response(400, {})
return

if len(body) == 0:
await send_response(400, {})
return
try:
numbers = [float(x) for x in body]
mean_value = sum(numbers) / len(numbers)
await send_response(200, {'result': mean_value})
return
except (ValueError, TypeError):
await send_response(400, {})
return

else:
await send_response(404, {})
return

except Exception:
await send_response(500, {})
return

if __name__ == "__main__":
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)
19 changes: 19 additions & 0 deletions hw2/hw/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[run]
source = shop_api
concurrency = thread,greenlet
omit =
*/tests/*
*/test_*.py
*/__pycache__/*
*/venv/*
*/env/*

[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
26 changes: 26 additions & 0 deletions hw2/hw/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.git
.gitignore
.env
.venv
env/
venv/
ENV/
.pytest_cache
.coverage
htmlcov/
*.log
.DS_Store
Thumbs.db
README.md
docker-compose.yml
Dockerfile
Loading
Loading