hh-applicant-tool 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hh-applicant-tool might be problematic. Click here for more details.

Files changed (23) hide show
  1. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/PKG-INFO +25 -9
  2. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/README.md +23 -8
  3. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/api/errors.py +6 -6
  4. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/constants.py +1 -1
  5. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/add_handler.py +11 -0
  6. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/apply_similar.py +54 -18
  7. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/authorize.py +19 -0
  8. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/utils.py +1 -1
  9. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/pyproject.toml +1 -1
  10. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/__init__.py +0 -0
  11. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/__main__.py +0 -0
  12. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/api/__init__.py +0 -0
  13. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/api/client.py +0 -0
  14. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/color_log.py +0 -0
  15. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/main.py +0 -0
  16. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/__init__.py +0 -0
  17. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/call_api.py +0 -0
  18. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
  19. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/list_resumes.py +0 -0
  20. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/refresh_token.py +0 -0
  21. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/update_resumes.py +0 -0
  22. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/operations/whoami.py +0 -0
  23. {hh_applicant_tool-0.2.1 → hh_applicant_tool-0.2.3}/hh_applicant_tool/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -8,13 +8,21 @@ Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
11
12
  Requires-Dist: prettytable (>=3.6.0,<4.0.0)
12
13
  Requires-Dist: requests (>=2.28.2,<3.0.0)
13
14
  Description-Content-Type: text/markdown
14
15
 
15
16
  # HH Applicant Tool
16
17
 
17
- ![Publish to PyPI](https://github.com/s3rgeym/hh-applicant-tool/actions/workflows/publish.yml/badge.svg) [![PyPi Version](https://img.shields.io/pypi/v/hh-applicant-tool)](https://img.shields.io/pypi/v/hh-applicant-tool) [![Python Versions](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg)](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hh-applicant-tool)](https://img.shields.io/pypi/dm/hh-applicant-tool)
18
+ > ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
19
+
20
+ ![Publish to PyPI](https://github.com/s3rgeym/hh-applicant-tool/actions/workflows/publish.yml/badge.svg)
21
+ [![PyPi Version](https://img.shields.io/pypi/v/hh-applicant-tool)]()
22
+ [![Python Versions](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg)]()
23
+ [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/s3rgeym/hh-applicant-tool)]()
24
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/hh-applicant-tool)]()
25
+ [![Total Downloads](https://static.pepy.tech/badge/hh-applicant-tool)]()
18
26
 
19
27
  Утилита для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме.
20
28
 
@@ -23,13 +31,23 @@ Description-Content-Type: text/markdown
23
31
  - socat
24
32
  - python >= 3.10
25
33
 
26
- Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить.
34
+ Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить. В Arch-based:
35
+
36
+ ```bash
37
+ yay -S socat
38
+ ```
39
+
40
+ Пример работы:
41
+
42
+ ![image](https://github.com/user-attachments/assets/4936a8dd-f671-4788-8106-0470d5ce1dd6)
43
+ ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
44
+
27
45
 
28
- Данная утилита не может работать от root. Я не планирую добавлять поддержку Windows, но никто не мешает вам ее реализовать.
46
+ Данная утилита работает только в Linux. Для авторизации требуется наличие графического окружения (это не совсем верно, но мне лень расписывать...), после нее вы можете перенести `~/.config/hh-applicant-tool/config.json` на сервер и запускать утилиту через systemd или cron, либо же вообще делать это через **WSL**. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
29
47
 
30
48
  Предыстория.
31
49
 
32
- Был один знакомый знакомого, который работал хером. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отлчить Java от JavaScript, но я думаю, что в значительном числе случаев, тут имеют место такие вот рассылки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу. Долгое время (пару недель в октябре 2022) я делал массовые заявки с помощью консоли браузера:
50
+ Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отлчить Java от JavaScript, но я думаю, что в значительном числе случаев, тут имеют место такие вот рассылки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу. Долгое время (пару недель в октябре 2022) я делал массовые заявки с помощью консоли браузера:
33
51
 
34
52
  ```js
35
53
  $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
@@ -71,9 +89,7 @@ $ hh-applicant-tool apply-similar
71
89
  # Поднимаем резюме
72
90
  $ hh-applicant-tool update-resumes
73
91
 
74
- # Чистим заявки и баним за отказы говноконторы, нанявших на работу филолохинь,
75
- # астралохинь и прочих 3.14дарасов по блату, считающих, что погроммистом может
76
- # быть только ДОЦЕНТ МАТЕМАТИЧЕСКИХ НАУК 🤡
92
+ # Чистим заявки и баним за отказы говноконторы
77
93
  $ hh-applicant-tool clear-negotiations --blacklist-discard
78
94
  ```
79
95
 
@@ -119,7 +135,7 @@ https://hh.ru/employer/1918903
119
135
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
120
136
  | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
121
137
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
122
- | **call-api** | Вызов произвольного метода API с вводом результата. |
138
+ | **call-api** | Вызов произвольного метода API с выводом результата. |
123
139
  | **refresh-token** | Обновляет access_token. |
124
140
 
125
141
  Для начала нужно добавить обработчик протокола `hhandroid`, который используется Android-приложением для усложнения жизни честным автоматизаторам:
@@ -1,6 +1,13 @@
1
1
  # HH Applicant Tool
2
2
 
3
- ![Publish to PyPI](https://github.com/s3rgeym/hh-applicant-tool/actions/workflows/publish.yml/badge.svg) [![PyPi Version](https://img.shields.io/pypi/v/hh-applicant-tool)](https://img.shields.io/pypi/v/hh-applicant-tool) [![Python Versions](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg)](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hh-applicant-tool)](https://img.shields.io/pypi/dm/hh-applicant-tool)
3
+ > ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
4
+
5
+ ![Publish to PyPI](https://github.com/s3rgeym/hh-applicant-tool/actions/workflows/publish.yml/badge.svg)
6
+ [![PyPi Version](https://img.shields.io/pypi/v/hh-applicant-tool)]()
7
+ [![Python Versions](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg)]()
8
+ [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/s3rgeym/hh-applicant-tool)]()
9
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/hh-applicant-tool)]()
10
+ [![Total Downloads](https://static.pepy.tech/badge/hh-applicant-tool)]()
4
11
 
5
12
  Утилита для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме.
6
13
 
@@ -9,13 +16,23 @@
9
16
  - socat
10
17
  - python >= 3.10
11
18
 
12
- Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить.
19
+ Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить. В Arch-based:
20
+
21
+ ```bash
22
+ yay -S socat
23
+ ```
24
+
25
+ Пример работы:
26
+
27
+ ![image](https://github.com/user-attachments/assets/4936a8dd-f671-4788-8106-0470d5ce1dd6)
28
+ ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
29
+
13
30
 
14
- Данная утилита не может работать от root. Я не планирую добавлять поддержку Windows, но никто не мешает вам ее реализовать.
31
+ Данная утилита работает только в Linux. Для авторизации требуется наличие графического окружения (это не совсем верно, но мне лень расписывать...), после нее вы можете перенести `~/.config/hh-applicant-tool/config.json` на сервер и запускать утилиту через systemd или cron, либо же вообще делать это через **WSL**. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
15
32
 
16
33
  Предыстория.
17
34
 
18
- Был один знакомый знакомого, который работал хером. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отлчить Java от JavaScript, но я думаю, что в значительном числе случаев, тут имеют место такие вот рассылки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу. Долгое время (пару недель в октябре 2022) я делал массовые заявки с помощью консоли браузера:
35
+ Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отлчить Java от JavaScript, но я думаю, что в значительном числе случаев, тут имеют место такие вот рассылки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу. Долгое время (пару недель в октябре 2022) я делал массовые заявки с помощью консоли браузера:
19
36
 
20
37
  ```js
21
38
  $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
@@ -57,9 +74,7 @@ $ hh-applicant-tool apply-similar
57
74
  # Поднимаем резюме
58
75
  $ hh-applicant-tool update-resumes
59
76
 
60
- # Чистим заявки и баним за отказы говноконторы, нанявших на работу филолохинь,
61
- # астралохинь и прочих 3.14дарасов по блату, считающих, что погроммистом может
62
- # быть только ДОЦЕНТ МАТЕМАТИЧЕСКИХ НАУК 🤡
77
+ # Чистим заявки и баним за отказы говноконторы
63
78
  $ hh-applicant-tool clear-negotiations --blacklist-discard
64
79
  ```
65
80
 
@@ -105,7 +120,7 @@ https://hh.ru/employer/1918903
105
120
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
106
121
  | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
107
122
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
108
- | **call-api** | Вызов произвольного метода API с вводом результата. |
123
+ | **call-api** | Вызов произвольного метода API с выводом результата. |
109
124
  | **refresh-token** | Обновляет access_token. |
110
125
 
111
126
  Для начала нужно добавить обработчик протокола `hhandroid`, который используется Android-приложением для усложнения жизни честным автоматизаторам:
@@ -37,11 +37,11 @@ class ApiError(Exception):
37
37
  def response_headers(self) -> CaseInsensitiveDict:
38
38
  return self._response.headers
39
39
 
40
- def __getattr__(self, name: str) -> Any:
41
- try:
42
- return self._raw[name]
43
- except KeyError as ex:
44
- raise AttributeError(name) from ex
40
+ # def __getattr__(self, name: str) -> Any:
41
+ # try:
42
+ # return self._raw[name]
43
+ # except KeyError as ex:
44
+ # raise AttributeError(name) from ex
45
45
 
46
46
  def __str__(self) -> str:
47
47
  return str(self._raw)
@@ -58,7 +58,7 @@ class ClientError(ApiError):
58
58
  class BadRequest(ClientError):
59
59
  @property
60
60
  def limit_exceeded(self) -> bool:
61
- return any(x["value"] == "limit_exceeded" for x in self.errors)
61
+ return any(x["value"] == "limit_exceeded" for x in self._raw["errors"])
62
62
 
63
63
 
64
64
  class Forbidden(ClientError):
@@ -1,4 +1,4 @@
1
- DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
1
+ DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
2
2
  ANDROID_CLIENT_ID = (
3
3
  "HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
4
4
  )
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  import logging
3
+ import os
3
4
  from pathlib import Path
4
5
  from subprocess import check_call
5
6
 
@@ -36,6 +37,12 @@ class Operation(BaseOperation):
36
37
  )
37
38
 
38
39
  def run(self, args: Namespace) -> None:
40
+ # Проверка, запущен ли скрипт в WSL
41
+ if self.is_wsl():
42
+ print_err("⚠️ Предупреждение: Скрипт запущен в WSL 💩. Функциональность может быть ограничена или не работать вовсе.")
43
+ print_err("Рекомендуется запуск на нативных Linux-системах.")
44
+ return 1
45
+
39
46
  # TODO: с root не будет работать
40
47
  desktop_path = Path(
41
48
  "~/.local/share/applications/hhandroid.desktop"
@@ -48,3 +55,7 @@ class Operation(BaseOperation):
48
55
  else:
49
56
  print_err("⛔ Обработчик уже существует!")
50
57
  return 1
58
+
59
+ def is_wsl(self) -> bool:
60
+ """Проверяет, запущен ли скрипт в WSL."""
61
+ return "WSL_DISTRO_NAME" in os.environ
@@ -1,8 +1,8 @@
1
- # Этот модуль можно использовать как образец для других
2
1
  import argparse
3
2
  import logging
4
3
  import random
5
- from typing import TextIO
4
+ import time
5
+ from typing import TextIO, Tuple
6
6
 
7
7
  from ..api import ApiClient, ApiError, BadRequest
8
8
  from ..main import BaseOperation
@@ -16,6 +16,9 @@ logger = logging.getLogger(__package__)
16
16
  class Namespace(BaseNamespace):
17
17
  resume_id: str | None
18
18
  message_list: TextIO
19
+ force_message: bool
20
+ apply_interval: Tuple[float, float]
21
+ page_interval: Tuple[float, float]
19
22
 
20
23
 
21
24
  class Operation(BaseOperation):
@@ -34,6 +37,27 @@ class Operation(BaseOperation):
34
37
  default=False,
35
38
  action=argparse.BooleanOptionalAction,
36
39
  )
40
+ parser.add_argument(
41
+ "--apply-interval",
42
+ help="Интервал между отправкой откликов в секундах (X, X-Y)",
43
+ default="1-5",
44
+ type=self._parse_interval,
45
+ )
46
+ parser.add_argument(
47
+ "--page-interval",
48
+ help="Интервал между получением следующей страницы рекомендованных вакансий в секундах (X, X-Y)",
49
+ default="1-3",
50
+ type=self._parse_interval,
51
+ )
52
+
53
+ @staticmethod
54
+ def _parse_interval(interval: str) -> Tuple[float, float]:
55
+ """Парсит строку интервала и возвращает кортеж с минимальным и максимальным значениями."""
56
+ if "-" in interval:
57
+ min_interval, max_interval = map(float, interval.split("-"))
58
+ else:
59
+ min_interval = max_interval = float(interval)
60
+ return min(min_interval, max_interval), max(min_interval, max_interval)
37
61
 
38
62
  def run(self, args: Namespace) -> None:
39
63
  assert args.config["token"]
@@ -45,8 +69,6 @@ class Operation(BaseOperation):
45
69
  resume_id := args.resume_id or args.config["default_resume_id"]
46
70
  ):
47
71
  resumes: ApiListResponse = api.get("/resumes/mine")
48
- # Используем id первого резюме
49
- # TODO: создать 10 резюме и рассылать по 2000 откликов в сутки
50
72
  resume_id = resumes["items"][0]["id"]
51
73
  if args.message_list:
52
74
  application_messages = list(
@@ -54,33 +76,41 @@ class Operation(BaseOperation):
54
76
  )
55
77
  else:
56
78
  application_messages = [
57
- "Меня заинтересовала Ваша вакансия %(name)s",
58
- "Прошу рассмотреть мою кандидатуру на вакансию %(name)s",
79
+ "Меня заинтересовала ваша вакансия %(name)s",
80
+ "Прошу рассмотреть мою жалкую кандидатуру на вакансию %(name)s",
81
+ "Ваша вакансия %(name)s соответствует моим навыкам и опыту",
82
+ "Хочу присоединиться к вашей успешной команде лидеров рынка в качестве %(name)s",
83
+ "Мое резюме содержит все баззворды, указанные в вашей вакансии %(name)s",
59
84
  ]
85
+
86
+ apply_min_interval, apply_max_interval = args.apply_interval
87
+ page_min_interval, page_max_interval = args.page_interval
88
+
60
89
  self._apply_similar(
61
- api, resume_id, args.force_message, application_messages
90
+ api, resume_id, args.force_message, application_messages, apply_min_interval, apply_max_interval, page_min_interval, page_max_interval
62
91
  )
63
92
 
64
93
  def _get_vacancies(
65
- self, api: ApiClient, resume_id: str
94
+ self, api: ApiClient, resume_id: str, page_min_interval: float, page_max_interval: float
66
95
  ) -> list[VacancyItem]:
67
96
  rv = []
68
- # работает ограничение: глубина возвращаемых результатов не может быть больше 2000
69
- # Номер страницы (считается от 0, по умолчанию - 0)
70
97
  per_page = 100
71
98
  for page in range(20):
72
99
  res: ApiListResponse = api.get(
73
100
  f"/resumes/{resume_id}/similar_vacancies",
74
101
  page=page,
75
102
  per_page=per_page,
76
- # Мне кажется, что так поисковая выдача можно забиться неадекватами, которые по полгода кого-то ищут
77
- # Но так откликается на что-то уж совсем нерелевантное
78
- # order_by="publication_time",
79
103
  order_by="relevance",
80
104
  )
81
105
  rv.extend(res["items"])
82
106
  if page >= res["pages"] - 1:
83
107
  break
108
+
109
+ # Задержка перед получением следующей страницы
110
+ if page > 0:
111
+ interval = random.uniform(page_min_interval, page_max_interval)
112
+ time.sleep(interval)
113
+
84
114
  return rv
85
115
 
86
116
  def _apply_similar(
@@ -89,15 +119,20 @@ class Operation(BaseOperation):
89
119
  resume_id: str,
90
120
  force_message: bool,
91
121
  application_messages: list[str],
122
+ apply_min_interval: float,
123
+ apply_max_interval: float,
124
+ page_min_interval: float,
125
+ page_max_interval: float,
92
126
  ) -> None:
93
- # Получаем список рекомендованных вакансий и отправляем заявки
94
- # Проблема тут в том, что вакансии на которые мы отклимкались должны исчезать из поиска, но ОНИ ТАМ ПРИСУТСТВУЮТ. Так же есть вакансии с ебучими тестами, которые всегда вверху. Вроде можно отсортировать по дате, а потом постепенно уменьшать диапазон, но он не точный и округляется до 5 минут, а потому там повторы
95
127
  item: VacancyItem
96
- for item in self._get_vacancies(api, resume_id):
97
- # В рот я ебал вас и ваши тесты, пидоры
128
+ for item in self._get_vacancies(api, resume_id, page_min_interval, page_max_interval):
98
129
  if item["has_test"]:
99
130
  continue
100
- # Откликаемся на ваканчию
131
+
132
+ # Задержка перед отправкой отклика
133
+ interval = random.uniform(apply_min_interval, apply_max_interval)
134
+ time.sleep(interval)
135
+
101
136
  params = {
102
137
  "resume_id": resume_id,
103
138
  "vacancy_id": item["id"],
@@ -121,4 +156,5 @@ class Operation(BaseOperation):
121
156
  print_err("❗ Ошибка:", ex)
122
157
  if isinstance(ex, BadRequest) and ex.limit_exceeded:
123
158
  break
159
+
124
160
  print("📝 Отклики на вакансии разосланы!")
@@ -61,6 +61,17 @@ class Operation(BaseOperation):
61
61
  pass
62
62
 
63
63
  def run(self, args: Namespace) -> None:
64
+ # Проверяем, установлен ли socat
65
+ if not self.is_socat_installed():
66
+ print("⚠️ Предупреждение: socat не установлен. Для работы с unix-сокетами рекомендуется установить socat.")
67
+ print()
68
+ print("Вы можете установить socat с помощью вашего пакетного менеджера, например:")
69
+ print()
70
+ print(" - Debian/Ubuntu: sudo apt-get install socat")
71
+ print(" - Fedora: sudo dnf install socat")
72
+ print(" - Arch/Manjaro: sudo pacman -S socat")
73
+ print()
74
+
64
75
  oauth = OAuthClient(
65
76
  user_agent=(
66
77
  args.config["oauth_user_agent"] or args.config["user_agent"]
@@ -76,3 +87,11 @@ class Operation(BaseOperation):
76
87
  HHANDROID_SOCKET_PATH, oauth_client=oauth, config=args.config
77
88
  )
78
89
  server.serve_forever()
90
+
91
+ def is_socat_installed(self) -> bool:
92
+ """Проверяет, установлен ли socat в системе."""
93
+ try:
94
+ subprocess.run(["socat", "-h"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
95
+ return True
96
+ except FileNotFoundError:
97
+ return False
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from threading import Lock
8
8
  from typing import Any
9
9
 
10
- print_err = partial(print, file=sys.stderr)
10
+ print_err = partial(print, file=sys.stderr, flush=True)
11
11
 
12
12
 
13
13
  class AttrDict(dict):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hh-applicant-tool"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = ""
5
5
  authors = ["Senior YAML Developer <yamldeveloper@proton.me>"]
6
6
  readme = "README.md"