Skip to content
Open

hw_1 #133

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: 119 additions & 10 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,128 @@
import math
import statistics
import json
from typing import Any, Awaitable, Callable
from urllib.parse import parse_qs
from http import HTTPStatus


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 __name__ == "__main__":
# --- lifespan поддержка для тест-клиента ---
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
return

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

path = scope.get('path', '/')
method = scope.get('method', 'GET').upper()
query_string = scope.get('query_string', b'').decode()
params = parse_qs(query_string)

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

# --- поддерживаем только GET ---
if method != 'GET':
await send_response(HTTPStatus.NOT_FOUND, {'detail': 'Not Found'})
return

# --- factorial ---
if path == '/factorial':
if 'n' not in params or params['n'][0] == '':
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'Missing n'})
return
try:
n = int(params['n'][0])
except Exception:
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'n must be int'})
return

if n < 0 or n > 100:
await send_response(HTTPStatus.BAD_REQUEST, {'detail': 'n must be between 0 and 100'})
return

result = math.factorial(n)
await send_response(HTTPStatus.OK, {'result': result})
return

# --- fibonacci ---
elif path.startswith('/fibonacci'):
parts = path.split('/')
if len(parts) != 3 or parts[2] == '':
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'Invalid path'})
return
try:
n = int(parts[2])
except Exception:
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'n must be int'})
return

if n < 0 or n > 100:
await send_response(HTTPStatus.BAD_REQUEST, {'detail': 'n must be between 0 and 100'})
return

a, b = 0, 1
seq = []
for _ in range(n):
seq.append(a)
a, b = b, a + b
await send_response(HTTPStatus.OK, {'result': seq})
return

# --- mean ---
elif path == '/mean':
event = await receive()
body = b''
if event.get('type') == 'http.request':
body = event.get('body', b'') or b''

if not body:
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'Missing JSON body'})
return

try:
data = json.loads(body)
except Exception:
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'Invalid JSON'})
return

if not isinstance(data, list):
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'Body must be a list'})
return

if len(data) == 0:
await send_response(HTTPStatus.BAD_REQUEST, {'detail': 'Empty list'})
return

try:
numbers = [float(x) for x in data]
result = statistics.mean(numbers)
await send_response(HTTPStatus.OK, {'result': result})
return
except Exception:
await send_response(HTTPStatus.UNPROCESSABLE_ENTITY, {'detail': 'Invalid numbers'})
return

# --- not found ---
await send_response(HTTPStatus.NOT_FOUND, {'detail': 'Not Found'})


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)
1 change: 1 addition & 0 deletions hw1/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ httpx>=0.27.2
async-asgi-testclient>=1.4.11



203 changes: 201 additions & 2 deletions hw2/hw/shop_api/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,202 @@
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, confloat, conint, PositiveInt
from typing import Dict, List, Optional

app = FastAPI(title="Shop API")
app = FastAPI()

# ---------- MODELS ----------

class ItemBase(BaseModel):
name: str
price: confloat(ge=0.0)

class Config:
extra = 'forbid' # запрет лишних полей во всех моделях


class ItemCreate(ItemBase):
pass


class ItemPatch(BaseModel):
name: Optional[str] = None
price: Optional[confloat(ge=0.0)] = None

class Config:
extra = 'forbid' # PATCH должен возвращать 422 при любых неожиданных полях


class Item(ItemBase):
id: int
deleted: bool = False


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


class CartOut(BaseModel):
id: int
items: List[CartItemOut]
price: float


# ---------- IN-MEMORY "БАЗА" ----------

_next_item_id = 1
_items: Dict[int, Item] = {}

_next_cart_id = 1
_carts: Dict[int, Dict[int, int]] = {}


# ---------- ITEM ENDPOINTS ----------

@app.post('/item', response_model=Item, status_code=201)
def create_item(item_in: ItemCreate):
global _next_item_id
item = Item(id=_next_item_id, name=item_in.name, price=item_in.price, deleted=False)
_items[_next_item_id] = item
_next_item_id += 1
return item


@app.get('/item/{id}', response_model=Item)
def get_item(id: int):
item = _items.get(id)
if item is None or item.deleted:
raise HTTPException(status_code=404)
return item


@app.get('/item', response_model=List[Item])
def list_items(
offset: conint(ge=0) = 0,
limit: PositiveInt = 10,
min_price: Optional[confloat(ge=0.0)] = None,
max_price: Optional[confloat(ge=0.0)] = None,
show_deleted: bool = False
):
items = [
item for item in _items.values()
if (show_deleted or not item.deleted)
and (min_price is None or item.price >= min_price)
and (max_price is None or item.price <= max_price)
]
return sorted(items, key=lambda x: x.id)[offset:offset + limit]


@app.put('/item/{id}', response_model=Item)
def replace_item(id: int, item_in: ItemCreate):
if id not in _items:
raise HTTPException(status_code=404)
old_item = _items[id]
new_item = Item(id=id, name=item_in.name, price=item_in.price, deleted=old_item.deleted)
_items[id] = new_item
return new_item


@app.patch('/item/{id}', response_model=Item)
def patch_item(id: int, patch: ItemPatch):
item = _items.get(id)
if item is None:
raise HTTPException(status_code=404)
if item.deleted:
return JSONResponse(status_code=304, content=item.model_dump())

data = patch.dict(exclude_unset=True)
if not data:
return item

if 'name' in data:
item.name = data['name']
if 'price' in data:
item.price = data['price']

_items[id] = item
return item


@app.delete('/item/{id}')
def delete_item(id: int):
item = _items.get(id)
if item is None:
return {'ok': True}
item.deleted = True
_items[id] = item
return {'ok': True}


# ---------- CART ENDPOINTS ----------

@app.post('/cart')
def create_cart():
global _next_cart_id
cid = _next_cart_id
_next_cart_id += 1
_carts[cid] = {}
response = JSONResponse(status_code=201, content={'id': cid})
response.headers['Location'] = f'/cart/{cid}'
return response


@app.post('/cart/{cart_id}/add/{item_id}')
def add_item_to_cart(cart_id: int, item_id: int):
if cart_id not in _carts:
raise HTTPException(status_code=404)
if item_id not in _items:
raise HTTPException(status_code=404)

item_map = _carts[cart_id]
item_map[item_id] = item_map.get(item_id, 0) + 1
return {'ok': True}


def _cart_to_response(cart_id: int) -> CartOut:
item_map = _carts.get(cart_id, {})
items_out = []
total_price = 0.0

for iid, qty in item_map.items():
item = _items.get(iid)
available = item is not None and not item.deleted
name = item.name if item else ''
if available:
total_price += item.price * qty
items_out.append(CartItemOut(id=iid, name=name, quantity=qty, available=available))

return CartOut(id=cart_id, items=items_out, price=total_price)


@app.get('/cart/{id}', response_model=CartOut)
def get_cart(id: int):
if id not in _carts:
raise HTTPException(status_code=404)
return _cart_to_response(id)


@app.get('/cart', response_model=List[CartOut])
def list_carts(
offset: conint(ge=0) = 0,
limit: PositiveInt = 10,
min_price: Optional[confloat(ge=0.0)] = None,
max_price: Optional[confloat(ge=0.0)] = None,
min_quantity: Optional[conint(ge=0)] = None,
max_quantity: Optional[conint(ge=0)] = None,
):
carts_out = []
for cid in sorted(_carts.keys()):
cart_resp = _cart_to_response(cid)
total_quantity = sum(i.quantity for i in cart_resp.items)
if (
(min_price is None or cart_resp.price >= min_price)
and (max_price is None or cart_resp.price <= max_price)
and (min_quantity is None or total_quantity >= min_quantity)
and (max_quantity is None or total_quantity <= max_quantity)
):
carts_out.append(cart_resp)
return carts_out[offset:offset + limit]
12 changes: 12 additions & 0 deletions hw3/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file added hw3/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions hw3/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
version: '3.8'

services:
app:
build: .
container_name: shop_api
ports:
- "8000:8000"
networks:
- monitoring

prometheus:
image: prom/prometheus
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
networks:
- monitoring

grafana:
image: grafana/grafana
container_name: grafana
ports:
- "3000:3000"
networks:
- monitoring

networks:
monitoring:
driver: bridge
Binary file added hw3/grafana/dashboard_process_cpu.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hw3/grafana/dashboard_requests_total.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading