Skip to content

Commit

Permalink
Merge pull request #62 from a1d4r/develop
Browse files Browse the repository at this point in the history
Deploy: Refresh tokens when expired
  • Loading branch information
a1d4r authored Nov 28, 2024
2 parents b585499 + 544af76 commit 5217ef9
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 2 deletions.
18 changes: 17 additions & 1 deletion vkusvill_green_labels/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)"
Expand All @@ -56,6 +64,8 @@ def webhook_full_path(self) -> str | None:


class DatabaseSettings(BaseSettings):
"""Настройки подключения к базе данных."""

dialect: str = "postgresql"
driver: str = "asyncpg"
username: SecretStr
Expand All @@ -80,14 +90,16 @@ def url(self) -> str:


class RedisSettings(BaseSettings):
"""Настройки Redis."""
"""Настройки подключения к Redis."""

model_config = SettingsConfigDict(extra="ignore")

dsn: RedisDsn


class SentrySettings(BaseSettings):
"""Настройки Sentry."""

model_config = SettingsConfigDict(extra="ignore")

dsn: str | None = None
Expand All @@ -105,13 +117,17 @@ def validate_environment(cls, v: str | None) -> str | None:


class WebServerSettings(BaseSettings):
"""Настройки веб-сервера."""

model_config = SettingsConfigDict(extra="ignore")

host: str = "127.0.0.1"
port: int = 8080


class Settings(BaseSettings):
"""Все настройки приложения."""

model_config = SettingsConfigDict(env_nested_delimiter="__")

vkusvill: VkusvillSettings
Expand Down
2 changes: 2 additions & 0 deletions vkusvill_green_labels/services/notification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []

Expand All @@ -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]] = []

# Собираем заголовок (Наименование товара и оценка)
Expand Down
16 changes: 15 additions & 1 deletion vkusvill_green_labels/services/updater_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta

import httpx

Expand All @@ -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")
Expand All @@ -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()
Expand Down Expand Up @@ -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]
Expand Down
11 changes: 11 additions & 0 deletions vkusvill_green_labels/services/vkusvill_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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(
Expand All @@ -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")

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 5217ef9

Please sign in to comment.