2025-06-27 09:57:34 +02:00
|
|
|
|
import os
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AppConstants:
|
|
|
|
|
|
|
|
|
|
HH_BASE_URL = "https://api.hh.ru"
|
|
|
|
|
HH_SITE_URL = "https://hh.ru"
|
|
|
|
|
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
|
|
|
|
GEMINI_MODEL = "gemini-2.0-flash"
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
DEFAULT_TIMEOUT = 20
|
2025-06-27 09:57:34 +02:00
|
|
|
|
API_PAUSE_SECONDS = 0.5
|
|
|
|
|
AI_REQUEST_PAUSE = 1
|
|
|
|
|
|
|
|
|
|
MAX_VACANCIES_PER_PAGE = 50
|
|
|
|
|
MAX_SEARCH_PAGES = 5
|
|
|
|
|
DEFAULT_MAX_APPLICATIONS = 40
|
|
|
|
|
|
|
|
|
|
DEFAULT_EXPERIENCE_FILE = "data/experience.txt"
|
|
|
|
|
DEFAULT_ABOUT_FILE = "data/about_me.txt"
|
|
|
|
|
DEFAULT_SKILLS_FILE = "data/skills.txt"
|
|
|
|
|
|
|
|
|
|
DEFAULT_AI_THRESHOLD = 0.7
|
|
|
|
|
MIN_AI_SCORE = 0.0
|
|
|
|
|
MAX_AI_SCORE = 1.0
|
|
|
|
|
|
|
|
|
|
SHORT_SEPARATOR_LENGTH = 50
|
|
|
|
|
LONG_SEPARATOR_LENGTH = 60
|
|
|
|
|
SHORT_TEXT_LIMIT = 50
|
|
|
|
|
MEDIUM_TEXT_LIMIT = 60
|
|
|
|
|
|
|
|
|
|
GEMINI_TEMPERATURE = 0.3
|
|
|
|
|
GEMINI_MAX_OUTPUT_TOKENS = 1000
|
|
|
|
|
|
|
|
|
|
LOG_FILE_MAX_SIZE_MB = 10
|
|
|
|
|
PERCENT_MULTIPLIER = 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class HHSearchConfig:
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
keywords: str = "python"
|
2025-06-27 09:57:34 +02:00
|
|
|
|
area: str = "1"
|
|
|
|
|
experience: str = "noExperience"
|
|
|
|
|
per_page: int = AppConstants.MAX_VACANCIES_PER_PAGE
|
|
|
|
|
max_pages: int = 3
|
|
|
|
|
order_by: str = "publication_time"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class BrowserConfig:
|
|
|
|
|
|
|
|
|
|
headless: bool = False
|
|
|
|
|
wait_timeout: int = 15
|
|
|
|
|
page_load_timeout: int = 30
|
|
|
|
|
implicit_wait: int = 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ApplicationConfig:
|
|
|
|
|
|
|
|
|
|
max_applications: int = AppConstants.DEFAULT_MAX_APPLICATIONS
|
|
|
|
|
pause_min: float = 3.0
|
|
|
|
|
pause_max: float = 6.0
|
|
|
|
|
manual_login: bool = True
|
2025-06-28 18:39:23 +02:00
|
|
|
|
use_ai_cover_letters: bool = True
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class GeminiConfig:
|
|
|
|
|
|
|
|
|
|
api_key: str = ""
|
|
|
|
|
model: str = AppConstants.GEMINI_MODEL
|
|
|
|
|
base_url: str = AppConstants.GEMINI_BASE_URL
|
|
|
|
|
match_threshold: float = AppConstants.DEFAULT_AI_THRESHOLD
|
2025-06-28 19:06:40 +02:00
|
|
|
|
|
|
|
|
|
max_requests_per_minute: int = 15
|
|
|
|
|
rate_limit_window_seconds: int = 61
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ResumeConfig:
|
|
|
|
|
|
|
|
|
|
experience_file: str = AppConstants.DEFAULT_EXPERIENCE_FILE
|
|
|
|
|
about_me_file: str = AppConstants.DEFAULT_ABOUT_FILE
|
|
|
|
|
skills_file: str = AppConstants.DEFAULT_SKILLS_FILE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ResumeFileManager:
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def create_sample_files() -> None:
|
|
|
|
|
data_dir = Path("data")
|
|
|
|
|
data_dir.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
experience_file = data_dir / "experience.txt"
|
|
|
|
|
if not experience_file.exists():
|
|
|
|
|
experience_file.write_text(
|
|
|
|
|
"""
|
|
|
|
|
Опыт работы:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
- ноль
|
2025-06-27 09:57:34 +02:00
|
|
|
|
""".strip(),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
print(f"✅ Создан файл: {experience_file}")
|
|
|
|
|
|
|
|
|
|
about_file = data_dir / "about_me.txt"
|
|
|
|
|
if not about_file.exists():
|
|
|
|
|
about_file.write_text(
|
|
|
|
|
"""
|
|
|
|
|
О себе:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
Котенок.
|
2025-06-27 09:57:34 +02:00
|
|
|
|
""".strip(),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
print(f"✅ Создан файл: {about_file}")
|
|
|
|
|
|
|
|
|
|
skills_file = data_dir / "skills.txt"
|
|
|
|
|
if not skills_file.exists():
|
|
|
|
|
skills_file.write_text(
|
|
|
|
|
"""
|
|
|
|
|
Технические навыки:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
- Мяу
|
2025-06-27 09:57:34 +02:00
|
|
|
|
""".strip(),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
print(f"✅ Создан файл: {skills_file}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UIFormatter:
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def create_separator(long: bool = False) -> str:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
length = (
|
|
|
|
|
AppConstants.LONG_SEPARATOR_LENGTH
|
|
|
|
|
if long
|
|
|
|
|
else AppConstants.SHORT_SEPARATOR_LENGTH
|
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
return "=" * length
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def truncate_text(text: str, medium: bool = False) -> str:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
limit = (
|
|
|
|
|
AppConstants.MEDIUM_TEXT_LIMIT if medium else AppConstants.SHORT_TEXT_LIMIT
|
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
return text[:limit]
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def format_percentage(value: float, total: float) -> str:
|
|
|
|
|
if total <= 0:
|
|
|
|
|
return "0.0%"
|
|
|
|
|
percentage = (value / total) * AppConstants.PERCENT_MULTIPLIER
|
|
|
|
|
return f"{percentage:.1f}%"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def print_section_header(title: str, long: bool = False) -> None:
|
|
|
|
|
separator = UIFormatter.create_separator(long)
|
|
|
|
|
print(f"\n{separator}")
|
|
|
|
|
print(title)
|
|
|
|
|
print(separator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Settings:
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self._load_env()
|
|
|
|
|
|
|
|
|
|
self.hh_search = HHSearchConfig()
|
|
|
|
|
self.browser = BrowserConfig()
|
|
|
|
|
self.application = ApplicationConfig()
|
|
|
|
|
self.gemini = GeminiConfig(api_key=os.getenv("GEMINI_API_KEY", ""))
|
|
|
|
|
self.resume = ResumeConfig()
|
|
|
|
|
|
|
|
|
|
self._validate_config()
|
|
|
|
|
|
|
|
|
|
def _load_env(self) -> None:
|
|
|
|
|
try:
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
except ImportError:
|
|
|
|
|
print("💡 Установите python-dotenv для работы с .env файлами")
|
|
|
|
|
|
|
|
|
|
def _validate_config(self) -> None:
|
|
|
|
|
if not self.gemini.api_key:
|
|
|
|
|
print("⚠️ GEMINI_API_KEY не установлен в переменных окружения")
|
|
|
|
|
|
|
|
|
|
data_dir = Path("data")
|
|
|
|
|
data_dir.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
logs_dir = Path("logs")
|
|
|
|
|
logs_dir.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
def update_search_keywords(self, keywords: str) -> None:
|
|
|
|
|
self.hh_search.keywords = keywords
|
|
|
|
|
print(f"🔄 Обновлены ключевые слова: {keywords}")
|
|
|
|
|
|
|
|
|
|
def enable_ai_matching(self) -> bool:
|
|
|
|
|
return bool(self.gemini.api_key)
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
def get_exclude_keywords(self) -> list:
|
2025-06-28 18:30:56 +02:00
|
|
|
|
return ["стажер", "cv"]
|
2025-06-28 17:58:36 +02:00
|
|
|
|
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
settings = Settings()
|