diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index ffc36ad..d68807f 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -1,6 +1,7 @@ from aiogram import Bot from handlers.handler import Handler +# Message handlers from handlers.message_handlers.delete_command import DeleteCommand from handlers.message_handlers.deleted_posts_command import DeletedPostsCommand from handlers.message_handlers.edit_command import EditCommand @@ -13,6 +14,7 @@ from handlers.message_handlers.reply_to_user import ReplyToUserCommand from handlers.message_handlers.restore_command import RestoreCommand from handlers.message_handlers.settings_command import SettingsCommand from handlers.message_handlers.update_settings import UpdateSettingsCommand +# Middlewares from handlers.middlewares.media_group import MediaGroupMiddleware from handlers.middlewares.user import AdminMiddleware from neuroapi.types import BotSettings as BotSettingsType @@ -22,8 +24,9 @@ class AdminCommands(Handler): settings: BotSettingsType def __init__(self, bot: Bot) -> None: + """Initialize the group of admin commands""" super().__init__(bot) - self.router.message.middleware(AdminMiddleware()) + self.router.message.middleware(AdminMiddleware()) # Admin checking self.add_handlers([ InfoCommand, @@ -33,7 +36,7 @@ class AdminCommands(Handler): RestoreCommand, SettingsCommand, ]) - self.router.message.middleware(MediaGroupMiddleware()) + self.router.message.middleware(MediaGroupMiddleware()) # Media group handling self.add_handlers([ NewPostCommand, NewPostSoloCommand, diff --git a/handlers/filters/new_post.py b/handlers/filters/new_post.py index e9e1ef2..2ee1581 100644 --- a/handlers/filters/new_post.py +++ b/handlers/filters/new_post.py @@ -3,6 +3,7 @@ from aiogram.filters import Filter class NewPostFilter(Filter): + """Check if the message is in a media group of photos""" async def __call__(self, message: types.Message) -> bool: if message.media_group_id is None or message.content_type != 'photo': return False @@ -10,10 +11,12 @@ class NewPostFilter(Filter): class NewSoloPostFilter(Filter): + """Check if the message is /newpost command with photo""" async def __call__(self, message: types.Message) -> bool: return message.media_group_id is None and message.content_type == 'photo' and message.caption and message.caption.startswith('/newpost ') class ChangePosts(Filter): + """Change command filter""" async def __call__(self, message: types.Message) -> bool: return message.text and message.text.startswith("/change") and message.chat.type == 'private' diff --git a/handlers/filters/reply_to_user.py b/handlers/filters/reply_to_user.py index 9c88187..00a343b 100644 --- a/handlers/filters/reply_to_user.py +++ b/handlers/filters/reply_to_user.py @@ -3,6 +3,7 @@ from aiogram.filters import Filter class ReplyToUser(Filter): + """Replying to user filter""" async def __call__(self, message: types.Message) -> bool: if message.reply_to_message is None or message.chat.type != 'private': return False diff --git a/handlers/handler.py b/handlers/handler.py index caae808..7e90d2b 100644 --- a/handlers/handler.py +++ b/handlers/handler.py @@ -9,10 +9,27 @@ from .message_handlers.handler import MessageHandlerABC class NeuroApiRouter(Router): bot: Bot def __init__(self, *, name: str | None = None, bot: Bot) -> None: + """ + Initializes a new instance of the class with the provided name and bot. + + Args: + name (str, optional): The name of the instance. Defaults to None. + bot (Bot): The bot instance. + + Returns: + None + """ super().__init__(name=name) self.bot = bot def add_message_handler(self, callback: MessageHandlerABC, *args: Any): + """ + Add a message handler to the bot. + + :param callback: The message handler callback. + :param args: Additional arguments for the message handler. + :return: None + """ handler = callback(self.bot, *args) self.message.register(handler.handler, handler.filter) @@ -23,6 +40,15 @@ class Handler: router: NeuroApiRouter def __init__(self, bot: Bot) -> None: + """ + Initializes the class with the given bot instance. + + Args: + bot (Bot): The bot instance to be initialized with. + + Returns: + None + """ assert isinstance(bot, Bot) self.bot = bot self.router = NeuroApiRouter(bot=bot) @@ -31,6 +57,15 @@ class Handler: return self.router def add_handlers(self, handlers: List[MessageHandlerABC] | List[Tuple[MessageHandlerABC] | Optional[Tuple[Any, ...]]]): + """ + Add multiple message handlers to the router. + + Args: + handlers (List[MessageHandlerABC] | List[Tuple[MessageHandlerABC] | Optional[Tuple[Any, ...]]]): The list of message handlers to be added. + + Returns: + None + """ for handler in handlers: if isinstance(handler, tuple): args = handler[1:] if len(handler)>1 else [] diff --git a/handlers/message_handlers/delete_command.py b/handlers/message_handlers/delete_command.py index c045a62..1601e6f 100644 --- a/handlers/message_handlers/delete_command.py +++ b/handlers/message_handlers/delete_command.py @@ -7,6 +7,7 @@ from .handler import MessageHandlerABC class DeleteCommand(MessageHandlerABC): + """Command to delete posts""" filter = Command('delete') async def _command(self, message: Message): text = message.text.split() diff --git a/handlers/message_handlers/deleted_posts_command.py b/handlers/message_handlers/deleted_posts_command.py index a39d560..4ada148 100644 --- a/handlers/message_handlers/deleted_posts_command.py +++ b/handlers/message_handlers/deleted_posts_command.py @@ -7,6 +7,7 @@ from .handler import MessageHandlerABC class DeletedPostsCommand(MessageHandlerABC): + """Command to show deleted posts""" filter = Command('deleted') async def _command(self, message: Message): try: diff --git a/handlers/message_handlers/edit_command.py b/handlers/message_handlers/edit_command.py index 532fb3b..d09e4f0 100644 --- a/handlers/message_handlers/edit_command.py +++ b/handlers/message_handlers/edit_command.py @@ -7,6 +7,7 @@ from .handler import MessageHandlerABC class EditCommand(MessageHandlerABC): + """Command to edit posts""" filter = Command('edit') async def _command(self, message: types.Message): diff --git a/handlers/message_handlers/forward_message.py b/handlers/message_handlers/forward_message.py index da3a9b4..8a0c103 100644 --- a/handlers/message_handlers/forward_message.py +++ b/handlers/message_handlers/forward_message.py @@ -11,6 +11,7 @@ from .handler import MessageHandlerABC class ForwardMessageCommand(MessageHandlerABC): + """Command to forward messages from users to admins. Also checking if they're in the channel""" filter = F.chat.type == 'private' async def _command(self, message: types.Message): self.settings = BotSettings.get_instance() diff --git a/handlers/message_handlers/handler.py b/handlers/message_handlers/handler.py index 4a11fbb..ed0bced 100644 --- a/handlers/message_handlers/handler.py +++ b/handlers/message_handlers/handler.py @@ -6,6 +6,7 @@ from aiogram.filters import Filter class MessageHandlerABC(ABC): + """Base class for all message handlers""" bot: Bot def __init__(self, bot: Bot, *args: Any, **kwargs: Dict[str, Any]) -> None: @@ -14,13 +15,16 @@ class MessageHandlerABC(ABC): @abstractmethod def _command(self, *args, **kwargs): + """Handler for the command""" raise NotImplementedError @property def handler(self) -> Coroutine[None, None, None]: + """Command handler method""" return self._command @property @abstractmethod def filter(self) -> Filter: + """Filter for the command""" raise NotImplementedError \ No newline at end of file diff --git a/handlers/message_handlers/info_command.py b/handlers/message_handlers/info_command.py index 1d656c3..6f82943 100644 --- a/handlers/message_handlers/info_command.py +++ b/handlers/message_handlers/info_command.py @@ -10,6 +10,7 @@ from .handler import MessageHandlerABC class InfoCommand(MessageHandlerABC): + """Command to show info about posts""" filter = Command('info') async def _command(self, message: types.Message): diff --git a/handlers/message_handlers/newpost_command.py b/handlers/message_handlers/newpost_command.py index e691361..e2861e5 100644 --- a/handlers/message_handlers/newpost_command.py +++ b/handlers/message_handlers/newpost_command.py @@ -9,6 +9,7 @@ from .handler import MessageHandlerABC class NewPostCommand(MessageHandlerABC): + """Command to add new posts with media groups""" filter = NewPostFilter() async def _command(self, message: types.Message, album: List[types.Message]): sorted_album = sorted(album, key=lambda x: x.message_id) @@ -20,6 +21,7 @@ class NewPostCommand(MessageHandlerABC): class NewPostSoloCommand(MessageHandlerABC): + """Command to add new posts without media groups""" filter = NewSoloPostFilter() async def _command(self, message: types.Message): await neuroapi.image.add(str(message.from_user.id), message.photo[-1].file_id, message.has_media_spoiler, message.message_id, message.caption, None, message.caption_entities, message) diff --git a/handlers/message_handlers/post_command.py b/handlers/message_handlers/post_command.py index 30faf5c..f196eb6 100644 --- a/handlers/message_handlers/post_command.py +++ b/handlers/message_handlers/post_command.py @@ -9,6 +9,7 @@ from .handler import MessageHandlerABC class PostCommand(MessageHandlerABC): + """Command to post posts manually or by timer""" filter = Command('post') async def _command(self, message: types.Message | None = None): settings = neuroTypes.BotSettings.get_instance() diff --git a/handlers/message_handlers/preview_command.py b/handlers/message_handlers/preview_command.py index 805539d..c90e3c9 100644 --- a/handlers/message_handlers/preview_command.py +++ b/handlers/message_handlers/preview_command.py @@ -9,6 +9,7 @@ from .handler import MessageHandlerABC class PreviewCommand(MessageHandlerABC): + """Command to preview posts like it posted to channel""" filter = Command('preview') async def _command(self, message: Message): text = message.text.split() diff --git a/handlers/message_handlers/reply_to_user.py b/handlers/message_handlers/reply_to_user.py index 440c2ab..2afe677 100644 --- a/handlers/message_handlers/reply_to_user.py +++ b/handlers/message_handlers/reply_to_user.py @@ -6,6 +6,7 @@ from .handler import MessageHandlerABC class ReplyToUserCommand(MessageHandlerABC): + """Send reply to user from admins""" filter = ReplyToUser() async def _command(self, message: types.Message): if message.reply_to_message.forward_from is None: diff --git a/handlers/message_handlers/restore_command.py b/handlers/message_handlers/restore_command.py index 77a33b3..6a48187 100644 --- a/handlers/message_handlers/restore_command.py +++ b/handlers/message_handlers/restore_command.py @@ -7,6 +7,7 @@ from .handler import MessageHandlerABC class RestoreCommand(MessageHandlerABC): + """Command to restore deleted posts""" filter = Command('restore') async def _command(self, message: Message): try: diff --git a/handlers/message_handlers/settings_command.py b/handlers/message_handlers/settings_command.py index 9d49288..047b9f0 100644 --- a/handlers/message_handlers/settings_command.py +++ b/handlers/message_handlers/settings_command.py @@ -7,6 +7,7 @@ from .handler import MessageHandlerABC class SettingsCommand(MessageHandlerABC): + """Command to show active settings""" filter = Command('settings') async def _command(self, message: types.Message): self.settings = BotSettings.get_instance() diff --git a/handlers/message_handlers/start_command.py b/handlers/message_handlers/start_command.py index 72b0a57..3f79a79 100644 --- a/handlers/message_handlers/start_command.py +++ b/handlers/message_handlers/start_command.py @@ -5,6 +5,7 @@ from .handler import MessageHandlerABC class StartCommand(MessageHandlerABC): + """Command to get start message""" filter = CommandStart() async def _command(self, message: types.Message): diff --git a/handlers/message_handlers/update_settings.py b/handlers/message_handlers/update_settings.py index 0508b1f..ca6b160 100644 --- a/handlers/message_handlers/update_settings.py +++ b/handlers/message_handlers/update_settings.py @@ -14,21 +14,25 @@ from .handler import MessageHandlerABC class UpdateSettingsCommand(MessageHandlerABC): + """Command to update settings manually or by timer""" settings: BotSettings - post: Coroutine + post: Coroutine # async post command method to post posts to channel by timer filter = Command('update_settings') async def settings_and_schedule_checker(self): await self._auto_update_settings() async def _auto_update_settings(self): + """ + An asynchronous function that updates settings and schedules jobs. + """ self.settings = await neuroapi.bot_settings.get() self.scheduler.remove_all_jobs() - self.scheduler.add_job(self._auto_update_settings, 'interval', seconds=60) + self.scheduler.add_job(self._auto_update_settings, 'interval', seconds=60) # Auto updating settings # TODO: Сделать в бэке и в боте, чтоб дни тоже можно было в настройках хранить for i in self.settings.message_times: - self.scheduler.add_job(self.post, 'cron', day_of_week='mon-sun', hour=i.split(':')[0], minute=i.split(':')[1]) + self.scheduler.add_job(self.post, 'cron', day_of_week='mon-sun', hour=i.split(':')[0], minute=i.split(':')[1]) # Auto posting logging.debug(self.scheduler.get_jobs()) def __init__(self, bot: Bot, post_command: Coroutine, *args) -> None: @@ -41,6 +45,7 @@ class UpdateSettingsCommand(MessageHandlerABC): self.scheduler.start() async def _command(self, mes: types.Message): + """Clearing server cache and returning actual settings""" self.settings = await neuroapi.bot_settings.get_update() await mes.answer('Настройки обновлены') \ No newline at end of file diff --git a/handlers/middlewares/user.py b/handlers/middlewares/user.py index 9f10cee..54673dd 100644 --- a/handlers/middlewares/user.py +++ b/handlers/middlewares/user.py @@ -7,6 +7,7 @@ from neuroapi import neuroapi class AdminMiddleware(BaseMiddleware): + """Checking admin rights""" def __init__(self) -> None: pass diff --git a/handlers/states/change_post.py b/handlers/states/change_post.py index 4b57e68..436e6fc 100644 --- a/handlers/states/change_post.py +++ b/handlers/states/change_post.py @@ -1,5 +1,6 @@ from aiogram.fsm.state import State, StatesGroup +#TODO: Use states somewhere class ChangePost(StatesGroup): Text = State() diff --git a/handlers/user_commands.py b/handlers/user_commands.py index b5506b1..b5a4861 100644 --- a/handlers/user_commands.py +++ b/handlers/user_commands.py @@ -10,6 +10,7 @@ class UserCommands(Handler): settings: BotSettingsType def __init__(self, bot: Bot) -> None: + """Initialize the group of user commands""" super().__init__(bot) self.add_handlers([ diff --git a/main.py b/main.py index 2bb3c5a..d57999a 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,7 @@ from neuroapi.types import NeuroApiBot async def delay_bot()->None: + """Delay bot start function""" config = Config() if config.token is None or config.token == '': logging.warning('Delay bot needs token in environment') @@ -24,6 +25,7 @@ async def delay_bot()->None: await bot.start() async def proxy_bot()->None: + """Proxy bot start function""" config = Config() if config.proxy_token is None or config.proxy_token == '': logging.warning('Proxy bot needs token in environment') @@ -33,6 +35,7 @@ async def proxy_bot()->None: await bot.start() async def main() -> None: + """Checking connection to backend and starting bots""" for i in range(5): logging.warning(f'Checking connectivity to backend ({i+1}/5)...') try: @@ -53,6 +56,7 @@ async def main() -> None: if __name__ == '__main__': logging.basicConfig(level=Config().logging_lvl, stream=sys.stdout, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S') loop = asyncio.get_event_loop() + # Run program in uvloop if running on Linux if platform.system() == 'Windows': try: loop.run_until_complete(main()) diff --git a/neuroapi/__init__.py b/neuroapi/__init__.py index df4285b..a5bdf19 100644 --- a/neuroapi/__init__.py +++ b/neuroapi/__init__.py @@ -1 +1,2 @@ +"""Main neuroapi package""" from ._neuroapi import neuroapi diff --git a/neuroapi/_methods/admin.py b/neuroapi/_methods/admin.py index 826b675..92d003d 100644 --- a/neuroapi/_methods/admin.py +++ b/neuroapi/_methods/admin.py @@ -1,18 +1,31 @@ from aiohttp import ClientSession -from .api_method import ApiMethod - from neuroapi.types import Admin as AdminType +from .api_method import ApiMethod + class Admin(ApiMethod): - + """Class for admin methods""" async def get(self): + """ + Asynchronous function to retrieve data from the specified API endpoint and return a list of admins. + :return List[Admin] + """ async with ClientSession() as session: response = await session.get(self.api_url+'/admin/get') return [AdminType.from_dict(admin) for admin in await response.json()] async def is_admin(self, id: str): + """ + Asynchronous function to check if the user with the given ID is an admin. + + Args: + id (str): The ID of the user to be checked. + + Returns: + bool: True if the user is an admin, False otherwise. + """ async with ClientSession() as session: response = await session.get(self.api_url+f'/admin/is-admin/{id}') if await response.text() == 'false': diff --git a/neuroapi/_methods/api_method.py b/neuroapi/_methods/api_method.py index e3ae9e3..6a7102f 100644 --- a/neuroapi/_methods/api_method.py +++ b/neuroapi/_methods/api_method.py @@ -4,4 +4,5 @@ from ..config import GlobalConfig as Config class ApiMethod(BaseModel): + """Base class for API methods""" api_url: str = Field(Config().api_url) diff --git a/neuroapi/_methods/bot_settings.py b/neuroapi/_methods/bot_settings.py index d8b4eab..f2d71a3 100644 --- a/neuroapi/_methods/bot_settings.py +++ b/neuroapi/_methods/bot_settings.py @@ -6,13 +6,29 @@ from .api_method import ApiMethod class BotSettings(ApiMethod): + """Class for bot settings API methods""" async def get(self)-> BotSettingsType: + """ + Asynchronous function that retrieves bot settings from the API. + + Returns: + BotSettings: The bot settings retrieved from the API. + """ async with ClientSession() as session: response = await session.get(self.api_url+'/settings') settings = BotSettingsType.from_dict(await response.json()) return settings async def get_update(self) -> BotSettingsType: + """ + Asynchronously gets and returns the bot settings from the specified API URL. Clearing server cache. + + Parameters: + self: The instance of the class. + + Returns: + BotSettings: The bot settings obtained from the API. + """ async with ClientSession() as session: response = await session.get(self.api_url+'/settings/active') settings = BotSettingsType.from_dict(await response.json()) diff --git a/neuroapi/_methods/enums/__init__.py b/neuroapi/_methods/enums/__init__.py index 4ab1fb4..3c21048 100644 --- a/neuroapi/_methods/enums/__init__.py +++ b/neuroapi/_methods/enums/__init__.py @@ -1 +1,2 @@ +"""Enums package""" from .get_all import EGetAll diff --git a/neuroapi/_methods/enums/get_all.py b/neuroapi/_methods/enums/get_all.py index 9100757..238ea0c 100644 --- a/neuroapi/_methods/enums/get_all.py +++ b/neuroapi/_methods/enums/get_all.py @@ -1,7 +1,7 @@ from enum import Enum -class EGetAll(Enum): +class EGetAll(Enum): all = 'all' will_post = 'will-post' posted = 'posted' diff --git a/neuroapi/_methods/image.py b/neuroapi/_methods/image.py index 6a3ad71..0b79515 100644 --- a/neuroapi/_methods/image.py +++ b/neuroapi/_methods/image.py @@ -9,7 +9,24 @@ from .api_method import ApiMethod class Image(ApiMethod): + """Class for Image API methods""" async def add(self, from_id: str, file_id: str, has_spoiler: bool | None, message_id: int, text: str, media_group_id: str | None, message_entities: Optional[List[MessageEntity]], message: types.Message): + """ + An asynchronous function to add an image to post, along with its metadata, to a specific API endpoint. Also, creates a new post. + + Args: + from_id (str): The ID of the user who sent the image. + file_id (str): The ID of the file containing the image. + has_spoiler (bool | None): A boolean indicating whether the image has spoiler content. + message_id (int): The ID of the message containing the image. + text (str): The text associated with the image. + media_group_id (str | None): The ID of the media group containing the image, if applicable. + message_entities (Optional[List[MessageEntity]]): A list of message entities associated with the image. + message (types.Message): The message object associated with the image. + + Returns: + None + """ payload = {'from_user_id': from_id, 'file_id': file_id, 'has_spoiler': has_spoiler, 'message_id': message_id } if text != '': diff --git a/neuroapi/_methods/post.py b/neuroapi/_methods/post.py index f69621c..639eb4d 100644 --- a/neuroapi/_methods/post.py +++ b/neuroapi/_methods/post.py @@ -12,8 +12,20 @@ from .enums import EGetAll class Post(ApiMethod): - + """Class for Post API methods""" async def new(self, text: str, from_user_id: str, media_group_id: str = "None", message_entities: Optional[List[MessageEntity]] = None): + """ + Asynchronously creates a new post with the given text, from_user_id, media_group_id, and message_entities. + + Args: + text (str): The text of the post. + from_user_id (str): The ID of the user creating the post. + media_group_id (str, optional): The media group ID. Defaults to "None". + message_entities (List[MessageEntity], optional): List of message entities. Defaults to None. + + Returns: + Post: A new post created from the given data. + """ payload = {'text': text, 'from_user_id': from_user_id} if media_group_id != 'None': payload['media_group_id'] = media_group_id @@ -32,23 +44,46 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def __get_all(self, status: EGetAll): + """ + An asynchronous function to retrieve all items based on the given status using the provided API URL. + It takes a status parameter of type EGetAll. + The function returns the response obtained from the API call. + """ async with ClientSession() as session: response = await session.get(self.api_url+f'/post/get-all/{status.value}') return response async def get_all(self): + """ + Asynchronously retrieves all items and returns a list of Post objects. + """ result = await self.__get_all(EGetAll.all) return [neuroTypes.Post.from_dict(post) for post in await result.json()] async def get_will_post(self): + """ + Asynchronously retrieves and returns the will_post data from the API. + """ result = await self.__get_all(EGetAll.will_post) return [neuroTypes.Post.from_dict(post) for post in await result.json()] async def get_posted(self): + """ + Asynchronously gets all the posted items and returns a list of Post objects. + """ result = await self.__get_all(EGetAll.posted) return [neuroTypes.Post.from_dict(post) for post in await result.json()] async def get(self, post_id: str): + """ + An asynchronous function to retrieve a post by its ID from the API. + + Args: + post_id (str): The ID of the post to retrieve. + + Returns: + Post: The retrieved post object. + """ async with ClientSession() as session: response = await session.get(self.api_url+f'/post/get/{post_id}') data = await response.json() @@ -57,6 +92,15 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def get_by_order(self, post_order: str): + """ + Asynchronously gets a post by order from the API. + + Args: + post_order (str): The order of the post to retrieve. + + Returns: + Post: The post retrieved from the API. + """ async with ClientSession() as session: response = await session.get(self.api_url+f'/post/get-post-by-order/{post_order}') data = await response.json() @@ -65,6 +109,15 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def get_by_media_group_id(self, media_group_id: str): + """ + Asynchronous function to retrieve data by media group ID. + + Args: + media_group_id (str): The media group ID for retrieval. + + Returns: + neuroTypes.Post: The retrieved post data. + """ async with ClientSession() as session: response = await session.get(self.api_url+f'/post/get-by-media-group-id/{media_group_id}') data = await response.json() @@ -73,6 +126,16 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def edit_text(self, post_id: str, text: str): + """ + Asynchronously edits the text of a post. + + Args: + post_id (str): The ID of the post to edit. + text (str): The new text for the post. + + Returns: + Post: The edited post object. + """ response = requests.post( self.api_url+f"/post/edit/{post_id}", data={"text": text}) data = response.json() @@ -81,6 +144,20 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def edit_text_by_order_num(self, order: str, text: str, message_entities: Optional[List[MessageEntity]] = None): + """ + Asynchronously edits text by order number. + + Args: + order (str): The order number. + text (str): The new text. + message_entities (Optional[List[MessageEntity]], optional): A list of message entities. Defaults to None. + + Returns: + Post: The edited post. + + Raises: + Exception: If the response contains an error status code. + """ payload = {"text": text} if message_entities is not None: if message_entities is not None: @@ -98,6 +175,18 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def get_post_to_post(self): + """ + Retrieves a post from the API and returns it as a `neuroTypes.Post` object. + + Returns: + neuroTypes.Post: The retrieved post. + + Raises: + Exception: If the API returns a non-200 status code or if there is an error message in the response. + + Returns: + None: If the API returns a 404 status code. + """ response = requests.get(self.api_url+f"/post/post") data = response.json() if 'statusCode' in data: @@ -108,11 +197,29 @@ class Post(ApiMethod): return neuroTypes.Post.from_dict(data) async def delete_by_order(self, order: str): + """ + Asynchronously deletes a post by order. + + Args: + order (str): The order of the post to be deleted. + + Returns: + None + """ response = requests.delete(self.api_url+f"/post/delete-post-by-order/{order}") data = response.json() if 'statusCode' in data: raise Exception(data['message']) async def get_deleted_posts(self) -> List[neuroTypes.Post]: + """ + Asynchronously retrieves a list of deleted posts from the API. + + Parameters: + self: The instance of the class. + + Returns: + List[Post]: A list of Post objects representing the deleted posts. + """ async with ClientSession() as session: response = await session.get(self.api_url+f'/post/get-deleted') data = await response.json() @@ -121,6 +228,15 @@ class Post(ApiMethod): return [neuroTypes.Post.from_dict(post) for post in data] async def restore_post(self, order: str): + """ + Asynchronously restores a post using the given order string. + + Args: + order (str): The order string used to identify the post to be restored. + + Returns: + Post: A Post object representing the restored post. + """ async with ClientSession() as session: response = await session.put(self.api_url+f'/post/restore-post-by-order/{order}') data = await response.json() diff --git a/neuroapi/_methods/user.py b/neuroapi/_methods/user.py index 8cb55e9..0ed2784 100644 --- a/neuroapi/_methods/user.py +++ b/neuroapi/_methods/user.py @@ -4,7 +4,21 @@ from .api_method import ApiMethod class User(ApiMethod): + """User class for API Methods""" async def get(self, id: str, username: str): + """ + Asynchronous function to retrieve user information by ID and username. + + Args: + id (str): The user ID. + username (str): The username. + + Raises: + Exception: If the API request failing. + + Returns: + None + """ payload = {'id': id, 'username': username} async with ClientSession() as session: response = await session.post( diff --git a/neuroapi/_neuroapi.py b/neuroapi/_neuroapi.py index f22f373..1dacb83 100644 --- a/neuroapi/_neuroapi.py +++ b/neuroapi/_neuroapi.py @@ -6,6 +6,7 @@ from ._methods.user import User class neuroapi: + """Class with all neuroapi methods""" post = Post() admin = Admin() user = User() diff --git a/neuroapi/config.py b/neuroapi/config.py index f8be5d6..9575310 100644 --- a/neuroapi/config.py +++ b/neuroapi/config.py @@ -6,6 +6,7 @@ from pydantic_settings import BaseSettings class GlobalConfig(BaseSettings): + """Config class""" api_url: str = Field("http://localhost:3000", alias='API_URL') # Redis config @@ -39,8 +40,10 @@ class GlobalConfig(BaseSettings): @property def redis_url(self): + """Getter method to construct and return the redis URL using the provided redis password, host, port, and database number""" return f'redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}' class Config: + """Config file for pydantic settings""" env_file = '.env' \ No newline at end of file diff --git a/neuroapi/types/__init__.py b/neuroapi/types/__init__.py index 7e8f67d..be2fb25 100644 --- a/neuroapi/types/__init__.py +++ b/neuroapi/types/__init__.py @@ -1,3 +1,4 @@ +"""Types for neuroapi""" from ._admin import Admin from ._bot import NeuroApiBot from ._bot_settings import BotSettings diff --git a/neuroapi/types/_admin.py b/neuroapi/types/_admin.py index 955576a..fa7dd34 100644 --- a/neuroapi/types/_admin.py +++ b/neuroapi/types/_admin.py @@ -1,6 +1,7 @@ from ._api_model import ApiModel -class Admin(ApiModel): +class Admin(ApiModel): + """Represents an admin with fields user_id of type int and user_name of type str""" user_id: int user_name: str diff --git a/neuroapi/types/_api_model.py b/neuroapi/types/_api_model.py index 083aecb..00e5e13 100644 --- a/neuroapi/types/_api_model.py +++ b/neuroapi/types/_api_model.py @@ -6,7 +6,20 @@ from pydantic import BaseModel class ApiModel(BaseModel): @classmethod def from_dict(cls: 'ApiModel', obj: Dict[str, Any]) -> 'ApiModel': + """ + Create an instance of ApiModel from a dictionary object. + + Args: + cls: The class object. + obj: A dictionary containing attributes for the ApiModel. + + Returns: + ApiModel: An instance of the ApiModel class. + """ return cls(**obj) def to_dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Convert the object to a dictionary + """ return self.model_dump(**kwargs) \ No newline at end of file diff --git a/neuroapi/types/_bot.py b/neuroapi/types/_bot.py index 5e823b1..ca35de7 100644 --- a/neuroapi/types/_bot.py +++ b/neuroapi/types/_bot.py @@ -15,20 +15,59 @@ class NeuroApiBot: _instances = {} def __init__(self, token: str, storage: RedisStorage | None = None) -> None: + """ + Initializes the class with the provided token and optional RedisStorage object. + + Args: + token (str): The token for the bot. + storage (RedisStorage | None, optional): The RedisStorage object for storing data. Defaults to None. + + Returns: + None + """ token_data = Token(token=token) self.bot = Bot(token_data.token) self.dp = Dispatcher(storage=storage) def __new__(cls, token: str, storage: RedisStorage | None = None) -> 'NeuroApiBot': + """ + Create a new instance of NeuroApiBot, using the provided token and optional storage. Return bot if its created early with this token + + Args: + cls: The class itself. + token (str): The token for the instance. + storage (RedisStorage | None, optional): The optional storage for the instance. Defaults to None. + + Returns: + 'NeuroApiBot': The instance of NeuroApiBot. + """ token_data = Token(token=token) if token_data.token not in cls._instances: cls._instances[token_data.token] = super(NeuroApiBot, cls).__new__(cls) return cls._instances[token_data.token] def include_router(self, *routerClasses: Handler) -> None: + """ + Include the given router classes in the dispatcher. + + Parameters: + *routerClasses (Handler): The router classes to include. + + Returns: + None + """ for routerClass in routerClasses: assert issubclass(routerClass, Handler) self.dp.include_routers(routerClass(self.bot)()) async def start(self, skip_updates=True): + """ + Starts the bot by calling the `start_polling` method of the `dp` object. + + :param skip_updates: A boolean indicating whether to skip updates or not. Defaults to True. + :type skip_updates: bool + + :return: None + :rtype: None + """ await self.dp.start_polling(self.bot, skip_updates=skip_updates) \ No newline at end of file diff --git a/neuroapi/types/_bot_settings.py b/neuroapi/types/_bot_settings.py index 7faa1b5..276e927 100644 --- a/neuroapi/types/_bot_settings.py +++ b/neuroapi/types/_bot_settings.py @@ -8,10 +8,18 @@ from ._singleton import Singleton class BotSettings(ApiModel, Singleton): + """ + Bot settings model with UUID, message times, channel, and activity status. + """ uuid: UUID message_times: List[str] = Field([], alias='messageTimes') channel: str is_active: bool = Field(False, alias='isActive') def get_text(self): + """ + Method to get the text containing channel and message times. + + :return: string - the text containing channel and message times + """ return f"Канал: {self.channel}\nВремя: {', '.join(self.message_times)}" \ No newline at end of file diff --git a/neuroapi/types/_image.py b/neuroapi/types/_image.py index d6f9812..9f3fdfa 100644 --- a/neuroapi/types/_image.py +++ b/neuroapi/types/_image.py @@ -4,6 +4,14 @@ from ._api_model import ApiModel class Image(ApiModel): + """ + Represents an image with the following fields: + + - message_id: int + - file_id: str + - has_spoiler: bool + - post_uuid: UUID + """ message_id: int file_id: str has_spoiler: bool diff --git a/neuroapi/types/_post.py b/neuroapi/types/_post.py index 4029030..290c86b 100644 --- a/neuroapi/types/_post.py +++ b/neuroapi/types/_post.py @@ -11,6 +11,19 @@ from ._image import Image class Post(ApiModel): + """ + Represents a post with the following fields: + + - uuid: UUID + - posted: bool + - text: str + - media_group_id: int | str + - timestamp: datetime + - from_user_id: int + - images: Optional[List[Image]] = Field(None) + - message_entities: Optional[List[MessageEntity]] = Field(None) + + """ uuid: UUID posted: bool text: str @@ -23,6 +36,16 @@ class Post(ApiModel): @classmethod def from_dict(cls: 'Post', obj: Dict[str, Any]) -> 'Post': + """ + Create a Post object from a dictionary. + + Args: + cls: The class object. + obj: A dictionary containing post data. + + Returns: + Post: A Post object created from the dictionary data. + """ mes_ent = json.loads(obj.get('message_entities', '[]')) media_group_id_data = obj.get('media_group_id') media_group_id = media_group_id_data if media_group_id_data is not None else 'None' @@ -31,6 +54,11 @@ class Post(ApiModel): return cls(**obj) def to_dict(self) -> dict: + """ + Convert the object to a dictionary representation. + + :return: dict - A dictionary representation of the object. + """ obj = super().to_dict() obj['message_entities'] = json.dumps(obj['message_entities']) obj['media_group_id'] = str(obj['media_group_id']) diff --git a/neuroapi/types/_singleton.py b/neuroapi/types/_singleton.py index 9561b26..e8d7813 100644 --- a/neuroapi/types/_singleton.py +++ b/neuroapi/types/_singleton.py @@ -2,13 +2,32 @@ from typing import Self class Singleton: + """ + Returns the instance of the Singleton class. + + :return: Self - The instance of the Singleton class. + """ _instances = {} def __new__(cls, *args, **kwargs) -> Self: + """ + Create a new instance of the class if it doesn't exist, and return the existing instance if it does. + + Parameters: + cls: The class. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + Self: The instance of the class. + """ if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__new__(cls) return cls._instances[cls] @classmethod def get_instance(cls: Self): + """ + Return the instance belonging to the class. + """ return cls._instances[cls] \ No newline at end of file diff --git a/neuroapi/types/_user.py b/neuroapi/types/_user.py index 908f1fe..fbe4af5 100644 --- a/neuroapi/types/_user.py +++ b/neuroapi/types/_user.py @@ -7,6 +7,12 @@ from ._api_model import ApiModel class User(ApiModel): + """ + User model with fields: + - id: int + - username: str + - banned: bool | None + """ id: int username: str = Field(..., alias='user_name') banned: Optional[bool] = Field(None)