hh-applicant-tool 0.5.2__py3-none-any.whl → 0.5.4__py3-none-any.whl

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.

File without changes
@@ -0,0 +1,55 @@
1
+ import logging
2
+ from copy import deepcopy
3
+
4
+ import requests
5
+
6
+ logger = logging.getLogger(__package__)
7
+
8
+
9
+ class BlackboxError(Exception):
10
+ pass
11
+
12
+
13
+ class BlackboxChat:
14
+ chat_endpoint: str = "https://www.blackbox.ai/api/chat"
15
+
16
+ def __init__(
17
+ self,
18
+ session_id: str,
19
+ chat_payload: dict,
20
+ proxies: dict[str, str] = {},
21
+ session: requests.Session | None = None,
22
+ ):
23
+ self.session_id = session_id
24
+ self.chat_payload = chat_payload
25
+ self.proxies = proxies
26
+ self.session = session or requests.session()
27
+
28
+ def default_headers(self) -> dict[str, str]:
29
+ return {
30
+ "Accept": "*/*",
31
+ "Accept-Language": "en-US,en;q=0.9,ru;q=0.8",
32
+ "Content-Type": "application/json",
33
+ "Origin": "https://www.blackbox.ai",
34
+ "Priority": "u=0",
35
+ "Referer": "https://www.blackbox.ai/",
36
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
37
+ }
38
+
39
+ def send_message(self, message: str) -> str:
40
+ payload = deepcopy(self.chat_payload)
41
+ payload["messages"].append(
42
+ {**payload["messages"][0], "content": message}
43
+ )
44
+
45
+ try:
46
+ response = self.session.post(
47
+ self.chat_endpoint,
48
+ json=payload,
49
+ cookies={"sessionId": self.session_id},
50
+ headers=self.default_headers(),
51
+ proxies=self.proxies,
52
+ )
53
+ return response.text
54
+ except requests.exceptions.RequestException as ex:
55
+ raise BlackboxError(str(ex)) from ex
@@ -6,6 +6,7 @@ from collections import defaultdict
6
6
  from datetime import datetime, timedelta, timezone
7
7
  from typing import TextIO, Tuple
8
8
 
9
+ from ..ai.blackbox import BlackboxChat, BlackboxError
9
10
  from ..api import ApiError, BadRequest
10
11
  from ..main import BaseOperation
11
12
  from ..main import Namespace as BaseNamespace
@@ -15,6 +16,7 @@ from ..telemetry_client import TelemetryClient, TelemetryError
15
16
  from ..types import ApiListResponse, VacancyItem
16
17
  from ..utils import (fix_datetime, parse_interval, parse_invalid_datetime,
17
18
  random_text, truncate_string)
19
+ from hh_applicant_tool.ai import blackbox
18
20
 
19
21
  logger = logging.getLogger(__package__)
20
22
 
@@ -23,8 +25,10 @@ class Namespace(BaseNamespace):
23
25
  resume_id: str | None
24
26
  message_list: TextIO
25
27
  force_message: bool
26
- apply_interval: Tuple[float, float]
27
- page_interval: Tuple[float, float]
28
+ use_ai: bool
29
+ pre_prompt: str
30
+ apply_interval: tuple[float, float]
31
+ page_interval: tuple[float, float]
28
32
  order_by: str
29
33
  search: str
30
34
  dry_run: bool
@@ -36,16 +40,32 @@ class Operation(BaseOperation, GetResumeIdMixin):
36
40
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
37
41
  parser.add_argument("--resume-id", help="Идентефикатор резюме")
38
42
  parser.add_argument(
43
+ "-L",
39
44
  "--message-list",
40
45
  help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки.",
41
- type=argparse.FileType(),
46
+ type=argparse.FileType('r', encoding='utf-8', errors='replace'),
42
47
  )
43
48
  parser.add_argument(
49
+ "-f",
44
50
  "--force-message",
51
+ "--force",
45
52
  help="Всегда отправлять сообщение при отклике",
46
53
  default=False,
47
54
  action=argparse.BooleanOptionalAction,
48
55
  )
56
+ parser.add_argument(
57
+ "--use-ai",
58
+ "--ai",
59
+ help="Использовать AI для генерации сообщений",
60
+ default=False,
61
+ action=argparse.BooleanOptionalAction,
62
+ )
63
+ parser.add_argument(
64
+ "--pre-prompt",
65
+ "--prompt",
66
+ help="Добавочный промпт для генерации сопроводительного письма",
67
+ default="Сгенерируй сопроводительное письмо не более 5-7 предложений от моего имени для вакансии",
68
+ )
49
69
  parser.add_argument(
50
70
  "--apply-interval",
51
71
  help="Интервал перед отправкой откликов в секундах (X, X-Y)",
@@ -86,24 +106,35 @@ class Operation(BaseOperation, GetResumeIdMixin):
86
106
  def run(self, args: Namespace) -> None:
87
107
  self.enable_telemetry = True
88
108
  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("Спасибо за то что оставили телеметрию включенной!")
109
+ # print(
110
+ # "👁️ Телеметрия используется только для сбора данных о работодателях и их вакансиях, персональные данные пользователей не передаются на сервер."
111
+ # )
112
+ # if (
113
+ # input("Вы действительно хотите отключить телеметрию (д/Н)? ")
114
+ # .lower()
115
+ # .startswith(("д", "y"))
116
+ # ):
117
+ # self.enable_telemetry = False
118
+ # logger.info("Телеметрия отключена.")
119
+ # else:
120
+ # logger.info("Спасибо за то что оставили телеметрию включенной!")
121
+ self.enable_telemetry = False
101
122
 
102
123
  self.api = get_api(args)
103
124
  self.resume_id = args.resume_id or self._get_resume_id()
104
125
  self.application_messages = self._get_application_messages(
105
126
  args.message_list
106
127
  )
128
+ self.chat = None
129
+
130
+ if config := args.config.get("blackbox"):
131
+ self.chat = BlackboxChat(
132
+ session_id=config["session_id"],
133
+ chat_payload=config["chat_payload"],
134
+ proxies=self.api.proxies or {},
135
+ )
136
+
137
+ self.pre_prompt = args.pre_prompt
107
138
 
108
139
  self.apply_min_interval, self.apply_max_interval = args.apply_interval
109
140
  self.page_min_interval, self.page_max_interval = args.page_interval
@@ -255,7 +286,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
255
286
 
256
287
  if not do_apply:
257
288
  logger.debug(
258
- "Проопускаем вакансию так как достигла лимита заявок: %s",
289
+ "Пропускаем вакансию так как достигла лимита заявок: %s",
259
290
  vacancy["alternate_url"],
260
291
  )
261
292
  continue
@@ -276,11 +307,25 @@ class Operation(BaseOperation, GetResumeIdMixin):
276
307
  if self.force_message or vacancy.get(
277
308
  "response_letter_required"
278
309
  ):
279
- msg = params["message"] = (
280
- random_text(random.choice(self.application_messages))
281
- % message_placeholders
282
- )
310
+ if self.chat:
311
+ try:
312
+ msg = self.pre_prompt + "\n\n"
313
+ msg += message_placeholders["vacancy_name"]
314
+ logger.debug(msg)
315
+ msg = self.chat.send_message(msg)
316
+ except BlackboxError as ex:
317
+ logger.error(ex)
318
+ continue
319
+ else:
320
+ msg = (
321
+ random_text(
322
+ random.choice(self.application_messages)
323
+ )
324
+ % message_placeholders
325
+ )
326
+
283
327
  logger.debug(msg)
328
+ params["message"] = msg
284
329
 
285
330
  if self.dry_run:
286
331
  logger.info(
@@ -0,0 +1,36 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import subprocess
5
+
6
+ from ..main import BaseOperation
7
+ from ..main import Namespace as BaseNamespace
8
+
9
+ logger = logging.getLogger(__package__)
10
+
11
+ EDITOR = os.getenv("EDITOR", "nano")
12
+
13
+
14
+ class Namespace(BaseNamespace):
15
+ print: bool
16
+
17
+
18
+ class Operation(BaseOperation):
19
+ """Редактировать конфигурационный файл или показать путь до него"""
20
+
21
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
+ parser.add_argument(
23
+ "-p",
24
+ "--print",
25
+ type=bool,
26
+ default=False,
27
+ action=argparse.BooleanOptionalAction,
28
+ help="Напечатать путь и выйти",
29
+ )
30
+
31
+ def run(self, args: Namespace) -> None:
32
+ config_path = str(args.config._config_path)
33
+ if args.print:
34
+ print(config_path)
35
+ else:
36
+ subprocess.call([EDITOR, config_path])
@@ -44,7 +44,7 @@ class Config(dict):
44
44
  def load(self) -> None:
45
45
  if self._config_path.exists():
46
46
  with self._lock:
47
- with self._config_path.open() as f:
47
+ with self._config_path.open("r", encoding="utf-8", errors="replace") as f:
48
48
  try:
49
49
  self.update(json.load(f))
50
50
  except ValueError:
@@ -54,7 +54,7 @@ class Config(dict):
54
54
  self.update(*args, **kwargs)
55
55
  self._config_path.parent.mkdir(exist_ok=True, parents=True)
56
56
  with self._lock:
57
- with self._config_path.open("w+") as fp:
57
+ with self._config_path.open("w+", encoding="utf-8", errors="replace") as fp:
58
58
  json.dump(
59
59
  self,
60
60
  fp,
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: hh-applicant-tool
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -32,7 +32,9 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  ### Описание
34
34
 
35
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Для этого собираются данные о работодателях и их вакансиях (персональные данные пользователя не передаются ни в каком виде). Отправку данных на сервер разработчика можно отключить, но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги или после угроз судом удаляют отзывы. Единственное место где можно написать отзыв — это **Telegram**.
35
+ > Утилита для генерации сопроводительного письма может использовать AI
36
+
37
+ Утилита для успешных волчат и старых волков с опытом, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме (бесплатный аналог услуги на HH). Но данная утилита больше чем просто спамилка откликами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Для этого собираются данные о работодателях и их вакансиях (персональные данные пользователя не передаются ни в каком виде). Отправку данных на сервер разработчика можно отключить, но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги или после угроз судом удаляют отзывы. Единственное место где можно написать отзыв — это **Telegram**.
36
38
 
37
39
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
38
40
  asdf/pyenv/conda и что-то еще.
@@ -166,6 +168,11 @@ $ hh-applicant-tool refresh-token
166
168
  | **macOS** | `~/Library/Application Support/hh-applicant-tool/config.json` |
167
169
  | **Linux** | `~/.config/hh-applicant-tool/config.json` |
168
170
 
171
+ Полный путь до конфигурационного файла можно вывести с помощью команды:
172
+
173
+ ```bash
174
+ hh-applicant-tool config -p
175
+ ```
169
176
 
170
177
  Через конфиг можно задать дополнительные настройки:
171
178
 
@@ -238,11 +245,12 @@ https://hh.ru/employer/1918903
238
245
  | **whoami** | Выводит информацию об авторизованном пользователе |
239
246
  | **list-resumes** | Список резюме |
240
247
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
241
- | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками. |
248
+ | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками, иначе рискуете попасть в теневой бан. |
242
249
  | **reply-employers** | Ответить во все чаты с работодателями, где нет ответа либо не прочитали ваш предыдущий ответ |
243
250
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
244
251
  | **call-api** | Вызов произвольного метода API с выводом результата. |
245
252
  | **refresh-token** | Обновляет access_token. |
253
+ | **config** | Редактировать конфигурационный файл. |
246
254
  | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Это функционал для избранных, но в группе есть бесплатный бот с тем же функционалом. |
247
255
 
248
256
  ### Формат текста сообщений
@@ -276,6 +284,31 @@ https://hh.ru/employer/1918903
276
284
  Привет, как ты?
277
285
  ```
278
286
 
287
+ ### Использование AI для генерации сопроводительного письма
288
+
289
+ * Перейдите на сайт [blackbox.ai](https://www.blackbox.ai) и создайте чат.
290
+ * В первом сообщении опишите свой опыт и тп.
291
+ * Далее откройте devtools, нажав `F12`.
292
+ * Во вкладке `Network` последним должен быть POST-запрос на `https://www.blackbox.ai/api/chat`.
293
+ * Запустите редактирование конфига:
294
+ ```sh
295
+ hh-applicant-tool config
296
+ ```
297
+ * Измените конфиг:
298
+ ```json
299
+ {
300
+ // ...
301
+ "blackbox": {
302
+ "session_id": "<В заголовках запроса найдите Cookie, скопируйте сюда значение sessionId до ;>",
303
+ "chat_payload": <Сюда вставьте тело запроса типа {"messages":[{"id":"IXqdOx9","content":"Я программист fullstack-разработчик...","role":"user"}],"id":"IXqdOx9","previewToken":null,"userId":null,...,"webSearchModePrompt":false,"deepSearchMode":false}>
304
+ }
305
+ }
306
+ ```
307
+ * Пример рассылки откликов с генерированным письмом:
308
+ ```sh
309
+ hh-applicant-tool apply-similar -f --ai
310
+ ```
311
+
279
312
  ### Написание плагинов
280
313
 
281
314
  Утилита использует систему плагинов. Все они лежат в [operations](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations). Модули расположенные там автоматически добавляются как доступные операции. За основу для своего плагина можно взять [whoami.py](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations/whoami.py).
@@ -1,5 +1,7 @@
1
1
  hh_applicant_tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  hh_applicant_tool/__main__.py,sha256=cwKJAAML0RRKT9Qbzcwf07HHcuSd8oh7kx4P1apndWQ,84
3
+ hh_applicant_tool/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ hh_applicant_tool/ai/blackbox.py,sha256=vqkEpsX7q7bgX49dmmifYJmrbuz_WBg5u9M9J9XdQlI,1670
3
5
  hh_applicant_tool/api/__init__.py,sha256=kgFSHibRaAugN2BA3U1djEa20qgKJUUVouwJzjEB0DU,84
4
6
  hh_applicant_tool/api/client.py,sha256=um9NX22hNOtSuPCobCKf1anIFp-jiZlIXm4BuqN-L7k,7997
5
7
  hh_applicant_tool/api/errors.py,sha256=0SoWKnsSUA0K2YgQA8GwGhe-pRMwtfK94MR6_MgbORQ,1722
@@ -8,10 +10,11 @@ hh_applicant_tool/constants.py,sha256=lpgKkP2chWgTnBXvzxbSPWpKcfzp8VxMTlkssFcQhH
8
10
  hh_applicant_tool/main.py,sha256=z_SAW7cV83P5mVEkuddSzETUGLqocsNyEKlA6HBHjQ0,4806
9
11
  hh_applicant_tool/mixins.py,sha256=66LmyYSsDfhrpUwoAONjzrd5aoXqaZVoQ-zXhyYbYMk,418
10
12
  hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- hh_applicant_tool/operations/apply_similar.py,sha256=iMHbuqNYogL-cfP_RYxKTkWnWQDRWgiY0YACWw3Glzw,14811
13
+ hh_applicant_tool/operations/apply_similar.py,sha256=IYpf8PabOJF6vmbsKYir1NgW0Y2XviBanIGjsbz9AD4,16649
12
14
  hh_applicant_tool/operations/authorize.py,sha256=TyUTCSOGwSYVJMEd5vSI981LRRI-RZf8hnlVYhtRVwA,3184
13
15
  hh_applicant_tool/operations/call_api.py,sha256=qwPDrWP9tiqHkaYYWYBZtymj9AaxObB86Eny-Bf5q_c,1314
14
16
  hh_applicant_tool/operations/clear_negotiations.py,sha256=GEsL1snxz8TJ3i7Nz6LkP4FZOzRBdAofX3xf3Ywj64k,4369
17
+ hh_applicant_tool/operations/config.py,sha256=PJpPPDJaQwqyZ_T55Mlf0oAoeNeMUG_yJoz605XZGxI,959
15
18
  hh_applicant_tool/operations/get_employer_contacts.py,sha256=7BEzEnNAp87RlOP6HX0LR6cbtud2FuKCKK5sq6fINq8,4066
16
19
  hh_applicant_tool/operations/list_resumes.py,sha256=XBrVFTnl45BUtuvjVm70h_CXZrOvAULnLkLkyUh7gxw,1134
17
20
  hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
@@ -20,8 +23,8 @@ hh_applicant_tool/operations/update_resumes.py,sha256=gGxMYMoT9GqJjwn4AgrOAEJCZu
20
23
  hh_applicant_tool/operations/whoami.py,sha256=sg0r7m6oKkpMEmGt4QdtYdS-1gf-1KKdnk32yblbRJs,714
21
24
  hh_applicant_tool/telemetry_client.py,sha256=wYLbKnx3sOmESFHqjLt-0Gww1O3lJiXFYdWnsorIhK8,3261
22
25
  hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
23
- hh_applicant_tool/utils.py,sha256=OsdjzUa76VC8vihHu27rD5df9DWqRzpvJr3PsdfJzNE,3071
24
- hh_applicant_tool-0.5.2.dist-info/METADATA,sha256=SRkuIiIKfSA6WF8_4sh_iDeutRuyDbxgYBjo-x9eHms,18778
25
- hh_applicant_tool-0.5.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
26
- hh_applicant_tool-0.5.2.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
27
- hh_applicant_tool-0.5.2.dist-info/RECORD,,
26
+ hh_applicant_tool/utils.py,sha256=pKmyG55HGmOvOXtHK0x9JBdEfm-hd1b8XroG9tEPSPQ,3146
27
+ hh_applicant_tool-0.5.4.dist-info/METADATA,sha256=d8j1CSX0o5ezl2xq1J-jd5T-ckWLgsgMAd9W2aD8oGM,20687
28
+ hh_applicant_tool-0.5.4.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
29
+ hh_applicant_tool-0.5.4.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
30
+ hh_applicant_tool-0.5.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any