hh-applicant-tool 0.6.0__tar.gz → 0.6.1__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 (31) hide show
  1. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/PKG-INFO +5 -1
  2. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/README.md +4 -0
  3. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/main.py +3 -0
  4. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/apply_similar.py +7 -22
  5. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/call_api.py +1 -1
  6. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/get_employer_contacts.py +105 -106
  7. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/reply_employers.py +90 -14
  8. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/telemetry_client.py +1 -1
  9. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/pyproject.toml +1 -1
  10. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/__init__.py +0 -0
  11. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/__main__.py +0 -0
  12. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/ai/__init__.py +0 -0
  13. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/ai/blackbox.py +0 -0
  14. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/api/__init__.py +0 -0
  15. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/api/client.py +0 -0
  16. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/api/errors.py +0 -0
  17. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/color_log.py +0 -0
  18. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/constants.py +0 -0
  19. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/jsonc.py +0 -0
  20. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/mixins.py +0 -0
  21. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/__init__.py +0 -0
  22. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/authorize.py +0 -0
  23. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
  24. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/config.py +0 -0
  25. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/delete_telemetry.py +0 -0
  26. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/list_resumes.py +0 -0
  27. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/refresh_token.py +0 -0
  28. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/update_resumes.py +0 -0
  29. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/whoami.py +0 -0
  30. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/types.py +0 -0
  31. {hh_applicant_tool-0.6.0 → hh_applicant_tool-0.6.1}/hh_applicant_tool/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hh-applicant-tool
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -209,6 +209,10 @@ $ hh-applicant-tool update-resumes
209
209
 
210
210
  # Чистим заявки и баним за отказы говноконторы
211
211
  $ hh-applicant-tool clear-negotiations --blacklist-discard
212
+
213
+ # Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
214
+ # приглашение
215
+ $ hh-applicant-tool get-employer-contacts --export -f html > report.html
212
216
  ```
213
217
 
214
218
  Можно вызвать любой метод API:
@@ -190,6 +190,10 @@ $ hh-applicant-tool update-resumes
190
190
 
191
191
  # Чистим заявки и баним за отказы говноконторы
192
192
  $ hh-applicant-tool clear-negotiations --blacklist-discard
193
+
194
+ # Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
195
+ # приглашение
196
+ $ hh-applicant-tool get-employer-contacts --export -f html > report.html
193
197
  ```
194
198
 
195
199
  Можно вызвать любой метод API:
@@ -150,6 +150,9 @@ class HHApplicantTool:
150
150
  if (token := api_client.get_access_token()) != args.config["token"]:
151
151
  args.config.save(token=token)
152
152
  return res
153
+ except KeyboardInterrupt:
154
+ logger.warning("Interrupted by user")
155
+ return 1
153
156
  except Exception as e:
154
157
  logger.exception(e)
155
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
 
@@ -107,97 +107,97 @@ def generate_html_report(data: list[dict]) -> str:
107
107
  """
108
108
  Генерирует HTML-отчет на основе предоставленных данных.
109
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
- """
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
201
 
202
202
  for item in data:
203
203
  name = item.get("name", "N/A")
@@ -219,21 +219,21 @@ def generate_html_report(data: list[dict]) -> str:
219
219
  if "username" in tu
220
220
  ]
221
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>
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
226
  """
227
227
 
228
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>
229
+ html_content += f"""\
230
+ <div class="employer-info">
231
+ <h3>Работодатель: {employer_name}</h3>
232
+ <p><strong>Город:</strong> {employer_area}</p>
233
233
  """
234
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>
235
+ html_content += f"""\
236
+ <p><strong>Сайт:</strong> <a href="{employer_site_url}" target="_blank">{employer_site_url}</a></p>
237
237
  """
238
238
  html_content += "</div>" # Закрываем employer-info
239
239
  else:
@@ -261,11 +261,10 @@ def generate_html_report(data: list[dict]) -> str:
261
261
 
262
262
  html_content += "</div>" # Закрываем person-card
263
263
 
264
- html_content += """
265
- </div>
266
- </body>
267
- </html>
268
- """
264
+ html_content += """\
265
+ </div>
266
+ </body>
267
+ </html>"""
269
268
  return html_content
270
269
 
271
270
 
@@ -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
  [tool.poetry]
2
2
  name = "hh-applicant-tool"
3
- version = "0.6.0"
3
+ version = "0.6.1"
4
4
  description = ""
5
5
  authors = ["Senior YAML Developer <yamldeveloper@proton.me>"]
6
6
  readme = "README.md"