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

@@ -25,8 +25,9 @@ class Namespace(BaseNamespace):
25
25
  page_interval: Tuple[float, float]
26
26
 
27
27
 
28
+ # https://api.hh.ru/openapi/redoc
28
29
  class Operation(BaseOperation):
29
- """Откликнуться на все подходящие вакансии"""
30
+ """Откликнуться на все подходящие вакансии. По умолчанию применяются значения, которые были отмечены галочками в форме для поиска на сайте"""
30
31
 
31
32
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
32
33
  parser.add_argument("--resume-id", help="Идентефикатор резюме")
@@ -53,6 +54,24 @@ class Operation(BaseOperation):
53
54
  default="1-3",
54
55
  type=self._parse_interval,
55
56
  )
57
+ parser.add_argument(
58
+ "--order-by",
59
+ help="Сортировка вакансий",
60
+ choices=[
61
+ "publication_time",
62
+ "salary_desc",
63
+ "salary_asc",
64
+ "relevance",
65
+ "distance",
66
+ ],
67
+ default="relevance",
68
+ )
69
+ parser.add_argument(
70
+ "--search",
71
+ help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500', те можно и город указать, и ожидаемую зряплату",
72
+ type=str,
73
+ default=None,
74
+ )
56
75
 
57
76
  @staticmethod
58
77
  def _parse_interval(interval: str) -> Tuple[float, float]:
@@ -84,6 +103,8 @@ class Operation(BaseOperation):
84
103
  apply_max_interval,
85
104
  page_min_interval,
86
105
  page_max_interval,
106
+ args.order_by,
107
+ args.search,
87
108
  )
88
109
 
89
110
  def _get_resume_id(self, args: Namespace, api: ApiClient) -> str:
@@ -119,12 +140,20 @@ class Operation(BaseOperation):
119
140
  apply_max_interval: float,
120
141
  page_min_interval: float,
121
142
  page_max_interval: float,
143
+ order_by: str,
144
+ search: str | None = None,
122
145
  ) -> None:
123
146
  telemetry_client = get_telemetry_client()
124
147
  telemetry_data = defaultdict(dict)
125
148
 
126
149
  vacancies = self._get_vacancies(
127
- api, resume_id, page_min_interval, page_max_interval, per_page=100
150
+ api,
151
+ resume_id,
152
+ page_min_interval,
153
+ page_max_interval,
154
+ per_page=100,
155
+ order_by=order_by,
156
+ search=search,
128
157
  )
129
158
 
130
159
  self._collect_vacancy_telemetry(telemetry_data, vacancies)
@@ -134,7 +163,7 @@ class Operation(BaseOperation):
134
163
  if getenv("TEST_TELEMETRY"):
135
164
  break
136
165
 
137
- if vacancy["has_test"]:
166
+ if vacancy.get("has_test"):
138
167
  print("🚫 Пропускаем тест", vacancy["alternate_url"])
139
168
  continue
140
169
 
@@ -147,16 +176,23 @@ class Operation(BaseOperation):
147
176
  )
148
177
  continue
149
178
 
150
- employer_id = vacancy["employer"]["id"]
151
- employer = api.get(f"/employers/{employer_id}")
179
+ try:
180
+ employer_id = vacancy["employer"]["id"]
181
+ except IndexError:
182
+ logger.warning(
183
+ f"Вакансия без работодателя: {vacancy['alternate_url']}"
184
+ )
185
+ else:
186
+ employer = api.get(f"/employers/{employer_id}")
187
+
188
+ telemetry_data["employers"][employer_id] = {
189
+ "name": employer.get("name"),
190
+ "type": employer.get("type"),
191
+ "description": employer.get("description"),
192
+ "site_url": employer.get("site_url"),
193
+ "area": employer.get("area", {}).get("name"), # город
194
+ }
152
195
 
153
- telemetry_data["employers"][employer_id] = {
154
- "name": employer.get("name"),
155
- "type": employer.get("type"),
156
- "description": employer.get("description"),
157
- "site_url": employer.get("site_url"),
158
- "area": employer.get("area", {}).get("name"), # город
159
- }
160
196
  # Задержка перед отправкой отклика
161
197
  interval = random.uniform(
162
198
  apply_min_interval, apply_max_interval
@@ -166,13 +202,23 @@ class Operation(BaseOperation):
166
202
  params = {
167
203
  "resume_id": resume_id,
168
204
  "vacancy_id": vacancy["id"],
169
- "message": (
170
- random.choice(application_messages) % vacancy
171
- if force_message or vacancy["response_letter_required"]
172
- else ""
173
- ),
205
+ "message": "",
174
206
  }
175
207
 
208
+ if vacancy.get("response_letter_required"):
209
+ message_template = random.choice(application_messages)
210
+
211
+ try:
212
+ params["message"] = template_message % vacancy
213
+ except TypeError as ex:
214
+ # TypeError: not enough arguments for format string
215
+ # API HH все кривое, иногда нет идентификатора работодателя, иногда у вакансии нет названия.
216
+ # И это типа рашн хайлоад, где из-за дрочки на аджайл слепили кривую говнину.
217
+ logger.error(
218
+ f"Ошибка форматирования шаблона сообщения {template_message!r} для {vacancy = }"
219
+ )
220
+ continue
221
+
176
222
  res = api.post("/negotiations", params)
177
223
  assert res == {}
178
224
  print(
@@ -198,14 +244,20 @@ class Operation(BaseOperation):
198
244
  page_min_interval: float,
199
245
  page_max_interval: float,
200
246
  per_page: int,
247
+ order_by: str,
248
+ search: str | None = None,
201
249
  ) -> list[VacancyItem]:
202
250
  rv = []
203
251
  for page in range(20):
252
+ params = {
253
+ "page": page,
254
+ "per_page": per_page,
255
+ "order_by": order_by,
256
+ }
257
+ if search:
258
+ params["text"] = search
204
259
  res: ApiListResponse = api.get(
205
- f"/resumes/{resume_id}/similar_vacancies",
206
- page=page,
207
- per_page=per_page,
208
- order_by="relevance",
260
+ f"/resumes/{resume_id}/similar_vacancies", params
209
261
  )
210
262
  rv.extend(res["items"])
211
263
 
@@ -242,7 +294,11 @@ class Operation(BaseOperation):
242
294
  "contacts": vacancy.get(
243
295
  "contacts"
244
296
  ), # пиздорванки там телеграм для связи указывают
245
- "employer_id": int(vacancy["employer"]["id"]),
297
+ # HH с точки зрения перфикциониста — кусок говна, где кривые
298
+ # форматы даты, у вакансий может не быть работодателя...
299
+ "employer_id": int(vacancy["employer"]["id"])
300
+ if "employer" in vacancy and "id" in vacancy["employer"]
301
+ else None,
246
302
  # Остальное неинтересно
247
303
  }
248
304
 
@@ -250,7 +306,9 @@ class Operation(BaseOperation):
250
306
  self, telemetry_client, telemetry_data: defaultdict
251
307
  ) -> None:
252
308
  try:
253
- res = telemetry_client.send_telemetry("/collect", dict(telemetry_data))
309
+ res = telemetry_client.send_telemetry(
310
+ "/collect", dict(telemetry_data)
311
+ )
254
312
  logger.debug(res)
255
313
  except TelemetryError as ex:
256
314
  logger.error(ex)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -37,7 +37,7 @@ 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` токены из официального приложения и добавить их в конфиг.
40
+ Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (`C:\Users\%username%\AppData\Roaming\hh-applicant-tool\config.json` в Windows) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
41
41
 
42
42
  Пример работы:
43
43
 
@@ -67,9 +67,34 @@ $ pipx install 'hh-applicant-tool[qt]'
67
67
  $ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
68
68
 
69
69
  # Для обновления до новой версии
70
- $ pipx upgrade 'hh-applicant-tool'
70
+ $ pipx upgrade hh-applicant-tool
71
71
  ```
72
72
 
73
+ Отдельно я распишу процесс установки в **Windows** в подробностях:
74
+
75
+ * Для начала поставьте Python 3 любым удобным способом.
76
+ * Запустите терминал/консоль от Администратора и выполните:
77
+ ```ps
78
+ Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
79
+ ```
80
+ Без этой настройки не будут работать виртуальные окружения.
81
+ * Создайте и активируйте виртуальное окружение:
82
+ ```ps
83
+ PS> python -m pip venv hh-applicant-venv
84
+ PS> .\hh-applicant-venv\Scripts\activate
85
+ ```
86
+ * Поставьте все пакеты в виртуальное окружение `hh-applicant-tool`:
87
+ ```ps
88
+ (hh-applicant-venv) PS> pip install hh-applicant-tool[qt]
89
+ ```
90
+ * Проверьте работает ли оно:
91
+ ```ps
92
+ (hh-applicant-venv) PS> hh-applicant-tool -h
93
+ ```
94
+ * В случае неудачи, вернитесь к первому шагу. Для последующих запусков сперва активируйте виртуальное окружение.
95
+
96
+
97
+
73
98
  Использование:
74
99
 
75
100
  ```bash
@@ -135,7 +160,7 @@ https://hh.ru/employer/1918903
135
160
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
136
161
  | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
137
162
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
138
- | **call-api** | Вызов произвольного метода API с выводом результата. |
163
+ | **call-api** | Вызов произвольного метода API с выводом результата. |
139
164
  | **refresh-token** | Обновляет access_token. |
140
165
 
141
166
  Авторизуемся:
@@ -146,6 +171,10 @@ $ hh-applicant-tool -vv authorize
146
171
 
147
172
  ![image](https://github.com/user-attachments/assets/88961e31-4ea3-478f-8c43-914d6785bc3b)
148
173
 
174
+ > В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
175
+
176
+
177
+
149
178
  В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
150
179
 
151
180
  ```json
@@ -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=DNvYGbcnyV5LwjIHCOOXWtFEAUgpJHyZFG4Iy2doDJ0,10188
10
+ hh_applicant_tool/operations/apply_similar.py,sha256=RUV-hVyZZGEBBM7sfi7Ssg5c4plyc2JNETlL5PoDpPY,12875
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
@@ -18,7 +18,7 @@ hh_applicant_tool/operations/whoami.py,sha256=kdLQ_FjYzpJPKxFlocxf7vgXhW1zocb0bd
18
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.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,,
21
+ hh_applicant_tool-0.3.5.dist-info/METADATA,sha256=qUVl-yMhKOXWP6AhIIwdu5AFYX05npx9lQvsRMXYbx8,17247
22
+ hh_applicant_tool-0.3.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
+ hh_applicant_tool-0.3.5.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
+ hh_applicant_tool-0.3.5.dist-info/RECORD,,