From 1dbb261ba17e66f4c1d599b352311ff21b2320ff Mon Sep 17 00:00:00 2001 From: Aidar Garikhanov Date: Fri, 29 Nov 2024 01:32:39 +0300 Subject: [PATCH] Refresh tokens when expired. Add docstrings --- vkusvill_green_labels/core/settings.py | 18 +++++++++++++++++- .../services/notification_service.py | 2 ++ .../services/updater_service.py | 16 +++++++++++++++- vkusvill_green_labels/services/vkusvill_api.py | 11 +++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/vkusvill_green_labels/core/settings.py b/vkusvill_green_labels/core/settings.py index ad2c08a..4f207b3 100644 --- a/vkusvill_green_labels/core/settings.py +++ b/vkusvill_green_labels/core/settings.py @@ -17,6 +17,8 @@ def load_vkusvill_settings() -> "VkusvillSettings": class EndpointSettings(BaseModel): + """Настройки конкретного API endpoint'а.""" + headers: dict[str, str] = Field(..., description="Headers for the request") query: dict[str, str] = Field(..., description="Query parameters for the request") url: HttpUrl = Field(..., description="URL of the endpoint") @@ -25,14 +27,20 @@ class EndpointSettings(BaseModel): class VkusvillSettings(BaseModel): + """Настройки API Вкусвилла.""" + green_labels: EndpointSettings create_token: EndpointSettings shop_info: EndpointSettings address_info: EndpointSettings update_cart: EndpointSettings + token_lifetime_seconds: PositiveInt = 30 * 24 * 3600 # 30 дней + # Однако по наблюдениям реальное время жизни токена около 2 месяцев. class TelegramSettings(BaseModel): + """Настройки телеграм бота.""" + bot_token: SecretStr = Field(..., description="Token from @BotFather") base_webhook_url: str | None = Field( None, description="Base URL for webhook (public DNS with HTTPS support)" @@ -56,6 +64,8 @@ def webhook_full_path(self) -> str | None: class DatabaseSettings(BaseSettings): + """Настройки подключения к базе данных.""" + dialect: str = "postgresql" driver: str = "asyncpg" username: SecretStr @@ -80,7 +90,7 @@ def url(self) -> str: class RedisSettings(BaseSettings): - """Настройки Redis.""" + """Настройки подключения к Redis.""" model_config = SettingsConfigDict(extra="ignore") @@ -88,6 +98,8 @@ class RedisSettings(BaseSettings): class SentrySettings(BaseSettings): + """Настройки Sentry.""" + model_config = SettingsConfigDict(extra="ignore") dsn: str | None = None @@ -105,6 +117,8 @@ def validate_environment(cls, v: str | None) -> str | None: class WebServerSettings(BaseSettings): + """Настройки веб-сервера.""" + model_config = SettingsConfigDict(extra="ignore") host: str = "127.0.0.1" @@ -112,6 +126,8 @@ class WebServerSettings(BaseSettings): class Settings(BaseSettings): + """Все настройки приложения.""" + model_config = SettingsConfigDict(env_nested_delimiter="__") vkusvill: VkusvillSettings diff --git a/vkusvill_green_labels/services/notification_service.py b/vkusvill_green_labels/services/notification_service.py index d6f2765..ae9e114 100644 --- a/vkusvill_green_labels/services/notification_service.py +++ b/vkusvill_green_labels/services/notification_service.py @@ -16,6 +16,7 @@ class NotificationService: _batch_size: ClassVar[int] = 20 async def notify_about_new_green_labels(self, user: User, items: list[GreenLabelItem]) -> None: + """Отправить уведомления о новых товарах с зелёными ценниками.""" for items_batch in batched(items, self._batch_size): text_items: list[fmt.Text] = [] @@ -28,6 +29,7 @@ async def notify_about_new_green_labels(self, user: User, items: list[GreenLabel @staticmethod def _format_item_text(item: GreenLabelItem) -> fmt.Text: + """Сформировать текст уведомления.""" item_line_elements: list[fmt.Text | list[fmt.Text]] = [] # Собираем заголовок (Наименование товара и оценка) diff --git a/vkusvill_green_labels/services/updater_service.py b/vkusvill_green_labels/services/updater_service.py index f384c6b..f820544 100644 --- a/vkusvill_green_labels/services/updater_service.py +++ b/vkusvill_green_labels/services/updater_service.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from datetime import UTC, datetime, timedelta import httpx @@ -22,6 +23,7 @@ class UpdaterService: notification_service: NotificationService async def update_green_labels(self) -> None: + """Обновить товары с зелёными ценниками для активных пользователей.""" users = await self.user_repo.get_users_for_notifications() if not users: logger.info("No users found for notifications") @@ -42,12 +44,13 @@ async def update_green_labels(self) -> None: logger.exception("Failed to send notification to user {}", user.tg_id) async def fetch_new_green_labels_for_user(self, user: User) -> list[GreenLabelItem]: + """Получить новые товары с зелеными ценники для заданного пользователя.""" logger.info("Fetching new green labels for user {}", user.tg_id) if not user.settings.locations: logger.warning("No locations found for user {}", user.tg_id) return [] location = user.settings.locations[0] - if user.settings.vkusvill_settings is None: + if self._need_to_authorize(user): async with httpx.AsyncClient() as client: vkusvill_api = VkusvillApi(client, settings.vkusvill) await vkusvill_api.authorize() @@ -75,6 +78,17 @@ async def fetch_new_green_labels_for_user(self, user: User) -> list[GreenLabelIt ) return filtered_green_labels + def _need_to_authorize(self, user: User) -> bool: + """Проверяет необходимость авторизации пользователя.""" + if not user.settings.vkusvill_settings: + return True + # Если токен протух, то нужно также переавторизовать пользователя + # поскольку API не выдаёт ошибку в этом случае + token_expiration_date = user.settings.vkusvill_settings.created_at + timedelta( + seconds=settings.vkusvill.token_lifetime_seconds + ) + return token_expiration_date < datetime.now(UTC) + @staticmethod def _get_items_difference( new_items: list[GreenLabelItem], old_items: list[GreenLabelItem] diff --git a/vkusvill_green_labels/services/vkusvill_api.py b/vkusvill_green_labels/services/vkusvill_api.py index dc983c7..b713db3 100644 --- a/vkusvill_green_labels/services/vkusvill_api.py +++ b/vkusvill_green_labels/services/vkusvill_api.py @@ -42,6 +42,8 @@ class VkusvillUnauthorizedError(VkusvillError): class GreenLabelItemResponse(GreenLabelItem): + """Информацию о товаре с зелённым ценником.""" + item_id: int = Field(..., validation_alias="id") rating: str = Field(..., validation_alias=AliasPath("rating", "all")) price: Decimal = Field(..., validation_alias=AliasPath("price", "price")) @@ -69,11 +71,14 @@ class CartInfoResponse(BaseModel): @dataclass class VkusvillApi: + """Клиент для мобильного API Вкусвилла.""" + client: httpx.AsyncClient settings: VkusvillSettings user_settings: VkusvillUserSettings | None = None async def create_new_user_token(self, device_id: str | None = None) -> TokenInfo: + """Сгенерировать новый токен для анонимного пользователя.""" if device_id is None: device_id = str(uuid4()) @@ -105,6 +110,7 @@ async def create_new_user_token(self, device_id: str | None = None) -> TokenInfo raise VkusvillApiError("Could not validate response") from exc async def authorize(self) -> None: + """Авторизовать пользователя.""" device_id = str(uuid4()) user_token = await self.create_new_user_token(device_id) self.user_settings = VkusvillUserSettings( @@ -114,6 +120,7 @@ async def authorize(self) -> None: async def get_shop_info( self, latitude: decimal.Decimal, longitude: decimal.Decimal ) -> ShopInfo | None: + """Получить информацию о ближайшем магазине по его координатам.""" if self.user_settings is None: raise VkusvillUnauthorizedError("User settings are not provided") @@ -144,6 +151,7 @@ async def get_shop_info( async def get_address_info( self, latitude: decimal.Decimal, longitude: decimal.Decimal ) -> AddressInfo | None: + """Получить информацию об адресе по его координатам.""" if self.user_settings is None: raise VkusvillUnauthorizedError("User settings are not provided") @@ -176,6 +184,7 @@ async def get_address_info( async def update_cart( self, latitude: decimal.Decimal, longitude: decimal.Decimal ) -> CartInfoResponse | None: + """Обновить информацию о корзине.""" if self.user_settings is None: raise VkusvillUnauthorizedError("User settings are not provided") @@ -205,6 +214,7 @@ async def update_cart( return cart_info async def fetch_green_labels(self) -> list[GreenLabelItem]: + """Получить все товары с зелёнными ценниками.""" if self.user_settings is None: raise VkusvillUnauthorizedError("User settings are not provided") @@ -246,6 +256,7 @@ async def fetch_green_labels(self) -> list[GreenLabelItem]: return all_items def _check_response_successful(self, response: httpx.Response) -> None: + """Проверить что запрос к API был успешен. В случае ошибки выбросить исключение.""" if response.status_code != 200: try: response_body = response.json()