Fix Rate Limiter
This commit is contained in:
parent
4585d181f8
commit
c0a5d97e6c
|
@ -250,7 +250,7 @@ class JobApplicationManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
LoggingConfigurator.setup_logging(
|
LoggingConfigurator.setup_logging(
|
||||||
log_file="logs/hh_bot.log", console_output=False
|
log_file="logs/hh_bot.log", console_output=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self.orchestrator = AutomationOrchestrator()
|
self.orchestrator = AutomationOrchestrator()
|
||||||
|
|
|
@ -412,41 +412,44 @@ class VacancyApplicator:
|
||||||
logger.warning("Форма отклика не найдена в модальном окне - пропускаем")
|
logger.warning("Форма отклика не найдена в модальном окне - пропускаем")
|
||||||
return SubmissionResult.SKIPPED
|
return SubmissionResult.SKIPPED
|
||||||
|
|
||||||
self._add_cover_letter_if_possible(vacancy)
|
submit_button = None
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
for selector in submit_selectors:
|
for selector in submit_selectors:
|
||||||
try:
|
try:
|
||||||
submit_button = WebDriverWait(self.driver, 3).until(
|
submit_button = WebDriverWait(self.driver, 3).until(
|
||||||
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
||||||
)
|
)
|
||||||
if submit_button:
|
if submit_button:
|
||||||
button_text = submit_button.text.strip().lower()
|
break
|
||||||
|
|
||||||
if self._is_already_applied(button_text):
|
|
||||||
logger.warning(
|
|
||||||
f"⚠️ Кнопка указывает что уже откликались: "
|
|
||||||
f"{submit_button.text.strip()}"
|
|
||||||
)
|
|
||||||
return SubmissionResult.ALREADY_APPLIED
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Нажимаем кнопку отправки: "
|
|
||||||
f"{submit_button.text.strip()}"
|
|
||||||
)
|
|
||||||
self.driver.execute_script(
|
|
||||||
"arguments[0].click();", submit_button
|
|
||||||
)
|
|
||||||
time.sleep(3)
|
|
||||||
if self._check_success_message():
|
|
||||||
return SubmissionResult.SUCCESS
|
|
||||||
else:
|
|
||||||
return SubmissionResult.FAILED
|
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.warning("Кнопка отправки в модальном окне не найдена")
|
if not submit_button:
|
||||||
return SubmissionResult.FAILED
|
logger.warning("Кнопка отправки в модальном окне не найдена")
|
||||||
|
return SubmissionResult.FAILED
|
||||||
|
|
||||||
|
button_text = submit_button.text.strip().lower()
|
||||||
|
if self._is_already_applied(button_text):
|
||||||
|
logger.warning(
|
||||||
|
f"⚠️ Кнопка указывает что уже откликались: "
|
||||||
|
f"{submit_button.text.strip()}"
|
||||||
|
)
|
||||||
|
return SubmissionResult.ALREADY_APPLIED
|
||||||
|
|
||||||
|
self._add_cover_letter_if_possible(vacancy)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Нажимаем кнопку отправки: "
|
||||||
|
f"{submit_button.text.strip()}"
|
||||||
|
)
|
||||||
|
self.driver.execute_script(
|
||||||
|
"arguments[0].click();", submit_button
|
||||||
|
)
|
||||||
|
time.sleep(3)
|
||||||
|
if self._check_success_message():
|
||||||
|
return SubmissionResult.SUCCESS
|
||||||
|
else:
|
||||||
|
return SubmissionResult.FAILED
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в модальном окне: {e}")
|
logger.error(f"Ошибка в модальном окне: {e}")
|
||||||
|
@ -458,6 +461,8 @@ class VacancyApplicator:
|
||||||
if not settings.application.use_ai_cover_letters:
|
if not settings.application.use_ai_cover_letters:
|
||||||
logger.info("ИИ-сопроводительные письма отключены в настройках")
|
logger.info("ИИ-сопроводительные письма отключены в настройках")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.info("Ищем кнопку сопроводительного письма...")
|
||||||
cover_letter_button_selectors = [
|
cover_letter_button_selectors = [
|
||||||
'[data-qa="add-cover-letter"]',
|
'[data-qa="add-cover-letter"]',
|
||||||
'button[data-qa*="cover-letter"]',
|
'button[data-qa*="cover-letter"]',
|
||||||
|
@ -486,13 +491,14 @@ class VacancyApplicator:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not cover_letter_button:
|
if not cover_letter_button:
|
||||||
logger.info("Кнопка сопроводительного письма не найдена")
|
logger.info("📝 Кнопка сопроводительного письма не найдена")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Найдена кнопка сопроводительного письма, нажимаем...")
|
logger.info("📝 Найдена кнопка сопроводительного письма, нажимаем...")
|
||||||
self.driver.execute_script("arguments[0].click();", cover_letter_button)
|
self.driver.execute_script("arguments[0].click();", cover_letter_button)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
logger.info("Ищем поле для ввода письма...")
|
||||||
cover_letter_field_selectors = [
|
cover_letter_field_selectors = [
|
||||||
'textarea[data-qa*="cover-letter"]',
|
'textarea[data-qa*="cover-letter"]',
|
||||||
'textarea[name*="letter"]',
|
'textarea[name*="letter"]',
|
||||||
|
@ -509,15 +515,16 @@ class VacancyApplicator:
|
||||||
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
||||||
)
|
)
|
||||||
if cover_letter_field:
|
if cover_letter_field:
|
||||||
|
logger.info(f"Поле найдено: {selector}")
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not cover_letter_field:
|
if not cover_letter_field:
|
||||||
logger.warning("Поле для сопроводительного письма не найдено")
|
logger.warning("📝 Поле для сопроводительного письма не найдено")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Генерация сопроводительного письма...")
|
logger.info("📝 Генерация сопроводительного письма...")
|
||||||
|
|
||||||
from ..services.gemini_service import GeminiAIService
|
from ..services.gemini_service import GeminiAIService
|
||||||
|
|
||||||
|
@ -525,15 +532,15 @@ class VacancyApplicator:
|
||||||
cover_letter_text = gemini_service.generate_cover_letter(vacancy)
|
cover_letter_text = gemini_service.generate_cover_letter(vacancy)
|
||||||
|
|
||||||
if cover_letter_text:
|
if cover_letter_text:
|
||||||
logger.info("Заполняем сопроводительное письмо...")
|
logger.info("📝 Заполняем сопроводительное письмо...")
|
||||||
cover_letter_field.clear()
|
cover_letter_field.clear()
|
||||||
cover_letter_field.send_keys(cover_letter_text)
|
cover_letter_field.send_keys(cover_letter_text)
|
||||||
logger.info("✅ Сопроводительное письмо добавлено")
|
logger.info("✅ Сопроводительное письмо добавлено")
|
||||||
else:
|
else:
|
||||||
logger.warning("Не удалось сгенерировать сопроводительное письмо")
|
logger.warning("📝 Не удалось сгенерировать сопроводительное письмо")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Ошибка при добавлении сопроводительного письма: {e}")
|
logger.warning(f"📝 Ошибка при добавлении сопроводительного письма: {e}")
|
||||||
logger.info("Продолжаем без сопроводительного письма")
|
logger.info("Продолжаем без сопроводительного письма")
|
||||||
|
|
||||||
def _check_success_message(self) -> bool:
|
def _check_success_message(self) -> bool:
|
||||||
|
|
|
@ -23,29 +23,36 @@ class RateLimiter:
|
||||||
|
|
||||||
def wait_if_needed(self) -> None:
|
def wait_if_needed(self) -> None:
|
||||||
"""Ожидание если превышен лимит запросов"""
|
"""Ожидание если превышен лимит запросов"""
|
||||||
|
while True:
|
||||||
|
current_time = time.time()
|
||||||
|
self._cleanup_old_requests(current_time)
|
||||||
|
|
||||||
|
if len(self.request_times) < self.max_requests:
|
||||||
|
break
|
||||||
|
|
||||||
|
oldest_request_time = self.request_times[0]
|
||||||
|
wait_time = self.window_seconds - (current_time - oldest_request_time) + 0.1
|
||||||
|
|
||||||
|
logger.info(f"⏳ Достигнут лимит {self.max_requests} запросов. "
|
||||||
|
f"Ожидание {wait_time:.1f} секунд...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
def record_request(self) -> None:
|
||||||
|
"""Записать новый запрос"""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
self._cleanup_old_requests(current_time)
|
||||||
|
self.request_times.append(current_time)
|
||||||
|
|
||||||
|
def _cleanup_old_requests(self, current_time: float) -> None:
|
||||||
|
"""Удаление старых запросов из окна"""
|
||||||
while (self.request_times and
|
while (self.request_times and
|
||||||
current_time - self.request_times[0] >= self.window_seconds):
|
current_time - self.request_times[0] >= self.window_seconds):
|
||||||
self.request_times.popleft()
|
self.request_times.popleft()
|
||||||
|
|
||||||
if len(self.request_times) >= self.max_requests:
|
|
||||||
wait_time = self.window_seconds - (current_time - self.request_times[0])
|
|
||||||
if wait_time > 0:
|
|
||||||
logger.info(f"⏳ Достигнут лимит {self.max_requests} запросов в минуту. "
|
|
||||||
f"Ожидание {wait_time:.1f} секунд...")
|
|
||||||
time.sleep(wait_time + 0.1)
|
|
||||||
|
|
||||||
self.request_times.append(current_time)
|
|
||||||
|
|
||||||
def get_remaining_requests(self) -> int:
|
def get_remaining_requests(self) -> int:
|
||||||
"""Количество оставшихся запросов в текущем окне"""
|
"""Количество оставшихся запросов в текущем окне"""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
self._cleanup_old_requests(current_time)
|
||||||
while (self.request_times and
|
|
||||||
current_time - self.request_times[0] >= self.window_seconds):
|
|
||||||
self.request_times.popleft()
|
|
||||||
|
|
||||||
return max(0, self.max_requests - len(self.request_times))
|
return max(0, self.max_requests - len(self.request_times))
|
||||||
|
|
||||||
def get_status(self) -> str:
|
def get_status(self) -> str:
|
||||||
|
@ -71,8 +78,8 @@ class GeminiApiClient:
|
||||||
|
|
||||||
def generate_content(self, prompt: str) -> Optional[Dict]:
|
def generate_content(self, prompt: str) -> Optional[Dict]:
|
||||||
"""Генерация контента через Gemini API"""
|
"""Генерация контента через Gemini API"""
|
||||||
# Применяем rate limiting
|
|
||||||
self.rate_limiter.wait_if_needed()
|
self.rate_limiter.wait_if_needed()
|
||||||
|
self.rate_limiter.record_request()
|
||||||
|
|
||||||
url = f"{self.base_url}/models/{self.model}:generateContent"
|
url = f"{self.base_url}/models/{self.model}:generateContent"
|
||||||
|
|
||||||
|
@ -88,7 +95,8 @@ class GeminiApiClient:
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Отправка запроса к Gemini API. {self.rate_limiter.get_status()}")
|
status_after = self.rate_limiter.get_status()
|
||||||
|
logger.info(f"Отправка запроса к Gemini API. {status_after}")
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url,
|
url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
|
Loading…
Reference in New Issue