Skip to content
Merged
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
10 changes: 10 additions & 0 deletions students_folder/zemliakov_alexey/lab7/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.11-slim

WORKDIR /app

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

COPY . .

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file.
24 changes: 24 additions & 0 deletions students_folder/zemliakov_alexey/lab7/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from fastapi import FastAPI

from app.router_room import router as room_router

from app.router_booking import router as booking_router

from app.operations.database_migrations import create_table, delete_table

from contextlib import asynccontextmanager


@asynccontextmanager
async def lifespan(app: FastAPI):
await create_table()
print("Таблица готова")
yield
await delete_table()
print("Таблица очищена")


app = FastAPI(lifespan=lifespan)

app.include_router(room_router)
app.include_router(booking_router)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import DateTime, BIGINT, ForeignKey
from datetime import datetime
from zoneinfo import ZoneInfo


def utc_now() -> datetime:
return datetime.now(ZoneInfo("UTC"))


class TableModel(DeclarativeBase):
pass


class RoomORM(TableModel):
__tablename__ = "room"
__table_args__ = {"schema": "public"}

id: Mapped[int] = mapped_column(BIGINT, primary_key=True, autoincrement=True)
name: Mapped[str]
capacity: Mapped[int]
location: Mapped[str]


class BookingORM(TableModel):
__tablename__ = "booking"
__table_args__ = {"schema": "public"}
id: Mapped[int] = mapped_column(BIGINT, primary_key=True, autoincrement=True)
room_id: Mapped[int] = mapped_column(BIGINT, ForeignKey("public.room.id"))
user_name: Mapped[str]
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
end_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
83 changes: 83 additions & 0 deletions students_folder/zemliakov_alexey/lab7/app/models/store_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from pydantic import BaseModel, AfterValidator, Field, ConfigDict, RootModel, model_validator
from typing_extensions import Self
from typing import Annotated, Literal, List
from datetime import datetime
from zoneinfo import ZoneInfo


# 2025-04-05T12:00:00Z
def validate_aware(dt: datetime) -> datetime:
if dt.tzinfo is None:
raise ValueError("datetime must include timezone information")
return dt


AwareDatetime = Annotated[datetime, AfterValidator(validate_aware)]


def default_execution_time():
return datetime.now(ZoneInfo("UTC"))


class Room(BaseModel):
name: str
capacity: int
location: str


class RoomAdd(Room):
pass


class RoomGet(Room):
id: int

model_config = ConfigDict(from_attributes=True)


class RoomListGet(RootModel[List[RoomGet]]):
model_config = ConfigDict(from_attributes=True)


class RoomFreeGet(BaseModel):
id: int
start_time: AwareDatetime = Field(default_factory=default_execution_time)
end_time: AwareDatetime = Field(default_factory=default_execution_time)

model_config = ConfigDict(from_attributes=True)


class Booking(BaseModel):
room_id: int
user_name: str
start_time: AwareDatetime = Field(default_factory=default_execution_time)
end_time: AwareDatetime = Field(default_factory=default_execution_time)

@model_validator(mode="after")
def check_date(self) -> Self:
if self.start_time >= self.end_time:
raise ValueError("start date must be before end")
return self


class BookingReserve(Booking):
pass


class DeleteBookingReserve(BaseModel):
booking_id: int
user_name: str

model_config = ConfigDict(from_attributes=True)


class UserBookingGet(BaseModel):
user_name: str

model_config = ConfigDict(from_attributes=True)


class UserBookingGetRes(Booking):
id: int

model_config = ConfigDict(from_attributes=True)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.models.database_models import TableModel

engine = create_async_engine("postgresql+asyncpg://postgres:postgres@postgres_container:5432/postgres")


async def create_table():
async with engine.begin() as conn:
await conn.run_sync(TableModel.metadata.create_all)


async def delete_table():
async with engine.begin() as conn:
await conn.run_sync(TableModel.metadata.drop_all)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from app.models.database_models import RoomORM, BookingORM
from app.models.store_models import RoomAdd, RoomFreeGet, RoomListGet, BookingReserve, DeleteBookingReserve, \
UserBookingGet, UserBookingGetRes
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy import select, delete
from typing import List

engine = create_async_engine("postgresql+asyncpg://postgres:postgres@postgres_container:5432/postgres")
new_session = async_sessionmaker(engine, expire_on_commit=False)


class RoomWorkflow:
@classmethod
async def add_room(cls, room: RoomAdd) -> int:
async with new_session() as session:
data = room.model_dump()
new_room = RoomORM(**data)
session.add(new_room)
await session.flush()
await session.commit()
return new_room.id

@classmethod
async def get_rooms(cls) -> RoomListGet:
async with new_session() as session:
query = select(RoomORM)
result = await session.execute(query)
room_models = result.scalars().all()

rooms = RoomListGet.model_validate(room_models)
return rooms.root


class BookingWorkflow:
@classmethod
async def reserve_room(cls, booking: BookingReserve) -> int:
async with new_session() as session:
data = booking.model_dump()
if not await cls.check_available(RoomFreeGet(id=booking.room_id, start_time=booking.start_time,
end_time=booking.end_time)):
await session.commit()
raise KeyError
new_booking = BookingORM(**data)
session.add(new_booking)
await session.flush()
await session.commit()
return new_booking.id

@classmethod
async def delete_reserve_room(cls, booking: DeleteBookingReserve) -> str:
async with new_session() as session:
data = booking.model_dump()
query = delete(BookingORM).where(
BookingORM.id == data["booking_id"] and BookingORM.user_name == data["user_name"])
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQL WHERE clause uses Python's 'and' operator instead of SQLAlchemy's bitwise '&' operator for combining conditions. This will not produce the correct SQL query. In SQLAlchemy, you must use '&' to combine WHERE conditions or use multiple .where() calls.

Suggested change
BookingORM.id == data["booking_id"] and BookingORM.user_name == data["user_name"])
(BookingORM.id == data["booking_id"]) & (BookingORM.user_name == data["user_name"]))

Copilot uses AI. Check for mistakes.
results = await session.execute(query)
await session.commit()
if results.rowcount() > 0:
return f"success deleted {results.rowcount()} rows"
Comment on lines +57 to +58
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rowcount attribute is being called as a method with parentheses 'rowcount()', but rowcount is a property, not a method. It should be accessed as 'results.rowcount' without parentheses.

Suggested change
if results.rowcount() > 0:
return f"success deleted {results.rowcount()} rows"
if results.rowcount > 0:
return f"success deleted {results.rowcount} rows"

Copilot uses AI. Check for mistakes.

return "nothing for delete"

@classmethod
async def get_users_booking(cls, user: UserBookingGet) -> List[UserBookingGetRes]:
async with new_session() as session:
data = user.model_dump()
query = select(BookingORM).where(BookingORM.user_name == data["user_name"])
result = await session.execute(query)
await session.commit()
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'await session.commit()' call is unnecessary for a read-only SELECT query. Commits are only needed when modifying data. This adds unnecessary overhead.

Copilot uses AI. Check for mistakes.
res = []
for row in result.scalars().all():
res.append(UserBookingGetRes(id=row.id, room_id=row.room_id, user_name=row.user_name,
start_time=row.start_time, end_time=row.end_time))

return res

@classmethod
async def check_available(cls, free_room: RoomFreeGet) -> bool:
async with new_session() as session:
data = free_room.model_dump()
query = select(BookingORM).where(BookingORM.room_id == data["id"])
result = await session.execute(query)
room_models = result.scalars().all()
for row in room_models:
if check_intersection_time(data["start_time"], data["end_time"], row.start_time, row.end_time):
return False
await session.commit()
return True


def check_intersection_time(t1_start, t1_end, t2_start, t2_end) -> bool:
return (t1_start <= t2_start < t1_end) or (t2_start <= t1_start < t2_end)
45 changes: 45 additions & 0 deletions students_folder/zemliakov_alexey/lab7/app/router_booking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from fastapi import APIRouter, Depends

from app.operations.database_operations import BookingWorkflow
from app.models.store_models import BookingReserve, DeleteBookingReserve, UserBookingGet, UserBookingGetRes, RoomFreeGet

from typing import List

router = APIRouter(
prefix="/booking",
tags=["бронирования"],
)


@router.post(
"/add_booking",
summary="Добавление бронирования")
async def add_booking(booking: BookingReserve = Depends()) -> dict:
try:
id = await BookingWorkflow.reserve_room(booking)
except ValueError:
return {"error": "unprocessable entity"}
Comment on lines +20 to +21
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "unprocessable entity" is generic and doesn't provide helpful information about what went wrong with the booking. Consider providing more specific details about the validation error.

Suggested change
except ValueError:
return {"error": "unprocessable entity"}
except ValueError as exc:
return {"error": f"validation failed: {exc}"}

Copilot uses AI. Check for mistakes.
except KeyError:
return {"error": "this room reserved for this time"}
return {"id": id}


@router.get("/get_users_booking",
summary="Получение бронирований пользователя")
async def get_rooms(user: UserBookingGet = Depends()) -> List[UserBookingGetRes]:
rooms = await BookingWorkflow.get_users_booking(user)
return rooms


@router.delete("/delete_booking",
summary="Удаление бронирования пользователя")
async def get_rooms(user: DeleteBookingReserve = Depends()) -> str:
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is named 'get_rooms' but it actually deletes a booking. This is very misleading and inconsistent with what the function does. The function name should reflect that it's deleting a booking.

Suggested change
async def get_rooms(user: DeleteBookingReserve = Depends()) -> str:
async def delete_booking(user: DeleteBookingReserve = Depends()) -> str:

Copilot uses AI. Check for mistakes.
res = await BookingWorkflow.delete_reserve_room(user)
return res


@router.get("/check_room",
summary="Проверка доступности бронирования комнаты на время")
async def get_rooms(room: RoomFreeGet = Depends()) -> bool:
res = await BookingWorkflow.check_available(room)
return res
25 changes: 25 additions & 0 deletions students_folder/zemliakov_alexey/lab7/app/router_room.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from fastapi import APIRouter, Depends

from app.operations.database_operations import RoomWorkflow
from app.models.store_models import RoomAdd, RoomGet

from typing import List


router = APIRouter(
prefix="/rooms",
tags=["комнаты"],
)

@router.post(
"/add_room",
summary="Добавление новой комнаты")
async def add_room(room: RoomAdd = Depends()) -> dict:
id = await RoomWorkflow.add_room(room)
return {"id": id}

@router.get("/get_rooms",
summary="Получение комнат")
async def get_rooms() -> List[RoomGet]:
rooms = await RoomWorkflow.get_rooms()
return rooms
45 changes: 45 additions & 0 deletions students_folder/zemliakov_alexey/lab7/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: '3.9'

services:
postgres:
image: postgres:13
container_name: postgres_container
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
volumes:
- ./pgdata:/var/lib/postgresql/data/pgdata
command: >
postgres -c max_connections=1000
-c shared_buffers=256MB
-c effective_cache_size=768MB
-c maintenance_work_mem=64MB
-c checkpoint_completion_target=0.7
-c wal_buffers=16MB
-c default_statistics_target=100
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 3s
timeout: 5s
retries: 10
start_period: 10s
restart: unless-stopped

app:
build: .
container_name: fastapi_app
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
depends_on:
postgres:
condition: service_healthy
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
volumes:
- .:/app
restart: unless-stopped
5 changes: 5 additions & 0 deletions students_folder/zemliakov_alexey/lab7/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fastapi==0.110.2
pydantic==2.7.0
SQLAlchemy==2.0.30
uvicorn==0.29.0
asyncpg==0.29.0
Binary file not shown.
Binary file not shown.