-
Notifications
You must be signed in to change notification settings - Fork 1
Lab 7 #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lab 7 #69
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"] | ||
| 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) |
| 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) |
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine | ||
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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)): | ||||||||||
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| await session.commit() | ||||||||||
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| 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"]) | ||||||||||
|
||||||||||
| BookingORM.id == data["booking_id"] and BookingORM.user_name == data["user_name"]) | |
| (BookingORM.id == data["booking_id"]) & (BookingORM.user_name == data["user_name"])) |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
| if results.rowcount() > 0: | |
| return f"success deleted {results.rowcount()} rows" | |
| if results.rowcount > 0: | |
| return f"success deleted {results.rowcount} rows" |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
| 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
|
||||||||||
| except ValueError: | |
| return {"error": "unprocessable entity"} | |
| except ValueError as exc: | |
| return {"error": f"validation failed: {exc}"} |
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
| async def get_rooms(user: DeleteBookingReserve = Depends()) -> str: | |
| async def delete_booking(user: DeleteBookingReserve = Depends()) -> str: |
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
| 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} | ||
zezOtik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @router.get("/get_rooms", | ||
| summary="Получение комнат") | ||
| async def get_rooms() -> List[RoomGet]: | ||
| rooms = await RoomWorkflow.get_rooms() | ||
| return rooms | ||
| 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 |
| 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 |
Uh oh!
There was an error while loading. Please reload this page.