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

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

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

services:
postgres:
image: postgres:15
env:
POSTGRES_DB: shop_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--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 system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpq-dev

- name: Install Python dependencies
working-directory: hw4-5
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

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

- name: Run tests with coverage
working-directory: hw4-5
env:
PYTHONPATH: ${{ github.workspace }}/hw4-5
DATABASE_URL: postgresql+psycopg2://postgres:password@localhost:5432/shop_db
run: |
coverage run --source=service -m pytest tests -v
coverage xml
coverage report --fail-under=95
continue-on-error: false

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ matrix.python-version }}
path: hw4-5/coverage.xml
8 changes: 8 additions & 0 deletions hw2/hw/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from websocket import create_connection

ws = create_connection("ws://localhost:8000/chat/chat1")

while True:
print(ws.recv())
message = input("Enter message: ")
ws.send(message)
8 changes: 8 additions & 0 deletions hw2/hw/client2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from websocket import create_connection

ws = create_connection("ws://localhost:8000/chat/chat2")

while True:
print(ws.recv())
message = input("Enter message: ")
ws.send(message)
3 changes: 3 additions & 0 deletions hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
fastapi>=0.117.1
uvicorn>=0.24.0

# Доп WebSocket
websocket-client>=1.8.0

# Зависимости для тестирования
pytest>=7.4.0
pytest-asyncio>=0.21.0
Expand Down
53 changes: 53 additions & 0 deletions hw2/hw/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from dataclasses import dataclass, field
from uuid import uuid4
from typing import Optional

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI(title="Chat API")

@dataclass(slots=True)
class Broadcaster:
subscribers: dict[str, WebSocket] = field(init=False, default_factory=dict)

async def subscribe(self, ws: WebSocket, username: str) -> None:
await ws.accept()
self.subscribers[username] = ws

async def unsubscribe(self, username: str) -> None:
self.subscribers.pop(username, None)

async def publish(self, message: str, sender_username: Optional[str] = None) -> None:
if sender_username is not None:
formatted_message = f"{sender_username} :: {message}"
for username, ws in self.subscribers.items():
if username != sender_username:
await ws.send_text(formatted_message)
else:
for ws in self.subscribers.values():
await ws.send_text(message)

chat_channels: dict[str, Broadcaster] = {}

@app.websocket("/chat/{chat_name}")
async def ws_chat(ws: WebSocket, chat_name: str):
username = str(uuid4().hex[:8])
if chat_name not in chat_channels:
chat_channels[chat_name] = Broadcaster()
broadcaster = chat_channels[chat_name]

await broadcaster.subscribe(ws, username)
await broadcaster.publish(f"user {username} joined the chat")

try:
while True:
text = await ws.receive_text()
await broadcaster.publish(text, username)
except WebSocketDisconnect:
await broadcaster.unsubscribe(username)
await broadcaster.publish(f"left the chat", username)
await broadcaster.publish(f"user {username} left the chat")
if not broadcaster.subscribers:
chat_channels.pop(chat_name, None)


162 changes: 161 additions & 1 deletion hw2/hw/shop_api/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,163 @@
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException, Query, Response
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional
from http import HTTPStatus

app = FastAPI(title="Shop API")

class Item(BaseModel):
id: int
name: str
price: float
deleted: bool = False

class ItemInCart(BaseModel):
id: int
name: str
quantity: int
available: bool

class Cart(BaseModel):
id: int
items: List[ItemInCart] = []
price: float

class ItemCreatingObj(BaseModel):
name: str
price: float = Field(..., gt=0)

class ItemUpdatingObj(BaseModel):
name: Optional[str] = None
price: Optional[float] = None

model_config = ConfigDict(extra="forbid")

items_dict = {}
carts_dict = {}
item_id_counter = 0
cart_id_counter = 0

@app.post("/cart", response_model=dict, status_code=HTTPStatus.CREATED)
async def create_cart(response: Response):
global cart_id_counter
cart_id_counter += 1
carts_dict[cart_id_counter] = {"id": cart_id_counter, "items": [], "price": 0.0}
response.headers["location"] = f"/cart/{cart_id_counter}"
return {"id": cart_id_counter}

@app.get("/cart/{id}", response_model=Cart)
async def get_cart(id: int):
if id not in carts_dict:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Cart not found")
return carts_dict[id]

@app.get("/cart", response_model=List[Cart])
async def list_carts(
offset: int = Query(0, ge=0),
limit: int = Query(10, gt=0),
min_price: Optional[float] = Query(None, ge=0),
max_price: Optional[float] = Query(None, ge=0),
min_quantity: Optional[int] = Query(None, ge=0),
max_quantity: Optional[int] = Query(None, ge=0)
):
carts = list(carts_dict.values())
if min_price is not None:
carts = [cart for cart in carts if cart["price"] >= min_price]
if max_price is not None:
carts = [cart for cart in carts if cart["price"] <= max_price]
if min_quantity is not None:
carts = [cart for cart in carts if sum(item["quantity"] for item in cart["items"]) >= min_quantity]
if max_quantity is not None:
carts = [cart for cart in carts if sum(item["quantity"] for item in cart["items"]) <= max_quantity]
return carts[offset:offset + limit]

@app.post("/cart/{cart_id}/add/{item_id}", response_model=Cart)
async def add_item_to_cart(cart_id: int, item_id: int):
if cart_id not in carts_dict:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Cart not found")
if item_id not in items_dict or items_dict[item_id]["deleted"]:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found")

cart = carts_dict[cart_id]
for cart_item in cart["items"]:
if cart_item["id"] == item_id:
cart_item["quantity"] += 1
cart["price"] += items_dict[item_id]["price"]
return cart

cart["items"].append({
"id": item_id,
"name": items_dict[item_id]["name"],
"quantity": 1,
"available": True
})
cart["price"] += items_dict[item_id]["price"]
return cart

@app.post("/item", response_model=Item, status_code=HTTPStatus.CREATED)
async def create_item(item: ItemCreatingObj):
global item_id_counter
item_id_counter += 1
items_dict[item_id_counter] = {
"id": item_id_counter,
"name": item.name,
"price": item.price,
"deleted": False
}
return items_dict[item_id_counter]

@app.get("/item/{id}", response_model=Item)
async def get_item(id: int):
if id not in items_dict or items_dict[id]["deleted"]:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found")
return items_dict[id]

@app.get("/item", response_model=List[Item])
async def list_items(
offset: int = Query(0, ge=0),
limit: int = Query(10, gt=0),
min_price: Optional[float] = Query(None, ge=0),
max_price: Optional[float] = Query(None, ge=0),
show_deleted: bool = False
):
items = [item for item in items_dict.values() if show_deleted or not item["deleted"]]
if min_price is not None:
items = [item for item in items if item["price"] >= min_price]
if max_price is not None:
items = [item for item in items if item["price"] <= max_price]
return items[offset:offset + limit]

@app.put("/item/{id}", response_model=Item)
async def update_item(id: int, item: ItemCreatingObj):
if id not in items_dict:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found")
items_dict[id].update({
"name": item.name,
"price": item.price,
"deleted": items_dict[id]["deleted"]
})
return items_dict[id]

@app.patch("/item/{id}", response_model=Item)
async def partial_update_item(id: int, item: ItemUpdatingObj):
if id not in items_dict:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found")
if items_dict[id]["deleted"]:
raise HTTPException(status_code=HTTPStatus.NOT_MODIFIED, detail="Item is deleted")
if item.name is not None:
items_dict[id]["name"] = item.name
if item.price is not None:
items_dict[id]["price"] = item.price
return items_dict[id]

@app.delete("/item/{id}", response_model=dict)
async def delete_item(id: int):
if id not in items_dict:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found")
items_dict[id]["deleted"] = True
for cart in carts_dict.values():
for cart_item in cart["items"]:
if cart_item["id"] == id:
cart_item["available"] = False
cart["price"] -= cart_item["quantity"] * items_dict[id]["price"]
return {"status_code": HTTPStatus.OK}
23 changes: 23 additions & 0 deletions hw3/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"]
27 changes: 27 additions & 0 deletions hw3/ddoser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from concurrent.futures import ThreadPoolExecutor
import requests
from faker import Faker

faker = Faker()

def create_items():
for _ in range(500):
response = requests.post(
"http://localhost:8080/item",
json={"name": faker.word(), "price": faker.pyfloat(min_value=1, max_value=100, positive=True)}
)
print(f"Create item: {response.status_code}")

def create_carts_and_add():
for _ in range(500):
cart_response = requests.post("http://localhost:8080/cart")
if cart_response.status_code == 201:
cart_id = cart_response.json()["id"]
item_id = faker.random_int(min=1, max=50)
response = requests.post(f"http://localhost:8080/cart/{cart_id}/add/{item_id}")
print(f"Add to cart: {response.status_code}")

with ThreadPoolExecutor() as executor:
for _ in range(5):
executor.submit(create_items)
executor.submit(create_carts_and_add)
Loading