hh-applicant-tool 0.4.0__tar.gz → 0.5.0__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 (27) hide show
  1. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/PKG-INFO +8 -24
  2. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/README.md +7 -23
  3. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/main.py +10 -9
  4. hh_applicant_tool-0.5.0/hh_applicant_tool/mixins.py +13 -0
  5. hh_applicant_tool-0.5.0/hh_applicant_tool/operations/apply_similar.py +356 -0
  6. hh_applicant_tool-0.5.0/hh_applicant_tool/operations/get_employer_contacts.py +103 -0
  7. hh_applicant_tool-0.5.0/hh_applicant_tool/operations/reply_employers.py +154 -0
  8. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/telemetry_client.py +27 -6
  9. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/utils.py +29 -13
  10. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/pyproject.toml +2 -1
  11. hh_applicant_tool-0.4.0/hh_applicant_tool/operations/apply_similar.py +0 -443
  12. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/__init__.py +0 -0
  13. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/__main__.py +0 -0
  14. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/api/__init__.py +0 -0
  15. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/api/client.py +0 -0
  16. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/api/errors.py +0 -0
  17. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/color_log.py +0 -0
  18. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/constants.py +0 -0
  19. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/__init__.py +0 -0
  20. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/authorize.py +0 -0
  21. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/call_api.py +0 -0
  22. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
  23. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/list_resumes.py +0 -0
  24. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/refresh_token.py +0 -0
  25. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/update_resumes.py +0 -0
  26. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/hh_applicant_tool/operations/whoami.py +0 -0
  27. {hh_applicant_tool-0.4.0 → hh_applicant_tool-0.5.0}/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.4.0
3
+ Version: 0.5.0
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -32,10 +32,10 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  ### Описание
34
34
 
35
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/+aSjr8qM_AP85ZDBi (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
35
+ Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Обсуждения без отзывов удаляются. Данные на сервер передаются анонимно. Для этого собираются данные о работодателях и их вакансиях. Никакие персональные данные пользователя утилиты под которыми, вы авторизуетесь никуда не отправляются — только работодателей и рекрутеров. Отправку этих данных можно отключить (флаг `--disable-telemetry`), но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги отзывы удаляет, или мудак его запугает и жалоб в РКН накидает, а последний всегда найдет за что сайт заблокировать. Единственное место где можно написать отзыв — это **Telegram**.
36
36
 
37
37
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
38
- asdf/pyenv/conda и что-то еще...
38
+ asdf/pyenv/conda и что-то еще.
39
39
 
40
40
  Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести конфиг на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
41
41
 
@@ -240,10 +240,11 @@ https://hh.ru/employer/1918903
240
240
  | **list-resumes** | Список резюме |
241
241
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
242
242
  | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками. |
243
+ | **reply-employers** | Ответить во все чаты с работодателями, где нет ответа либо не прочитали ваш предыдущий ответ |
243
244
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
244
245
  | **call-api** | Вызов произвольного метода API с выводом результата. |
245
246
  | **refresh-token** | Обновляет access_token. |
246
- | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, с сайта фирмы и могут быть удалены только по просьбе уполнамоченного лица. Данная функция готова и будет доступна после 100 ⭐ |
247
+ | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Это функционал для избранных, но в группе есть бесплатный бот с тем же функционалом. |
247
248
 
248
249
  ### Формат текста сообщений
249
250
 
@@ -282,26 +283,9 @@ https://hh.ru/employer/1918903
282
283
 
283
284
  Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
284
285
 
285
- ### Сбор данных
286
+ Для создания своих плагинов прочитайте документацию:
286
287
 
287
- > Данный функционал можно отключить с помощью специльного флага, но ради котят и из-за ненависти к херкам не делайте этого!
288
-
289
- Утилита собирает и передает на сервер разработчика следующую информацию:
290
-
291
- 1. Название вакансии.
292
- 1. Тип вакансии (открытая/закрытая).
293
- 1. Город, в котором размещена вакансия.
294
- 1. Информация о зарплате (минимальная, максимальная, валюта, указана ли зарплата до вычета налогов).
295
- 1. Прямая ссылка на вакансию.
296
- 1. Дата создания вакансии.
297
- 1. Дата публикации вакансии.
298
- 1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, которая в дальнейшем _может_ храниться в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля. Данная информация может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и подписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова.
299
- 1. Название компании.
300
- 1. Тип компании.
301
- 1. Описание компании.
302
- 1. Ссылка на сайт компании.
303
- 1. Город, в котором находится компания.
304
-
305
- **УТИЛИТА НЕ ПЕРЕДАЕТ НИКАКИХ ПЕРСОНАЛЬНЫХ ДАННЫХ ПОЛЬЗОВАТЕЛЕЙ — ТОЛЬКО ДАННЫЕ ВСЯКИХ РАБОТАДАТЕЛЕЙ И ИХ ОВЧАРОК. СЕРВЕР НЕ ХРАНИТ IP ОТПРАВИТЕЛЯ. ЛОГИ НА СЕРВЕРЕ НЕ ВЕДУТСЯ. ТАК ЖЕ Я ОБЕЩАЮ, ЧТО УТИЛИТА ВСЕГДА БУДЕТ БЕСПЛАТНОЙ, ВСЕ КТО ЕЮ ПЫТАЮТСЯ ТОРГОВАТЬ — УЕБКИ, И У НИХ ПОЧЕРНЕЕТ И ОТВАЛИТСЯ ХУЙ. ЕДИНСТВЕННАЯ ПЛАТА ЗА ЕЕ ИСПОЛЬЗОВАНИЕ — ЭТО ПОМОЩЬ В ПАРСИНГЕ САЙТА HEADHUNTER (МЕНЯ ИНТЕРЕСУЕТ ЕГО БАЗА КОМПАНИЙ).**
288
+ * [HH.RU OpenAPI](https://api.hh.ru/openapi/redoc)
306
289
 
290
+ Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате.
307
291
 
@@ -13,10 +13,10 @@
13
13
 
14
14
  ### Описание
15
15
 
16
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/+aSjr8qM_AP85ZDBi (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
16
+ Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Обсуждения без отзывов удаляются. Данные на сервер передаются анонимно. Для этого собираются данные о работодателях и их вакансиях. Никакие персональные данные пользователя утилиты под которыми, вы авторизуетесь никуда не отправляются — только работодателей и рекрутеров. Отправку этих данных можно отключить (флаг `--disable-telemetry`), но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги отзывы удаляет, или мудак его запугает и жалоб в РКН накидает, а последний всегда найдет за что сайт заблокировать. Единственное место где можно написать отзыв — это **Telegram**.
17
17
 
18
18
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
19
- asdf/pyenv/conda и что-то еще...
19
+ asdf/pyenv/conda и что-то еще.
20
20
 
21
21
  Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести конфиг на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
22
22
 
@@ -221,10 +221,11 @@ https://hh.ru/employer/1918903
221
221
  | **list-resumes** | Список резюме |
222
222
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
223
223
  | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками. |
224
+ | **reply-employers** | Ответить во все чаты с работодателями, где нет ответа либо не прочитали ваш предыдущий ответ |
224
225
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
225
226
  | **call-api** | Вызов произвольного метода API с выводом результата. |
226
227
  | **refresh-token** | Обновляет access_token. |
227
- | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, с сайта фирмы и могут быть удалены только по просьбе уполнамоченного лица. Данная функция готова и будет доступна после 100 ⭐ |
228
+ | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Это функционал для избранных, но в группе есть бесплатный бот с тем же функционалом. |
228
229
 
229
230
  ### Формат текста сообщений
230
231
 
@@ -263,25 +264,8 @@ https://hh.ru/employer/1918903
263
264
 
264
265
  Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
265
266
 
266
- ### Сбор данных
267
+ Для создания своих плагинов прочитайте документацию:
267
268
 
268
- > Данный функционал можно отключить с помощью специльного флага, но ради котят и из-за ненависти к херкам не делайте этого!
269
-
270
- Утилита собирает и передает на сервер разработчика следующую информацию:
271
-
272
- 1. Название вакансии.
273
- 1. Тип вакансии (открытая/закрытая).
274
- 1. Город, в котором размещена вакансия.
275
- 1. Информация о зарплате (минимальная, максимальная, валюта, указана ли зарплата до вычета налогов).
276
- 1. Прямая ссылка на вакансию.
277
- 1. Дата создания вакансии.
278
- 1. Дата публикации вакансии.
279
- 1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, которая в дальнейшем _может_ храниться в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля. Данная информация может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и подписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова.
280
- 1. Название компании.
281
- 1. Тип компании.
282
- 1. Описание компании.
283
- 1. Ссылка на сайт компании.
284
- 1. Город, в котором находится компания.
285
-
286
- **УТИЛИТА НЕ ПЕРЕДАЕТ НИКАКИХ ПЕРСОНАЛЬНЫХ ДАННЫХ ПОЛЬЗОВАТЕЛЕЙ — ТОЛЬКО ДАННЫЕ ВСЯКИХ РАБОТАДАТЕЛЕЙ И ИХ ОВЧАРОК. СЕРВЕР НЕ ХРАНИТ IP ОТПРАВИТЕЛЯ. ЛОГИ НА СЕРВЕРЕ НЕ ВЕДУТСЯ. ТАК ЖЕ Я ОБЕЩАЮ, ЧТО УТИЛИТА ВСЕГДА БУДЕТ БЕСПЛАТНОЙ, ВСЕ КТО ЕЮ ПЫТАЮТСЯ ТОРГОВАТЬ — УЕБКИ, И У НИХ ПОЧЕРНЕЕТ И ОТВАЛИТСЯ ХУЙ. ЕДИНСТВЕННАЯ ПЛАТА ЗА ЕЕ ИСПОЛЬЗОВАНИЕ — ЭТО ПОМОЩЬ В ПАРСИНГЕ САЙТА HEADHUNTER (МЕНЯ ИНТЕРЕСУЕТ ЕГО БАЗА КОМПАНИЙ).**
269
+ * [HH.RU OpenAPI](https://api.hh.ru/openapi/redoc)
287
270
 
271
+ Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате.
@@ -3,28 +3,29 @@ from __future__ import annotations
3
3
  import argparse
4
4
  import logging
5
5
  import sys
6
- from abc import ABCMeta, abstractmethod
7
6
  from importlib import import_module
7
+ from os import getenv
8
8
  from pathlib import Path
9
9
  from pkgutil import iter_modules
10
- from typing import Sequence, Literal
10
+ from typing import Literal, Sequence
11
+
11
12
  from .api import ApiClient
12
13
  from .color_log import ColorHandler
13
14
  from .utils import Config, get_config_path
14
- from os import getenv
15
15
 
16
16
  DEFAULT_CONFIG_PATH = (
17
- get_config_path() / __package__.replace("_", "-") / "config.json"
17
+ get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
18
18
  )
19
19
 
20
20
  logger = logging.getLogger(__package__)
21
21
 
22
22
 
23
- class BaseOperation(metaclass=ABCMeta):
24
- def setup_parser(self, parser: argparse.ArgumentParser) -> None: ...
23
+ class BaseOperation:
24
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
25
+ ...
25
26
 
26
- @abstractmethod
27
- def run(self, args: argparse.Namespace) -> None | int: ...
27
+ def run(self, args: argparse.Namespace) -> None | int:
28
+ raise NotImplementedError()
28
29
 
29
30
 
30
31
  OPERATIONS = "operations"
@@ -63,7 +64,7 @@ class HHApplicantTool:
63
64
 
64
65
  Исходники и предложения: <https://github.com/s3rgeym/hh-applicant-tool>
65
66
 
66
- Группа поддержки: <https://t.me/+aSjr8qM_AP85ZDBi>
67
+ Группа поддержки: <https://t.me/otzyvy_headhunter>
67
68
  """
68
69
 
69
70
  class ArgumentFormatter(
@@ -0,0 +1,13 @@
1
+ from .api import ApiError
2
+ from .types import ApiListResponse
3
+
4
+
5
+ class GetResumeIdMixin:
6
+ def _get_resume_id(self) -> str:
7
+ try:
8
+ resumes: ApiListResponse = self.api.get("/resumes/mine")
9
+ return resumes["items"][0]["id"]
10
+ except (ApiError, KeyError, IndexError) as ex:
11
+ raise Exception("Не могу получить идентификатор резюме") from ex
12
+
13
+
@@ -0,0 +1,356 @@
1
+ import argparse
2
+ import logging
3
+ import random
4
+ import time
5
+ from collections import defaultdict
6
+ from datetime import datetime, timedelta, timezone
7
+ from typing import TextIO, Tuple
8
+
9
+ from ..api import ApiError, BadRequest
10
+ from ..main import BaseOperation
11
+ from ..main import Namespace as BaseNamespace
12
+ from ..main import get_api
13
+ from ..mixins import GetResumeIdMixin
14
+ from ..telemetry_client import TelemetryClient, TelemetryError
15
+ from ..types import ApiListResponse, VacancyItem
16
+ from ..utils import (fix_datetime, parse_interval, parse_invalid_datetime,
17
+ random_text, truncate_string)
18
+
19
+ logger = logging.getLogger(__package__)
20
+
21
+
22
+ class Namespace(BaseNamespace):
23
+ resume_id: str | None
24
+ message_list: TextIO
25
+ force_message: bool
26
+ apply_interval: Tuple[float, float]
27
+ page_interval: Tuple[float, float]
28
+ order_by: str
29
+ search: str
30
+ dry_run: bool
31
+
32
+
33
+ class Operation(BaseOperation, GetResumeIdMixin):
34
+ """Откликнуться на все подходящие вакансии."""
35
+
36
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
37
+ parser.add_argument("--resume-id", help="Идентефикатор резюме")
38
+ parser.add_argument(
39
+ "--message-list",
40
+ help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки.",
41
+ type=argparse.FileType(),
42
+ )
43
+ parser.add_argument(
44
+ "--force-message",
45
+ help="Всегда отправлять сообщение при отклике",
46
+ default=False,
47
+ action=argparse.BooleanOptionalAction,
48
+ )
49
+ parser.add_argument(
50
+ "--apply-interval",
51
+ help="Интервал перед отправкой откликов в секундах (X, X-Y)",
52
+ default="1-5",
53
+ type=parse_interval,
54
+ )
55
+ parser.add_argument(
56
+ "--page-interval",
57
+ help="Интервал перед получением следующей страницы рекомендованных вакансий в секундах (X, X-Y)",
58
+ default="1-3",
59
+ type=parse_interval,
60
+ )
61
+ parser.add_argument(
62
+ "--order-by",
63
+ help="Сортировка вакансий",
64
+ choices=[
65
+ "publication_time",
66
+ "salary_desc",
67
+ "salary_asc",
68
+ "relevance",
69
+ "distance",
70
+ ],
71
+ default="relevance",
72
+ )
73
+ parser.add_argument(
74
+ "--search",
75
+ help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500'",
76
+ type=str,
77
+ default=None,
78
+ )
79
+ parser.add_argument(
80
+ "--dry-run",
81
+ help="Не отправлять отклики, а только выводить параметры запроса",
82
+ default=False,
83
+ action=argparse.BooleanOptionalAction,
84
+ )
85
+
86
+ def run(self, args: Namespace) -> None:
87
+ self.enable_telemetry = True
88
+ if args.disable_telemetry:
89
+ print(
90
+ "👁️ Телеметрия используется только для сбора данных о работодателях и их вакансиях, персональные данные пользователей не передаются на сервер."
91
+ )
92
+ if (
93
+ input("Вы действительно хотите отключить телеметрию (д/Н)? ")
94
+ .lower()
95
+ .startswith(("д", "y"))
96
+ ):
97
+ self.enable_telemetry = False
98
+ logger.info("Телеметрия отключена.")
99
+ else:
100
+ logger.info("Спасибо за то что оставили телеметрию включенной!")
101
+
102
+ self.api = get_api(args)
103
+ self.resume_id = args.resume_id or self._get_resume_id()
104
+ self.application_messages = self._get_application_messages(
105
+ args.message_list
106
+ )
107
+
108
+ self.apply_min_interval, self.apply_max_interval = args.apply_interval
109
+ self.page_min_interval, self.page_max_interval = args.page_interval
110
+
111
+ self.force_message = args.force_message
112
+ self.order_by = args.order_by
113
+ self.search = args.search
114
+ self.dry_run = args.dry_run
115
+ self._apply_similar()
116
+
117
+ def _get_application_messages(
118
+ self, message_list: TextIO | None
119
+ ) -> list[str]:
120
+ if message_list:
121
+ application_messages = list(
122
+ filter(None, map(str.strip, message_list))
123
+ )
124
+ else:
125
+ application_messages = [
126
+ "{Меня заинтересовала|Мне понравилась} ваша вакансия %(vacancy_name)s",
127
+ "{Прошу рассмотреть|Предлагаю рассмотреть} {мою кандидатуру|мое резюме} на вакансию %(vacancy_name)s",
128
+ ]
129
+ return application_messages
130
+
131
+ def _apply_similar(self) -> None:
132
+ telemetry_client = TelemetryClient(proxies=self.api.proxies)
133
+ telemetry_data = defaultdict(dict)
134
+
135
+ vacancies = self._get_vacancies()
136
+
137
+ if self.enable_telemetry:
138
+ for vacancy in vacancies:
139
+ vacancy_id = vacancy["id"]
140
+ telemetry_data["vacancies"][vacancy_id] = {
141
+ "name": vacancy.get("name"),
142
+ "type": vacancy.get("type", {}).get("id"), # open/closed
143
+ "area": vacancy.get("area", {}).get("name"), # город
144
+ "salary": vacancy.get(
145
+ "salary"
146
+ ), # from, to, currency, gross
147
+ "direct_url": vacancy.get(
148
+ "alternate_url"
149
+ ), # ссылка на вакансию
150
+ "created_at": fix_datetime(
151
+ vacancy.get("created_at")
152
+ ), # будем вычислять говно-вакансии, которые по полгода висят
153
+ "published_at": fix_datetime(vacancy.get("published_at")),
154
+ "contacts": vacancy.get(
155
+ "contacts"
156
+ ), # пиздорванки там телеграм для связи указывают
157
+ # HH с точки зрения перфикциониста — кусок говна, где кривые
158
+ # форматы даты, у вакансий может не быть работодателя...
159
+ "employer_id": int(vacancy["employer"]["id"])
160
+ if "employer" in vacancy and "id" in vacancy["employer"]
161
+ else None,
162
+ # "relations": vacancy.get("relations", []),
163
+ # Остальное неинтересно
164
+ }
165
+
166
+ me = self.api.get("/me")
167
+
168
+ basic_message_placeholders = {
169
+ "first_name": me.get("first_name", ""),
170
+ "last_name": me.get("last_name", ""),
171
+ "email": me.get("email", ""),
172
+ "phone": me.get("phone", ""),
173
+ }
174
+
175
+ do_apply = True
176
+ complained_employers = set()
177
+
178
+ for vacancy in vacancies:
179
+ try:
180
+ message_placeholders = {
181
+ "vacancy_name": vacancy.get("name", ""),
182
+ "employer_name": vacancy.get("employer", {}).get(
183
+ "name", ""
184
+ ),
185
+ **basic_message_placeholders,
186
+ }
187
+
188
+ logger.debug(
189
+ "Вакансия %(vacancy_name)s от %(employer_name)s"
190
+ % message_placeholders
191
+ )
192
+
193
+ if vacancy.get("has_test"):
194
+ logger.debug(
195
+ "Пропускаем вакансию с тестом: %s",
196
+ vacancy["alternate_url"],
197
+ )
198
+ continue
199
+
200
+ if vacancy.get("archived"):
201
+ logger.warning(
202
+ "Пропускаем вакансию в архиве: %s",
203
+ vacancy["alternate_url"],
204
+ )
205
+ continue
206
+
207
+ relations = vacancy.get("relations", [])
208
+ employer_id = vacancy.get("employer", {}).get("id")
209
+
210
+ if (
211
+ self.enable_telemetry
212
+ and employer_id
213
+ and employer_id not in telemetry_data["employers"]
214
+ and employer_id not in complained_employers
215
+ and (
216
+ not relations
217
+ or parse_invalid_datetime(vacancy["created_at"])
218
+ + timedelta(days=7)
219
+ > datetime.now(tz=timezone.utc)
220
+ )
221
+ ):
222
+ employer = self.api.get(f"/employers/{employer_id}")
223
+
224
+ employer_data = {
225
+ "name": employer.get("name"),
226
+ "type": employer.get("type"),
227
+ "description": employer.get("description"),
228
+ "site_url": employer.get("site_url"),
229
+ "area": employer.get("area", {}).get("name"), # город
230
+ }
231
+ if "got_rejection" in relations:
232
+ try:
233
+ print(
234
+ "🚨 Вы получили отказ от https://hh.ru/employer/%s"
235
+ % employer_id
236
+ )
237
+ response = telemetry_client.send_telemetry(
238
+ f"/employers/{employer_id}/complaint",
239
+ employer_data,
240
+ )
241
+ if "topic_url" in response:
242
+ print(
243
+ "Ссылка на обсуждение работодателя:",
244
+ response["topic_url"],
245
+ )
246
+ else:
247
+ print(
248
+ "Создание темы для обсуждения работодателя добавлено в очередь..."
249
+ )
250
+ complained_employers.add(employer_id)
251
+ except TelemetryError as ex:
252
+ logger.error(ex)
253
+ elif do_apply:
254
+ telemetry_data["employers"][employer_id] = employer_data
255
+
256
+ if not do_apply:
257
+ logger.debug(
258
+ "Проопускаем вакансию так как достигла лимита заявок: %s",
259
+ vacancy["alternate_url"],
260
+ )
261
+ continue
262
+
263
+ if relations:
264
+ logger.debug(
265
+ "Пропускаем вакансию с откликом: %s",
266
+ vacancy["alternate_url"],
267
+ )
268
+ continue
269
+
270
+ params = {
271
+ "resume_id": self.resume_id,
272
+ "vacancy_id": vacancy["id"],
273
+ "message": "",
274
+ }
275
+
276
+ if self.force_message or vacancy.get(
277
+ "response_letter_required"
278
+ ):
279
+ msg = params["message"] = (
280
+ random_text(random.choice(self.application_messages))
281
+ % message_placeholders
282
+ )
283
+ logger.debug(msg)
284
+
285
+ if self.dry_run:
286
+ logger.info(
287
+ "Dry Run: Отправка отклика на вакансию %s с параметрами: %s",
288
+ vacancy["alternate_url"],
289
+ params,
290
+ )
291
+ continue
292
+
293
+ # Задержка перед отправкой отклика
294
+ interval = random.uniform(
295
+ self.apply_min_interval, self.apply_max_interval
296
+ )
297
+ time.sleep(interval)
298
+
299
+ res = self.api.post("/negotiations", params)
300
+ assert res == {}
301
+ print(
302
+ "📨 Отправили отклик",
303
+ vacancy["alternate_url"],
304
+ "(",
305
+ truncate_string(vacancy["name"]),
306
+ ")",
307
+ )
308
+ except ApiError as ex:
309
+ logger.error(ex)
310
+ if isinstance(ex, BadRequest) and ex.limit_exceeded:
311
+ do_apply = False
312
+
313
+ print("📝 Отклики на вакансии разосланы!")
314
+
315
+ if self.enable_telemetry:
316
+ if self.dry_run:
317
+ # С --dry-run можно посмотреть что отправляется
318
+ logger.info(
319
+ "Dry Run: Данные телеметрии для отправки на сервер: %r",
320
+ telemetry_data,
321
+ )
322
+ return
323
+
324
+ try:
325
+ response = telemetry_client.send_telemetry(
326
+ "/collect", dict(telemetry_data)
327
+ )
328
+ logger.debug(response)
329
+ except TelemetryError as ex:
330
+ logger.error(ex)
331
+
332
+ def _get_vacancies(self, per_page: int = 100) -> list[VacancyItem]:
333
+ rv = []
334
+ for page in range(20):
335
+ params = {
336
+ "page": page,
337
+ "per_page": per_page,
338
+ "order_by": self.order_by,
339
+ }
340
+ if self.search:
341
+ params["text"] = self.search
342
+ res: ApiListResponse = self.api.get(
343
+ f"/resumes/{self.resume_id}/similar_vacancies", params
344
+ )
345
+ rv.extend(res["items"])
346
+ if page >= res["pages"] - 1:
347
+ break
348
+
349
+ # Задержка перед получением следующей страницы
350
+ if page > 0:
351
+ interval = random.uniform(
352
+ self.page_min_interval, self.page_max_interval
353
+ )
354
+ time.sleep(interval)
355
+
356
+ return rv