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
59 changes: 59 additions & 0 deletions .github/workflows/hw4-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: "HW4 Tests"

# Запускаем тесты при изменении файлов в hw2/hw/
on:
pull_request:
branches: [ main ]
paths: [ 'hw2/hw/**' ]
push:
branches: [ main ]
paths: [ 'hw2/hw/**' ]

jobs:
test-hw4:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]

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 all tests with coverage (HW4)
working-directory: hw2/hw
env:
PYTHONPATH: ${{ github.workspace }}/hw2/hw
run: |
pytest --cov=shop_api --cov-report=term-missing --cov-report=xml --cov-fail-under=95 test_homework2.py test_edge_cases.py test_websocket.py -v

- name: Upload coverage to Codecov
if: matrix.python-version == '3.11'
uses: codecov/codecov-action@v3
with:
files: ./hw2/hw/coverage.xml
fail_ci_if_error: false

- name: Run transaction isolation demos
if: matrix.python-version == '3.11'
working-directory: hw2/hw
run: |
echo "Running transaction isolation demonstrations..."
python transaction_demos/01_dirty_read_demo.py
python transaction_demos/02_no_dirty_read_demo.py
python transaction_demos/03_non_repeatable_read_demo.py
python transaction_demos/04_no_non_repeatable_read_demo.py
python transaction_demos/05_phantom_read_demo.py
python transaction_demos/06_no_phantom_read_demo.py
echo "All transaction demos completed successfully!"
109 changes: 105 additions & 4 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,120 @@
import math
from http import HTTPStatus
from typing import Any, Awaitable, Callable
import json


async def application(
scope: dict[str, Any],
receive: Callable[[], Awaitable[dict[str, Any]]],
send: Callable[[dict[str, Any]], Awaitable[None]],
scope: dict[str, Any],
receive: Callable[[], Awaitable[dict[str, Any]]],
send: Callable[[dict[str, Any]], Awaitable[None]],
):
"""
Args:
scope: Словарь с информацией о запросе
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь

async def send_response(status: int, body: dict[str, Any] | None = None):
content = b""
headers = [(b"content-type", b"application/json")]
if body is not None:
content = json.dumps(body).encode("utf-8")
headers.append((b"content-length", str(len(content)).encode("utf-8")))
else:
headers.append((b"content-length", b"0"))

await send(
{"type": "http.response.start", "status": status, "headers": headers}
)
await send({"type": "http.response.body", "body": content})

# --- lifespan events (startup/shutdown) ---
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

# --- обычные HTTP запросы ---
if scope["type"] != "http":
return

method = scope["method"]
path = scope["path"]

# factorial
if method == "GET" and path == "/factorial":
query_string = scope.get("query_string", b"").decode()
query = dict(
(q.split("=") + [""])[:2]
for q in query_string.split("&")
if q
)
n_str = query.get("n")
if n_str is None or n_str == "":
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)
try:
n = int(n_str)
except ValueError:
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)
if n < 0:
return await send_response(HTTPStatus.BAD_REQUEST)
return await send_response(HTTPStatus.OK, {"result": math.factorial(n)})

# fibonacci
if method == "GET" and path.startswith("/fibonacci"):
parts = path.split("/")
if len(parts) != 3:
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)
try:
n = int(parts[2])
except ValueError:
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)
if n < 0:
return await send_response(HTTPStatus.BAD_REQUEST)
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return await send_response(HTTPStatus.OK, {"result": a})

# mean
if method == "GET" and path == "/mean":
body_event = await receive()
if body_event.get("type") != "http.request":
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)
body_bytes = body_event.get("body", b"") or b""
if not body_bytes:
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)

try:
data = json.loads(body_bytes.decode())
except json.JSONDecodeError:
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)

if not isinstance(data, list):
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)
if len(data) == 0:
return await send_response(HTTPStatus.BAD_REQUEST)

try:
nums = [float(x) for x in data]
except (TypeError, ValueError):
return await send_response(HTTPStatus.UNPROCESSABLE_ENTITY)

return await send_response(
HTTPStatus.OK, {"result": sum(nums) / len(nums)}
)

# всё остальное → 404
return await send_response(HTTPStatus.NOT_FOUND)


if __name__ == "__main__":
import uvicorn

uvicorn.run("app:application", host="0.0.0.0", port=8000, reload=True)
15 changes: 15 additions & 0 deletions hw2/hw/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
__pycache__
*.pyc
*.pyo
*.pyd
.pytest_cache
.venv
venv
*.egg-info
.git
.gitignore
README.md
MONITORING.md
CLAUDE.md
chat.html
ddoser.py
23 changes: 23 additions & 0 deletions hw2/hw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.12 AS base

ARG PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=on \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=500

RUN apt-get update && apt-get install -y gcc
RUN python -m pip install --upgrade pip

WORKDIR $APP_ROOT/src
COPY . ./

ENV VIRTUAL_ENV=$APP_ROOT/src/.venv \
PATH=$APP_ROOT/src/.venv/bin:$PATH

RUN pip install -r requirements.txt

FROM base as local

CMD ["uvicorn", "shop_api.main:app", "--port", "8080", "--host", "0.0.0.0"]
86 changes: 86 additions & 0 deletions hw2/hw/MONITORING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Monitoring Setup Guide for Shop API

This guide explains how to set up monitoring for the Shop API using Docker, Prometheus, and Grafana.

## Prerequisites

- Docker and Docker Compose installed
- Ports 3000 (Grafana), 8080 (API), and 9090 (Prometheus) available

## Quick Start

### 1. Build and Start Services

```bash
docker-compose up --build
```

This will start three services:
- **shop-api**: The FastAPI application on port 8080
- **prometheus**: Metrics collection on port 9090
- **grafana**: Visualization dashboard on port 3000

### 2. Verify Services are Running

- API: http://localhost:8080/docs
- Prometheus: http://localhost:9090
- Grafana: http://localhost:3000

## Setting Up Grafana

### 1. Login to Grafana

- Navigate to http://localhost:3000
- Username: `admin`
- Password: `admin`
- You'll be prompted to change the password (you can skip this for local development)

### 2. Add Prometheus Data Source

1. Click on the **gear icon** (⚙️) in the left sidebar → **Data Sources**
2. Click **Add data source**
3. Select **Prometheus**
4. Configure:
- **Name**: `Prometheus`
- **URL**: `http://prometheus:9090`
- Leave other settings as default
5. Click **Save & Test** - you should see "Data source is working"

### 3. Create Dashboards


1. Click **+** icon in left sidebar → **Import**
2. Enter data from `settings/grafana_config.json`
5. Click **Import**

## Running Load Tests

To generate traffic for monitoring:

```bash
# Make sure the API is running via docker-compose
python ddoser.py
```

The load test will:
- Create random items
- Query items with various filters
- Create carts
- Add items to carts
- Update and delete items
- Generate traffic across all API endpoints

## Stopping Services

```bash
docker-compose down
```

To remove volumes as well:
```bash
docker-compose down -v
```

## HW done

![grafana.jpg](grafana.jpg)
13 changes: 12 additions & 1 deletion hw2/hw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
Чтобы запустить тесты только для этого задания вызовите:

```sh
pytest -vv --showlocals --strict ./hw2/test_homework_2_1.py
pytest -vv --showlocals --strict ./hw2/hw/test_homework2.py
```

Если получаете ошибку на подобии `No module named 'shop_api'`
Expand All @@ -121,3 +121,14 @@ export PYTHONPATH=${PWD}/hw2/hw
начале в следующем виде: `{username} :: {message}`.

Если делаете его, напишите, пожалуйста, прямо в PR-e об этом. Мне будет сильно проще это заметить<3

### Тест чата

Добавил в `requirements.txt`: `uvicorn[standard]`, без этого вебсокеты не работали.

Чтобы потестить чат, запускаем:
```
uvicorn shop_api.main:app --reload
```

Затем открываем: ```chat.html``` из нескольких вкладок, коннектимся и наслаждаемся.
32 changes: 32 additions & 0 deletions hw2/hw/chat.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head><title>Chat Test</title></head>
<body>
<input id="chatName" placeholder="Chat room name" value="test-room">
<button onclick="connect()">Connect</button>
<button onclick="disconnect()">Disconnect</button>
<br><br>
<input id="messageInput" placeholder="Type message">
<button onclick="sendMessage()">Send</button>
<div id="messages"></div>

<script>
let ws;
function connect() {
const chatName = document.getElementById('chatName').value;
ws = new WebSocket(`ws://localhost:8000/chat/${chatName}`);
ws.onmessage = (event) => {
const div = document.createElement('div');
div.textContent = event.data;
document.getElementById('messages').appendChild(div);
};
}
function disconnect() { ws?.close(); }
function sendMessage() {
const msg = document.getElementById('messageInput').value;
ws.send(msg);
document.getElementById('messageInput').value = '';
}
</script>
</body>
</html>
26 changes: 26 additions & 0 deletions hw2/hw/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pytest
import os
import sys

# Add current directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from shop_api.database import init_db, engine, Base


@pytest.fixture(scope="session", autouse=True)
def setup_database():
"""Initialize database before tests and clean up after"""
# Remove old test database if exists
if os.path.exists("shop.db"):
os.remove("shop.db")

# Create tables
init_db()

yield

# Clean up after tests
Base.metadata.drop_all(bind=engine)
if os.path.exists("shop.db"):
os.remove("shop.db")
Loading