hh-applicant-tool 0.5.9__py3-none-any.whl → 0.6.0__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"
@@ -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,195 @@ 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
+ """
269
+ return html_content
270
+
271
+
272
+ def print_contacts(data: dict) -> None:
273
+ """Вывод всех контактов в древовидной структуре."""
274
+ page = data["page"]
275
+ pages = (data["total"] // data["per_page"]) + 1
276
+ print(f"Страница {page}/{pages}:")
277
+ contacts = data.get("contact_persons", [])
278
+ for idx, contact in enumerate(contacts):
279
+ is_last_contact = idx == len(contacts) - 1
280
+ print_contact(contact, is_last_contact)
281
+ print()
282
+
76
283
 
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)
284
+ def print_contact(contact: dict, is_last_contact: bool) -> None:
285
+ """Вывод информации о конкретном контакте."""
286
+ prefix = "└──" if is_last_contact else "├──"
287
+ print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
288
+ prefix2 = " " if is_last_contact else " │ "
289
+ print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
290
+ employer = contact.get("employer") or {}
291
+ print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
292
+ print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
293
+ print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
294
+ print(prefix2)
@@ -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.0
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
 
@@ -257,7 +256,8 @@ https://hh.ru/employer/1918903
257
256
  | **call-api** | Вызов произвольного метода API с выводом результата. |
258
257
  | **refresh-token** | Обновляет access_token. |
259
258
  | **config** | Редактировать конфигурационный файл. |
260
- | **get-employer-contacts** | Получить список контактов работадателей. |
259
+ | **get-employer-contacts** | Получить список полученных вами контактов работодателей. Поддерживается так же экспорт в html/jsonl. Если хотите собирать контакты с нескольких акков, то укажите им одинаковый `client_telemetry_id` в конфигах. |
260
+ | **delete-telemetry** | Удадяет телеметрию, если та была включена. |
261
261
 
262
262
  ### Формат текста сообщений
263
263
 
@@ -8,7 +8,7 @@ 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=oNbQLRwbqGa8iSdUhaeCi-x7jE0Z-Cp6oNEUW78sEoc,5535
12
12
  hh_applicant_tool/mixins.py,sha256=8VoyrNgdlljy6pLTSFGJOYd9kagWT3yFOZYIGR6MEbI,425
13
13
  hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  hh_applicant_tool/operations/apply_similar.py,sha256=Kr8sIYx0TJn64S0G91MmZ4gO7S0tB30mB4RNE6CFqvE,16531
@@ -17,7 +17,7 @@ hh_applicant_tool/operations/call_api.py,sha256=tKwqVHMiQs98X7wRPhpRHNm5KVF2fr_B
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=5hDauxwmbMi8qMfmrpyW_gIjBvi7ZIuim9eMLiXJ-Lw,10332
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
23
  hh_applicant_tool/operations/reply_employers.py,sha256=Ao5FxzjKc2DbmF8crgdhCEfKr57Zz5Xc5aBwyQMgT3I,8769
@@ -26,7 +26,7 @@ hh_applicant_tool/operations/whoami.py,sha256=pNWJMmEQLBk3U6eiGz4CHcX7eXzDXcfezF
26
26
  hh_applicant_tool/telemetry_client.py,sha256=gg0ZUzWcdq-_9xlAm1Zu57DBn4u0_sBVT3i6qPn0Zwk,3818
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.0.dist-info/METADATA,sha256=KC1rQJ0pfgsxRJbTEJUqh_fjlOfGnbZ6wMqV2zMPyds,20357
30
+ hh_applicant_tool-0.6.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
31
+ hh_applicant_tool-0.6.0.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
32
+ hh_applicant_tool-0.6.0.dist-info/RECORD,,