| | import time |
| | from functools import wraps |
| | from loguru import logger |
| | from typing import Any, Callable |
| | import os |
| | import asyncio |
| | import httpx |
| |
|
| | def timing_decorator_async(func: Callable) -> Callable: |
| | """ |
| | Decorator đo thời gian thực thi của hàm async, log thời lượng xử lý. |
| | Dùng cho async def. |
| | """ |
| | @wraps(func) |
| | async def wrapper(*args: Any, **kwargs: Any) -> Any: |
| | start_time = time.time() |
| | result = await func(*args, **kwargs) |
| | end_time = time.time() |
| | duration = end_time - start_time |
| | logger.info(f"[TIMING][async] {func.__name__} took {duration:.2f} seconds to execute") |
| | return result |
| | return wrapper |
| |
|
| | def timing_decorator_sync(func: Callable) -> Callable: |
| | """ |
| | Decorator đo thời gian thực thi của hàm sync, log thời lượng xử lý. |
| | Dùng cho def thường. |
| | """ |
| | @wraps(func) |
| | def wrapper(*args: Any, **kwargs: Any) -> Any: |
| | start_time = time.time() |
| | result = func(*args, **kwargs) |
| | end_time = time.time() |
| | duration = end_time - start_time |
| | logger.info(f"[TIMING][sync] {func.__name__} took {duration:.2f} seconds to execute") |
| | return result |
| | return wrapper |
| |
|
| | def setup_logging(log_level: str = "INFO") -> None: |
| | """ |
| | Thiết lập logging với loguru, log ra file và console. |
| | Input: log_level (str) - mức log. |
| | Output: None. |
| | """ |
| | logger.remove() |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | logger.add( |
| | lambda msg: print(msg), |
| | level=log_level, |
| | format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", |
| | ) |
| |
|
| | def extract_command(text: str) -> tuple[str, str]: |
| | """ |
| | Tách lệnh (bắt đầu bằng \) và phần còn lại từ message. |
| | Input: text (str) - message từ user. |
| | Output: (command, remaining_text) - tuple (str, str). |
| | """ |
| | if not text.startswith("\\"): |
| | return "", text |
| | |
| | parts = text.split(maxsplit=1) |
| | command = parts[0][1:] |
| | remaining = parts[1] if len(parts) > 1 else "" |
| | return command, remaining |
| |
|
| | def extract_keywords(text: str, keywords: list[str]) -> list[str]: |
| | """ |
| | Tìm các từ khóa xuất hiện trong message. |
| | Input: text (str), keywords (list[str]) |
| | Output: list[str] các từ khóa tìm thấy. |
| | """ |
| | return [keyword for keyword in keywords if keyword.lower() in text.lower()] |
| |
|
| | def ensure_log_dir(): |
| | """ |
| | Đảm bảo thư mục logs tồn tại, tạo nếu chưa có. |
| | Input: None |
| | Output: None |
| | """ |
| | |
| |
|
| | def validate_config(settings) -> None: |
| | """ |
| | Kiểm tra các biến môi trường/config bắt buộc, raise lỗi nếu thiếu. |
| | Input: settings (Settings) |
| | Output: None (raise RuntimeError nếu thiếu) |
| | """ |
| | missing = [] |
| | for field in [ |
| | 'facebook_verify_token', 'facebook_app_secret', |
| | 'google_sheets_credentials_file', 'google_sheets_token_file', 'conversation_sheet_id', |
| | 'supabase_url', 'supabase_key']: |
| | if not getattr(settings, field, None): |
| | missing.append(field) |
| | if missing: |
| | raise RuntimeError(f"Missing config: {', '.join(missing)}") |
| |
|
| | def get_logger(): |
| | return logger |
| |
|
| | async def call_endpoint_with_retry(client, url, payload, max_retries=3, base_timeout=30, headers=None): |
| | logger = get_logger() |
| | timeout = base_timeout |
| | for attempt in range(1, max_retries + 1): |
| | try: |
| | response = await client.post(url, json=payload, timeout=timeout, headers=headers) |
| | response.raise_for_status() |
| | return response |
| | except httpx.TimeoutException as e: |
| | if attempt == max_retries: |
| | raise |
| | else: |
| | logger.warning(f"Timeout (attempt {attempt}/{max_retries}), retrying with timeout={timeout * 2}s...") |
| | timeout *= 2 |
| | await asyncio.sleep(1) |
| | except httpx.HTTPStatusError as e: |
| | logger.error(f"HTTP error: {e.response.status_code} - {e.response.text}") |
| | raise |
| | except Exception as e: |
| | logger.error(f"Other error: {e}") |
| | raise |