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

@@ -9,12 +9,10 @@ from typing import TextIO, Tuple
9
9
  from ..api import ApiClient, ApiError, BadRequest
10
10
  from ..main import BaseOperation
11
11
  from ..main import Namespace as BaseNamespace
12
+ from ..telemetry_client import TelemetryError
13
+ from ..telemetry_client import get_client as get_telemetry_client
12
14
  from ..types import ApiListResponse, VacancyItem
13
- from ..utils import print_err, truncate_string, fix_datetime
14
- from ..telemetry_client import (
15
- get_client as get_telemetry_client,
16
- TelemetryError,
17
- )
15
+ from ..utils import fix_datetime, print_err, truncate_string
18
16
 
19
17
  logger = logging.getLogger(__package__)
20
18
 
@@ -71,11 +69,32 @@ class Operation(BaseOperation):
71
69
  access_token=args.config["token"]["access_token"],
72
70
  user_agent=args.config["user_agent"],
73
71
  )
72
+ resume_id = self._get_resume_id(args, api)
73
+ application_messages = self._get_application_messages(args)
74
+
75
+ apply_min_interval, apply_max_interval = args.apply_interval
76
+ page_min_interval, page_max_interval = args.page_interval
77
+
78
+ self._apply_similar(
79
+ api,
80
+ resume_id,
81
+ args.force_message,
82
+ application_messages,
83
+ apply_min_interval,
84
+ apply_max_interval,
85
+ page_min_interval,
86
+ page_max_interval,
87
+ )
88
+
89
+ def _get_resume_id(self, args: Namespace, api: ApiClient) -> str:
74
90
  if not (
75
91
  resume_id := args.resume_id or args.config["default_resume_id"]
76
92
  ):
77
93
  resumes: ApiListResponse = api.get("/resumes/mine")
78
94
  resume_id = resumes["items"][0]["id"]
95
+ return resume_id
96
+
97
+ def _get_application_messages(self, args: Namespace) -> list[str]:
79
98
  if args.message_list:
80
99
  application_messages = list(
81
100
  filter(None, map(str.strip, args.message_list))
@@ -88,51 +107,7 @@ class Operation(BaseOperation):
88
107
  "Хочу присоединиться к вашей успешной команде лидеров рынка в качестве %(name)s",
89
108
  "Мое резюме содержит все баззворды, указанные в вашей вакансии %(name)s",
90
109
  ]
91
-
92
- apply_min_interval, apply_max_interval = args.apply_interval
93
- page_min_interval, page_max_interval = args.page_interval
94
-
95
- self._apply_similar(
96
- api,
97
- resume_id,
98
- args.force_message,
99
- application_messages,
100
- apply_min_interval,
101
- apply_max_interval,
102
- page_min_interval,
103
- page_max_interval,
104
- )
105
-
106
- def _get_vacancies(
107
- self,
108
- api: ApiClient,
109
- resume_id: str,
110
- page_min_interval: float,
111
- page_max_interval: float,
112
- ) -> list[VacancyItem]:
113
- rv = []
114
- per_page = 100
115
- for page in range(20):
116
- res: ApiListResponse = api.get(
117
- f"/resumes/{resume_id}/similar_vacancies",
118
- page=page,
119
- per_page=per_page,
120
- order_by="relevance",
121
- )
122
- rv.extend(res["items"])
123
-
124
- if getenv("TEST_TELEMETRY"):
125
- break
126
-
127
- if page >= res["pages"] - 1:
128
- break
129
-
130
- # Задержка перед получением следующей страницы
131
- if page > 0:
132
- interval = random.uniform(page_min_interval, page_max_interval)
133
- time.sleep(interval)
134
-
135
- return rv
110
+ return application_messages
136
111
 
137
112
  def _apply_similar(
138
113
  self,
@@ -145,42 +120,34 @@ class Operation(BaseOperation):
145
120
  page_min_interval: float,
146
121
  page_max_interval: float,
147
122
  ) -> None:
148
- item: VacancyItem
149
-
150
- # Телеметрия не включает ваши персональные данные, она нужна для сбора информации о работодателях и их вакансиях
151
123
  telemetry_client = get_telemetry_client()
152
124
  telemetry_data = defaultdict(dict)
153
125
 
154
- for item in self._get_vacancies(
155
- api, resume_id, page_min_interval, page_max_interval
156
- ):
126
+ vacancies = self._get_vacancies(
127
+ api, resume_id, page_min_interval, page_max_interval, per_page=100
128
+ )
129
+
130
+ self._collect_vacancy_telemetry(telemetry_data, vacancies)
131
+
132
+ for vacancy in vacancies:
157
133
  try:
158
- # Информация о вакансии
159
- vacancy_id = item["id"]
160
-
161
- telemetry_data["vacancies"][vacancy_id] = {
162
- "name": item.get("name"),
163
- "type": item.get("type", {}).get("id"), # open/closed
164
- "area": item.get("area", {}).get("name"), # город
165
- "salary": item.get("salary"), # from, to, currency, gross
166
- "direct_url": item.get(
167
- "alternate_url"
168
- ), # ссылка на вакансию
169
- "created_at": fix_datetime(
170
- item.get("created_at")
171
- ), # будем вычислять говно-вакансии, которые по полгода висят
172
- "published_at": fix_datetime(item.get("published_at")),
173
- "contacts": item.get(
174
- "contacts"
175
- ), # пиздорванки там телеграм для связи указывают
176
- # Остальное неинтересно
177
- }
134
+ if getenv("TEST_TELEMETRY"):
135
+ break
136
+
137
+ if vacancy["has_test"]:
138
+ print("🚫 Пропускаем тест", vacancy["alternate_url"])
139
+ continue
178
140
 
179
- employer_id = item["employer"][
180
- "id"
181
- ] # меня интересуют только название и ссылка на сайт
141
+ relations = vacancy.get("relations", [])
142
+
143
+ if relations:
144
+ print(
145
+ "🚫 Пропускаем ответ на заявку",
146
+ vacancy["alternate_url"],
147
+ )
148
+ continue
182
149
 
183
- # так еще эмулируем какое-то иное действие нежели набор однотипных
150
+ employer_id = vacancy["employer"]["id"]
184
151
  employer = api.get(f"/employers/{employer_id}")
185
152
 
186
153
  telemetry_data["employers"][employer_id] = {
@@ -190,29 +157,6 @@ class Operation(BaseOperation):
190
157
  "site_url": employer.get("site_url"),
191
158
  "area": employer.get("area", {}).get("name"), # город
192
159
  }
193
-
194
- if getenv("TEST_TELEMETRY"):
195
- break
196
-
197
- if item["has_test"]:
198
- print("Пропускаем тест", item["alternate_url"])
199
- continue
200
-
201
- relations = item.get("relations", [])
202
-
203
- # Там черезжопно нужно хеш отклика получать чтобы его отменить
204
- # if "got_response" in relations:
205
- # # Тупая пизда ее даже не рассматривала
206
- # print(
207
- # "Отменяем заявку чтобы отправить ее снова",
208
- # item["alternate_url"],
209
- # )
210
- # api.delete(f"/negotiations/active/{item['id']}")
211
- # elif relations:
212
- if relations:
213
- print("Пропускаем ответ на заявку", item["alternate_url"])
214
- continue
215
-
216
160
  # Задержка перед отправкой отклика
217
161
  interval = random.uniform(
218
162
  apply_min_interval, apply_max_interval
@@ -221,10 +165,10 @@ class Operation(BaseOperation):
221
165
 
222
166
  params = {
223
167
  "resume_id": resume_id,
224
- "vacancy_id": item["id"],
168
+ "vacancy_id": vacancy["id"],
225
169
  "message": (
226
- random.choice(application_messages) % item
227
- if force_message or item["response_letter_required"]
170
+ random.choice(application_messages) % vacancy
171
+ if force_message or vacancy["response_letter_required"]
228
172
  else ""
229
173
  ),
230
174
  }
@@ -233,9 +177,9 @@ class Operation(BaseOperation):
233
177
  assert res == {}
234
178
  print(
235
179
  "📨 Отправили отклик",
236
- item["alternate_url"],
180
+ vacancy["alternate_url"],
237
181
  "(",
238
- truncate_string(item["name"]),
182
+ truncate_string(vacancy["name"]),
239
183
  ")",
240
184
  )
241
185
  except ApiError as ex:
@@ -245,8 +189,68 @@ class Operation(BaseOperation):
245
189
 
246
190
  print("📝 Отклики на вакансии разосланы!")
247
191
 
248
- # Отправляем telemetry_data
192
+ self._send_telemetry(telemetry_client, telemetry_data)
193
+
194
+ def _get_vacancies(
195
+ self,
196
+ api: ApiClient,
197
+ resume_id: str,
198
+ page_min_interval: float,
199
+ page_max_interval: float,
200
+ per_page: int,
201
+ ) -> list[VacancyItem]:
202
+ rv = []
203
+ for page in range(20):
204
+ res: ApiListResponse = api.get(
205
+ f"/resumes/{resume_id}/similar_vacancies",
206
+ page=page,
207
+ per_page=per_page,
208
+ order_by="relevance",
209
+ )
210
+ rv.extend(res["items"])
211
+
212
+ if getenv("TEST_TELEMETRY"):
213
+ break
214
+
215
+ if page >= res["pages"] - 1:
216
+ break
217
+
218
+ # Задержка перед получением следующей страницы
219
+ if page > 0:
220
+ interval = random.uniform(page_min_interval, page_max_interval)
221
+ time.sleep(interval)
222
+
223
+ return rv
224
+
225
+ def _collect_vacancy_telemetry(
226
+ self, telemetry_data: defaultdict, vacancies: list[VacancyItem]
227
+ ) -> None:
228
+ for vacancy in vacancies:
229
+ vacancy_id = vacancy["id"]
230
+ telemetry_data["vacancies"][vacancy_id] = {
231
+ "name": vacancy.get("name"),
232
+ "type": vacancy.get("type", {}).get("id"), # open/closed
233
+ "area": vacancy.get("area", {}).get("name"), # город
234
+ "salary": vacancy.get("salary"), # from, to, currency, gross
235
+ "direct_url": vacancy.get(
236
+ "alternate_url"
237
+ ), # ссылка на вакансию
238
+ "created_at": fix_datetime(
239
+ vacancy.get("created_at")
240
+ ), # будем вычислять говно-вакансии, которые по полгода висят
241
+ "published_at": fix_datetime(vacancy.get("published_at")),
242
+ "contacts": vacancy.get(
243
+ "contacts"
244
+ ), # пиздорванки там телеграм для связи указывают
245
+ "employer_id": int(vacancy["employer"]["id"]),
246
+ # Остальное неинтересно
247
+ }
248
+
249
+ def _send_telemetry(
250
+ self, telemetry_client, telemetry_data: defaultdict
251
+ ) -> None:
249
252
  try:
250
- telemetry_client.send_telemetry("/collect", dict(telemetry_data))
251
- except TelemetryError as err:
252
- logger.error("Не могу отправить телеметрию")
253
+ res = telemetry_client.send_telemetry("/collect", dict(telemetry_data))
254
+ logger.debug(res)
255
+ except TelemetryError as ex:
256
+ logger.error(ex)
@@ -19,7 +19,9 @@ class TelemetryError(Exception):
19
19
  class TelemetryClient:
20
20
  """Клиент для отправки телеметрии на сервер."""
21
21
 
22
- server_address = base64.b64decode('aHR0cDovLzMxLjEzMS4yNTEuMTA3OjU0MTU2').decode()
22
+ server_address = base64.b64decode(
23
+ "aHR0cDovLzMxLjEzMS4yNTEuMTA3OjU0MTU2"
24
+ ).decode()
23
25
 
24
26
  def __init__(
25
27
  self,
@@ -53,8 +55,12 @@ class TelemetryClient:
53
55
 
54
56
  try:
55
57
  response = self.session.post(url, json=data)
56
- response.raise_for_status()
57
- return response.json()
58
+ # response.raise_for_status()
59
+ result = response.json()
60
+ if "error" in result:
61
+ raise TelemetryError(result)
62
+ return result
63
+
58
64
  except (
59
65
  requests.exceptions.RequestException,
60
66
  json.JSONDecodeError,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -37,13 +37,13 @@ Description-Content-Type: text/markdown
37
37
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
38
38
  asdf/pyenv/conda и что-то еще...
39
39
 
40
+ Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
41
+
40
42
  Пример работы:
41
43
 
42
44
  ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
43
45
 
44
46
 
45
- Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, в Windows, но в WSL она не работает, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
46
-
47
47
  Предыстория.
48
48
 
49
49
  Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отличить Java от JavaScript, но я думаю, что <s>в значительном числе случаев, тут имеют место такие вот рассылки</s> они просто идиотки... И я тупо стал спамить как они. Мне уже было просто лень читать весь этот бред, что пишут долбоебы в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое.
@@ -60,10 +60,14 @@ $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
60
60
 
61
61
  ```bash
62
62
  # Версия с поддержкой авторизации через запуск окна с браузером (эта версия очень много весит)
63
+ # Можно использовать обычный pip
63
64
  $ pipx install 'hh-applicant-tool[qt]'
64
65
 
65
66
  # Если хочется использовать самую последнюю версию, то можно установить ее через git
66
67
  $ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
68
+
69
+ # Для обновления до новой версии
70
+ $ pipx upgrade 'hh-applicant-tool'
67
71
  ```
68
72
 
69
73
  Использование:
@@ -142,7 +146,7 @@ $ hh-applicant-tool -vv authorize
142
146
 
143
147
  ![image](https://github.com/user-attachments/assets/88961e31-4ea3-478f-8c43-914d6785bc3b)
144
148
 
145
- В случае успешной авторизации токены будут сохранены `~/.config/hh-applicant-tool/config.json`:
149
+ В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
146
150
 
147
151
  ```json
148
152
  {
@@ -231,8 +235,6 @@ rm -f ~/.local/share/applications/hhandroid.desktop
231
235
 
232
236
  Утилита собирает и передает на сервер разработчика следующую ифнормацию:
233
237
 
234
- Утилита собирает следующую информацию:
235
-
236
238
  1. Название вакансии.
237
239
  1. Тип вакансии (открытая/закрытая).
238
240
  1. Город, в котором размещена вакансия.
@@ -247,6 +249,8 @@ rm -f ~/.local/share/applications/hhandroid.desktop
247
249
  1. Ссылка на сайт компании.
248
250
  1. Город, в котором находится компания.
249
251
 
250
- !!! УТИЛИТА НЕ СОБИРАЕТ НИКАКИХ ПЕРСОНАЛЬНЫХ ДАННЫХ ПОЛЬЗОВАТЕЛЕЙ — ТОЛЬКО ВСЯКИХ РАБОТАДАТЕЛЕЙ И ИХ ОВЧАРОК. ТАК ЖЕ Я ОБЕЩАЮ, ЧТО УТИЛИТА ВСЕГДА БУДЕТ БЕСПЛАТНОЙ, ВСЕ КТО ЕЮ ПЫТАЮТСЯ ТОРГОВАТЬ — УЕБКИ И У НИХ ПОЧЕРНЕЕТ И ОТВАЛИТСЯ ХУЙ
252
+ [Исходники сервера](https://gist.github.com/s3rgeym/b9fb04ef529a511326413c1090597ac5)
253
+
254
+ !!! УТИЛИТА НЕ СОБИРАЕТ НИКАКИХ ПЕРСОНАЛЬНЫХ ДАННЫХ ПОЛЬЗОВАТЕЛЕЙ (IP ТОЖЕ НЕ СОХРАНЯЕТ) — ТОЛЬКО ДАННЫЕ ВСЯКИХ РАБОТАДАТЕЛЕЙ И ИХ ОВЧАРОК. ТАК ЖЕ Я ОБЕЩАЮ, ЧТО УТИЛИТА ВСЕГДА БУДЕТ БЕСПЛАТНОЙ, ВСЕ КТО ЕЮ ПЫТАЮТСЯ ТОРГОВАТЬ — УЕБКИ И У НИХ ПОЧЕРНЕЕТ И ОТВАЛИТСЯ ХУЙ. ЕДИНСТВЕННАЯ ПЛАТА ЗА ЕЕ ИСПОЛЬЗОВАНИЕ — ЭТО ПОМОЩЬ В ПАРСИНГЕ САЙТА HEADHUNTER (МЕНЯ ИНТЕРЕСУЕТ ЕГО БАЗА КОМПАНИЙ)
251
255
 
252
256
 
@@ -7,7 +7,7 @@ hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3
7
7
  hh_applicant_tool/constants.py,sha256=YdNz0CF4swJ9OO5vVpOZ39RP-05xSU0Ghw4Y6BhISoE,468
8
8
  hh_applicant_tool/main.py,sha256=XLKsd8mpCKuFoP3ESMpgJKdGL3m6tOQ8kKrOzCN-dwA,3075
9
9
  hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- hh_applicant_tool/operations/apply_similar.py,sha256=HEeNKELm1mK5oNwRxFTUByrK2-IMjmgsjPXSvzcVt-I,10504
10
+ hh_applicant_tool/operations/apply_similar.py,sha256=DNvYGbcnyV5LwjIHCOOXWtFEAUgpJHyZFG4Iy2doDJ0,10188
11
11
  hh_applicant_tool/operations/authorize.py,sha256=TyUTCSOGwSYVJMEd5vSI981LRRI-RZf8hnlVYhtRVwA,3184
12
12
  hh_applicant_tool/operations/call_api.py,sha256=oWAfvy4LwbsQ8HsgI_3en3sMTlu3ZWU7NzpxssrUNSU,1472
13
13
  hh_applicant_tool/operations/clear_negotiations.py,sha256=5ybdJMUfV9XYxnn2y4zvWdS_ZE8yXAbFJoXOMyqetw8,4041
@@ -15,10 +15,10 @@ hh_applicant_tool/operations/list_resumes.py,sha256=HYxjDALrWl_YBIitDldhhEs-WO_0
15
15
  hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
16
16
  hh_applicant_tool/operations/update_resumes.py,sha256=FKtwEL7i0vaOxLEn1nARi72SuMYB5VjN6Fihe6rlt-Y,1196
17
17
  hh_applicant_tool/operations/whoami.py,sha256=kdLQ_FjYzpJPKxFlocxf7vgXhW1zocb0bd5IAWmsQuA,861
18
- hh_applicant_tool/telemetry_client.py,sha256=0AdLzE37_1gbFsXjvBSm6PD5vfsdInEiCTpLMp3afsk,2172
18
+ hh_applicant_tool/telemetry_client.py,sha256=TlsNKlclPyJqLPO0xHkHKBIhT8bmgx1ZBup4PjE8w5E,2296
19
19
  hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
20
20
  hh_applicant_tool/utils.py,sha256=wXcxs5IinK7PH6ipCCpngw7faUiboEpi9a8j1wDEPKw,2282
21
- hh_applicant_tool-0.3.2.dist-info/METADATA,sha256=6xG0Rkcb6sAgziizY146FNdiAXlyYP9smfAoqHTaXWw,15451
22
- hh_applicant_tool-0.3.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
- hh_applicant_tool-0.3.2.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
- hh_applicant_tool-0.3.2.dist-info/RECORD,,
21
+ hh_applicant_tool-0.3.3.dist-info/METADATA,sha256=W8nPWVupEP0MLc9K-VOs9E8cEbAMH31jf8gFR0drlF0,15881
22
+ hh_applicant_tool-0.3.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
+ hh_applicant_tool-0.3.3.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
+ hh_applicant_tool-0.3.3.dist-info/RECORD,,