{"id":347336,"date":"2023-03-27T15:00:48","date_gmt":"2023-03-27T15:00:48","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=347336"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=347336","title":{"rendered":"<span>\u041a\u0430\u043a \u044f \u0441\u0434\u0435\u043b\u0430\u043b Telegram-\u0431\u043e\u0442\u0430 \u0434\u043b\u044f \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u043e\u0432 \u0420\u0422\u0421\u0423<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u041d\u0430\u0447\u0430\u043b\u043e<\/h2>\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u042f \u0443\u0447\u0443\u0441\u044c \u0432 \u0420\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e-\u0422\u0430\u0434\u0436\u0438\u043a\u0441\u043a\u043e\u043c \u0421\u043b\u0430\u0432\u044f\u043d\u0441\u043a\u043e\u043c \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u0435 (\u043d\u0430 \u043f\u0435\u0440\u0432\u043e\u043c \u043a\u0443\u0440\u0441\u0435), \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0443 \u043d\u0430\u0441 \u0432 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u0430\u043a \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u043e-\u0431\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430.<\/p>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043d\u0430\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0431\u0430\u043b\u043b\u043e\u0432 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435, \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u044b\u043b\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043e \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u043e\u043c.<\/p>\n<p>\u041e\u043d\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0434\u043b\u044f Android.<\/p>\n<figure class=\"float full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/e82\/b5f\/66b\/e82b5f66b5ba1f4c95e3a6522cac9e7a.jpg\" width=\"1080\" height=\"2400\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e82\/b5f\/66b\/e82b5f66b5ba1f4c95e3a6522cac9e7a.jpg\" data-blurred=\"true\"\/><\/figure>\n<\/p>\n<p>\u041e\u0434\u043d\u0430\u043a\u043e, \u044f \u043d\u0435\u0434\u0430\u0432\u043d\u043e \u043f\u0435\u0440\u0435\u0448\u0451\u043b \u043d\u0430 iOS-\u0441\u0438\u0441\u0442\u0435\u043c\u0443, \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u043a \u043c\u043e\u0435\u043c\u0443 \u0443\u0434\u0438\u0432\u043b\u0435\u043d\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0442\u0430\u043c \u043d\u0435 \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c.<\/p>\n<figure class=\"float full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7cc\/c4a\/a6e\/7ccc4aa6e9af3559dd19a8c6b3e07628.PNG\" alt=\"\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u043f\u043e\u0438\u0441\u043a\u0430 \u0432 App Store\" title=\"\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u043f\u043e\u0438\u0441\u043a\u0430 \u0432 App Store\" width=\"1179\" height=\"2556\"\/><\/p>\n<div><figcaption>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u043f\u043e\u0438\u0441\u043a\u0430 \u0432 App Store<\/figcaption><\/div>\n<\/figure>\n<p>\u041d\u0443 \u0438 \u0442\u0443\u0442, \u044f \u043f\u043e\u0434\u0443\u043c\u0430\u043b \u0447\u0442\u043e \u043d\u0430\u0434\u043e \u0431\u044b \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0447\u0442\u043e-\u0442\u043e \u0442\u0438\u043f\u0430 Telegram-\u0431\u043e\u0442\u0430 \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0443\u0441\u043f\u0435\u0432\u0430\u0435\u043c\u043e\u0441\u0442\u0438, \u0432 \u043a\u043e\u043d\u0446\u0435 \u043a\u043e\u043d\u0446\u043e\u0432 \u044d\u0442\u043e \u043c\u043d\u043e\u0433\u0438\u043c \u0434\u043e\u043b\u0436\u043d\u043e <strong>\u043f\u043e\u043c\u043e\u0447\u044c.<\/strong><\/p>\n<p>\u0414\u0430 \u0438 \u0432\u043e\u043e\u0431\u0449\u0435, Telegram \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432\u0435\u0437\u0434\u0435, \u044d\u0442\u043e \u043c\u043e\u044f \u043b\u044e\u0431\u0438\u043c\u0430\u044f \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u0434\u043b\u044f \u043e\u0431\u043c\u0435\u043d\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c\u0438.<\/p>\n<h2>\u041f\u043e\u0438\u0441\u043a endpoint&#8217;\u043e\u0432&#8230;<\/h2>\n<p>\u0420\u0430\u0437\u0443\u043c\u0435\u0435\u0442\u0441\u044f, \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442 \u0441\u0434\u0435\u043b\u0430\u043b \u043d\u0435\u043a\u0438\u0439 API \u0434\u043b\u044f \u0441\u0432\u043e\u0435\u0433\u043e Android-\u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430, \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u043e\u0432 \u043d\u0435 \u044f, \u043c\u043e\u0439 \u0434\u0440\u0443\u0433, \u0434\u0435\u043a\u043e\u043c\u043f\u0438\u043b\u0438\u0440\u043e\u0432\u0430\u043b APK \u0444\u0430\u0439\u043b \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u043b \u0435\u0433\u043e \u043c\u043d\u0435, \u043f\u043e\u0437\u0436\u0435 \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0432 &#171;\u0432\u044b\u0445\u043b\u043e\u043f&#187; \u044f \u043d\u0430\u0448\u0435\u043b \u0447\u0435\u0442\u044b\u0440\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u043c\u043d\u0435 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430<\/p>\n<p>\u0412 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u044b \u0434\u043b\u044f<\/p>\n<ul>\n<li>\n<p>\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u0440\u043e\u0444\u0438\u043b\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430 <\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u0435\u043c\u0435\u0441\u0442\u0440\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0431 \u0443\u0441\u043f\u0435\u0432\u0430\u0435\u043c\u043e\u0441\u0442\u0438 \u043f\u043e \u0432\u0441\u0435\u043c \u0434\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0430\u043c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0441\u0435\u043c\u0435\u0441\u0442\u0440\u0430.<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0443 \u0430 \u0434\u0430\u043b\u044c\u0448\u0435, \u0434\u0435\u043b\u043e \u0437\u0430 \u043c\u0430\u043b\u044b\u043c, \u043d\u0430\u0434\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e API \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435.<\/p>\n<h2>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Poetry, \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043e\u0431\u0451\u0440\u0442\u043a\u0438 \u043f\u043e\u0434 API<\/h2>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u043f\u0440\u043e\u0435\u043a\u0442<\/p>\n<pre><code class=\"bash\">poetry init <\/code><\/pre>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/eeb\/586\/550\/eeb5865502d0dd02d239c822ac5130ba.png\" width=\"367\" height=\"359\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/eeb\/586\/550\/eeb5865502d0dd02d239c822ac5130ba.png\"\/><\/figure>\n<p>\u042f \u0441\u0440\u0430\u0437\u0443 \u0441\u043e\u0437\u0434\u0430\u043b \u043f\u043e\u0447\u0442\u0438 \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0440\u043e\u0432\u043d\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043f\u0430\u043a\u0435\u0442\u043d\u0438\u043a <code>rtsu <\/code>\u0433\u0434\u0435 \u0438 \u0431\u0443\u0434\u0435\u0442 \u043b\u0435\u0436\u0430\u0442\u044c \u043d\u0430\u0448\u0430 \u043e\u0431\u0451\u0440\u0442\u043a\u0430.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430 <code>api.py<\/code><\/p>\n<pre><code class=\"python\">from aiohttp import ClientSession, ContentTypeError, client_exceptions from cashews import cache from typing import Optional, Union, Dict, TypeVar, Type, List, Self  from pydantic import BaseModel, parse_obj_as  from .exceptions import NotAuthorizedError, RtsuContentTypeError, ServerError, AuthError from .schemas import AuthSchema, Profile, Subject, AcademicYear  RTSU_API_BASE_URL = \"https:\/\/mobile.rtsu.tj\/api\/v1\" P = TypeVar(\"P\", bound=BaseModel)   class RTSUApi:     \"\"\"     This class provides for you functionality of RTSU public API     \"\"\"      def __init__(self, token: Optional[str] = None):         \"\"\"         Initializes `self`         :param token: A rtsu-api token (optional)         \"\"\"          self._api_token = token         self._http_client = ClientSession()      def set_token(self, token: str):         \"\"\"         Setups token         :param token: A token         :return:         \"\"\"         self._api_token = token      async def _make_request(             self,             method: str,             url_part: str,             response_model: Type[Union[List[BaseModel], BaseModel]],             json: Optional[Dict[str, Union[str, int]]] = None,             params: Optional[Dict[str, str]] = None,             auth_required: bool = False,     ) -> Union[P, List[P]]:         \"\"\"         Makes call to RTSU API         :param url_part: Part of RTSU-API url, example - \/auth         :param json: A json for sending         :param params: URI parameters for sending         :return: Response object         \"\"\"          if not json:             json = {}          if not params:             params = {}          headers = {}          if auth_required:             if not self._api_token:                 raise NotAuthorizedError(\"Not authorized, use `.auth` method.\")              headers['token'] = self._api_token          try:             response = await self._http_client.request(                 method,                 f\"{RTSU_API_BASE_URL}\/{url_part}\",                 json=json,                 params=params,                 headers=headers,                 ssl=False,             )         except (client_exceptions.ClientConnectionError, client_exceptions.ClientConnectorError) as e:             raise ServerError(f\"Connection error, details: {e}\")          if response.status != 200:             details = await response.text()             raise ServerError(                 f\"Server returned {response.status}, details: {details}\"             )          try:             deserialized_data = await response.json()         except ContentTypeError as e:             raise RtsuContentTypeError(                 e.message,             )          return parse_obj_as(response_model, deserialized_data)      async def auth(self, login: str, password: str) -> AuthSchema:         \"\"\"         Authenticates user         :param login: A login of user         :param password: A password of user         :return: RTSU token on success         \"\"\"          try:             response: AuthSchema = await self._make_request(                 \"POST\",                 \"auth\",                 AuthSchema,                 params={                     \"login\": login,                     \"password\": password,                 }             )         except ServerError as e:             raise AuthError(                 f\"Auth error, check login and password, message from server: {e.message}\"             )          self._api_token = response.token          return response      @cache.soft(ttl=\"24h\", soft_ttl=\"1m\")     async def get_profile(self) -> Profile:         \"\"\"         Returns profile of RTSU student         :return: `Profile`-response         \"\"\"          return await self._make_request(             \"GET\",             \"student\/profile\",             Profile,             auth_required=True,         )      async def get_academic_years(self) -> List[AcademicYear]:         \"\"\"         Returns `List` with `AcademicYear` objects         :return:         \"\"\"          return await self._make_request(             \"GET\",             \"student\/academic_years\",             List[AcademicYear],             auth_required=True,         )      @cache.soft(ttl=\"24h\", soft_ttl=\"1m\")     async def get_academic_year_subjects(self, year_id: int) -> List[Subject]:         \"\"\"         Returns `List` with `Subjects` of some year         :return:         \"\"\"          return await self._make_request(             \"GET\",             f\"student\/grades\/{year_id}\",             List[Subject],             auth_required=True,         )      async def get_current_year_id(self) -> int:         \"\"\"         Returns identifier of current year         :return:         \"\"\"          years = await self.get_academic_years()          return years[0].id      async def __aenter__(self) -> Self:         return self      async def __aexit__(self, exc_type, exc_val, exc_tb):         await self.close_session()      def __str__(self) -> str:         \"\"\"         Stringifies `RTSUApi` objects         :return:         \"\"\"          return f\"{self.__class__.__name__}&lt;token={self._api_token}>\"      async def close_session(self):         \"\"\"Frees inner resources\"\"\"         await self._http_client.close() <\/code><\/pre>\n<p>\u0427\u0442\u043e \u0442\u0443\u0442 \u0443 \u043d\u0430\u0441? \u0412 <code>_make_request<\/code> \u0443 \u043d\u0430\u0441 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f json \u0432 pydantic-\u0441\u0445\u0435\u043c\u0443 (\u043d\u0443 \u0438\u043b\u0438 \u043c\u043e\u0434\u0435\u043b\u044c?)<\/p>\n<p>\u041f\u0440\u043e\u0448\u0443 \u0437\u0430\u043c\u0435\u0442\u0438\u0442\u044c, \u0447\u0442\u043e \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u0437\u0430\u043c\u0435\u0447\u0430\u0442\u0435\u043b\u044c\u043d\u0443\u044e cashews \u0434\u043b\u044f \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432, \u0432 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438 soft-ttl \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0435\u0449\u0435 \u0438 \u0441\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442 \u043a\u043e\u0433\u0434\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u0430 \u043f\u0430\u0434\u0430\u044e\u0442.<\/p>\n<p>\u0412 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u0436\u0435 \u043c\u0435\u0442\u043e\u0434\u0430\u0445 \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u044e \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \u0438 <code>response-schema<\/code> \u043d\u0443 \u0438 \u0434\u0451\u0440\u0433\u0430\u044e \u0442\u043e\u0442 \u0436\u0435 <code>_make_request<\/code><\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u0442\u0443\u0442 \u0435\u0441\u0442\u044c \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043a\u0440\u044b\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0443\u044e aiohttp-\u0441\u0435\u0441\u0441\u0438\u044e, \u043d\u0443 \u0442\u0443\u0442 \u043f\u043e\u043d\u044f\u0442\u043d\u043e, \u043f\u043e\u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u044b \u043c\u0430\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u044b <code>__aenter__<\/code> \u0438 <code>__aexit__ <\/code>\u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442 \u0432 <code>with<\/code>\u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u0445.<\/p>\n<p>\u041d\u0443 \u0438 <code>set_token<\/code>\u043c\u0435\u0442\u043e\u0434 \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e &#171;\u0432\u043f\u0438\u0445\u043d\u0443\u0442\u044c&#187; \u0442\u043e\u043a\u0435\u043d \u0432 \u043a\u043b\u0438\u0435\u043d\u0442, \u044d\u0442\u043e \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u043c \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435.<\/p>\n<h2>Pydantic-\u0441\u0445\u0435\u043c\u044b<\/h2>\n<p>\u0414\u043e\u043f\u0443\u0441\u0442\u0438\u043c, \u0437\u0430\u0433\u043b\u044f\u043d\u0435\u043c \u0432 <code>profile.py<\/code> \u0433\u0434\u0435 \u043b\u0435\u0436\u0438\u0442 <code>Profile-schema<\/code><\/p>\n<pre><code class=\"python\">from pydantic import Field  from .base import Base   class FullName(Base):     ru: str = Field(alias='RU')     tj: str = Field(alias='TJ')   class Faculty(FullName):     ...   class Speciality(FullName):     ...   class Profile(Base):     id: int = Field(alias='RecordBookNumber')     full_name: FullName = Field(alias='FullName')     faculty: Faculty = Field(alias=\"Faculty\")     course: int = Field(alias='Course')     training_period: int = Field(alias='TrainingPeriod')     level: str = Field(alias=\"TrainingLevel\")     entrance_year: str = Field(alias='YearUniversityEntrance') <\/code><\/pre>\n<p>\u041f\u043e\u0447\u0435\u043c\u0443 \u0442\u0430\u043a? API \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u043d\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0441\u0440\u0430\u0437\u0443 \u043d\u0430 \u0434\u0432\u0443\u0445 \u044f\u0437\u044b\u043a\u0430\u0445 (\u0440\u0443\u0441\u0441\u043a\u043e\u043c \u0438 \u0442\u0430\u0434\u0436\u0438\u043a\u0441\u043a\u043e\u043c)<\/p>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u044d\u0442\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0443\u0435\u0442 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e API \u0441\u0434\u0435\u043b\u0430\u043b\u0438 \u043e\u0447\u0435\u043d\u044c \u043a\u0440\u0438\u0432\u043e \u0438 \u0443\u0431\u043e\u0433\u043e, \u043d\u043e \u0447\u0442\u043e \u043f\u043e\u0434\u0435\u043b\u0430\u0442\u044c, \u0442\u0443\u0442 \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u0443\u044e \u043a\u0430\u0436\u0434\u044b\u0439 field \u043e\u0442 FullName \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u043f\u043e \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 RU, TJ \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435.<\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u043f\u0440\u043e\u0448\u0443 \u0437\u0430\u043c\u0435\u0442\u0438\u0442\u044c \u0442\u043e, \u043a\u0430\u043a \u044d\u043b\u0435\u0433\u0430\u043d\u0442\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c field&#8217;\u044b pythonic \u043f\u0440\u0438 \u043f\u043e\u043c\u043e\u0449\u0438 <code>pydantic-aliases<\/code><\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0433\u043e \u044d\u0442\u043e\u0433\u043e, \u044f \u043f\u0440\u0438\u043d\u044f\u043b\u0441\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442, \u043d\u0430 \u0443\u0434\u0438\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u043d \u0445\u043e\u0440\u043e\u0448\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0442\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043e\u043d\u0438 \u043c\u043d\u0435 \u043d\u0443\u0436\u043d\u044b.<\/p>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0442\u0435\u0441\u0442\u044b \u043a \u044d\u0442\u043e\u043c\u0443 \u044f \u0442\u043e\u0436\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043b<\/p>\n<pre><code class=\"python\">import pytest import pytest_asyncio  from rtsu_students_bot.rtsu import RTSUApi  from .config import settings  pytest_plugins = ('pytest_asyncio',)   @pytest_asyncio.fixture() async def rtsu_client():     \"\"\"     Initializes client     :return: Prepared `RTSUApi` client     \"\"\"      async with RTSUApi() as api:         yield api   @pytest.mark.asyncio async def test_rtsu_login(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu login     :param rtsu_client: A RTSU API client     :return:     \"\"\"      resp = await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      assert resp.token is not None   @pytest.mark.asyncio async def test_rtsu_profile_fetching(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu profile fetching     :param rtsu_client:     :return:     \"\"\"      await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      profile = await rtsu_client.get_profile()      assert profile is not None     assert profile.full_name is not None   @pytest.mark.asyncio async def test_rtsu_academic_years_fetching(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu academic years fetching     :param rtsu_client:     :return:     \"\"\"      await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      years = await rtsu_client.get_academic_years()      assert type(years) == list     assert len(years) > 0   @pytest.mark.asyncio async def test_rtsu_academic_year_subjects_fetching(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu academic year fetching     :param rtsu_client:     :return:     \"\"\"      await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      ac_years = await rtsu_client.get_academic_years()     year = ac_years[0].id     years = await rtsu_client.get_academic_year_subjects(year)      assert type(years) == list     assert len(years) > 0 <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0442\u0435\u0441\u0442\u044b \u0441\u0434\u0435\u043b\u0430\u043d\u044b \u043d\u0430\u0432\u0435\u0440\u043d\u043e\u0435 \u0433\u0440\u044f\u0437\u043d\u043e \u0438 \u0442\u0443\u043f\u043e, \u044f \u043f\u043e\u043a\u0430 \u043d\u0435 \u0447\u0438\u0442\u0430\u043b \u043f\u0440\u043e best-practises \u0432 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 API, \u0431\u0443\u0434\u0443 \u0440\u0430\u0434 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u043c \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0445 \u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u043f\u043e\u0432\u043e\u0434\u0443.<\/p>\n<p>\u0410\u0445, \u0434\u0430, \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432 \u0442\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433, \u0432 \u043a\u043e\u043d\u0446\u0435 \u0441\u0442\u0430\u0442\u044c\u0438 \u044f \u043f\u043e\u043a\u0430\u0436\u0443 \u043a\u0430\u043a \u0435\u0433\u043e \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c.<\/p>\n<p>\u041d\u0430 \u0440\u0443\u043a\u0430\u0445 \u0443 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c <code>wrapper<\/code><\/p>\n<h2>\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 SQLAlchemy<\/h2>\n<p>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0432 \u0430\u043b\u0445\u0438\u043c\u0438\u044e, \u044f \u043f\u0440\u0438\u043d\u044f\u043b\u0441\u044f \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043f\u0430\u043a\u0435\u0442 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 <\/p>\n<p>\u041d\u043e \u0434\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430, \u043f\u043e\u043a\u0430\u0436\u0443 \u0432\u0430\u043c \u0444\u0430\u0439\u043b\u0438\u043a database.py \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0430\u0437\u0435.<\/p>\n<pre><code class=\"python\">from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker  from rtsu_students_bot.config import settings  engine = create_async_engine(     settings.db.url, )  SessionLocal = sessionmaker(bind=engine, class_=AsyncSession) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u043d\u0435\u0442 \u043d\u0438\u0447\u0435\u0433\u043e \u0442\u0430\u043a\u043e\u0433\u043e, \u043f\u0440\u043e\u0441\u0442\u043e \u0432\u043c\u0435\u0441\u0442\u043e \u043e\u0431\u044b\u0447\u043d\u043e\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u0443\u044e (\u043f\u043e \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u044b\u043c \u043f\u0440\u0438\u0447\u0438\u043d\u0430\u043c)<\/p>\n<p>\u0412\u0435\u0440\u043d\u0451\u043c\u0441\u044f \u043a \u043f\u0430\u043a\u0435\u0442\u043d\u0438\u043a\u0443 \u0441 \u043c\u043e\u0434\u0435\u043b\u044f\u043c\u0438.<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6ee\/4ea\/b56\/6ee4eab56d514279e6cbf70089a82ec0.png\" width=\"283\" height=\"154\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/6ee\/4ea\/b56\/6ee4eab56d514279e6cbf70089a82ec0.png\"\/><\/figure>\n<pre><code class=\"python\">from sqlalchemy import Integer, Column, String, Boolean, BigInteger  from .base import Base   class User(Base):     __tablename__ = \"users\"      id = Column(Integer, primary_key=True, index=True)     full_name = Column(String(length=255), nullable=True)     token = Column(String(length=600), nullable=True)     is_authorized = Column(Boolean, default=False)     telegram_id = Column(BigInteger)      def __str__(self):         return f\"{self.__class__.__name__}&lt;id={self.id}, name={self.full_name}>\" <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0432\u0441\u0451 \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u043e, \u0443 \u043d\u0430\u0441 \u0432 \u0431\u0430\u0437\u0435 \u0431\u0443\u0434\u0435\u0442 \u0445\u0440\u0430\u043d\u0438\u0442\u044c\u0441\u044f \u0442\u043e\u043a\u0435\u043d, telegram-id, \u043d\u0443 \u0438 \u0444\u043b\u0430\u0436\u043e\u043a \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u043e\u0431\u0449\u0430\u0442\u044c \u043e \u0442\u043e\u043c, \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u043b\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c.<\/p>\n<p>\u0414\u0430\u043b\u0435\u0435, \u043f\u0438\u043b\u0438\u043c \u043f\u0430\u043a\u0435\u0442\u043d\u0438\u043a <code>service<\/code>\u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043c\u043e\u0433\u0430\u0442\u044c \u043d\u0430\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/11c\/7e0\/fdc\/11c7e0fdc5dd4ab4028983023bd09e85.png\" width=\"236\" height=\"107\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/11c\/7e0\/fdc\/11c7e0fdc5dd4ab4028983023bd09e85.png\"\/><\/figure>\n<p>\u0421\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 <code>user.py<\/code><\/p>\n<pre><code class=\"python\">from typing import Optional  from sqlalchemy import select, update, delete from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.models import User  from .exceptions import UserNotFound, UserAlreadyExists   async def get_user_by_tg_id(         session: AsyncSession,         telegram_id: int, ) -> Optional[User]:     \"\"\"     Returns user by tg-id     :param session: An `AsyncSession` object     :param telegram_id: A telegram-ID     :return: `User` or `None`     \"\"\"      stmt = select(User).where(User.telegram_id == telegram_id)      result = await session.execute(stmt)      return result.scalars().first()   async def get_user_by_id(         session: AsyncSession,         user_id: int, ) -> Optional[User]:     \"\"\"     Returns user by its id     :param session: An `AsyncSession` object     :param user_id: An ID     :return: `User` or `None`     \"\"\"      stmt = select(User).where(User.id == user_id)      result = await session.execute(stmt)      return result.scalars().first()   async def create_user(         session: AsyncSession,         telegram_id: int,         full_name: Optional[str] = None,         token: Optional[str] = None, ):     \"\"\"     Creates `User` object     :param session: An `AsyncSession` object     :param telegram_id: A telegram-id     :param full_name: Fullname of user     :param token: A token of user     :return: Created `User`     \"\"\"      existed_user = await get_user_by_tg_id(session, telegram_id)      if existed_user is not None:         raise UserAlreadyExists(f\"User with ID {telegram_id} already exists.\")      is_authorized = token is not None      obj = User(         telegram_id=telegram_id,         full_name=full_name,         token=token,         is_authorized=is_authorized,     )      session.add(obj)     await session.flush()     await session.refresh(obj)      return obj   async def update_user_token(         session: AsyncSession,         telegram_id: int,         token: Optional[str] = None, ) -> User:     \"\"\"     Authorizes `User`     :param telegram_id:     :param session:     :param token:     :return:     \"\"\"      user = await get_user_by_tg_id(session, telegram_id)      if not user:         raise UserNotFound(f\"User with telegram-id {telegram_id} not found.\")      is_authorized = token is not None      stmt = update(User).where(         int(user.id) == User.id     ).values(         is_authorized=is_authorized,         token=token,     )     await session.execute(stmt)      return await get_user_by_tg_id(session, user.telegram_id)   async def update_user(         session: AsyncSession,         user_id: int,         telegram_id: Optional[int] = None,         full_name: Optional[str] = None, ) -> User:     \"\"\"     Updates telegram user     :param session:     :param user_id:     :param telegram_id:     :param full_name:     :return:     \"\"\"      user = await get_user_by_id(session, user_id)      if user is None:         raise UserNotFound(f\"User with ID {user_id} not found.\")      stmt = update(User).where(User.id == user_id)      if telegram_id is not None:         stmt = stmt.values(             telegram_id=telegram_id,         )      if full_name is not None:         stmt = stmt.values(             full_name=full_name         )      await session.execute(stmt)      return await get_user_by_id(session, user_id)   async def delete_user(session: AsyncSession, user_id: int):     \"\"\"     Deletes `User` object     :param user_id:     :param session: An `AsyncSession` object     :return:     \"\"\"      if await get_user_by_id(session, user_id) is None:         raise ValueError(\"Invalid user-id passed.\")      stmt = delete(User).where(User.id == user_id)      await session.execute(stmt) <\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b \u043e\u0431\u044b\u0447\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u043e\u0433\u0430\u0442\u044c \u043c\u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u0431\u0430\u0437\u043e\u0439, \u0445\u043e\u0442\u044f \u043d\u0430\u0432\u0435\u0440\u043d\u043e\u0435 \u043b\u0443\u0447\u0448\u0435 \u0431\u044b \u044f \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043b \u043f\u0430\u0442\u0442\u0435\u0440\u043d &#171;\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439&#187;<\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 CRUD<\/h2>\n<pre><code class=\"python\">import pytest import pytest_asyncio  from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker  from rtsu_students_bot.service import user from rtsu_students_bot.models import Base  from .config import settings  pytest_plugins = ('pytest_asyncio',)  engine = create_async_engine(     settings.db_url, )  SessionLocal = sessionmaker(autoflush=True, bind=engine, class_=AsyncSession)   @pytest_asyncio.fixture() async def session():     \"\"\"     Initializes client     :return: Prepared `RTSUApi` client     \"\"\"      async with SessionLocal() as e, e.begin():         yield e   @pytest.mark.asyncio async def test_tables_creating():     async with engine.begin() as conn:         await conn.run_sync(Base.metadata.create_all)   @pytest.mark.asyncio async def test_user_creation(session: AsyncSession):     \"\"\"     Tests user-creation     :return:     \"\"\"      user_data = {         \"full_name\": \"Vladimir Putin\",         \"telegram_id\": 1,     }      created_user = await user.create_user(session, **user_data)      assert created_user.full_name == user_data.get(\"full_name\")     assert created_user.telegram_id == user_data.get(\"telegram_id\")   @pytest.mark.asyncio async def test_user_update(session: AsyncSession):     \"\"\"     Tests user updating     :param session:     :return:     \"\"\"      updating_data = {         \"full_name\": \"Volodymir Zelensky\"     }      first_user = await user.get_user_by_tg_id(session, 1)      updated_user = await user.update_user(session, first_user.id, **updating_data)      assert first_user.id == updated_user.id     assert first_user.telegram_id == updated_user.telegram_id     assert updated_user.full_name == updating_data.get(\"full_name\")   @pytest.mark.asyncio async def test_user_token_updating(session: AsyncSession):     \"\"\"     Tests user-token updating     :param session:     :return:     \"\"\"      first_user = await user.get_user_by_tg_id(session, 1)      assert not first_user.is_authorized      first_user = await user.update_user_token(session, first_user.telegram_id, token=\"test token\")      assert first_user.is_authorized     assert first_user.token == \"test token\"     assert first_user.telegram_id == 1   @pytest.mark.asyncio async def test_user_deleting(session: AsyncSession):     \"\"\"     Tests user-token updating     :param session:     :return:     \"\"\"      first_user = await user.get_user_by_tg_id(session, 1)      assert first_user is not None      await user.delete_user(session, first_user.id)      first_user = await user.get_user_by_tg_id(session, 1)      assert first_user is None <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0442\u0430\u043a\u0436\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044e CRUD \u043d\u0430 \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c, \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u043f\u043e\u043c\u043e\u0433\u043b\u0438 \u043c\u043d\u0435 \u043d\u0430\u0439\u0442\u0438 \u043e\u0434\u043d\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u044f \u0434\u0443\u043c\u0430\u044e \u043f\u0438\u0441\u0430\u043b \u044f \u0438\u0445 \u043d\u0435 \u0437\u0440\u044f =)<\/p>\n<h2>\u041f\u0438\u043b\u0438\u043c \u0441\u0430\u043c\u043e\u0433\u043e \u0431\u043e\u0442\u0430<\/h2>\n<p>\u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Telegram Bot API \u044f \u043d\u0435\u0441\u043e\u043c\u043d\u0435\u043d\u043d\u043e \u0432\u044b\u0431\u0440\u0430\u043b Aiogram \u043d\u0443 \u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0434\u0435\u043b\u0430\u043b \u0435\u0449\u0435 \u043e\u0434\u0438\u043d \u043f\u0430\u043a\u0435\u0442\u043d\u0438\u043a.<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/11c\/cab\/b76\/11ccabb76e5103623d88fac3ecabe3b1.png\" width=\"467\" height=\"633\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/11c\/cab\/b76\/11ccabb76e5103623d88fac3ecabe3b1.png\"\/><\/figure>\n<h2>Middlewares<\/h2>\n<p>\u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b &#171;\u043f\u0440\u043e\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u0442\u044c&#187; \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u043a \u0445\u0435\u043d\u0434\u043b\u0435\u0440\u0430\u043c (API-\u043a\u043b\u0438\u0435\u043d\u0442 \u0438 AsyncSession), \u043c\u043d\u0435 \u043d\u0443\u0436\u0435\u043d \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0446\u0435\u043b\u0435\u0439 \u043c\u0438\u0434\u043b\u0432\u0430\u0440\u044c<\/p>\n<pre><code class=\"python\">import logging  from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram import types from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.rtsu import RTSUApi from rtsu_students_bot.database import engine   class ResourcesMiddleware(BaseMiddleware):     \"\"\"     Middleware for providing resources like db-connection and RTSU-client     \"\"\"      def __init__(self):         \"\"\"         Initializes self         \"\"\"          self._logger = logging.getLogger(\"resources_middleware\")          super().__init__()      @staticmethod     async def _provide_api_client() -> RTSUApi:         \"\"\"         Provides `RTSU` api client         :return: Initialized client         \"\"\"          client = RTSUApi()          return client      @staticmethod     async def _provide_db_session() -> AsyncSession:         \"\"\"         Provides `AsyncSession` object         :return: Initialized session         \"\"\"          session = AsyncSession(engine)          return session      async def _provide_resources(self) -> dict:         \"\"\"         Initializes &amp; provides needed resources, such as `RTSU-api-client` and `AsyncSession`         :return:         \"\"\"         self._logger.debug(\"Providing resources\")         api_client = await self._provide_api_client()         db_session = await self._provide_db_session()          resources = {             \"rtsu\": api_client,             \"db_session\": db_session,         }          return resources      async def _cleanup(self, data: dict):         \"\"\"         Closes connections &amp; etc.         :param data:         :return:         \"\"\"          self._logger.debug(\"Cleaning resources\")          if \"db_session\" in data:             self._logger.debug(\"SQLAlchemy session detected, closing connection.\")             session: AsyncSession = data[\"db_session\"]             await session.commit()  # Commit changes             await session.close()          if \"rtsu\" in data:             self._logger.debug(\"RTSU API Client detected, closing resource.\")             api_client: RTSUApi = data[\"rtsu\"]             await api_client.close_session()      async def on_pre_process_message(self, update: types.Message, data: dict):         \"\"\"         For pre-processing `types.Update`         :param data: Data from other middlewares         :param update: A telegram-update         :return:         \"\"\"         resources = await self._provide_resources()          data.update(resources)          return data      async def on_pre_process_callback_query(self, query: types.CallbackQuery, data: dict):         \"\"\"         Method for preprocessing callback-queries         :param query: A callback-query         :param data: A data from another middleware         :return:         \"\"\"          resources = await self._provide_resources()          data.update(resources)          return data      async def on_post_process_callback_query(self, query: types.CallbackQuery, data_from_handler: list, data: dict):         \"\"\"         Method for post-processing callback query         :param data_from_handler: Data from handler         :param query: A callback query         :param data: A data from another middleware         :return:         \"\"\"          await self._cleanup(data)      async def on_post_process_message(self, message: types.Message, data_from_handler: list, data: dict):         \"\"\"         For post-processing message         :param data_from_handler:         :param message:         :param data:         :return:         \"\"\"         await self._cleanup(data) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0432\u0441\u0435 \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u043e, \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439\/CallbackQuery \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0430\u044e \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u044e \u0438\u0445 \u0432 data<\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u043f\u043e\u0441\u043b\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0432\u0441\u0435\u0433\u043e \u044d\u0442\u043e\u0433\u043e \u0434\u0435\u043b\u0430, \u044f \u0432\u044b\u0437\u044b\u0432\u0430\u044e <code>_cleanup<\/code> \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0441\u0435\u0441\u0441\u0438\u0439 \u0437\u0430\u043a\u0440\u043e\u0435\u0442 \u0438\u0445 (\u043d\u0443 \u0438 \u0441\u0434\u0435\u043b\u0430\u0435\u0442 \u043a\u043e\u043c\u043c\u0438\u0442 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441 \u0430\u043b\u0445\u0438\u043c\u0438\u0435\u0439)<\/p>\n<h2>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f<\/h2>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0442\u043e\u0436\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u0438\u0434\u043b\u0432\u0430\u0440\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0430\u0437\u0443\u043c\u0435\u0435\u0442\u0441\u044f \u043e\u0442\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u043e\u0433\u043e<\/p>\n<pre><code class=\"python\">import logging  from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram import types from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.service import user from rtsu_students_bot.rtsu import RTSUApi   class UserMiddleware(BaseMiddleware):     \"\"\"     Middleware for providing a `User` object     \"\"\"      def __init__(self):         \"\"\"         Initializes self         \"\"\"          self._logger = logging.getLogger(\"users_middleware\")         super().__init__()      async def _provide_user(self, user_id: int, data: dict) -> dict:         \"\"\"         Fetches and returns user         \"\"\"          if 'db_session' not in data:             raise RuntimeError(\"AsyncSession not found.\")          if 'rtsu' not in data:             raise RuntimeError(\"RTSU API client not found.\")          db_session: AsyncSession = data.get(\"db_session\")         rtsu_client: RTSUApi = data.get(\"rtsu\")          self._logger.debug(f\"Getting user with ID {user_id}\")          u = await user.get_user_by_tg_id(db_session, user_id)          if u is None:             self._logger.debug(f\"User with ID {user_id} not found, creating...\")             u = await user.create_user(db_session, telegram_id=user_id)          self._logger.debug(f\"User provided, {u}\")          # If user is authorized, lets setup `RTSU` client         if u.is_authorized:             rtsu_client.set_token(u.token)             self._logger.debug(\"User is authorized, API-client's token initialized.\")          data[\"user\"] = u          return data      async def on_pre_process_message(self, message: types.message, data: dict):         \"\"\"         Method for preprocessing messages (provides user)         :param message: A message         :param data: A data from another middleware         :return: None         \"\"\"          return await self._provide_user(message.from_user.id, data)      async def on_pre_process_callback_query(self, query: types.CallbackQuery, data: dict):         \"\"\"         Method for preprocessing callback-queries (provides user)         :param data:         :param query:         :return:         \"\"\"          return await self._provide_user(query.from_user.id, data) <\/code><\/pre>\n<p>\u041a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e \u0442\u0443\u0442, \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0435\u0441\u043b\u0438 \u043d\u0430 \u043d\u0430\u0448\u043b\u0438 \u0435\u0433\u043e \u0432 \u0431\u0434, \u043f\u043e\u043c\u0438\u043c\u043e \u0442\u043e\u0433\u043e, \u0435\u0441\u043b\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d (\u0442\u043e\u043a\u0435\u043d \u0435\u0441\u0442\u044c \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445) \u043c\u044b \u0442\u0430\u043a\u0436\u0435 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0442\u043e\u043a\u0435\u043dAPI-\u043a\u043b\u0438\u0435\u043d\u0442\u0430.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u043e\u0447\u0442\u0438 \u0432\u0441\u0451 \u0447\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439.<\/p>\n<h2>\u0428\u0430\u0431\u043b\u043e\u043d\u0438\u0437\u0430\u0442\u043e\u0440 Jinja2<\/h2>\n<p>\u0425\u043e\u0440\u043e\u0448\u043e \u0431\u044b \u0432\u044b\u0434\u0435\u043b\u0438\u0442\u044c &#171;\u0448\u0430\u0431\u043b\u043e\u043d\u044b&#187; \u043a\u0443\u0434\u0430-\u0442\u043e, \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u044f \u0440\u0435\u0448\u0438\u043b \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0441\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0438\u0437\u0430\u0442\u043e\u0440\u043e\u043c &#171;\u041d\u0438\u043d\u0437\u0434\u044f&#187;<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u0438\u0445 \u0446\u0435\u043b\u0435\u0439 \u044f \u0441\u043e\u0437\u0434\u0430\u043b \u0444\u0430\u0439\u043b\u0438\u043a <code>template_engine.py<\/code><\/p>\n<pre><code class=\"python\">from typing import Optional, Any, Dict  from jinja2 import Environment, PackageLoader, select_autoescape  env = Environment(     loader=PackageLoader('rtsu_students_bot', 'templates'),     autoescape=select_autoescape(['html']) )   def render_template(name: str, values: Optional[Dict[str, Any]] = None, **kwargs):     \"\"\"     Renders template &amp; returns text     :param name: Name of template     :param values: Values for template (optional)     :param kwargs: Keyword-arguments for template (high-priority)     \"\"\"      template = env.get_template(name)      if values:         rendered_template = template.render(values, **kwargs)     else:         rendered_template = template.render(**kwargs)      return rendered_template <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043b \u0448\u0430\u0431\u043b\u043e\u043d\u0438\u0437\u0430\u0442\u043e\u0440 \u0438 \u0441\u0434\u0435\u043b\u0430\u043b \u0444\u0443\u043d\u043a\u0446\u0438\u044e <code>render_template<\/code> \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u044b\u0439 \u0442\u0435\u043a\u0441\u0442.<\/p>\n<h2>\u0428\u0430\u0431\u043b\u043e\u043d\u044b<\/h2>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e3b\/d1f\/fa7\/e3bd1ffa7df0a4ccf626bfb45883b55d.png\" width=\"364\" height=\"432\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e3b\/d1f\/fa7\/e3bd1ffa7df0a4ccf626bfb45883b55d.png\"\/><\/figure>\n<p>\u0414\u043b\u044f \u043d\u0438\u0445 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0442\u0430\u043c \u043d\u0438\u0447\u0435\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e\u0433\u043e \u044f \u0434\u0443\u043c\u0430\u044e \u0432\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u0442\u0435.<\/p>\n<p>\u0412\u043e\u0442 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430.<\/p>\n<pre><code class=\"xml\">&lt;b>? \u041f\u0440\u043e\u0444\u0438\u043b\u044c&lt;\/b>  &lt;b>? \u041f\u043e\u043b\u043d\u043e\u0435 \u0438\u043c\u044f: &lt;em> {{ profile.full_name.ru }} &lt;\/em>&lt;\/b>  &lt;b>\u2699\ufe0f ID \u0421\u0442\u0443\u0434\u0435\u043d\u0442\u0430: &lt;code>{{ profile.id }}&lt;\/code>&lt;\/b>  &lt;b>? \u0424\u0430\u043a\u0443\u043b\u044c\u0442\u0435\u0442: &lt;em>{{ profile.faculty.ru }} &lt;\/em>&lt;\/b>  &lt;b>\u2139\ufe0f \u041a\u0443\u0440\u0441: &lt;code>{{ profile.course }}&lt;\/code>&lt;\/b>  &lt;b>\u23f3 \u041f\u0435\u0440\u0438\u043e\u0434 \u043e\u0431\u0443\u0447\u0435\u043d\u0438\u044f: {{ profile.training_period }}{% if profile.training_period &lt; 5 %} \u0433\u043e\u0434\u0430 {% else %} \u043b\u0435\u0442 {% endif %}&lt;\/b>  &lt;b>? \u0413\u043e\u0434 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f: {{ profile.entrance_year }}&lt;\/b>  &lt;b>? \u0421\u0442\u0435\u043f\u0435\u043d\u044c \u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f: {{ profile.level }} &lt;\/b><\/code><\/pre>\n<p>\u0412 \u043e\u0431\u0449\u0435\u043c, \u043a\u0430\u043a-\u0442\u043e \u0442\u0430\u043a.<\/p>\n<h2>\u0411\u0435\u0436\u0438\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0445\u0435\u043d\u0434\u043b\u0435\u0440\u044b<\/h2>\n<p>\u0412 <code>core.py<\/code> \u044f \u043f\u043e\u043b\u043e\u0436\u0438\u043b \u043e\u0431\u0449\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0445\u0435\u043d\u0434\u043b\u0435\u0440\u0430\u0445, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u043e\u0432\u0430\u0442\u044c DRY<\/p>\n<pre><code class=\"python\">\"\"\" `core.py` - Core-functionality of bot \"\"\"  from typing import Union, List  from aiogram import types from aiogram.dispatcher import FSMContext from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.service import user from rtsu_students_bot.rtsu import RTSUApi, exceptions, schemas from rtsu_students_bot.bot.keyboards import inline, reply from rtsu_students_bot.bot.states import AuthState from rtsu_students_bot.models import User from rtsu_students_bot.template_engine import render_template   async def start_auth(         update: Union[types.CallbackQuery, types.Message],         user_in_db: User ):     \"\"\"     Core-function, starts authentification process     :param update: An `update` (message or query)     :param user_in_db: An User in database     :return:     \"\"\"      markup = None     text = render_template(         \"auth.html\",         user=user_in_db     )      if not user_in_db.is_authorized:         await AuthState.first()         markup = inline.cancellation_keyboard_factory()      await update.bot.send_message(         update.from_user.id,         text=text,         reply_markup=markup     )   async def show_profile(         message: types.Message,         rtsu_client: RTSUApi,         user_in_db: User, ):     \"\"\"     Shows information about profile     :param message:     :param rtsu_client:     :param user_in_db:     :return:     \"\"\"      profile = await rtsu_client.get_profile()      text = render_template(         \"profile.html\",         profile=profile,         user=user_in_db,         telegram_user=message.from_user     )      await message.bot.send_message(         message.from_user.id,         text,         reply_markup=inline.message_hiding_keyboard()     )   async def show_statistics(         message: types.Message,         rtsu_client: RTSUApi, ):     \"\"\"     Shows user's statistics     :param message: A message     :param rtsu_client: Initialized RTSU API client     :return:     \"\"\"      current_year_id = await rtsu_client.get_current_year_id()     subjects: List[schemas.Subject] = await rtsu_client.get_academic_year_subjects(current_year_id)      await message.bot.send_message(         message.chat.id,         text=render_template(\"statistics.html\", subjects=subjects),         reply_markup=inline.message_hiding_keyboard()     )   async def authorize_user(         update: Union[types.CallbackQuery, types.Message],         user_in_db: User,         login: str,         password: str,         db_session: AsyncSession,         rtsu_client: RTSUApi,         state: FSMContext ):     \"\"\"     Authorizes user, on success auth, saves token to database     :param state: A state (fsm-context)     :param rtsu_client: An initialized RTSU api client     :param db_session: `AsyncSession` object     :param password: A password     :param login: A login     :param user_in_db: A user in database     :param update: Update (message or query)     \"\"\"      try:         auth_schema = await rtsu_client.auth(login, password)     except exceptions.AuthError:         await update.bot.send_message(             update.from_user.id,             text=render_template(\"auth_error.html\"),             reply_markup=inline.cancellation_keyboard_factory()         )         await AuthState.first()         return      profile = await rtsu_client.get_profile()      await user.update_user(         db_session,         user_in_db.id,         full_name=profile.full_name.ru,     )      await user.update_user_token(         db_session,         update.from_user.id,         auth_schema.token,     )      await update.bot.send_message(         update.from_user.id,         text=render_template(\"auth_success.html\", full_name=profile.full_name.ru),         reply_markup=reply.main_menu_factory()     )     await state.finish()   async def show_subjects(         message: types.Message,         rtsu_client: RTSUApi ):     \"\"\"     Shows user's subjects     :param message:     :param rtsu_client:     :return:     \"\"\"      current_year = await rtsu_client.get_current_year_id()     subjects = await rtsu_client.get_academic_year_subjects(current_year)      await message.bot.send_message(         message.chat.id,         text=\"? \u0414\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u044b\",         reply_markup=inline.subjects_keyboard_factory(subjects)     )   async def logout_user(         message: types.Message,         user_in_db: User,         db_session: AsyncSession ):     \"\"\"     Sets user's token to `NULL`     :param db_session: `AsyncSession` object     :param message: A message     :param user_in_db: A user in db     :return:     \"\"\"      await user.update_user_token(         db_session,         user_in_db.telegram_id,         token=None     )      await message.bot.send_message(         message.from_user.id,         text=render_template(\"logout.html\", user=user_in_db),         reply_markup=inline.auth_keyboard_factory()     )   async def show_help(         message: types.Message ):     \"\"\"     Shows help-menu     :param message: A message     :return:     \"\"\"      await message.bot.send_message(         message.from_user.id,         text=render_template(\"help.html\"),         reply_markup=inline.message_hiding_keyboard()     )   async def show_about(         message: types.Message ):     \"\"\"     Shows about-menu     :param message: A message     :return:     \"\"\"      await message.bot.send_message(         message.from_user.id,         text=render_template(\"about.html\"),         disable_web_page_preview=True,         reply_markup=inline.message_hiding_keyboard()     ) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0432\u0430\u043c \u0438 \u0441\u0442\u0435\u0439\u0442-\u043c\u0430\u0448\u0438\u043d\u0430 \u043d\u0443 \u0438 \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u043a\u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0442\u043e\u0436\u0435 \u0435\u0441\u0442\u044c.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u0433\u043b\u044f\u043d\u0435\u043c \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0432 \u0445\u0435\u043d\u0434\u043b\u0435\u0440\u044b, \u0432 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438 \u0432 <code>commands.py<\/code><\/p>\n<pre><code class=\"python\">from aiogram import types, Dispatcher from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.rtsu import RTSUApi from rtsu_students_bot.bot.filters import AuthorizationFilter from rtsu_students_bot.models import User from rtsu_students_bot.bot.keyboards import inline, reply from rtsu_students_bot.template_engine import render_template  from . import core   async def start(message: types.Message, user: User):     \"\"\"     Handles `\/start` cmd     :param user:     :param message: A message     \"\"\"      markup = reply.main_menu_factory()      if not user.is_authorized:         markup = inline.auth_keyboard_factory()      await message.reply(         text=render_template(             \"start.html\",             user=user,             telegram_user=message.from_user         ),         reply_markup=markup,     )   async def auth(message: types.Message, user: User):     \"\"\"     Handles `\/auth` cmd     :param user: A User     :param message: A message     \"\"\"      await core.start_auth(message, user)   async def help_cmd(message: types.Message):     \"\"\"     Handles `help` cmd     :param message: A message     \"\"\"      await core.show_help(message)   async def statistics(message: types.Message, rtsu: RTSUApi):     \"\"\"     Handles `statistics` cmd     :param message: A message     :param rtsu: Initialized RTSU API client     \"\"\"      await core.show_statistics(message, rtsu)   async def subjects(message: types.Message, rtsu: RTSUApi):     \"\"\"     Handles `subjects` cmd     :param message: A message     :param rtsu: Initialized RTSU API client     \"\"\"      await core.show_subjects(message, rtsu)   async def profile(message: types.Message, rtsu: RTSUApi, user: User):     \"\"\"     Handles `profile` cmd     :param message: A message     :param user: User in db     :param rtsu: Initialized RTSU API client     \"\"\"      await core.show_profile(message, rtsu, user)   async def logout(message: types.Message, user: User, db_session: AsyncSession):     \"\"\"     Handles `logout` cmd     :param db_session: `AsyncSession` object     :param user: A user in db     :param message: A message     \"\"\"      await core.logout_user(message, user, db_session)   async def about(message: types.Message):     \"\"\"     Handles `about` cmd     :param message: A message     \"\"\"      await core.show_about(message)   def setup(dp: Dispatcher):     \"\"\"     Setups commands-handlers     :param dp:     :return:     \"\"\"     dp.register_message_handler(start, commands=[\"start\"])     dp.register_message_handler(help_cmd, commands=[\"help\"])     dp.register_message_handler(about, commands=[\"about\"])     dp.register_message_handler(logout, AuthorizationFilter(True), commands=[\"logout\"])     dp.register_message_handler(profile, AuthorizationFilter(True), commands=[\"profile\"])     dp.register_message_handler(subjects, AuthorizationFilter(True), commands=[\"subjects\"])     dp.register_message_handler(statistics, AuthorizationFilter(True), commands=[\"stat\"])     dp.register_message_handler(auth, AuthorizationFilter(False), commands=[\"auth\"])     dp.register_message_handler(auth, AuthorizationFilter(authorized=True), commands=[\"auth\"]) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u044e \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0438 \u0434\u0451\u0440\u0433\u0430\u044e \u0432\u0441\u0451 \u0447\u0442\u043e \u043c\u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0438\u0437 <code>core.py<\/code><\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 <code>AuthorizationFilter<\/code>, \u043e\u043d \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.<\/p>\n<p>\u0421\u0440\u0430\u0437\u0443 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u043b\u043e\u0433\u0438\u0447\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441, \u0430 \u043f\u043e\u0447\u0435\u043c\u0443 \u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b <code>BoundFilter ?<\/code><\/p>\n<p>\u041e\u0442\u0432\u0435\u0442 \u043a\u0440\u043e\u0435\u0442\u0441\u044f \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u043e\u043d \u0431\u0430\u0433\u043d\u0443\u0442\u044b\u0439, \u0432 \u043d\u0430\u0447\u0430\u043b\u0435 \u044f \u0435\u0433\u043e \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b, \u043e\u0434\u043d\u0430\u043a\u043e \u043e\u043d \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u043e \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e, \u044f \u0433\u0434\u0435-\u0442\u043e \u0447\u0430\u0441 \u0431\u0438\u043b\u0441\u044f \u0441 \u043d\u0438\u043c \u043d\u043e \u0432 \u0438\u0442\u043e\u0433\u0435 \u0440\u0435\u0448\u0438\u043b \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043e\u0431\u044b\u0447\u043d\u044b\u0439 <code>AbstractFilter<\/code> \u043e\u043d \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0441\u0440\u0430\u0437\u0443 \u0436\u0435, \u0441 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0440\u0430\u0437\u0430. <\/p>\n<p>\u0421\u043e\u043e\u0431\u0449\u0430\u0442\u044c \u043e\u0431 \u044d\u0442\u043e\u043c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\u043c Aiogram &#8212; \u044f \u043d\u0435 \u043e\u0441\u043e\u0431\u043e \u0438 \u0438\u043c\u0435\u044e \u0436\u0435\u043b\u0430\u043d\u0438\u044f, \u043e\u043d\u0438 \u0432 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043e\u0447\u0435\u043d\u044c \u043d\u0435\u0434\u0440\u0443\u0436\u0435\u043b\u044e\u0431\u043d\u044b \u0438 \u043e\u0447\u0435\u043d\u044c \u0441\u0442\u0440\u0430\u043d\u043d\u043e \u043e\u0442\u043d\u043e\u0441\u044f\u0442\u0441\u044f \u043a \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e\u0432\u043e\u0440\u044f\u0449\u0438\u043c.<\/p>\n<p>\u041c\u0435\u043d\u044f \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0432 \u0447\u0430\u0442\u0435 aiogram \u0442\u0443\u043f\u043e \u0437\u0430\u0431\u0430\u043d\u0438\u043b\u0438 :\/<\/p>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043f\u0440\u0438\u0432\u043e\u0436\u0443 <code>filters\/auth.py<\/code> \u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 <code>AuthorizationFilter<\/code><\/p>\n<pre><code class=\"python\">from typing import Union  from aiogram.dispatcher.filters import Filter from aiogram.dispatcher.handler import ctx_data from aiogram import types  from rtsu_students_bot.models import User from rtsu_students_bot.template_engine import render_template   class AuthorizationFilter(Filter):     \"\"\"     Filter for checking user's authorization     \"\"\"      def __init__(self, authorized: bool):         \"\"\"         Initializes self         :param authorized:Is admin?         \"\"\"         self.authorized = authorized      async def check(self, message: Union[types.Message, types.CallbackQuery]):         \"\"\"         Checks for user's authorization status         :param message: A message         \"\"\"          data = ctx_data.get()          user: User = data.get(\"user\")          if self.authorized is None:             return True          if self.authorized and not user.is_authorized:             await message.bot.send_message(                 message.from_user.id,                 text=render_template(\"not_authorized.html\")             )             return False         elif not self.authorized and user.is_authorized:             await message.bot.send_message(                 message.from_user.id,                 text=render_template(\"already_authorized.html\")             )             return False          return True <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u043d\u0438\u0447\u0435\u0433\u043e \u0441\u043b\u043e\u0436\u043d\u043e\u0433\u043e, \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0437\u0430\u0431\u0438\u0440\u0430\u044e \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043c\u0438\u0434\u043b\u0432\u0430\u0440\u044f, \u043d\u0443 \u0438 \u0435\u0441\u043b\u0438 \u044e\u0437\u0435\u0440\u0443 \u043d\u0443\u0436\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u043e \u0435\u0451 &#8212; \u043d\u0435\u0442, \u044f \u043f\u0440\u0435\u0440\u044b\u0432\u0430\u044e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0430\u043f\u0434\u0435\u0439\u0442\u0430 \u0432\u0435\u0440\u043d\u0443\u0432 <code>False<\/code>, \u043d\u0443 \u0438\u043b\u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442 \u0435\u0441\u043b\u0438 \u044e\u0437\u0435\u0440\u0443 \u043d\u0435 \u043d\u0443\u0436\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u043e \u043e\u043d\u0430 \u0443 \u043d\u0435\u0433\u043e \u0435\u0441\u0442\u044c, \u044f \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0440\u044b\u0432\u0430\u044e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0430\u043f\u0434\u0435\u0439\u0442\u0430 \u043d\u0443 \u0438 \u043d\u0435 \u0437\u0430\u0431\u044b\u0432\u0430\u044e \u0432\u044b\u0434\u0430\u0432\u0430\u0442\u044c \u0435\u043c\u0443 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043d\u0438\u0447\u0435\u0433\u043e \u0442\u0430\u043a\u043e\u0433\u043e.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0432\u0437\u0433\u043b\u044f\u043d\u0435\u043c \u043d\u0430 <code>text.py<\/code> \u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u0442\u0435\u043a\u0441\u0442\u0430<\/p>\n<pre><code class=\"python\">\"\"\" `text.py` - Text handlers \"\"\"  from aiogram import types, Dispatcher from aiogram.dispatcher import FSMContext from aiogram.dispatcher.filters import Text from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.bot.filters import AuthorizationFilter from rtsu_students_bot.rtsu import RTSUApi from rtsu_students_bot.models import User from rtsu_students_bot.bot.states import AuthState from rtsu_students_bot.template_engine import render_template from rtsu_students_bot.bot.keyboards import inline  from . import core   async def login_handler(         message: types.Message,         state: FSMContext ):     \"\"\"     Handles `login` of user     :param message: A message with login     :param state: A current state (fsm-context)     \"\"\"      async with state.proxy() as data:         data[\"login\"] = message.text      await AuthState.next()     await message.delete()      await message.bot.send_message(         message.from_user.id,         render_template(\"enter_password.html\"),         reply_markup=inline.cancellation_keyboard_factory()     )   async def password_handler(         message: types.Message,         state: FSMContext, ):     \"\"\"     Handles password of user     :param message: A message (with password)     :param state: A state (fsm-context)     \"\"\"      async with state.proxy() as data:         password = data[\"password\"] = message.text         login = data[\"login\"]      await AuthState.next()     await message.delete()      await message.bot.send_message(         message.from_user.id,         render_template(\"credentials_confirmation.html\", login=login, password=password),         reply_markup=inline.confirmation_keyboard_factory()     )   async def show_profile_handler(         message: types.Message,         rtsu: RTSUApi,         user: User, ):     \"\"\"     Handles 'Show profile' request     :param message: A message     :param rtsu: Initialized RTSU API client     :param user: A user from db     :return:     \"\"\"      await core.show_profile(message, rtsu, user)   async def show_statistics_handler(         message: types.Message,         rtsu: RTSUApi, ):     \"\"\"     Shows user's statistics     :param message:     :param rtsu:     :return:     \"\"\"      await core.show_statistics(message, rtsu)   async def show_subjects_handler(         message: types.Message,         rtsu: RTSUApi ):     \"\"\"     Handles 'Show statistics' request     :param rtsu: Initialized RTSU API client     :param message: A message     :return:     \"\"\"      await core.show_subjects(message, rtsu)   async def logout_handler(         message: types.Message,         user: User,         db_session: AsyncSession, ):     \"\"\"     Handles 'logout-request'     :param db_session: `AsyncSession` object     :param message: A message     :param user: A user in db     \"\"\"      await core.logout_user(message, user, db_session)   async def auth_handler(message: types.Message, user: User):     \"\"\"     Handles 'auth-request'     :param message: A message     :param user: A user in db     :return:     \"\"\"      await core.start_auth(message, user)   async def help_handler(message: types.Message):     \"\"\"     Handles 'help-request'     :param message: A message     :return:     \"\"\"      await core.show_help(message)   async def about_handler(message: types.Message):     \"\"\"     Handles 'about-request'     :param message: A message     :return:     \"\"\"      await core.show_about(message)   def setup(dp: Dispatcher):     \"\"\"     Setups text-handlers     :param dp: A `Dispatcher` instance     \"\"\"     dp.register_message_handler(         login_handler,         state=AuthState.login,         content_types=[types.ContentType.TEXT],     )     dp.register_message_handler(password_handler, state=AuthState.password, content_types=[types.ContentType.TEXT])     dp.register_message_handler(         show_profile_handler, Text(equals=\"? \u041f\u0440\u043e\u0444\u0438\u043b\u044c\"), AuthorizationFilter(authorized=True)     )     dp.register_message_handler(         show_statistics_handler, Text(equals=\"? \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430\"), AuthorizationFilter(authorized=True)     )     dp.register_message_handler(         show_subjects_handler, Text(equals=\"? \u0414\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u044b\"), AuthorizationFilter(authorized=True)     )     dp.register_message_handler(         logout_handler, Text(equals=\"\u25c0\ufe0f \u0412\u044b\u0445\u043e\u0434 \u0438\u0437 \u0441\u0438\u0441\u0442\u0435\u043c\u044b\"), AuthorizationFilter(authorized=True)     )     dp.register_message_handler(         auth_handler, Text(equals=\"? \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\"), AuthorizationFilter(authorized=False)     )     dp.register_message_handler(         help_handler, Text(equals=\"? \u0418\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\"),     )      dp.register_message_handler(         about_handler, Text(equals=\"\u2139\ufe0f \u041e \u0431\u043e\u0442\u0435\"),     ) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u043c\u044b \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u043c \u043a\u043d\u043e\u043f\u043a\u0438 \u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u043c\u0435\u043d\u044e\u0448\u043a\u0438 \u0438 \u0442\u0430\u043a\u0436\u0435 \u0434\u0451\u0440\u0433\u0430\u0435\u043c <code>core.py<\/code> \u0434\u043b\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0433\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u0430, \u043d\u0443 \u0438 \u043f\u043e\u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e \u0442\u0443\u0442 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u043b\u043e\u0433\u0438\u043d\/\u043f\u0430\u0440\u043e\u043b\u044c <\/p>\n<p>\u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u043b\u0443\u0447\u0448\u0435 \u0431\u044b \u0432\u044b\u0434\u0435\u043b\u0438\u0442\u044c \u0442\u0435\u043a\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043e\u043a \u043a\u0443\u0434\u0430-\u0442\u043e \u0432 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b, \u0447\u0435\u043c \u044f \u0438 \u0437\u0430\u0439\u043c\u0443\u0441\u044c \u0441\u043a\u043e\u0440\u043e.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0432\u0437\u0433\u043b\u044f\u043d\u0435\u043c \u043d\u0430 CallbackQuery-handlers<\/p>\n<pre><code class=\"python\">from aiogram import types, Dispatcher from aiogram.dispatcher import FSMContext from sqlalchemy.ext.asyncio import AsyncSession  from rtsu_students_bot.bot.filters import AuthorizationFilter from rtsu_students_bot.rtsu import RTSUApi from rtsu_students_bot.template_engine import render_template from rtsu_students_bot.models import User from rtsu_students_bot.bot.keyboards import callbacks, inline from rtsu_students_bot.bot.states import AuthState  from .core import start_auth, authorize_user   async def auth_callback_processor(         query: types.CallbackQuery,         user: User ):     \"\"\"     Handles `callbacks.AUTH_CALLBACK`     :param query: A callback-query     :param user: An User     \"\"\"      await start_auth(query, user)   async def cancel_callback_processor(         query: types.CallbackQuery,         state: FSMContext ):     \"\"\"     Handles `callbacks.CANCELLATION_CALLBACK`     :param state: A current state (fsm-context)     :param query:     :return:     \"\"\"      await query.message.delete()      await query.bot.send_message(         query.from_user.id,         text=render_template(\"cancellation.html\")     )     await state.finish()   async def credentials_confirmation_callback_processor(         query: types.CallbackQuery,         callback_data: dict,         db_session: AsyncSession,         user: User,         rtsu: RTSUApi,         state: FSMContext ):     \"\"\"     Processes `callbacks.CONFIRMATION_CALLBACK`     :param state: A current state (fsm-context)     :param rtsu: An initialized rtsu-api client     :param user: A user in database     :param db_session: `AsyncSession` object     :param query: A callback-query     :param callback_data: Callback's data     \"\"\"      # If user clicks `Yes` - `1` will be passed     # If user clicks `No` - `0` will be passed     # So, all data will be represented as strings in telegram-callbacks     # For getting boolean some converting needed     # Firstly, we convert string to int, after, we convert this int to boolean     ok = bool(int(callback_data.get(\"ok\")))      await query.answer()     await query.message.delete()      async with state.proxy() as data:         login = data.get(\"login\")         password = data.get(\"password\")      if ok:         await authorize_user(query, user, login, password, db_session, rtsu, state)     else:         await query.bot.send_message(             query.from_user.id,             text=render_template(\"auth.html\", user=user),             reply_markup=inline.cancellation_keyboard_factory()         )         await AuthState.first()   async def show_subject_processor(         query: types.CallbackQuery,         rtsu: RTSUApi,         callback_data: dict, ):     \"\"\"     Handles `callbacks.SUBJECT_CALLBACK`     :param callback_data: A callback-data     :param query: A query     :param rtsu: Initialized RTSU API client     \"\"\"      await query.answer()      needed_subject_id = int(callback_data.get(\"id\"))      year = await rtsu.get_current_year_id()      subjects = await rtsu.get_academic_year_subjects(year)      needed_subject = list(filter(lambda x: x.id == needed_subject_id, subjects))      if not needed_subject:         await query.bot.send_message(             query.from_user.id,             \"\u0414\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.\"         )         return      await query.bot.send_message(         query.from_user.id,         text=render_template(             \"subject.html\",             subject=needed_subject[0]         )     )   async def delete_message_callback_processor(query: types.CallbackQuery):     \"\"\"     Processes deletion-callback     :param query: A callback-query     :return:     \"\"\"     await query.answer()     await query.message.delete()   def setup(dp: Dispatcher):     \"\"\"     Registers callback-query handlers     :param dp: A `Dispatcher` instance     \"\"\"     dp.register_callback_query_handler(         auth_callback_processor, callbacks.AUTH_CALLBACK.filter(), AuthorizationFilter(False)     )     dp.register_callback_query_handler(cancel_callback_processor, callbacks.CANCELLATION_CALLBACK.filter())     dp.register_callback_query_handler(         credentials_confirmation_callback_processor, callbacks.CONFIRMATION_CALLBACK.filter(), state=AuthState.confirm     )     dp.register_callback_query_handler(         show_subject_processor, callbacks.SUBJECT_CALLBACK.filter(), AuthorizationFilter(True)     )     dp.register_callback_query_handler(         delete_message_callback_processor, callbacks.DELETE_MSG_CALLBACK.filter()     ) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0442\u0430\u043a\u0436\u0435 \u0434\u0451\u0440\u0433\u0430\u0435\u0442\u0441\u044f \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b \u0438\u0437 core.py \u043d\u043e \u043f\u043e\u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e, \u0435\u0441\u0442\u044c \u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e \u0441\u0443\u0442\u0438 \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0438 \u0432\u044b\u0434\u0430\u044e\u0442 \u043a\u0430\u043a\u0443\u044e-\u0442\u043e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e.<\/p>\n<p>\u0422\u0443\u0442 \u0432\u0430\u043c \u0438 \u0441\u0430\u0431\u043c\u0438\u0442\u044b \u043b\u043e\u0433\u0438\u043d\u043e\u0432\/\u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0438 \u0432\u044b\u0434\u0430\u0447\u0430 \u043e\u0442\u0447\u0451\u0442\u0430 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0434\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0435 \u043d\u0443 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435 \u0432\u0441\u0451, \u0447\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 inline-\u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u043e\u0439.<\/p>\n<p>\u0412 \u0446\u0435\u043b\u043e\u043c, \u0435\u0441\u0442\u044c \u0435\u0449\u0435 \u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u043e\u0448\u0438\u0431\u043e\u043a, \u0432\u043e\u0442 \u043e\u043d\u0438<\/p>\n<pre><code class=\"python\">import logging  from aiogram import Dispatcher, types from aiogram.utils.exceptions import InvalidQueryID, MessageNotModified  from rtsu_students_bot.rtsu.exceptions import ServerError from rtsu_students_bot.template_engine import render_template   async def invalid_query_error_handler(update, error):     \"\"\"     Handles `InvalidQueryError`     :param update:     :param error:     :return:     \"\"\"     logging.info(f\"OK, Invalid query ID, {error}, {update}\")     return True   async def message_not_modified_error_handler(update, error):     \"\"\"     Handles `MessageNotModifiedError`     :param update:     :param error:     :return:     \"\"\"     logging.info(f\"OK, Message not modified, {error}, {update}\")     return True   async def server_error_handler(update: types.Update, error: ServerError):     \"\"\"     Handles `ServerError`     :param update:     :param error:     :return:     \"\"\"      logging.exception(\"Server error\", exc_info=error)      if update.message:         chat_id = update.message.from_user.id     else:         chat_id = update.callback_query.from_user.id      await update.bot.send_message(         chat_id=chat_id,         text=render_template(\"server_error.html\")     )      return True   async def any_exception_handler(update: types.Update, error: Exception):     \"\"\"     Handles `Exception`     :param update:     :param error:     :return:     \"\"\"     logging.error(f\"{error.__class__.__name__} has been thrown\")     logging.exception(\"Exception\", exc_info=error)     return True   def setup(dp: Dispatcher):     \"\"\"     Registers error handlers     :param dp: A `Dispatcher` instance     \"\"\"     dp.register_errors_handler(         server_error_handler,         exception=ServerError     )     dp.register_errors_handler(         invalid_query_error_handler,         exception=InvalidQueryID     )     dp.register_errors_handler(         message_not_modified_error_handler,         exception=MessageNotModified     )     dp.register_errors_handler(         any_exception_handler,         exception=Exception     ) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u043c \u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438, \u0430 \u043e\u0431 <code>ServerError<\/code> \u043c\u044b \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e.<\/p>\n<p>\u0414\u0443\u043c\u0430\u044e \u0442\u043e\u0436\u0435 \u043b\u043e\u0433\u0438\u0447\u043d\u043e.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0432\u0437\u0433\u043b\u044f\u043d\u0435\u043c \u043d\u0430 \u0433\u043b\u0430\u0432\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0432 \u043f\u0430\u043a\u0435\u0442\u0435 <code>bot<\/code>, \u0430 \u0438\u043c\u0435\u043d\u043d\u043e  <code>app.py<\/code><\/p>\n<pre><code class=\"python\">from aiogram import Bot, Dispatcher from aiogram.contrib.fsm_storage.memory import MemoryStorage  from rtsu_students_bot.config import settings  from . import handlers, middlewares   def get_app() -> Dispatcher:     \"\"\"     Initializes &amp; returns `Dispatcher`     \"\"\"      # Create bot &amp; dispatcher      memory_storage = MemoryStorage()      bot = Bot(settings.bot.token, parse_mode=\"html\")     dp = Dispatcher(bot, storage=memory_storage)      # Setup handlers      handlers.setup(dp)      # Setup middlewares      middlewares.setup(dp)      return dp <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u0438\u0441\u043f\u0435\u0442\u0447\u0435\u0440\u0430 \u0438 \u0431\u043e\u0442\u0430, \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438, \u043c\u0438\u0434\u043b\u0432\u0430\u0440\u0438 \u043d\u0443 \u0438 \u0437\u0430\u0434\u0430\u0435\u043c storage \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e FSM<\/p>\n<p>\u0414\u0430, \u0443 \u043c\u0435\u043d\u044f \u043f\u043e\u043a\u0430 \u0447\u0442\u043e \u0432\u0441\u0451 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0432 <code>MemoryStorage<\/code>, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0437\u0436\u0435 \u0441\u0434\u0435\u043b\u0430\u044e <code>RedisStorage<\/code>, \u0443\u0432\u0438\u0434\u0438\u043c.<\/p>\n<p>\u041d\u0443 \u0438 \u043d\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u043a, <code>cli.py<\/code> &amp; <code>config.py<\/code><\/p>\n<pre><code class=\"python\">from aiogram import executor from typer import Typer, Option  from .config import settings from .bot import get_app, handlers  typer_app = Typer()   @typer_app.command() def start(         skip_updates: bool = Option(default=False, help=\"Skip telegram updates on start?\"),         use_webhook: bool = Option(default=False, help=\"Use webhook for receiving updates?\") ):     \"\"\"     Starts bot     :param skip_updates: Skip telegram updates on start?     :param use_webhook: Use webhook mode for receiving updates?     \"\"\"      # Build bot     dp = get_app()      # Build startup-handler      startup_handler = handlers.startup.startup_handler_factory()      if use_webhook:         # Check for `webhook` settings are not `None`          if settings.webhooks is None:             print(\"Please, fill webhook's settings.\")             exit(-1)          startup_handler = handlers.startup.startup_handler_factory(f\"{settings.webhooks.host}{settings.webhooks.path}\")          executor.start_webhook(             dispatcher=dp,             on_startup=startup_handler,             skip_updates=skip_updates,             host=settings.webhooks.webapp_host,             port=settings.webhooks.webapp_port,             webhook_path=settings.webhooks.path,             check_ip=True         )     else:         executor.start_polling(             dp,             skip_updates=skip_updates,             on_startup=startup_handler         ) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0443 \u043c\u0435\u043d\u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Typer \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0438, \u0432 \u0446\u0435\u043b\u043e\u043c \u0442\u0443\u0442 \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0430\u044e \u043a\u043e\u043d\u0444\u0438\u0433 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e \u0431\u043e\u0442\u0430, startup-\u0445\u0435\u043d\u0434\u043b\u0435\u0440\u044b \u044f \u0434\u0443\u043c\u0430\u044e \u0432\u044b \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0443\u0436\u0435 \u0441\u0430\u043c\u0438, \u0435\u0441\u043b\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e, \u0442\u0430\u043c \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043b\u043e\u0433\u043e\u0432, \u0431\u0434, \u043a\u0435\u0448\u0430 \u0438 \u0432\u0441\u0435\u0433\u043e \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u0433\u043e, \u0432\u0440\u044f\u0434-\u043b\u0438 \u0432\u0430\u043c \u044d\u0442\u043e \u043f\u043e\u043a\u0430\u0436\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u043c.<\/p>\n<p>\u041d\u0443 \u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e <code>config.py<\/code><\/p>\n<pre><code class=\"python\">from typing import Optional  from pydantic import BaseSettings  from .constants import DEFAULT_ENCODING, SETTINGS_FILE   class DatabaseSettings(BaseSettings):     \"\"\"Settings of database\"\"\"     url: str   class BotSettings(BaseSettings):     \"\"\"Settings of telegram-bot\"\"\"     token: str   class Logging(BaseSettings):     format: str     debug: bool   class Webhooks(BaseSettings):     host: str     path: str     webapp_host: str     webapp_port: int   class Settings(BaseSettings):     \"\"\"Class for settings\"\"\"     bot: BotSettings     logging: Logging     db: DatabaseSettings     webhooks: Optional[Webhooks] = None   settings = Settings.parse_file(     path=SETTINGS_FILE,     encoding=DEFAULT_ENCODING ) <\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0442\u043e\u0436\u0435 \u043d\u0438\u0447\u0435\u0433\u043e \u0442\u0430\u043a\u043e\u0433\u043e \u043d\u0435\u0442, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u043e\u0431\u044b\u0447\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0437 Pydantic \u0438 \u0433\u0440\u0443\u0436\u0443 \u0438\u0445 \u0432 <code>settings<\/code><\/p>\n<p>\u041c\u043e\u0434\u0443\u043b\u0438 \u0432 \u043f\u0438\u0442\u043e\u043d\u0435 \u043a\u0435\u0448\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0442\u0430\u043a\u043e\u0439 \u0432\u0438\u0434 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0441\u0447\u0438\u0442\u0430\u044e \u0432 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0435 \u0441\u043f\u0440\u0430\u0432\u0435\u0434\u043b\u0438\u0432\u044b\u043c =)<\/p>\n<h2>\u041d\u0443 \u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u0441\u043a\u0440\u0438\u043d\u0448\u043e\u0442\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0432\u0448\u0435\u0433\u043e\u0441\u044f \u0431\u043e\u0442\u0430<\/h2>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/84f\/950\/3fa\/84f9503fae3d50076d3d8f25fd3ec814.png\" alt=\"\u041e\u0431\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430\" title=\"\u041e\u0431\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430\" width=\"683\" height=\"766\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/84f\/950\/3fa\/84f9503fae3d50076d3d8f25fd3ec814.png\"\/><\/p>\n<div><figcaption>\u041e\u0431\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/2bc\/961\/173\/2bc9611736e9ffdfbfc76b8845b4032e.png\" alt=\"\u041f\u0440\u043e\u0444\u0438\u043b\u044c\" title=\"\u041f\u0440\u043e\u0444\u0438\u043b\u044c\" width=\"859\" height=\"770\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/2bc\/961\/173\/2bc9611736e9ffdfbfc76b8845b4032e.png\"\/><\/p>\n<div><figcaption>\u041f\u0440\u043e\u0444\u0438\u043b\u044c<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/7c7\/69d\/2eb\/7c769d2eb170e8dcf2d241803790061a.png\" alt=\"\u0414\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u044b\" title=\"\u0414\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u044b\" width=\"689\" height=\"723\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7c7\/69d\/2eb\/7c769d2eb170e8dcf2d241803790061a.png\"\/><\/p>\n<div><figcaption>\u0414\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u044b<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1de\/581\/020\/1de5810207db832dc728088dff34b889.png\" alt=\"\u0421\u0442\u0430\u0442\u0430 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0434\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0435\" title=\"\u0421\u0442\u0430\u0442\u0430 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0434\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0435\" width=\"626\" height=\"707\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1de\/581\/020\/1de5810207db832dc728088dff34b889.png\"\/><\/p>\n<div><figcaption>\u0421\u0442\u0430\u0442\u0430 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0434\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0435<\/figcaption><\/div>\n<\/figure>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043d\u0430 \u044d\u0442\u043e\u043c \u0434\u0443\u043c\u0430\u044e \u0432\u0441\u0451, \u0432\u0441\u0435 \u0441\u0441\u044b\u043b\u043a\u0438 \u043f\u0440\u0438\u043a\u0440\u0435\u043f\u043b\u044f\u044e.<\/p>\n<ul>\n<li>\n<p><a href=\"https:\/\/github.com\/ilyas-kalandar\/RtsuStudentsBot\" rel=\"noopener noreferrer nofollow\">\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430.<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/t.me\/rtsu_students_bot\" rel=\"noopener noreferrer nofollow\">\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u0431\u043e\u0442\u0430<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/t.me\/awaitable\" rel=\"noopener noreferrer nofollow\">\u041c\u043e\u0439 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c<\/a><\/p>\n<\/li>\n<\/ul>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438\/\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0438<\/p>\n<ul>\n<li>\n<p>Aiogram<\/p>\n<\/li>\n<li>\n<p>Pydantic<\/p>\n<\/li>\n<li>\n<p>Aiohttp<\/p>\n<\/li>\n<li>\n<p>Cashews<\/p>\n<\/li>\n<li>\n<p>SQLAlchemy<\/p>\n<\/li>\n<li>\n<p>Poetry<\/p>\n<\/li>\n<li>\n<p>Typer<\/p>\n<\/li>\n<li>\n<p>Jinja2<\/p>\n<\/li>\n<\/ul>\n<p>\u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0447\u0442\u043e \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043b\u0438 \u0434\u043e \u043a\u043e\u043d\u0446\u0430, \u043e\u0436\u0438\u0434\u0430\u044e \u043e\u0431\u044a\u0435\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043a\u0440\u0438\u0442\u0438\u043a\u0438 \u0438 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u043f\u043e \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u043a\u043e\u0434\u0430 \u0434\u043b\u044f \u0441\u0432\u043e\u0435\u0433\u043e \u0436\u0435 \u0440\u0430\u0437\u0432\u0438\u0442\u0438\u044f.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p> <!----> <!----><\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/725086\/\"> https:\/\/habr.com\/ru\/post\/725086\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u041d\u0430\u0447\u0430\u043b\u043e<\/h2>\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u042f \u0443\u0447\u0443\u0441\u044c \u0432 \u0420\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e-\u0422\u0430\u0434\u0436\u0438\u043a\u0441\u043a\u043e\u043c \u0421\u043b\u0430\u0432\u044f\u043d\u0441\u043a\u043e\u043c \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u0435 (\u043d\u0430 \u043f\u0435\u0440\u0432\u043e\u043c \u043a\u0443\u0440\u0441\u0435), \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0443 \u043d\u0430\u0441 \u0432 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u0430\u043a \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u043e-\u0431\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430.<\/p>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043d\u0430\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0431\u0430\u043b\u043b\u043e\u0432 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435, \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u044b\u043b\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043e \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u043e\u043c.<\/p>\n<p>\u041e\u043d\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0434\u043b\u044f Android.<\/p>\n<figure class=\"float full-width\"><\/figure>\n<\/p>\n<p>\u041e\u0434\u043d\u0430\u043a\u043e, \u044f \u043d\u0435\u0434\u0430\u0432\u043d\u043e \u043f\u0435\u0440\u0435\u0448\u0451\u043b \u043d\u0430 iOS-\u0441\u0438\u0441\u0442\u0435\u043c\u0443, \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u043a \u043c\u043e\u0435\u043c\u0443 \u0443\u0434\u0438\u0432\u043b\u0435\u043d\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0442\u0430\u043c \u043d\u0435 \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c.<\/p>\n<figure class=\"float full-width\">\n<div><figcaption>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u043f\u043e\u0438\u0441\u043a\u0430 \u0432 App Store<\/figcaption><\/div>\n<\/figure>\n<p>\u041d\u0443 \u0438 \u0442\u0443\u0442, \u044f \u043f\u043e\u0434\u0443\u043c\u0430\u043b \u0447\u0442\u043e \u043d\u0430\u0434\u043e \u0431\u044b \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0447\u0442\u043e-\u0442\u043e \u0442\u0438\u043f\u0430 Telegram-\u0431\u043e\u0442\u0430 \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0443\u0441\u043f\u0435\u0432\u0430\u0435\u043c\u043e\u0441\u0442\u0438, \u0432 \u043a\u043e\u043d\u0446\u0435 \u043a\u043e\u043d\u0446\u043e\u0432 \u044d\u0442\u043e \u043c\u043d\u043e\u0433\u0438\u043c \u0434\u043e\u043b\u0436\u043d\u043e <strong>\u043f\u043e\u043c\u043e\u0447\u044c.<\/strong><\/p>\n<p>\u0414\u0430 \u0438 \u0432\u043e\u043e\u0431\u0449\u0435, Telegram \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432\u0435\u0437\u0434\u0435, \u044d\u0442\u043e \u043c\u043e\u044f \u043b\u044e\u0431\u0438\u043c\u0430\u044f \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u0434\u043b\u044f \u043e\u0431\u043c\u0435\u043d\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c\u0438.<\/p>\n<h2>\u041f\u043e\u0438\u0441\u043a endpoint&#8217;\u043e\u0432&#8230;<\/h2>\n<p>\u0420\u0430\u0437\u0443\u043c\u0435\u0435\u0442\u0441\u044f, \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442 \u0441\u0434\u0435\u043b\u0430\u043b \u043d\u0435\u043a\u0438\u0439 API \u0434\u043b\u044f \u0441\u0432\u043e\u0435\u0433\u043e Android-\u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430, \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u043e\u0432 \u043d\u0435 \u044f, \u043c\u043e\u0439 \u0434\u0440\u0443\u0433, \u0434\u0435\u043a\u043e\u043c\u043f\u0438\u043b\u0438\u0440\u043e\u0432\u0430\u043b APK \u0444\u0430\u0439\u043b \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u043b \u0435\u0433\u043e \u043c\u043d\u0435, \u043f\u043e\u0437\u0436\u0435 \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0432 &#171;\u0432\u044b\u0445\u043b\u043e\u043f&#187; \u044f \u043d\u0430\u0448\u0435\u043b \u0447\u0435\u0442\u044b\u0440\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u043c\u043d\u0435 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430<\/p>\n<p>\u0412 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u044b \u0434\u043b\u044f<\/p>\n<ul>\n<li>\n<p>\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u0440\u043e\u0444\u0438\u043b\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430 <\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u0435\u043c\u0435\u0441\u0442\u0440\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0431 \u0443\u0441\u043f\u0435\u0432\u0430\u0435\u043c\u043e\u0441\u0442\u0438 \u043f\u043e \u0432\u0441\u0435\u043c \u0434\u0438\u0441\u0446\u0438\u043f\u043b\u0438\u043d\u0430\u043c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0441\u0435\u043c\u0435\u0441\u0442\u0440\u0430.<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0443 \u0430 \u0434\u0430\u043b\u044c\u0448\u0435, \u0434\u0435\u043b\u043e \u0437\u0430 \u043c\u0430\u043b\u044b\u043c, \u043d\u0430\u0434\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e API \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435.<\/p>\n<h2>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Poetry, \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043e\u0431\u0451\u0440\u0442\u043a\u0438 \u043f\u043e\u0434 API<\/h2>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u043f\u0440\u043e\u0435\u043a\u0442<\/p>\n<pre><code class=\"bash\">poetry init <\/code><\/pre>\n<figure class=\"\"><\/figure>\n<p>\u042f \u0441\u0440\u0430\u0437\u0443 \u0441\u043e\u0437\u0434\u0430\u043b \u043f\u043e\u0447\u0442\u0438 \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0440\u043e\u0432\u043d\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043f\u0430\u043a\u0435\u0442\u043d\u0438\u043a <code>rtsu <\/code>\u0433\u0434\u0435 \u0438 \u0431\u0443\u0434\u0435\u0442 \u043b\u0435\u0436\u0430\u0442\u044c \u043d\u0430\u0448\u0430 \u043e\u0431\u0451\u0440\u0442\u043a\u0430.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430 <code>api.py<\/code><\/p>\n<pre><code class=\"python\">from aiohttp import ClientSession, ContentTypeError, client_exceptions from cashews import cache from typing import Optional, Union, Dict, TypeVar, Type, List, Self  from pydantic import BaseModel, parse_obj_as  from .exceptions import NotAuthorizedError, RtsuContentTypeError, ServerError, AuthError from .schemas import AuthSchema, Profile, Subject, AcademicYear  RTSU_API_BASE_URL = \"https:\/\/mobile.rtsu.tj\/api\/v1\" P = TypeVar(\"P\", bound=BaseModel)   class RTSUApi:     \"\"\"     This class provides for you functionality of RTSU public API     \"\"\"      def __init__(self, token: Optional[str] = None):         \"\"\"         Initializes `self`         :param token: A rtsu-api token (optional)         \"\"\"          self._api_token = token         self._http_client = ClientSession()      def set_token(self, token: str):         \"\"\"         Setups token         :param token: A token         :return:         \"\"\"         self._api_token = token      async def _make_request(             self,             method: str,             url_part: str,             response_model: Type[Union[List[BaseModel], BaseModel]],             json: Optional[Dict[str, Union[str, int]]] = None,             params: Optional[Dict[str, str]] = None,             auth_required: bool = False,     ) -> Union[P, List[P]]:         \"\"\"         Makes call to RTSU API         :param url_part: Part of RTSU-API url, example - \/auth         :param json: A json for sending         :param params: URI parameters for sending         :return: Response object         \"\"\"          if not json:             json = {}          if not params:             params = {}          headers = {}          if auth_required:             if not self._api_token:                 raise NotAuthorizedError(\"Not authorized, use `.auth` method.\")              headers['token'] = self._api_token          try:             response = await self._http_client.request(                 method,                 f\"{RTSU_API_BASE_URL}\/{url_part}\",                 json=json,                 params=params,                 headers=headers,                 ssl=False,             )         except (client_exceptions.ClientConnectionError, client_exceptions.ClientConnectorError) as e:             raise ServerError(f\"Connection error, details: {e}\")          if response.status != 200:             details = await response.text()             raise ServerError(                 f\"Server returned {response.status}, details: {details}\"             )          try:             deserialized_data = await response.json()         except ContentTypeError as e:             raise RtsuContentTypeError(                 e.message,             )          return parse_obj_as(response_model, deserialized_data)      async def auth(self, login: str, password: str) -> AuthSchema:         \"\"\"         Authenticates user         :param login: A login of user         :param password: A password of user         :return: RTSU token on success         \"\"\"          try:             response: AuthSchema = await self._make_request(                 \"POST\",                 \"auth\",                 AuthSchema,                 params={                     \"login\": login,                     \"password\": password,                 }             )         except ServerError as e:             raise AuthError(                 f\"Auth error, check login and password, message from server: {e.message}\"             )          self._api_token = response.token          return response      @cache.soft(ttl=\"24h\", soft_ttl=\"1m\")     async def get_profile(self) -> Profile:         \"\"\"         Returns profile of RTSU student         :return: `Profile`-response         \"\"\"          return await self._make_request(             \"GET\",             \"student\/profile\",             Profile,             auth_required=True,         )      async def get_academic_years(self) -> List[AcademicYear]:         \"\"\"         Returns `List` with `AcademicYear` objects         :return:         \"\"\"          return await self._make_request(             \"GET\",             \"student\/academic_years\",             List[AcademicYear],             auth_required=True,         )      @cache.soft(ttl=\"24h\", soft_ttl=\"1m\")     async def get_academic_year_subjects(self, year_id: int) -> List[Subject]:         \"\"\"         Returns `List` with `Subjects` of some year         :return:         \"\"\"          return await self._make_request(             \"GET\",             f\"student\/grades\/{year_id}\",             List[Subject],             auth_required=True,         )      async def get_current_year_id(self) -> int:         \"\"\"         Returns identifier of current year         :return:         \"\"\"          years = await self.get_academic_years()          return years[0].id      async def __aenter__(self) -> Self:         return self      async def __aexit__(self, exc_type, exc_val, exc_tb):         await self.close_session()      def __str__(self) -> str:         \"\"\"         Stringifies `RTSUApi` objects         :return:         \"\"\"          return f\"{self.__class__.__name__}&lt;token={self._api_token}>\"      async def close_session(self):         \"\"\"Frees inner resources\"\"\"         await self._http_client.close() <\/code><\/pre>\n<p>\u0427\u0442\u043e \u0442\u0443\u0442 \u0443 \u043d\u0430\u0441? \u0412 <code>_make_request<\/code> \u0443 \u043d\u0430\u0441 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f json \u0432 pydantic-\u0441\u0445\u0435\u043c\u0443 (\u043d\u0443 \u0438\u043b\u0438 \u043c\u043e\u0434\u0435\u043b\u044c?)<\/p>\n<p>\u041f\u0440\u043e\u0448\u0443 \u0437\u0430\u043c\u0435\u0442\u0438\u0442\u044c, \u0447\u0442\u043e \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u0437\u0430\u043c\u0435\u0447\u0430\u0442\u0435\u043b\u044c\u043d\u0443\u044e cashews \u0434\u043b\u044f \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432, \u0432 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438 soft-ttl \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0435\u0449\u0435 \u0438 \u0441\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442 \u043a\u043e\u0433\u0434\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0442\u0435\u0442\u0430 \u043f\u0430\u0434\u0430\u044e\u0442.<\/p>\n<p>\u0412 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u0436\u0435 \u043c\u0435\u0442\u043e\u0434\u0430\u0445 \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u044e \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \u0438 <code>response-schema<\/code> \u043d\u0443 \u0438 \u0434\u0451\u0440\u0433\u0430\u044e \u0442\u043e\u0442 \u0436\u0435 <code>_make_request<\/code><\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u0442\u0443\u0442 \u0435\u0441\u0442\u044c \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043a\u0440\u044b\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0443\u044e aiohttp-\u0441\u0435\u0441\u0441\u0438\u044e, \u043d\u0443 \u0442\u0443\u0442 \u043f\u043e\u043d\u044f\u0442\u043d\u043e, \u043f\u043e\u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u044b \u043c\u0430\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u044b <code>__aenter__<\/code> \u0438 <code>__aexit__ <\/code>\u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442 \u0432 <code>with<\/code>\u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u0445.<\/p>\n<p>\u041d\u0443 \u0438 <code>set_token<\/code>\u043c\u0435\u0442\u043e\u0434 \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e &#171;\u0432\u043f\u0438\u0445\u043d\u0443\u0442\u044c&#187; \u0442\u043e\u043a\u0435\u043d \u0432 \u043a\u043b\u0438\u0435\u043d\u0442, \u044d\u0442\u043e \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u043c \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435.<\/p>\n<h2>Pydantic-\u0441\u0445\u0435\u043c\u044b<\/h2>\n<p>\u0414\u043e\u043f\u0443\u0441\u0442\u0438\u043c, \u0437\u0430\u0433\u043b\u044f\u043d\u0435\u043c \u0432 <code>profile.py<\/code> \u0433\u0434\u0435 \u043b\u0435\u0436\u0438\u0442 <code>Profile-schema<\/code><\/p>\n<pre><code class=\"python\">from pydantic import Field  from .base import Base   class FullName(Base):     ru: str = Field(alias='RU')     tj: str = Field(alias='TJ')   class Faculty(FullName):     ...   class Speciality(FullName):     ...   class Profile(Base):     id: int = Field(alias='RecordBookNumber')     full_name: FullName = Field(alias='FullName')     faculty: Faculty = Field(alias=\"Faculty\")     course: int = Field(alias='Course')     training_period: int = Field(alias='TrainingPeriod')     level: str = Field(alias=\"TrainingLevel\")     entrance_year: str = Field(alias='YearUniversityEntrance') <\/code><\/pre>\n<p>\u041f\u043e\u0447\u0435\u043c\u0443 \u0442\u0430\u043a? API \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u043d\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0441\u0440\u0430\u0437\u0443 \u043d\u0430 \u0434\u0432\u0443\u0445 \u044f\u0437\u044b\u043a\u0430\u0445 (\u0440\u0443\u0441\u0441\u043a\u043e\u043c \u0438 \u0442\u0430\u0434\u0436\u0438\u043a\u0441\u043a\u043e\u043c)<\/p>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u044d\u0442\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0443\u0435\u0442 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e API \u0441\u0434\u0435\u043b\u0430\u043b\u0438 \u043e\u0447\u0435\u043d\u044c \u043a\u0440\u0438\u0432\u043e \u0438 \u0443\u0431\u043e\u0433\u043e, \u043d\u043e \u0447\u0442\u043e \u043f\u043e\u0434\u0435\u043b\u0430\u0442\u044c, \u0442\u0443\u0442 \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u0443\u044e \u043a\u0430\u0436\u0434\u044b\u0439 field \u043e\u0442 FullName \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u043f\u043e \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 RU, TJ \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435.<\/p>\n<p>\u0422\u0430\u043a\u0436\u0435, \u043f\u0440\u043e\u0448\u0443 \u0437\u0430\u043c\u0435\u0442\u0438\u0442\u044c \u0442\u043e, \u043a\u0430\u043a \u044d\u043b\u0435\u0433\u0430\u043d\u0442\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c field&#8217;\u044b pythonic \u043f\u0440\u0438 \u043f\u043e\u043c\u043e\u0449\u0438 <code>pydantic-aliases<\/code><\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0433\u043e \u044d\u0442\u043e\u0433\u043e, \u044f \u043f\u0440\u0438\u043d\u044f\u043b\u0441\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442, \u043d\u0430 \u0443\u0434\u0438\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u043d \u0445\u043e\u0440\u043e\u0448\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0442\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043e\u043d\u0438 \u043c\u043d\u0435 \u043d\u0443\u0436\u043d\u044b.<\/p>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0442\u0435\u0441\u0442\u044b \u043a \u044d\u0442\u043e\u043c\u0443 \u044f \u0442\u043e\u0436\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043b<\/p>\n<pre><code class=\"python\">import pytest import pytest_asyncio  from rtsu_students_bot.rtsu import RTSUApi  from .config import settings  pytest_plugins = ('pytest_asyncio',)   @pytest_asyncio.fixture() async def rtsu_client():     \"\"\"     Initializes client     :return: Prepared `RTSUApi` client     \"\"\"      async with RTSUApi() as api:         yield api   @pytest.mark.asyncio async def test_rtsu_login(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu login     :param rtsu_client: A RTSU API client     :return:     \"\"\"      resp = await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      assert resp.token is not None   @pytest.mark.asyncio async def test_rtsu_profile_fetching(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu profile fetching     :param rtsu_client:     :return:     \"\"\"      await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      profile = await rtsu_client.get_profile()      assert profile is not None     assert profile.full_name is not None   @pytest.mark.asyncio async def test_rtsu_academic_years_fetching(rtsu_client: RTSUApi):     \"\"\"     Tests rtsu academic years fetching     :param rtsu_client:     :return:     \"\"\"      await rtsu_client.auth(settings.rtsu_api_login, settings.rtsu_api_password)      years = await rtsu_client.get_academic_years()      assert type(years) == list     assert len(years) > 0   @pytest.mark.asyncio async def<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-347336","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/347336","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=347336"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/347336\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=347336"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=347336"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=347336"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}