From ab14009afdd6e3080207e2250011adfd05a5869f Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 1 Sep 2026 11:56:15 +0300 Subject: [PATCH] Add docstrings --- app/main.py | 17 +++++++++++++++++ app/mongo_worker.py | 16 ++++++++++++++++ app/rabbit_worker.py | 9 +++++++++ app/tg_bot.py | 4 ++++ 4 files changed, 46 insertions(+) diff --git a/app/main.py b/app/main.py index 5cc7c20..7dc82c1 100644 --- a/app/main.py +++ b/app/main.py @@ -30,6 +30,7 @@ _rabbit_worker: Optional[RabbitWorker] = None def get_rabbit_worker() -> RabbitWorker: + """Returns a singleton instance of the RabbitWorker.""" global _rabbit_worker if _rabbit_worker is None: _rabbit_worker = RabbitWorker() @@ -38,6 +39,7 @@ def get_rabbit_worker() -> RabbitWorker: @asynccontextmanager async def lifespan(app: FastAPI): + """Manages application startup and shutdown events, such as database index creation and cleanup.""" await mongo_worker.create_indexes() if DEV_MODE: logger.warning("⚠️ DEV_MODE is enabled — docs are exposed and Telegram initData auth is DISABLED") @@ -92,6 +94,7 @@ MODERATION_SECRET = os.getenv("MODERATION_SECRET", "change-me-in-production") async def verify_moderation_secret( x_moderation_secret: str = Header(..., alias="X-Moderation-Secret"), ) -> str: + """Verifies the moderation secret provided in the request headers.""" if not secrets.compare_digest(x_moderation_secret, MODERATION_SECRET): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -103,6 +106,7 @@ async def verify_moderation_secret( async def check_user( user_id: int, mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Checks if a user exists in the database by their user_id.""" result = await mongo.check_user(user_id) return BaseResponse(result=result) @@ -111,6 +115,7 @@ async def get_user( user_id: int, response: Response, mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Retrieves a user's details by their user_id.""" if await mongo.check_user(user_id): result = await mongo.get_user(user_id) return BaseResponse(result=result) @@ -124,6 +129,7 @@ async def add_user( response: Response, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Registers a new user in the database if they do not already exist.""" user_id = auth_user_id if auth_user_id is not None else new_user.user_id if user_id is None: raise HTTPException(status_code=422, detail="user_id is required") @@ -145,6 +151,7 @@ async def get_card( card_id: int, response: Response, mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Retrieves a card's details by its card_id.""" card = await mongo.get_card(card_id) if card: return BaseResponse(result=card) @@ -158,6 +165,7 @@ async def get_random_cards( user_id: Optional[int] = None, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Fetches a set of random active cards that the user has not yet visited.""" resolved_user_id = auth_user_id if auth_user_id is not None else user_id if resolved_user_id is None: raise HTTPException(status_code=422, detail="user_id is required") @@ -182,6 +190,7 @@ async def add_card( response: Response, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Adds a new card to the database and sends it for moderation.""" author_id = auth_user_id if auth_user_id is not None else new_card.author_id if author_id is None: raise HTTPException(status_code=422, detail="author_id is required") @@ -201,6 +210,7 @@ async def card_accept( card_id: int, response: Response, mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Accepts a card after moderation, making it active and visible to users.""" result = await mongo.accept_card(card_id) if result.error: response.status_code = status.HTTP_404_NOT_FOUND @@ -211,6 +221,7 @@ async def card_reject( card_id: int, response: Response, mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Rejects a card during moderation and removes it from the database.""" result = await mongo.reject_card(card_id) if result.error: response.status_code = status.HTTP_404_NOT_FOUND @@ -222,6 +233,7 @@ async def select_choice( response: Response, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Records a user's choice (A or B) for a specific card.""" user_id = auth_user_id if auth_user_id is not None else choice_data.user_id if user_id is None: raise HTTPException(status_code=422, detail="user_id is required") @@ -250,6 +262,7 @@ async def like_card( response: Response, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Adds a like to a specific card from a user.""" user_id = auth_user_id if auth_user_id is not None else like_data.user_id if user_id is None: raise HTTPException(status_code=422, detail="user_id is required") @@ -265,6 +278,7 @@ async def dislike_card( response: Response, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Adds a dislike to a specific card from a user.""" user_id = auth_user_id if auth_user_id is not None else dislike_data.user_id if user_id is None: raise HTTPException(status_code=422, detail="user_id is required") @@ -281,6 +295,7 @@ async def comment( response: Response, auth_user_id: Optional[int] = Depends(get_current_user_id), mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Adds a comment to a specific card after passing basic moderation.""" author_id = auth_user_id if auth_user_id is not None else comment_info.author_id if author_id is None: raise HTTPException(status_code=422, detail="author_id is required") @@ -302,6 +317,7 @@ async def get_comments( card_id: int, response: Response, mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse: + """Retrieves all comments for a specific card.""" result = await mongo.get_comments(card_id) if result.error: response.status_code = status.HTTP_404_NOT_FOUND @@ -310,6 +326,7 @@ async def get_comments( async def main(): + """Starts the Uvicorn web server running the FastAPI application.""" config = uvicorn.Config("main:app", host="0.0.0.0", port=5000, log_level="warning") server = uvicorn.Server(config) await server.serve() diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 12352ae..12f2121 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -13,7 +13,9 @@ from logger import logger class MongoWorker: + """Worker class for handling all MongoDB database operations.""" def __init__(self): + """Initializes the MongoDB connection and sets up collection references.""" load_dotenv() self.client = motor.motor_asyncio.AsyncIOMotorClient( host=os.getenv('MONGO_HOST'), @@ -46,11 +48,13 @@ class MongoWorker: async def check_user(self, user_id: int) -> bool: + """Checks if a user exists in the database by their user_id.""" document = await self.users_data.find_one({"user_id": user_id}, {"_id": 1}) return document is not None async def add_user( self, user_id: int, username: str, first_name: str, last_name: str, photo_url: str) -> User: + """Creates a new user record in the database.""" new_user = User( user_id=user_id, username=username, @@ -64,6 +68,7 @@ class MongoWorker: return new_user async def get_user(self, user_id: int) -> User: + """Retrieves a user's details from the database.""" document = await self.users_data.find_one({"user_id": user_id}) return User.model_validate(document) @@ -80,6 +85,7 @@ class MongoWorker: async def get_visited_cards(self, user_id: int) -> BaseResponse: + """Retrieves the set of card IDs that a user has already visited.""" document = await self.visited_data.find_one({"user_id": user_id}) if not document: if await self.check_user(user_id): @@ -88,6 +94,7 @@ class MongoWorker: return BaseResponse(result=Visited.model_validate(document)) async def update_visited_cards(self, user_id: int, visited_card_id: int) -> Visited: + """Adds a specific card ID to the user's set of visited cards.""" updated = await self.visited_data.find_one_and_update( {"user_id": user_id}, {"$addToSet": {"cards_visited": visited_card_id}}, @@ -131,6 +138,7 @@ class MongoWorker: async def get_card(self, card_id: int) -> Optional[Card]: + """Retrieves a card's details from the database by its card_id.""" document = await self.game_data.find_one({"card_id": card_id}) if document: return Card.model_validate(document) @@ -153,11 +161,13 @@ class MongoWorker: return None def filter_cards(self, random_cards: list[Card], cards_visited: set) -> tuple[list[Card], list[int]]: + """Filters a list of random cards to exclude those already visited by the user.""" filtered_cards = [card for card in random_cards if card.card_id not in cards_visited] filtered_cards_id = [card.card_id for card in filtered_cards] return filtered_cards, filtered_cards_id async def add_card_by_api(self, choice_A: str, choice_B: str, author_id: int) -> Card: + """Creates a new card in the database with data received from the API.""" new_card = Card( card_id=await self.get_and_update_counter(counter_name="card"), choice_A=choice_A, @@ -170,6 +180,7 @@ class MongoWorker: return new_card async def add_card_by_base_model(self, new_card: Card) -> Optional[Card]: + """Inserts a Card model directly into the database.""" new_card.card_id = await self.get_and_update_counter(counter_name="card") try: await self.game_data.insert_one(new_card.model_dump()) @@ -204,6 +215,7 @@ class MongoWorker: return BaseResponse(result=f"Card {card_id} rejected and deleted") async def select_choice(self, card_id: int, choice: str) -> BaseResponse: + """Increments the vote count for the selected choice (A or B) and total votes on a card.""" if choice == "A": count_field = "count_choice_A" elif choice == "B": @@ -221,6 +233,7 @@ class MongoWorker: async def like_card(self, card_id: int, user_id: int) -> BaseResponse: + """Atomically adds a like to a card and records the user's like action.""" if not await self.check_user(user_id): return BaseResponse(result="User doesn't exist", error=True) @@ -256,6 +269,7 @@ class MongoWorker: return BaseResponse(result=True, error=False) async def dislike_card(self, card_id: int, user_id: int) -> BaseResponse: + """Atomically adds a dislike to a card and records the user's dislike action.""" if not await self.check_user(user_id): return BaseResponse(result="User doesn't exist", error=True) @@ -289,6 +303,7 @@ class MongoWorker: async def add_comment(self, user_id: int, card_id: int, comment_text: str) -> BaseResponse: + """Adds a new comment to a card and links it to the user.""" if not await self.check_user(user_id): return BaseResponse(result="User doesn't exist", error=True) if not await self.get_card(card_id): @@ -315,6 +330,7 @@ class MongoWorker: return BaseResponse(result=new_comment) async def get_comments(self, card_id: int) -> BaseResponse: + """Retrieves all comments associated with a specific card_id.""" if not await self.get_card(card_id): return BaseResponse(result="Card doesn't exist", error=True) comments = await self.comments_data.find({"card_id": card_id}).sort("creation_date", -1).to_list(length=None) diff --git a/app/rabbit_worker.py b/app/rabbit_worker.py index 2c91475..38189b1 100644 --- a/app/rabbit_worker.py +++ b/app/rabbit_worker.py @@ -12,7 +12,9 @@ from logger import logger class RabbitWorker: + """Handles RabbitMQ connections and message publishing/consuming for moderation.""" def __init__(self): + """Initializes the RabbitWorker with connection credentials from environment variables.""" load_dotenv() self.url = ( f"amqp://{os.getenv('RABBIT_USER')}:{os.getenv('RABBIT_PASS')}" @@ -21,6 +23,7 @@ class RabbitWorker: logger.info("RabbitWorker connection established.") async def send_to_moderation(self, card: Card) -> None: + """Publishes a card to the 'moderation' RabbitMQ queue.""" logger.debug("Preparing to send card {} to moderation queue...", card.card_id) connection = await aio_pika.connect_robust(self.url) async with connection: @@ -40,6 +43,12 @@ class RabbitWorker: self, callback: Callable[[Card], Awaitable[None]], ) -> None: + """ + Consumes messages from the 'moderation' queue and processes them using the provided callback. + + Args: + callback: An async function that takes a Card object and processes it. + """ connection = await aio_pika.connect_robust(self.url) async with connection: channel = await connection.channel() diff --git a/app/tg_bot.py b/app/tg_bot.py index 1d8580a..b68505e 100644 --- a/app/tg_bot.py +++ b/app/tg_bot.py @@ -117,6 +117,7 @@ async def call_moderation_api(action: str, card_id: int) -> dict: # ── Callback button handlers ─────────────────────────────── @dp.callback_query(F.data.startswith("accept:")) async def on_accept(callback: CallbackQuery) -> None: + """Handles the 'Accept' inline button click for a card.""" if not callback.data or not isinstance(callback.message, Message): return @@ -138,6 +139,7 @@ async def on_accept(callback: CallbackQuery) -> None: @dp.callback_query(F.data.startswith("reject:")) async def on_reject(callback: CallbackQuery) -> None: + """Handles the 'Reject' inline button click for a card.""" if not callback.data or not isinstance(callback.message, Message): return @@ -162,6 +164,7 @@ _rabbit_task: asyncio.Task | None = None @dp.startup() async def on_startup() -> None: + """Starts the RabbitMQ consumer task when the bot starts.""" global _rabbit_task _rabbit_task = asyncio.create_task( rabbit.consume_moderation(send_card_to_admin) @@ -171,6 +174,7 @@ async def on_startup() -> None: @dp.shutdown() async def on_shutdown() -> None: + """Cancels the RabbitMQ consumer task when the bot shuts down.""" if _rabbit_task: _rabbit_task.cancel() try: