diff --git a/README.md b/README.md index f178826..35a21a8 100644 --- a/README.md +++ b/README.md @@ -149,13 +149,12 @@ $ hh-applicant-tool refresh-token | **Linux** | `~/.config/hh-applicant-tool/config.json` | -Через этот файл, например, можно задать кастомный `user_agent`: +Через конфиг можно задать дополнительные настройки: -```json -{ - "user_agent": "Mozilla/5.0 YablanBrowser" -} -``` +| Имя атрибута | Описание | +| `user_agent` | Кастомный юзерагент, передаваемый при кажом запросе, например, `Mozilla/5.0 YablanBrowser` | +| `proxy_url` | Прокси, используемый для всех запросов, например, `socks5h://127.0.0.1:9050` | +| `reply_message` | Сообщение для ответа работодателю при отклике на вакансии, см. формат сообщений | ### Описание команд diff --git a/hh_applicant_tool/api/client.py b/hh_applicant_tool/api/client.py index ba752f8..868322e 100644 --- a/hh_applicant_tool/api/client.py +++ b/hh_applicant_tool/api/client.py @@ -37,6 +37,7 @@ class BaseClient: _: dataclasses.KW_ONLY # TODO: сделать генерацию User-Agent'а как в приложении user_agent: str | None = None + proxies: dict | None = None session: Session | None = None previous_request_time: float = 0.0 delay: float = 0.334 @@ -47,8 +48,8 @@ def __post_init__(self) -> None: self.session = session = requests.session() session.headers.update( { - **self.additional_headers(), "User-Agent": self.user_agent or self.default_user_agent(), + **self.additional_headers(), } ) logger.debug("Default Headers: %r", session.headers) @@ -101,10 +102,13 @@ def request( logger.debug("wait %fs before request", delay) time.sleep(delay) has_body = method in ["POST", "PUT"] + user_agent = self.user_agent or self.default_user_agent() + logger.debug(f"{user_agent = }") response = self.session.request( method, url, **{"data" if has_body else "params": params}, + proxies=self.proxies, allow_redirects=False, ) try: diff --git a/hh_applicant_tool/main.py b/hh_applicant_tool/main.py index 631c583..b10a3ab 100644 --- a/hh_applicant_tool/main.py +++ b/hh_applicant_tool/main.py @@ -5,13 +5,13 @@ import sys from abc import ABCMeta, abstractmethod from importlib import import_module -from os import getenv from pathlib import Path from pkgutil import iter_modules -from typing import Sequence +from typing import Sequence, Literal from .api import ApiClient from .color_log import ColorHandler from .utils import Config, get_config_path +from os import getenv DEFAULT_CONFIG_PATH = ( get_config_path() / __package__.replace("_", "-") / "config.json" @@ -34,6 +34,15 @@ class Namespace(argparse.Namespace): config: Config verbosity: int delay: float + user_agent: str + proxy_url: str + + +def get_proxies(args: Namespace) -> dict[Literal["http", "https"], str | None]: + return { + "http": args.config["proxy_url"] or getenv("HTTP_PROXY"), + "https": args.config["proxy_url"] or getenv("HTTPS_PROXY"), + } def get_api(args: Namespace) -> ApiClient: @@ -41,8 +50,9 @@ def get_api(args: Namespace) -> ApiClient: api = ApiClient( access_token=token.get("access_token"), refresh_token=token.get("refresh_token"), - user_agent=args.config["user_agent"], delay=args.delay, + user_agent=args.config["user_agent"], + proxies=get_proxies(args), ) return api @@ -87,6 +97,12 @@ def create_parser(self) -> argparse.ArgumentParser: default=0.334, help="Задержка между запросами к API HH", ) + parser.add_argument( + "--user-agent", help="User-Agent для каждого запроса" + ) + parser.add_argument( + "--proxy-url", help="Прокси, используемый для запросов к API" + ) subparsers = parser.add_subparsers(help="commands") package_dir = Path(__file__).resolve().parent / OPERATIONS for _, module_name, _ in iter_modules([str(package_dir)]): @@ -94,7 +110,8 @@ def create_parser(self) -> argparse.ArgumentParser: op: BaseOperation = mod.Operation() op_parser = subparsers.add_parser( module_name.replace("_", "-"), - description=op.__doc__, formatter_class=self.ArgumentFormatter + description=op.__doc__, + formatter_class=self.ArgumentFormatter, ) op_parser.set_defaults(run=op.run) op.setup_parser(op_parser) diff --git a/hh_applicant_tool/operations/apply_similar.py b/hh_applicant_tool/operations/apply_similar.py index b4a6115..035c83f 100644 --- a/hh_applicant_tool/operations/apply_similar.py +++ b/hh_applicant_tool/operations/apply_similar.py @@ -9,10 +9,10 @@ from ..api import ApiClient, ApiError, BadRequest from ..main import BaseOperation from ..main import Namespace as BaseNamespace, get_api -from ..telemetry_client import TelemetryError -from ..telemetry_client import get_client as get_telemetry_client +from ..telemetry_client import TelemetryClient, TelemetryError from ..types import ApiListResponse, VacancyItem -from ..utils import fix_datetime, print_err, truncate_string, random_text +from ..utils import fix_datetime, truncate_string, random_text +from requests import Session logger = logging.getLogger(__package__) @@ -159,7 +159,11 @@ def _apply_similar( search: str | None = None, reply_message: str | None = None, ) -> None: - telemetry_client = get_telemetry_client() + # TODO: вынести куда-нибудь в функцию + session = Session() + session.headers["User-Agent"] = "Mozilla/5.0 (HHApplicantTelemetry/1.0)" + session.proxies = dict(api.session.proxies) + telemetry_client = TelemetryClient(session=session) telemetry_data = defaultdict(dict) vacancies = self._get_vacancies( diff --git a/hh_applicant_tool/telemetry_client.py b/hh_applicant_tool/telemetry_client.py index a1f5ee8..b4858d4 100644 --- a/hh_applicant_tool/telemetry_client.py +++ b/hh_applicant_tool/telemetry_client.py @@ -3,7 +3,6 @@ from urllib.parse import urljoin import requests from typing import Optional, Dict, Any -from functools import cache import logging import base64 @@ -64,8 +63,3 @@ def send_telemetry( json.JSONDecodeError, ) as ex: raise TelemetryError(str(ex)) from ex - - -@cache -def get_client() -> TelemetryClient: - return TelemetryClient() diff --git a/hh_applicant_tool/utils.py b/hh_applicant_tool/utils.py index 57590e4..64c3894 100644 --- a/hh_applicant_tool/utils.py +++ b/hh_applicant_tool/utils.py @@ -10,7 +10,8 @@ from typing import Any from os import getenv from .constants import INVALID_ISO8601_FORMAT -import re, random +import re +import random print_err = partial(print, file=sys.stderr, flush=True) diff --git a/poetry.lock b/poetry.lock index 1fdb92f..6da2167 100644 --- a/poetry.lock +++ b/poetry.lock @@ -563,6 +563,18 @@ files = [ {file = "PyQt6_WebEngineSubwheel_Qt6-6.7.3-py3-none-win_amd64.whl", hash = "sha256:59414eca4c67a3cd2b97f993f8df284480817b0d69ea45ca9922be2157b2a87f"}, ] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "7.4.4" @@ -600,6 +612,7 @@ files = [ certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} urllib3 = ">=1.21.1,<3" [package.extras] @@ -752,4 +765,4 @@ qt = ["pyqt6", "pyqt6-webengine"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "bcedf3ec880f68130da203180f42339fdd9b45651c2fdfa387c71cf556985dd6" +content-hash = "edc4d8e66fdf9b15ecaf64ad872108989fba6405591434230ae806c316eeca31" diff --git a/pyproject.toml b/pyproject.toml index d699486..eaad1c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hh-applicant-tool" -version = "0.3.7" +version = "0.3.8" description = "" authors = ["Senior YAML Developer "] readme = "README.md" @@ -8,7 +8,7 @@ packages = [{include = "hh_applicant_tool"}] [tool.poetry.dependencies] python = "^3.10" -requests = "^2.28.2" +requests = {extras = ["socks"], version = "^2.32.3"} prettytable = "^3.6.0" pyqt6 = { version = "^6.7.1", optional = true } pyqt6-webengine = { version = "^6.7.0", optional = true }