hh-applicant-tool 0.5.9__py3-none-any.whl → 0.6.1__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.

hh_applicant_tool/main.py CHANGED
@@ -11,9 +11,8 @@ from typing import Literal, Sequence
11
11
 
12
12
  from .api import ApiClient
13
13
  from .color_log import ColorHandler
14
- from .utils import Config, get_config_path
15
14
  from .telemetry_client import TelemetryClient
16
-
15
+ from .utils import Config, get_config_path
17
16
 
18
17
  DEFAULT_CONFIG_PATH = (
19
18
  get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
@@ -151,6 +150,9 @@ class HHApplicantTool:
151
150
  if (token := api_client.get_access_token()) != args.config["token"]:
152
151
  args.config.save(token=token)
153
152
  return res
153
+ except KeyboardInterrupt:
154
+ logger.warning("Interrupted by user")
155
+ return 1
154
156
  except Exception as e:
155
157
  logger.exception(e)
156
158
  return 1
@@ -255,28 +255,13 @@ class Operation(BaseOperation, GetResumeIdMixin):
255
255
  "area": employer.get("area", {}).get("name"), # город
256
256
  }
257
257
  if "got_rejection" in relations:
258
- try:
259
- print(
260
- "🚨 Вы получили отказ от https://hh.ru/employer/%s"
261
- % employer_id
262
- )
263
- response = telemetry_client.send_telemetry(
264
- f"/employers/{employer_id}/complaint",
265
- employer_data,
266
- )
267
- if "topic_url" in response:
268
- print(
269
- "Ссылка на обсуждение работодателя:",
270
- response["topic_url"],
271
- )
272
- else:
273
- # print(
274
- # "Создание темы для обсуждения работодателя добавлено в очередь..."
275
- # )
276
- ...
277
- complained_employers.add(employer_id)
278
- except TelemetryError as ex:
279
- logger.error(ex)
258
+ print(
259
+ "🚨 Вы получили отказ от https://hh.ru/employer/%s"
260
+ % employer_id
261
+ )
262
+
263
+ complained_employers.add(employer_id)
264
+
280
265
  elif do_apply:
281
266
  telemetry_data["employers"][employer_id] = employer_data
282
267
 
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
  import sys
6
6
 
7
- from ..api import ApiError, ApiClient
7
+ from ..api import ApiClient, ApiError
8
8
  from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
10
 
@@ -1,7 +1,7 @@
1
1
  import argparse
2
2
  import logging
3
3
  from os import getenv
4
-
4
+ import pathlib
5
5
  from ..main import BaseOperation
6
6
  from ..main import Namespace as BaseNamespace
7
7
  from ..telemetry_client import TelemetryClient
@@ -10,50 +10,89 @@ logger = logging.getLogger(__package__)
10
10
 
11
11
 
12
12
  class Namespace(BaseNamespace):
13
- username: str | None = None
14
- password: str | None = None
15
- search: str | None = None
13
+ username: str | None
14
+ password: str | None
15
+ search: str | None
16
+ export: bool
16
17
 
17
18
 
18
19
  class Operation(BaseOperation):
19
- """Выведет контакты работодателя по заданной строке поиска"""
20
+ """Выведет контакты работодателей, которые высылали вам приглашения"""
20
21
 
21
22
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
- parser.add_argument(
23
- "-u",
24
- "--username",
25
- type=str,
26
- help="Имя пользователя для аутентификации",
27
- default=getenv("AUTH_USERNAME"),
28
- )
29
- parser.add_argument(
30
- "-P",
31
- "--password",
32
- type=str,
33
- help="Пароль для аутентификации",
34
- default=getenv("AUTH_PASSWORD"),
35
- )
23
+ # parser.add_argument(
24
+ # "-u",
25
+ # "--username",
26
+ # type=str,
27
+ # help="Имя пользователя для аутентификации",
28
+ # default=getenv("AUTH_USERNAME"),
29
+ # )
30
+ # parser.add_argument(
31
+ # "-P",
32
+ # "--password",
33
+ # type=str,
34
+ # help="Пароль для аутентификации",
35
+ # default=getenv("AUTH_PASSWORD"),
36
+ # )
36
37
  parser.add_argument(
37
38
  "-s",
38
39
  "--search",
39
40
  type=str,
40
41
  default="",
41
- help="Строка поиска для контактов работодателя",
42
+ help="Строка поиска контактов работодателя (email, имя, название компании)",
42
43
  )
43
44
  parser.add_argument(
44
45
  "-p",
45
46
  "--page",
46
47
  default=1,
47
- help="Номер страницы в выдаче",
48
+ help="Номер страницы в выдаче. Игнорируется при экспорте.",
49
+ )
50
+ parser.add_argument(
51
+ "--export",
52
+ action=argparse.BooleanOptionalAction,
53
+ default=False,
54
+ help="Экспортировать контакты работодателей.",
55
+ )
56
+ parser.add_argument(
57
+ "-f",
58
+ "--format",
59
+ default="html",
60
+ choices=["html", "jsonl"],
61
+ help="Формат вывода",
48
62
  )
49
63
 
50
64
  def run(self, args: Namespace, _, telemetry_client: TelemetryClient) -> None:
51
- results = telemetry_client.get_telemetry(
65
+ if args.export:
66
+ contact_persons = []
67
+ page = 1
68
+ per_page = 100
69
+ while True:
70
+ res = telemetry_client.get_telemetry(
71
+ "/contact/persons",
72
+ {"search": args.search, "per_page": per_page, "page": page},
73
+ )
74
+ assert "contact_persons" in res
75
+ contact_persons += res["contact_persons"]
76
+ if per_page * page >= res["total"]:
77
+ break
78
+ page += 1
79
+ if args.format == "jsonl":
80
+ import json, sys
81
+
82
+ for contact in contact_persons:
83
+ json.dump(contact, sys.stdout, ensure_ascii=False)
84
+ sys.stdout.write("\n")
85
+ sys.stdout.flush()
86
+ else:
87
+ print(generate_html_report(contact_persons))
88
+ return
89
+
90
+ res = telemetry_client.get_telemetry(
52
91
  "/contact/persons",
53
92
  {"search": args.search, "per_page": 10, "page": args.page},
54
93
  )
55
- if "contact_persons" not in results:
56
- print("❌", results)
94
+ if "contact_persons" not in res:
95
+ print("❌", res)
57
96
  return 1
58
97
 
59
98
  print(
@@ -61,27 +100,194 @@ class Operation(BaseOperation):
61
100
  )
62
101
  print()
63
102
 
64
- self._print_contacts(results)
65
-
66
- def _print_contacts(self, data: dict) -> None:
67
- """Вывод всех контактов в древовидной структуре."""
68
- page = data["page"]
69
- pages = (data["total"] // data["per_page"]) + 1
70
- print(f"Страница {page}/{pages}:")
71
- contacts = data.get("contact_persons", [])
72
- for idx, contact in enumerate(contacts):
73
- is_last_contact = idx == len(contacts) - 1
74
- self._print_contact(contact, is_last_contact)
75
- print()
103
+ print_contacts(res)
104
+
105
+
106
+ def generate_html_report(data: list[dict]) -> str:
107
+ """
108
+ Генерирует HTML-отчет на основе предоставленных данных.
109
+ """
110
+ html_content = """\
111
+ <!DOCTYPE html>
112
+ <html lang="ru">
113
+ <head>
114
+ <meta charset="UTF-8">
115
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
116
+ <title>Контакты работодателей</title>
117
+ <style>
118
+ body {
119
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
120
+ margin: 20px;
121
+ background-color: #f4f7f6;
122
+ color: #333;
123
+ }
124
+ .container {
125
+ max-width: 900px;
126
+ margin: 20px auto;
127
+ background-color: #ffffff;
128
+ padding: 30px;
129
+ border-radius: 10px;
130
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
131
+ }
132
+ h1 {
133
+ color: #0056b3;
134
+ text-align: center;
135
+ margin-bottom: 30px;
136
+ }
137
+ .person-card {
138
+ background-color: #e9f0f8;
139
+ border: 1px solid #cce5ff;
140
+ border-radius: 8px;
141
+ padding: 20px;
142
+ margin-bottom: 25px;
143
+ transition: transform 0.2s ease-in-out;
144
+ }
145
+ .person-card:hover {
146
+ transform: translateY(-5px);
147
+ }
148
+ .person-card h2 {
149
+ color: #004085;
150
+ margin-top: 0;
151
+ margin-bottom: 10px;
152
+ border-bottom: 2px solid #0056b3;
153
+ padding-bottom: 5px;
154
+ }
155
+ .person-card p {
156
+ margin: 5px 0;
157
+ }
158
+ .person-card strong {
159
+ color: #004085;
160
+ }
161
+ .employer-info {
162
+ background-color: #d1ecf1;
163
+ border-left: 5px solid #007bff;
164
+ padding: 15px;
165
+ margin-top: 15px;
166
+ border-radius: 5px;
167
+ }
168
+ .employer-info h3 {
169
+ color: #0056b3;
170
+ margin-top: 0;
171
+ margin-bottom: 10px;
172
+ }
173
+ ul {
174
+ list-style-type: none;
175
+ padding: 0;
176
+ }
177
+ ul li {
178
+ background-color: #f8fafd;
179
+ padding: 8px 12px;
180
+ margin-bottom: 5px;
181
+ border-radius: 4px;
182
+ border: 1px solid #e0e9f1;
183
+ }
184
+ a {
185
+ color: #007bff;
186
+ text-decoration: none;
187
+ }
188
+ a:hover {
189
+ text-decoration: underline;
190
+ }
191
+ .no-data {
192
+ color: #6c757d;
193
+ font-style: italic;
194
+ }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <div class="container">
199
+ <h1>Полученные контакты</h1>
200
+ """
201
+
202
+ for item in data:
203
+ name = item.get("name", "N/A")
204
+ email = item.get("email", "N/A")
205
+ employer = item.get("employer") or {}
206
+
207
+ employer_name = employer.get("name", "N/A")
208
+ employer_area = employer.get("area", "N/A")
209
+ employer_site_url = employer.get("site_url", "")
210
+
211
+ phone_numbers = [
212
+ pn["phone_number"]
213
+ for pn in item.get("phone_numbers", [])
214
+ if "phone_number" in pn
215
+ ]
216
+ telegram_usernames = [
217
+ tu["username"]
218
+ for tu in item.get("telegram_usernames", [])
219
+ if "username" in tu
220
+ ]
221
+
222
+ html_content += f"""\
223
+ <div class="person-card">
224
+ <h2>{name}</h2>
225
+ <p><strong>Email:</strong> <a href="mailto:{email}">{email}</a></p>
226
+ """
227
+
228
+ if employer_name != "N/A":
229
+ html_content += f"""\
230
+ <div class="employer-info">
231
+ <h3>Работодатель: {employer_name}</h3>
232
+ <p><strong>Город:</strong> {employer_area}</p>
233
+ """
234
+ if employer_site_url:
235
+ html_content += f"""\
236
+ <p><strong>Сайт:</strong> <a href="{employer_site_url}" target="_blank">{employer_site_url}</a></p>
237
+ """
238
+ html_content += "</div>" # Закрываем employer-info
239
+ else:
240
+ html_content += (
241
+ '<p class="no-data">Информация о работодателе отсутствует.</p>'
242
+ )
243
+
244
+ if phone_numbers:
245
+ html_content += "<p><strong>Номера телефонов:</strong></p><ul>"
246
+ for phone in phone_numbers:
247
+ html_content += f"<li><a href='tel:{phone}'>{phone}</a></li>"
248
+ html_content += "</ul>"
249
+ else:
250
+ html_content += '<p class="no-data">Номера телефонов отсутствуют.</p>'
251
+
252
+ if telegram_usernames:
253
+ html_content += "<p><strong>Имена пользователей Telegram:</strong></p><ul>"
254
+ for username in telegram_usernames:
255
+ html_content += f"<li><a href='https://t.me/{username}' target='_blank'>@{username}</a></li>"
256
+ html_content += "</ul>"
257
+ else:
258
+ html_content += (
259
+ '<p class="no-data">Имена пользователей Telegram отсутствуют.</p>'
260
+ )
261
+
262
+ html_content += "</div>" # Закрываем person-card
263
+
264
+ html_content += """\
265
+ </div>
266
+ </body>
267
+ </html>"""
268
+ return html_content
269
+
270
+
271
+ def print_contacts(data: dict) -> None:
272
+ """Вывод всех контактов в древовидной структуре."""
273
+ page = data["page"]
274
+ pages = (data["total"] // data["per_page"]) + 1
275
+ print(f"Страница {page}/{pages}:")
276
+ contacts = data.get("contact_persons", [])
277
+ for idx, contact in enumerate(contacts):
278
+ is_last_contact = idx == len(contacts) - 1
279
+ print_contact(contact, is_last_contact)
280
+ print()
281
+
76
282
 
77
- def _print_contact(self, contact: dict, is_last_contact: bool) -> None:
78
- """Вывод информации о конкретном контакте."""
79
- prefix = "└──" if is_last_contact else "├──"
80
- print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
81
- prefix2 = " " if is_last_contact else " │ "
82
- print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
83
- employer = contact.get("employer") or {}
84
- print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
85
- print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
86
- print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
87
- print(prefix2)
283
+ def print_contact(contact: dict, is_last_contact: bool) -> None:
284
+ """Вывод информации о конкретном контакте."""
285
+ prefix = "└──" if is_last_contact else "├──"
286
+ print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
287
+ prefix2 = " " if is_last_contact else " │ "
288
+ print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
289
+ employer = contact.get("employer") or {}
290
+ print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
291
+ print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
292
+ print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
293
+ print(prefix2)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+ import os
1
3
  import argparse
2
4
  import logging
3
5
  import random
@@ -9,6 +11,22 @@ from ..main import BaseOperation
9
11
  from ..main import Namespace as BaseNamespace
10
12
  from ..mixins import GetResumeIdMixin
11
13
  from ..utils import parse_interval, random_text
14
+ from ..telemetry_client import TelemetryClient, TelemetryError
15
+ import re
16
+
17
+ try:
18
+ import readline
19
+
20
+ readline.add_history("/cancel ")
21
+ readline.set_history_length(10_000)
22
+ except ImportError:
23
+ pass
24
+
25
+
26
+ GOOGLE_DOCS_RE = re.compile(
27
+ r"\b(?:https?:\/\/)?(?:docs|forms|sheets|slides|drive)\.google\.com\/(?:document|spreadsheets|presentation|forms|file)\/(?:d|u)\/[a-zA-Z0-9_\-]+(?:\/[a-zA-Z0-9_\-]+)?\/?(?:[?#].*)?\b|\b(?:https?:\/\/)?(?:goo\.gl|forms\.gle)\/[a-zA-Z0-9]+\b",
28
+ re.I,
29
+ )
12
30
 
13
31
  logger = logging.getLogger(__package__)
14
32
 
@@ -66,8 +84,12 @@ class Operation(BaseOperation, GetResumeIdMixin):
66
84
  action=argparse.BooleanOptionalAction,
67
85
  )
68
86
 
69
- def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
87
+ def run(
88
+ self, args: Namespace, api_client: ApiClient, telemetry_client: TelemetryClient
89
+ ) -> None:
70
90
  self.api_client = api_client
91
+ self.telemetry_client = telemetry_client
92
+ self.enable_telemetry = not args.disable_telemetry
71
93
  self.resume_id = self._get_resume_id()
72
94
  self.reply_min_interval, self.reply_max_interval = args.reply_interval
73
95
  self.reply_message = args.reply_message or args.config["reply_message"]
@@ -81,6 +103,8 @@ class Operation(BaseOperation, GetResumeIdMixin):
81
103
  def _reply_chats(self) -> None:
82
104
  me = self.me = self.api_client.get("/me")
83
105
 
106
+ telemetry_data = {"links": []}
107
+
84
108
  basic_message_placeholders = {
85
109
  "first_name": me.get("first_name", ""),
86
110
  "last_name": me.get("last_name", ""),
@@ -144,6 +168,32 @@ class Operation(BaseOperation, GetResumeIdMixin):
144
168
 
145
169
  page = messages_res["pages"] - 1
146
170
 
171
+ if self.enable_telemetry:
172
+ # Собираем ссылки на тестовые задания
173
+ for message in message_history:
174
+ if message.startswith("-> "):
175
+ continue
176
+ # Тестовые задания и тп
177
+ for link in GOOGLE_DOCS_RE.findall(message):
178
+ document_data = {
179
+ "vacancy_url": vacancy.get("alternate_url"),
180
+ "vacancy_name": vacancy.get("name"),
181
+ "salary": (
182
+ f"{salary.get('from', '...')}-{salary.get('to', '...')} {salary.get('currency', 'RUR')}" # noqa: E501
183
+ if salary
184
+ else None
185
+ ),
186
+ "employer_url": vacancy.get("employer", {}).get(
187
+ "alternate_url"
188
+ ),
189
+ "link": link,
190
+ }
191
+
192
+ telemetry_data["links"].append(document_data)
193
+
194
+ if os.getenv("TEST_SEND_TELEMETRY") in ["1", "y", "Y"]:
195
+ continue
196
+
147
197
  logger.debug(last_message)
148
198
 
149
199
  is_employer_message = (
@@ -152,8 +202,10 @@ class Operation(BaseOperation, GetResumeIdMixin):
152
202
 
153
203
  if is_employer_message or not negotiation.get("viewed_by_opponent"):
154
204
  if self.reply_message:
155
- message = random_text(self.reply_message) % message_placeholders
156
- logger.debug(message)
205
+ send_message = (
206
+ random_text(self.reply_message) % message_placeholders
207
+ )
208
+ logger.debug(send_message)
157
209
  else:
158
210
  print("🏢", message_placeholders["employer_name"])
159
211
  print("💼", message_placeholders["vacancy_name"])
@@ -173,8 +225,16 @@ class Operation(BaseOperation, GetResumeIdMixin):
173
225
  else message_history
174
226
  ):
175
227
  print(msg)
176
- print("-" * 10)
177
- message = input("Ваше сообщение: ").strip()
228
+ try:
229
+ print("-" * 10)
230
+ print()
231
+ print(
232
+ "Чтобы отменить отклик введите /cancel <необязательное сообщение для отказа>"
233
+ )
234
+ print()
235
+ send_message = input("Ваше сообщение: ").strip()
236
+ except EOFError:
237
+ continue
178
238
  if not message:
179
239
  print("🚶 Пропускаем чат")
180
240
  continue
@@ -183,7 +243,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
183
243
  logger.info(
184
244
  "Dry Run: Отправка сообщения в чат по вакансии %s: %s",
185
245
  vacancy["alternate_url"],
186
- message,
246
+ send_message,
187
247
  )
188
248
  continue
189
249
 
@@ -193,17 +253,33 @@ class Operation(BaseOperation, GetResumeIdMixin):
193
253
  self.reply_max_interval,
194
254
  )
195
255
  )
196
- self.api_client.post(
197
- f"/negotiations/{nid}/messages",
198
- message=message,
199
- )
200
- print(
201
- "📨 Отправили сообщение для",
202
- vacancy["alternate_url"],
203
- )
256
+
257
+ if send_message.startswith("/cancel"):
258
+ _, decline_allowed = send_message.split("/cancel", 1)
259
+ self.api_client.delete(
260
+ f"/negotiations/active/{negotiation['id']}",
261
+ with_decline_message=decline_allowed.strip(),
262
+ )
263
+ print("Отменили заявку", vacancy["alternate_url"])
264
+ else:
265
+ self.api_client.post(
266
+ f"/negotiations/{nid}/messages",
267
+ message=send_message,
268
+ )
269
+ print(
270
+ "📨 Отправили сообщение для",
271
+ vacancy["alternate_url"],
272
+ )
204
273
  except ApiError as ex:
205
274
  logger.error(ex)
206
275
 
276
+ if self.enable_telemetry and len(telemetry_data["links"]) > 0:
277
+ logger.debug(telemetry_data)
278
+ try:
279
+ self.telemetry_client.send_telemetry("/docs", telemetry_data)
280
+ except TelemetryError as ex:
281
+ logger.warning(ex, exc_info=True)
282
+
207
283
  print("📝 Сообщения разосланы!")
208
284
 
209
285
  def _get_negotiations(self) -> list[dict]:
@@ -84,7 +84,7 @@ class TelemetryClient:
84
84
  )
85
85
  # response.raise_for_status()
86
86
  result = response.json()
87
- if "error" in result:
87
+ if 200 > response.status_code >= 300:
88
88
  raise TelemetryError(result)
89
89
  return result
90
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hh-applicant-tool
3
- Version: 0.5.9
3
+ Version: 0.6.1
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -30,11 +30,6 @@ Description-Content-Type: text/markdown
30
30
  <img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
31
31
  </div>
32
32
 
33
- ### Внимание!!!
34
-
35
- Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/).
36
-
37
-
38
33
  ### Описание
39
34
 
40
35
  > Утилита для генерации сопроводительного письма может использовать AI
@@ -53,6 +48,10 @@ asdf/pyenv/conda и что-то еще. В школотронской Manjaro и
53
48
 
54
49
  > Если в веб-интерфейсе выставить фильтры, то они будут применяться в скрипте при отклике на подходящие
55
50
 
51
+ ### Внимание!!!
52
+
53
+ Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/). Оно обладает минимальным функционалом: обновление резюме (одного) и рассылка откликов (чистить их и тп нельзя).
54
+
56
55
  ### Предыстория
57
56
 
58
57
  Долгое время я делал массовые заявки с помощью консоли браузера:
@@ -184,7 +183,7 @@ hh-applicant-tool config -p
184
183
 
185
184
  | Имя атрибута | Описание |
186
185
  | --- | --- |
187
- | `user_agent` | Кастомный юзерагент, передаваемый при кажом запросе, например, `Mozilla/5.0 YablanBrowser` |
186
+ | `user_agent` | Кастомный юзерагент, передаваемый при кажом запросе. |
188
187
  | `proxy_url` | Прокси, используемый для всех запросов, например, `socks5h://127.0.0.1:9050` |
189
188
  | `reply_message` | Сообщение для ответа работодателю при отклике на вакансии, см. формат сообщений |
190
189
 
@@ -210,6 +209,10 @@ $ hh-applicant-tool update-resumes
210
209
 
211
210
  # Чистим заявки и баним за отказы говноконторы
212
211
  $ hh-applicant-tool clear-negotiations --blacklist-discard
212
+
213
+ # Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
214
+ # приглашение
215
+ $ hh-applicant-tool get-employer-contacts --export -f html > report.html
213
216
  ```
214
217
 
215
218
  Можно вызвать любой метод API:
@@ -257,7 +260,8 @@ https://hh.ru/employer/1918903
257
260
  | **call-api** | Вызов произвольного метода API с выводом результата. |
258
261
  | **refresh-token** | Обновляет access_token. |
259
262
  | **config** | Редактировать конфигурационный файл. |
260
- | **get-employer-contacts** | Получить список контактов работадателей. |
263
+ | **get-employer-contacts** | Получить список полученных вами контактов работодателей. Поддерживается так же экспорт в html/jsonl. Если хотите собирать контакты с нескольких акков, то укажите им одинаковый `client_telemetry_id` в конфигах. |
264
+ | **delete-telemetry** | Удадяет телеметрию, если та была включена. |
261
265
 
262
266
  ### Формат текста сообщений
263
267
 
@@ -8,25 +8,25 @@ hh_applicant_tool/api/errors.py,sha256=Rd1XE2OTtZDa3GDqght2LtOnTHWtOx7Zsow87nn4x
8
8
  hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3s,823
9
9
  hh_applicant_tool/constants.py,sha256=KV_jowi21ToMp8yqF1vWolnVZb8nAC3rYRkcFJ71m-Q,759
10
10
  hh_applicant_tool/jsonc.py,sha256=QNS4gRHfi7SAeOFnffAIuhH7auC4Y4HAkmH12eX5PkI,4002
11
- hh_applicant_tool/main.py,sha256=Ux183fYrF6qYECv2L-VoKbjp49Uh0Gm8YK3hGDfaH8A,5536
11
+ hh_applicant_tool/main.py,sha256=A4YPkNXAdZY0GoGm0iigiQtzXTrpR3SaIGo54q9-Dd0,5652
12
12
  hh_applicant_tool/mixins.py,sha256=8VoyrNgdlljy6pLTSFGJOYd9kagWT3yFOZYIGR6MEbI,425
13
13
  hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- hh_applicant_tool/operations/apply_similar.py,sha256=Kr8sIYx0TJn64S0G91MmZ4gO7S0tB30mB4RNE6CFqvE,16531
14
+ hh_applicant_tool/operations/apply_similar.py,sha256=EXY7LXuUGx5CjbHG22bR2-22gG3c--IniQjreHTG1pY,15568
15
15
  hh_applicant_tool/operations/authorize.py,sha256=NYrxe6oemUBcDHioT1t1lJmi9l45V4ZXzQPD_-nf6hk,3328
16
- hh_applicant_tool/operations/call_api.py,sha256=tKwqVHMiQs98X7wRPhpRHNm5KVF2fr_BAUcZR2QZE9g,1423
16
+ hh_applicant_tool/operations/call_api.py,sha256=o3GZgtqk6w4zpCm-JTHVjFrKVOwW-vsu1HdRi-hqAjo,1423
17
17
  hh_applicant_tool/operations/clear_negotiations.py,sha256=mu9nBdP7b_dlEMQk88w0IWX1lNTTFqnWbS1tCO1Mlws,4329
18
18
  hh_applicant_tool/operations/config.py,sha256=BzGWbHwNlXIpYHxnZUidDZTk1-7GZb8UL-asy8w4uN4,1390
19
19
  hh_applicant_tool/operations/delete_telemetry.py,sha256=JHdh_l7IJL_qy5AIIy8FQpUupmH60D3a6zjfEVKkT2U,986
20
- hh_applicant_tool/operations/get_employer_contacts.py,sha256=lUotOgxaySRNoSruJiU1qVdtGLp8BBj1vQEXvsDhkW0,3382
20
+ hh_applicant_tool/operations/get_employer_contacts.py,sha256=Sd-x3O08bmKm1OGVLtJ6rcPZ_j1jwjlqKV4z1n_G-38,9918
21
21
  hh_applicant_tool/operations/list_resumes.py,sha256=dILHyBCSEVqdNAvD8SML5f2Lau1R2AzTaKE9B4FG8Wg,1109
22
22
  hh_applicant_tool/operations/refresh_token.py,sha256=v_Fcw9mCfOdE6MLTCQjZQudhJPX0fup3k0BaIM394Qw,834
23
- hh_applicant_tool/operations/reply_employers.py,sha256=Ao5FxzjKc2DbmF8crgdhCEfKr57Zz5Xc5aBwyQMgT3I,8769
23
+ hh_applicant_tool/operations/reply_employers.py,sha256=MXJQi-GtNS5qvzXLAs6lCQAts2C95erkLhKlJjOgmU4,12141
24
24
  hh_applicant_tool/operations/update_resumes.py,sha256=_r7HA_vpYMs5DFY-mVP1ZRG9bggsv7ebKYrwteBmJ30,1053
25
25
  hh_applicant_tool/operations/whoami.py,sha256=pNWJMmEQLBk3U6eiGz4CHcX7eXzDXcfezFjX7zLjqyA,711
26
- hh_applicant_tool/telemetry_client.py,sha256=gg0ZUzWcdq-_9xlAm1Zu57DBn4u0_sBVT3i6qPn0Zwk,3818
26
+ hh_applicant_tool/telemetry_client.py,sha256=1EKZWc5kMx2yERW9SrR9vaf-OB6M_KKcMXeicH5YyY0,3834
27
27
  hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
28
28
  hh_applicant_tool/utils.py,sha256=3T4A2AykGqTwtGAttmYplIjHwFl3pNAcbWIVuA-OheQ,3080
29
- hh_applicant_tool-0.5.9.dist-info/METADATA,sha256=pmGVY2rc2LPxrmdAI_7d65qdLFX_KJYXwfhOC8mvD48,19814
30
- hh_applicant_tool-0.5.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
31
- hh_applicant_tool-0.5.9.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
32
- hh_applicant_tool-0.5.9.dist-info/RECORD,,
29
+ hh_applicant_tool-0.6.1.dist-info/METADATA,sha256=V7CpXjaY9umtZo_DT70WEQMfe25CvxMk14RpL-6Dndc,20600
30
+ hh_applicant_tool-0.6.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
31
+ hh_applicant_tool-0.6.1.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
32
+ hh_applicant_tool-0.6.1.dist-info/RECORD,,