Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
36142d0
Implement ASGI application with fibonacci, factorial, and mean endpoints
sidoine-projects Sep 20, 2025
bad24c0
Merge remote-tracking branch 'upstream/main'
sidoine-projects Oct 3, 2025
4faea1f
hw2 shop api
sidoine-projects Oct 4, 2025
c7868f9
Merge remote-tracking branch 'upstream/main'
sidoine-projects Oct 7, 2025
df445c1
Managing Docker tasks and monitoring Prometheus and Grafana
sidoine-projects Oct 8, 2025
a407ac5
Merge remote-tracking branch 'upstream/main' into HW4
sidoine-projects Oct 20, 2025
946ab01
HW4-Shop API with PostgreSQL and demonstration of transactions
sidoine-projects Oct 20, 2025
c5eeae8
Merge remote-tracking branch 'upstream/main'
sidoine-projects Oct 20, 2025
8124f2e
Merge branch 'HW4'
sidoine-projects Oct 20, 2025
db7047a
HW5
sidoine-projects Oct 20, 2025
767bdbc
HW5
sidoine-projects Oct 20, 2025
82834c8
HW5
sidoine-projects Oct 20, 2025
74fade0
HW5
sidoine-projects Oct 20, 2025
4d91d7f
test: Trigger HW5 CI pipeline
sidoine-projects Oct 20, 2025
4ab5787
test: Trigger HW5 CI pipeline
sidoine-projects Oct 20, 2025
a28a294
feat: Add HW5 CI/CD workflow
sidoine-projects Oct 20, 2025
5974481
test: Trigger HW5 CI
sidoine-projects Oct 20, 2025
ec2423d
test: Add debug workflow
sidoine-projects Oct 20, 2025
9f429ab
fix: Correct folder name from HW5 to hw5
sidoine-projects Oct 20, 2025
ee31bc1
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
25ed863
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
65c73cd
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
8236535
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
3cb5406
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
0142219
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
376ba10
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
06c3b39
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
ecf0a83
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
1deaad9
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
2331454
test: Trigger HW5 CI with corrected paths
sidoine-projects Oct 20, 2025
f17b7bb
test: Trigger HW5 CI with corrected paths1
sidoine-projects Oct 20, 2025
8074aab
test: Trigger HW5 CI with corrected paths2
sidoine-projects Oct 20, 2025
f2a302b
test: Trigger HW5 CI with corrected paths2
sidoine-projects Oct 20, 2025
b23551b
test: Trigger HW5 CI with corrected paths2
sidoine-projects Oct 20, 2025
257f37b
test: Trigger HW5 CI with corrected paths3
sidoine-projects Oct 20, 2025
14866a1
test: Trigger HW5 CI with corrected paths3
sidoine-projects Oct 21, 2025
9235718
test: Trigger HW5 CI with corrected paths4
sidoine-projects Oct 21, 2025
8fa0bec
test: Trigger HW5 CI with corrected paths5
sidoine-projects Oct 21, 2025
d975864
test: Trigger HW5 CI with corrected paths5
sidoine-projects Oct 21, 2025
495711a
test: Trigger HW5 CI with corrected paths5
sidoine-projects Oct 21, 2025
2f83617
test: Trigger HW5 CI with corrected paths6
sidoine-projects Oct 21, 2025
e47a2ae
test: Trigger HW5 CI with corrected paths6
sidoine-projects Oct 21, 2025
0351638
test: Trigger HW5 CI with corrected paths6
sidoine-projects Oct 21, 2025
fed7e7f
test: Trigger HW5 CI with corrected paths6
sidoine-projects Oct 21, 2025
54722a1
test: Trigger HW5 CI with corrected paths6
sidoine-projects Oct 21, 2025
0be5193
test: Trigger HW5 CI with corrected paths7
sidoine-projects Oct 21, 2025
022f093
test: Trigger HW5 CI with corrected paths8
sidoine-projects Oct 21, 2025
42193c1
test: Trigger HW5 CI with corrected paths7
sidoine-projects Oct 21, 2025
94f9638
test: Trigger HW5 CI with corrected paths7
sidoine-projects Oct 21, 2025
a049bad
test: Trigger HW5 CI with corrected paths7
sidoine-projects Oct 21, 2025
109afd7
test: Trigger HW5 CI with corrected paths10
sidoine-projects Oct 22, 2025
4593977
test: Trigger HW50
sidoine-projects Oct 23, 2025
81835ba
test: Trigger HW5
sidoine-projects Oct 23, 2025
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
69 changes: 69 additions & 0 deletions .github/workflows/h5-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: "HW5 Tests"

on:
push:
branches: [main]
pull_request:
branches: [main]

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

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: test_shop_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

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

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

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

- name: Wait for PostgreSQL
run: |
for i in {1..30}; do
pg_isready -h localhost -p 5432 -U postgres && echo "Postgres ready" && break
echo "Waiting for PostgreSQL..."
sleep 2
done

- name: Run tests with coverage
working-directory: hw5/hw
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/test_shop_db
run: |
export PYTHONPATH=$PYTHONPATH:$(pwd)
pytest --cov=shop_api --cov-report=xml --cov-report=term-missing -v

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

- name: Check coverage threshold
working-directory: hw5/hw
run: |
python -m coverage report --fail-under=95
193 changes: 191 additions & 2 deletions hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import math
import json
from http import HTTPStatus
from typing import Any, Awaitable, Callable
from urllib.parse import parse_qs


async def application(
Expand All @@ -12,8 +16,193 @@ async def application(
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь
if scope["type"] != "http":
await send_error(send, HTTPStatus.BAD_REQUEST, "Only HTTP supported")
return

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

# Обработка различных маршрутов
if method == "GET" and path == "/factorial":
await handle_factorial(scope, receive, send)
elif method == "GET" and path.startswith("/fibonacci/"):
await handle_fibonacci(scope, receive, send)
elif method in ["GET", "POST"] and path == "/mean":
await handle_mean(scope, receive, send) # Supporte GET et POST
else:
await send_error(send, HTTPStatus.NOT_FOUND, "Endpoint not found")


async def handle_factorial(scope, receive, send):
"""Обработка GET /factorial?n=5"""
query_params = parse_query_string(scope["query_string"])
n_str = query_params.get("n", [""])[0]

# Cas limite: paramètre manquant
if not n_str:
await send_error(send, HTTPStatus.UNPROCESSABLE_ENTITY, "Missing parameter 'n'")
return

try:
n = int(n_str)
except ValueError:
await send_error(send, HTTPStatus.UNPROCESSABLE_ENTITY, "Parameter 'n' must be an integer")
return

# Cas limite: nombre négatif
if n < 0:
await send_error(send, HTTPStatus.BAD_REQUEST, "Invalid value for n, must be non-negative")
return

# Cas limite: nombre trop grand
if n > 1000:
await send_error(send, HTTPStatus.BAD_REQUEST, "Value too large for n")
return

result = math.factorial(n)
await send_json_response(send, {"result": result})


async def handle_fibonacci(scope, receive, send):
"""Обработка GET /fibonacci/5"""
path_parts = scope["path"].split("/")

# Cas limite: format d'URL incorrect
if len(path_parts) < 3:
await send_error(send, HTTPStatus.UNPROCESSABLE_ENTITY, "Invalid URL format")
return

try:
n = int(path_parts[2])
except ValueError:
await send_error(send, HTTPStatus.UNPROCESSABLE_ENTITY, "Path parameter must be an integer")
return

# Cas limite: nombre négatif
if n < 0:
await send_error(send, HTTPStatus.BAD_REQUEST, "Invalid value for n, must be non-negative")
return

# Cas limite: nombre trop grand
if n > 1000:
await send_error(send, HTTPStatus.BAD_REQUEST, "Value too large for n")
return

# Расчет Fibonacci
if n == 0:
result = 0
elif n == 1:
result = 1
else:
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
result = b

await send_json_response(send, {"result": result})


async def handle_mean(scope, receive, send):
"""Обработка /mean - supporte GET et POST avec différents formats"""
# Essayer de lire le JSON depuis le body (pour les tests GET avec JSON)
body = await read_request_body(receive)

has_json_body = False
numbers = []

if body.strip():
try:
data = json.loads(body)
if isinstance(data, list):
numbers = [float(x) for x in data]
has_json_body = True
except (ValueError, TypeError, json.JSONDecodeError):
pass # Ignorer et essayer avec query parameter

# Si pas de JSON body valide, essayer avec query parameter
if not has_json_body:
query_params = parse_query_string(scope["query_string"])
numbers_str = query_params.get("numbers", [""])[0]

if not numbers_str:
await send_error(send, HTTPStatus.UNPROCESSABLE_ENTITY, "Missing parameter 'numbers' or JSON body")
return

try:
numbers = [float(x.strip()) for x in numbers_str.split(",") if x.strip()]
except ValueError:
await send_error(send, HTTPStatus.UNPROCESSABLE_ENTITY, "Parameter 'numbers' must be comma-separated floats")
return

# Validation commune
if len(numbers) == 0:
await send_error(send, HTTPStatus.BAD_REQUEST, "Numbers array must not be empty")
return

# Vérifier les valeurs non numériques
if any(math.isnan(x) or math.isinf(x) for x in numbers):
await send_error(send, HTTPStatus.BAD_REQUEST, "Numbers must be finite values")
return

# Calcul du résultat
result = sum(numbers) / len(numbers)
await send_json_response(send, {"result": result})


async def read_request_body(receive) -> str:
"""Чтение полного тела запроса"""
body = b""
more_body = True
while more_body:
message = await receive()
body += message.get("body", b"")
more_body = message.get("more_body", False)
return body.decode()


async def send_json_response(send, data: dict):
"""Отправка JSON ответа"""
response_body = json.dumps(data).encode()

await send({
"type": "http.response.start",
"status": HTTPStatus.OK.value,
"headers": [
[b"content-type", b"application/json"],
],
})

await send({
"type": "http.response.body",
"body": response_body,
})


async def send_error(send, status: HTTPStatus, message: str):
"""Отправка HTTP ошибки"""
error_data = {"error": message}
response_body = json.dumps(error_data).encode()

await send({
"type": "http.response.start",
"status": status.value,
"headers": [
[b"content-type", b"application/json"],
],
})

await send({
"type": "http.response.body",
"body": response_body,
})


def parse_query_string(query_string: bytes) -> dict:
"""Парсинг query string параметров"""
return parse_qs(query_string.decode())


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)
Empty file added hw2/hw/python
Empty file.
38 changes: 38 additions & 0 deletions hw2/hw/shop_api/cart/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations
from typing import Any

from pydantic import BaseModel

from shop_api.cart.store.models import CartEntity, CartInfo, CartItemInfo


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


# class CartItemRequest(BaseModel):
# items: list[CartItemInfo]
# price: float

# def as_cart_info(self) -> CartInfo:
# items = [CartItemInfo(**item.dict()) for item in self.items]
# # price =
# return CartInfo(items=items, price=self.price)



class CartResponse(BaseModel):
id: int
items: list[CartItemInfo]
price: float

@staticmethod
def from_entity(entity: CartEntity) -> CartResponse:
return CartResponse(
id=entity.id,
items=entity.info.items,
price=entity.info.price,
)
72 changes: 72 additions & 0 deletions hw2/hw/shop_api/cart/routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from http import HTTPStatus
from typing import Annotated

from fastapi import APIRouter, HTTPException, Query, Response
from pydantic import NonNegativeFloat, NonNegativeInt, PositiveInt


from shop_api.cart import store
from shop_api.cart.contracts import CartResponse

import shop_api
import shop_api.item
import shop_api.item.store


router = APIRouter(prefix="/cart")


@router.post("/", status_code=HTTPStatus.CREATED)
async def post_cart(response: Response) -> CartResponse:
entity = store.create()

response.headers["location"] = f"/cart/{entity.id}"
return CartResponse.from_entity(entity)


@router.get(
"/{id}",
responses={
HTTPStatus.OK: {
"description": "Successfully returned requested cart",
},
HTTPStatus.NOT_FOUND: {
"description": "Failed to return requested cart as one was not found",
},
},
)
async def get_cart_by_id(id: int):
entity = store.get_one(id)

if not entity:
raise HTTPException(
HTTPStatus.NOT_FOUND,
f"Request resource /cart/{id} was not found",
)

return CartResponse.from_entity(entity)


@router.get("/")
async def get_cart_list(
offset: Annotated[NonNegativeInt, Query()] = 0,
limit: Annotated[PositiveInt, Query()] = 10,
min_price: Annotated[NonNegativeFloat, Query()] | None = None,
max_price: Annotated[NonNegativeFloat, Query()] | None = None,
min_quantity: Annotated[NonNegativeFloat, Query()] | None = None,
max_quantity: Annotated[NonNegativeFloat, Query()] | None = None,
):
return [
CartResponse.from_entity(e)
for e in store.get_many(
offset, limit, min_price, max_price, min_quantity, max_quantity
)
]

@router.post("/{cart_id}/add/{item_id}")
async def add_to_cart(cart_id: int, item_id: int):
item_entity = shop_api.item.store.get_one(item_id)

entity = store.add(cart_id, item_entity)

return CartResponse.from_entity(entity)
Loading