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

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

jobs:
test-hw5:
runs-on: ubuntu-latest

# Добавляем PostgreSQL как service container
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: hw4_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5


strategy:
matrix:
python-version: ["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 tests
working-directory: hw2/hw
env:
PYTHONPATH: ${{ github.workspace }}/hw2/hw
POSTGRES_PASSWORD: password
POSTGRES_HOST: localhost
POSTGRES_USER: postgres
POSTGRES_DB: hw4_db
run: |
pytest -vv --cov=shop_api/ --cov-fail-under=100 ./shop_api/tests
80 changes: 79 additions & 1 deletion hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
from typing import Any, Awaitable, Callable
from math import factorial
import json


def fibonacci(n: int):
res = 0
if n > 0:
f = [0] * (n+1)
f[1] = 1
for i in range(2, n+1):
f[i] = f[i-1] + f[i-2]
res = f[n]
return res


async def send_response(response_status_code: int, response_content_type: bytes, response_body: bytes,
send: Callable[[dict[str, Any]], Awaitable[None]]):
await send(
{
"type": "http.response.start",
"status": response_status_code,
"headers": [
[b"content-type", response_content_type],
],
}
)
await send({"type": "http.response.body", "body": response_body})


async def application(
Expand All @@ -12,7 +39,58 @@ 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
elif scope['type'] == 'http':
path = scope["path"]
if path == "/" or path == "/not_found":
await send_response(404, b"text/plain", b"not found", send)
elif path == "/factorial":
try:
n = int(scope["query_string"].decode().replace("n=", ""))
if n < 0:
await send_response(400, b"text/plain", b"Invalid value for n, must be non-negative", send)
else:
result = factorial(n)
response_body = bytes(json.dumps({"result": result}), encoding="utf-8")
await send_response(200, b"application/json", response_body, send)
except ValueError:
await send_response(422, b"text/plain", b"unprocessible entity", send)
elif path.startswith("/fibonacci/"):
try:
n = int(path.split("/")[2])
if n < 0:
await send_response(400, b"text/plain", b"Invalid value for n, must be non-negative", send)
else:
result = fibonacci(n)
response_body = bytes(json.dumps({"result": result}), encoding="utf-8")
await send_response(200, b"application/json", response_body, send)
except ValueError:
await send_response(422, b"text/plain", b"unprocessible entity", send)
elif path == "/mean":
body = b''
event = await receive()
if event['type'] == 'http.request':
body = event.get('body', b'')
inp_arr = json.loads(body.decode())
if inp_arr == None:
await send_response(422, b"text/plain", b"unprocessible entity", send)
else:
if len(inp_arr) == 0:
await send_response(400, b"text/plain", b"Invalid value for body, must be non-empty array of floats", send)
else:
result = sum(inp_arr) / len(inp_arr)
response_body = bytes(json.dumps({"result": result}), encoding="utf-8")
await send_response(200, b"application/json", response_body, send)
else:
await send_response(422, b"text/plain", b"unprocessible entity", send)


if __name__ == "__main__":
import uvicorn
Expand Down
20 changes: 20 additions & 0 deletions hw2/hw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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

COPY . ./


RUN pip install -r requirements.txt

FROM base as local

CMD ["uvicorn", "shop_api.main:app", "--port", "8080", "--host", "0.0.0.0"]
56 changes: 56 additions & 0 deletions hw2/hw/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
version: "3"

services:

local:
build:
context: .
dockerfile: ./Dockerfile
target: local
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST: postgres
POSTGRES_DB: hw4_db
ports:
- 8080:8080
depends_on:
postgres:
condition: service_healthy

grafana:
image: grafana/grafana:latest
ports:
- 3000:3000
restart: always

prometheus:
image: prom/prometheus
volumes:
- ./settings/prometheus/:/etc/prometheus/
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
- "--web.console.templates=/usr/share/prometheus/consoles"
ports:
- 9090:9090
restart: always

postgres:
image: postgres:15
container_name: hw4_postgres
environment:
POSTGRES_DB: hw4_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
8 changes: 7 additions & 1 deletion hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0

prometheus-fastapi-instrumentator
# Зависимости для тестирования
pytest>=7.4.0
pytest-asyncio>=0.21.0
httpx>=0.27.2
Faker>=37.8.0
sqlalchemy==2.0.25
psycopg2-binary==2.9.9
python-dotenv==1.0.0
pytest-cov
pytest-mock
pytest
10 changes: 10 additions & 0 deletions hw2/hw/settings/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global:
scrape_interval: 10s
evaluation_interval: 10s

scrape_configs:
- job_name: demo-service-local
metrics_path: /metrics
static_configs:
- targets:
- local:8080
Empty file added hw2/hw/shop_api/api/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions hw2/hw/shop_api/api/cart/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .contracts import CartResponse
from .routes import cart_router

__all__ = [
"CartResponse",
"cart_router",
]
48 changes: 48 additions & 0 deletions hw2/hw/shop_api/api/cart/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from pydantic import BaseModel
from typing import Iterable

from shop_api.store.models import (
Cart,
CartItem
)

class CartMapper:
"""Маппер для преобразования между CartResponse и Cart (ORM)"""

@staticmethod
def to_domain(orm_cart: Cart) -> CartResponse:
"""Преобразование ORM модели в доменную"""
return CartResponse(
id=orm_cart.id,
items=[CartItemMapper.to_domain(item) for item in orm_cart.items],
price=orm_cart.price
)


class CartItemMapper:
"""Маппер для преобразования между CartItemResponse и CartItem (ORM)"""

@staticmethod
def to_domain(orm_cart_item: CartItem) -> CartItemResponse:
"""Преобразование ORM модели в доменную"""
return CartItemResponse(
id=orm_cart_item.id,
name=orm_cart_item.item.name,
quantity=orm_cart_item.quantity,
available=not orm_cart_item.item.deleted
)


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


class CartResponse(BaseModel):
id: int
items: Iterable[CartItemResponse]
price: float
Loading
Loading