hh-applicant-tool 0.3.4__tar.gz → 0.3.6__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.
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/PKG-INFO +37 -9
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/README.md +36 -8
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/api/client.py +28 -4
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/constants.py +1 -1
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/main.py +32 -8
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/apply_similar.py +59 -18
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/call_api.py +3 -7
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/clear_negotiations.py +2 -6
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/list_resumes.py +2 -6
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/update_resumes.py +2 -6
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/whoami.py +2 -6
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/telemetry_client.py +0 -2
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/pyproject.toml +1 -1
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/__init__.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/__main__.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/api/__init__.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/api/errors.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/color_log.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/__init__.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/authorize.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/refresh_token.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/types.py +0 -0
- {hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -19,8 +19,6 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
20
|
## HH Applicant Tool
|
|
21
21
|
|
|
22
|
-
> ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
|
|
23
|
-
|
|
24
22
|

|
|
25
23
|
[]()
|
|
26
24
|
[]()
|
|
@@ -37,7 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
37
35
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
38
36
|
asdf/pyenv/conda и что-то еще...
|
|
39
37
|
|
|
40
|
-
Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (
|
|
38
|
+
Данная утилита написана для 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
39
|
|
|
42
40
|
Пример работы:
|
|
43
41
|
|
|
@@ -67,9 +65,34 @@ $ pipx install 'hh-applicant-tool[qt]'
|
|
|
67
65
|
$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
|
|
68
66
|
|
|
69
67
|
# Для обновления до новой версии
|
|
70
|
-
$ pipx upgrade
|
|
68
|
+
$ pipx upgrade hh-applicant-tool
|
|
71
69
|
```
|
|
72
70
|
|
|
71
|
+
Отдельно я распишу процесс установки в **Windows** в подробностях:
|
|
72
|
+
|
|
73
|
+
* Для начала поставьте последнюю версию **Python 3** любым удобным способом.
|
|
74
|
+
* Запустите **Terminal** или **PowerShell** от Администратора и выполните:
|
|
75
|
+
```ps
|
|
76
|
+
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
|
|
77
|
+
```
|
|
78
|
+
Данная политика разрешает текущему пользователю (от которого зашли) запускать скрипты. Без нее не будут работать виртуальные окружения.
|
|
79
|
+
* Создайте и активируйте виртуальное окружение:
|
|
80
|
+
```ps
|
|
81
|
+
PS> python -m pip venv hh-applicant-venv
|
|
82
|
+
PS> .\hh-applicant-venv\Scripts\activate
|
|
83
|
+
```
|
|
84
|
+
* Поставьте все пакеты в виртуальное окружение `hh-applicant-venv`:
|
|
85
|
+
```ps
|
|
86
|
+
(hh-applicant-venv) PS> pip install hh-applicant-tool[qt]
|
|
87
|
+
```
|
|
88
|
+
* Проверьте работает ли оно:
|
|
89
|
+
```ps
|
|
90
|
+
(hh-applicant-venv) PS> hh-applicant-tool -h
|
|
91
|
+
```
|
|
92
|
+
* В случае неудачи вернитесь к первому шагу.
|
|
93
|
+
* Для последующих запусков сначала активируйте виртуальное окружение.
|
|
94
|
+
|
|
95
|
+
|
|
73
96
|
Использование:
|
|
74
97
|
|
|
75
98
|
```bash
|
|
@@ -133,10 +156,11 @@ https://hh.ru/employer/1918903
|
|
|
133
156
|
| **whoami** | Выводит информацию об авторизованном пользователе |
|
|
134
157
|
| **list-resumes** | Список резюме |
|
|
135
158
|
| **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
|
|
136
|
-
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в
|
|
159
|
+
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками. |
|
|
137
160
|
| **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
|
|
138
|
-
| **call-api** | Вызов произвольного метода API с выводом результата.
|
|
161
|
+
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
139
162
|
| **refresh-token** | Обновляет access_token. |
|
|
163
|
+
| **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, сайта фирмы и могут быть удалены по просьбе упал_намоченный лицо. Данная функция готова и будет доступна после 100 ⭐ |
|
|
140
164
|
|
|
141
165
|
Авторизуемся:
|
|
142
166
|
|
|
@@ -146,6 +170,10 @@ $ hh-applicant-tool -vv authorize
|
|
|
146
170
|
|
|
147
171
|

|
|
148
172
|
|
|
173
|
+
> В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
149
177
|
В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
|
|
150
178
|
|
|
151
179
|
```json
|
|
@@ -233,7 +261,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
|
|
|
233
261
|
|
|
234
262
|
Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
|
|
235
263
|
|
|
236
|
-
Утилита собирает и передает на сервер разработчика следующую
|
|
264
|
+
Утилита собирает и передает на сервер разработчика следующую информацию:
|
|
237
265
|
|
|
238
266
|
1. Название вакансии.
|
|
239
267
|
1. Тип вакансии (открытая/закрытая).
|
|
@@ -242,7 +270,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
|
|
|
242
270
|
1. Прямая ссылка на вакансию.
|
|
243
271
|
1. Дата создания вакансии.
|
|
244
272
|
1. Дата публикации вакансии.
|
|
245
|
-
1. Контактная информация
|
|
273
|
+
1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, хранящаеся в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля (может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и росписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова).
|
|
246
274
|
1. Название компании.
|
|
247
275
|
1. Тип компании.
|
|
248
276
|
1. Описание компании.
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
## HH Applicant Tool
|
|
2
2
|
|
|
3
|
-
> ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
|
|
4
|
-
|
|
5
3
|

|
|
6
4
|
[]()
|
|
7
5
|
[]()
|
|
@@ -18,7 +16,7 @@
|
|
|
18
16
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
19
17
|
asdf/pyenv/conda и что-то еще...
|
|
20
18
|
|
|
21
|
-
Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (
|
|
19
|
+
Данная утилита написана для 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` токены из официального приложения и добавить их в конфиг.
|
|
22
20
|
|
|
23
21
|
Пример работы:
|
|
24
22
|
|
|
@@ -48,9 +46,34 @@ $ pipx install 'hh-applicant-tool[qt]'
|
|
|
48
46
|
$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
|
|
49
47
|
|
|
50
48
|
# Для обновления до новой версии
|
|
51
|
-
$ pipx upgrade
|
|
49
|
+
$ pipx upgrade hh-applicant-tool
|
|
52
50
|
```
|
|
53
51
|
|
|
52
|
+
Отдельно я распишу процесс установки в **Windows** в подробностях:
|
|
53
|
+
|
|
54
|
+
* Для начала поставьте последнюю версию **Python 3** любым удобным способом.
|
|
55
|
+
* Запустите **Terminal** или **PowerShell** от Администратора и выполните:
|
|
56
|
+
```ps
|
|
57
|
+
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
|
|
58
|
+
```
|
|
59
|
+
Данная политика разрешает текущему пользователю (от которого зашли) запускать скрипты. Без нее не будут работать виртуальные окружения.
|
|
60
|
+
* Создайте и активируйте виртуальное окружение:
|
|
61
|
+
```ps
|
|
62
|
+
PS> python -m pip venv hh-applicant-venv
|
|
63
|
+
PS> .\hh-applicant-venv\Scripts\activate
|
|
64
|
+
```
|
|
65
|
+
* Поставьте все пакеты в виртуальное окружение `hh-applicant-venv`:
|
|
66
|
+
```ps
|
|
67
|
+
(hh-applicant-venv) PS> pip install hh-applicant-tool[qt]
|
|
68
|
+
```
|
|
69
|
+
* Проверьте работает ли оно:
|
|
70
|
+
```ps
|
|
71
|
+
(hh-applicant-venv) PS> hh-applicant-tool -h
|
|
72
|
+
```
|
|
73
|
+
* В случае неудачи вернитесь к первому шагу.
|
|
74
|
+
* Для последующих запусков сначала активируйте виртуальное окружение.
|
|
75
|
+
|
|
76
|
+
|
|
54
77
|
Использование:
|
|
55
78
|
|
|
56
79
|
```bash
|
|
@@ -114,10 +137,11 @@ https://hh.ru/employer/1918903
|
|
|
114
137
|
| **whoami** | Выводит информацию об авторизованном пользователе |
|
|
115
138
|
| **list-resumes** | Список резюме |
|
|
116
139
|
| **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
|
|
117
|
-
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в
|
|
140
|
+
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками. |
|
|
118
141
|
| **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
|
|
119
|
-
| **call-api** | Вызов произвольного метода API с выводом результата.
|
|
142
|
+
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
120
143
|
| **refresh-token** | Обновляет access_token. |
|
|
144
|
+
| **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, сайта фирмы и могут быть удалены по просьбе упал_намоченный лицо. Данная функция готова и будет доступна после 100 ⭐ |
|
|
121
145
|
|
|
122
146
|
Авторизуемся:
|
|
123
147
|
|
|
@@ -127,6 +151,10 @@ $ hh-applicant-tool -vv authorize
|
|
|
127
151
|
|
|
128
152
|

|
|
129
153
|
|
|
154
|
+
> В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
130
158
|
В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
|
|
131
159
|
|
|
132
160
|
```json
|
|
@@ -214,7 +242,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
|
|
|
214
242
|
|
|
215
243
|
Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
|
|
216
244
|
|
|
217
|
-
Утилита собирает и передает на сервер разработчика следующую
|
|
245
|
+
Утилита собирает и передает на сервер разработчика следующую информацию:
|
|
218
246
|
|
|
219
247
|
1. Название вакансии.
|
|
220
248
|
1. Тип вакансии (открытая/закрытая).
|
|
@@ -223,7 +251,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
|
|
|
223
251
|
1. Прямая ссылка на вакансию.
|
|
224
252
|
1. Дата создания вакансии.
|
|
225
253
|
1. Дата публикации вакансии.
|
|
226
|
-
1. Контактная информация
|
|
254
|
+
1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, хранящаеся в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля (может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и росписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова).
|
|
227
255
|
1. Название компании.
|
|
228
256
|
1. Тип компании.
|
|
229
257
|
1. Описание компании.
|
|
@@ -12,11 +12,12 @@ from urllib.parse import urlencode
|
|
|
12
12
|
|
|
13
13
|
import requests
|
|
14
14
|
from requests import Response, Session
|
|
15
|
+
import random
|
|
15
16
|
|
|
16
17
|
from ..constants import (
|
|
17
18
|
ANDROID_CLIENT_ID,
|
|
18
19
|
ANDROID_CLIENT_SECRET,
|
|
19
|
-
|
|
20
|
+
USER_AGENT_TEMPLATE,
|
|
20
21
|
)
|
|
21
22
|
from ..types import AccessToken
|
|
22
23
|
from . import errors
|
|
@@ -38,6 +39,7 @@ class BaseClient:
|
|
|
38
39
|
user_agent: str | None = None
|
|
39
40
|
session: Session | None = None
|
|
40
41
|
previous_request_time: float = 0.0
|
|
42
|
+
delay: float = 0.334
|
|
41
43
|
|
|
42
44
|
def __post_init__(self) -> None:
|
|
43
45
|
self.lock = Lock()
|
|
@@ -46,11 +48,31 @@ class BaseClient:
|
|
|
46
48
|
session.headers.update(
|
|
47
49
|
{
|
|
48
50
|
**self.additional_headers(),
|
|
49
|
-
"User-Agent": self.user_agent or
|
|
51
|
+
"User-Agent": self.user_agent or self.default_user_agent(),
|
|
50
52
|
}
|
|
51
53
|
)
|
|
52
54
|
logger.debug("Default Headers: %r", session.headers)
|
|
53
55
|
|
|
56
|
+
def default_user_agent(self) -> str:
|
|
57
|
+
return USER_AGENT_TEMPLATE % (
|
|
58
|
+
random.choice(["8.0", "8.1", "9", "10", "11", "12"]),
|
|
59
|
+
random.choice(
|
|
60
|
+
[
|
|
61
|
+
"SM-G998B", # Samsung Galaxy S21 Ultra
|
|
62
|
+
"Pixel 6", # Google Pixel 6
|
|
63
|
+
"Mi 11", # Xiaomi Mi 11
|
|
64
|
+
"OnePlus 9", # OnePlus 9
|
|
65
|
+
"P40", # Huawei P40
|
|
66
|
+
"LG G8", # LG G8
|
|
67
|
+
"Xperia 1 II", # Sony Xperia 1 II
|
|
68
|
+
"Moto G Power", # Motorola Moto G Power
|
|
69
|
+
"HTC U12+", # HTC U12+
|
|
70
|
+
"ROG Phone 5", # Asus ROG Phone 5
|
|
71
|
+
]
|
|
72
|
+
),
|
|
73
|
+
random.randint(88, 130),
|
|
74
|
+
)
|
|
75
|
+
|
|
54
76
|
def additional_headers(
|
|
55
77
|
self,
|
|
56
78
|
) -> dict[str, str]:
|
|
@@ -61,7 +83,7 @@ class BaseClient:
|
|
|
61
83
|
method: ALLOWED_METHODS,
|
|
62
84
|
endpoint: str,
|
|
63
85
|
params: dict | None = None,
|
|
64
|
-
delay: float =
|
|
86
|
+
delay: float | None = None,
|
|
65
87
|
**kwargs: Any,
|
|
66
88
|
) -> dict:
|
|
67
89
|
# Не знаю насколько это "правильно"
|
|
@@ -72,7 +94,9 @@ class BaseClient:
|
|
|
72
94
|
with self.lock:
|
|
73
95
|
# На серваке какая-то анти-DDOS система
|
|
74
96
|
if (
|
|
75
|
-
delay := delay
|
|
97
|
+
delay := (self.delay if delay is None else delay)
|
|
98
|
+
- time.monotonic()
|
|
99
|
+
+ self.previous_request_time
|
|
76
100
|
) > 0:
|
|
77
101
|
logger.debug("wait %fs before request", delay)
|
|
78
102
|
time.sleep(delay)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
USER_AGENT_TEMPLATE = "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Mobile Safari/537.36"
|
|
2
2
|
|
|
3
3
|
ANDROID_CLIENT_ID = (
|
|
4
4
|
"HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
|
|
@@ -9,14 +9,12 @@ from os import getenv
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from pkgutil import iter_modules
|
|
11
11
|
from typing import Sequence
|
|
12
|
-
|
|
12
|
+
from .api import ApiClient
|
|
13
13
|
from .color_log import ColorHandler
|
|
14
14
|
from .utils import Config, get_config_path
|
|
15
15
|
|
|
16
16
|
DEFAULT_CONFIG_PATH = (
|
|
17
|
-
get_config_path()
|
|
18
|
-
/ __package__.replace("_", "-")
|
|
19
|
-
/ "config.json"
|
|
17
|
+
get_config_path() / __package__.replace("_", "-") / "config.json"
|
|
20
18
|
)
|
|
21
19
|
|
|
22
20
|
logger = logging.getLogger(__package__)
|
|
@@ -35,6 +33,18 @@ OPERATIONS = "operations"
|
|
|
35
33
|
class Namespace(argparse.Namespace):
|
|
36
34
|
config: Config
|
|
37
35
|
verbosity: int
|
|
36
|
+
delay: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_api(args: Namespace) -> ApiClient:
|
|
40
|
+
token = args.config.get("token", {})
|
|
41
|
+
api = ApiClient(
|
|
42
|
+
access_token=token.get("access_token"),
|
|
43
|
+
refresh_token=token.get("refresh_token"),
|
|
44
|
+
user_agent=args.config["user_agent"],
|
|
45
|
+
delay=args.delay,
|
|
46
|
+
)
|
|
47
|
+
return api
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
class HHApplicantTool:
|
|
@@ -45,32 +55,46 @@ class HHApplicantTool:
|
|
|
45
55
|
Группа поддержки: <https://t.me/+aSjr8qM_AP85ZDBi>
|
|
46
56
|
"""
|
|
47
57
|
|
|
58
|
+
class ArgumentFormatter(
|
|
59
|
+
argparse.ArgumentDefaultsHelpFormatter,
|
|
60
|
+
argparse.RawDescriptionHelpFormatter,
|
|
61
|
+
):
|
|
62
|
+
pass
|
|
63
|
+
|
|
48
64
|
def create_parser(self) -> argparse.ArgumentParser:
|
|
49
65
|
parser = argparse.ArgumentParser(
|
|
50
66
|
description=self.__doc__,
|
|
51
|
-
formatter_class=
|
|
67
|
+
formatter_class=self.ArgumentFormatter,
|
|
52
68
|
)
|
|
53
69
|
parser.add_argument(
|
|
54
70
|
"-c",
|
|
55
71
|
"--config",
|
|
56
|
-
help="
|
|
72
|
+
help="Путь до файла конфигурации",
|
|
57
73
|
type=Config,
|
|
58
74
|
default=Config(DEFAULT_CONFIG_PATH),
|
|
59
75
|
)
|
|
60
76
|
parser.add_argument(
|
|
61
77
|
"-v",
|
|
62
78
|
"--verbosity",
|
|
63
|
-
help="
|
|
79
|
+
help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе",
|
|
64
80
|
action="count",
|
|
65
81
|
default=0,
|
|
66
82
|
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"-d",
|
|
85
|
+
"--delay",
|
|
86
|
+
type=float,
|
|
87
|
+
default=0.334,
|
|
88
|
+
help="Задержка между запросами к API HH",
|
|
89
|
+
)
|
|
67
90
|
subparsers = parser.add_subparsers(help="commands")
|
|
68
91
|
package_dir = Path(__file__).resolve().parent / OPERATIONS
|
|
69
92
|
for _, module_name, _ in iter_modules([str(package_dir)]):
|
|
70
93
|
mod = import_module(f"{__package__}.{OPERATIONS}.{module_name}")
|
|
71
94
|
op: BaseOperation = mod.Operation()
|
|
72
95
|
op_parser = subparsers.add_parser(
|
|
73
|
-
module_name.replace("_", "-"),
|
|
96
|
+
module_name.replace("_", "-"),
|
|
97
|
+
description=op.__doc__, formatter_class=self.ArgumentFormatter
|
|
74
98
|
)
|
|
75
99
|
op_parser.set_defaults(run=op.run)
|
|
76
100
|
op.setup_parser(op_parser)
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/apply_similar.py
RENAMED
|
@@ -8,7 +8,7 @@ from typing import TextIO, Tuple
|
|
|
8
8
|
|
|
9
9
|
from ..api import ApiClient, ApiError, BadRequest
|
|
10
10
|
from ..main import BaseOperation
|
|
11
|
-
from ..main import Namespace as BaseNamespace
|
|
11
|
+
from ..main import Namespace as BaseNamespace, get_api
|
|
12
12
|
from ..telemetry_client import TelemetryError
|
|
13
13
|
from ..telemetry_client import get_client as get_telemetry_client
|
|
14
14
|
from ..types import ApiListResponse, VacancyItem
|
|
@@ -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]:
|
|
@@ -64,11 +83,7 @@ class Operation(BaseOperation):
|
|
|
64
83
|
return min(min_interval, max_interval), max(min_interval, max_interval)
|
|
65
84
|
|
|
66
85
|
def run(self, args: Namespace) -> None:
|
|
67
|
-
|
|
68
|
-
api = ApiClient(
|
|
69
|
-
access_token=args.config["token"]["access_token"],
|
|
70
|
-
user_agent=args.config["user_agent"],
|
|
71
|
-
)
|
|
86
|
+
api = get_api(args)
|
|
72
87
|
resume_id = self._get_resume_id(args, api)
|
|
73
88
|
application_messages = self._get_application_messages(args)
|
|
74
89
|
|
|
@@ -84,6 +99,8 @@ class Operation(BaseOperation):
|
|
|
84
99
|
apply_max_interval,
|
|
85
100
|
page_min_interval,
|
|
86
101
|
page_max_interval,
|
|
102
|
+
args.order_by,
|
|
103
|
+
args.search,
|
|
87
104
|
)
|
|
88
105
|
|
|
89
106
|
def _get_resume_id(self, args: Namespace, api: ApiClient) -> str:
|
|
@@ -119,12 +136,20 @@ class Operation(BaseOperation):
|
|
|
119
136
|
apply_max_interval: float,
|
|
120
137
|
page_min_interval: float,
|
|
121
138
|
page_max_interval: float,
|
|
139
|
+
order_by: str,
|
|
140
|
+
search: str | None = None,
|
|
122
141
|
) -> None:
|
|
123
142
|
telemetry_client = get_telemetry_client()
|
|
124
143
|
telemetry_data = defaultdict(dict)
|
|
125
144
|
|
|
126
145
|
vacancies = self._get_vacancies(
|
|
127
|
-
api,
|
|
146
|
+
api,
|
|
147
|
+
resume_id,
|
|
148
|
+
page_min_interval,
|
|
149
|
+
page_max_interval,
|
|
150
|
+
per_page=100,
|
|
151
|
+
order_by=order_by,
|
|
152
|
+
search=search,
|
|
128
153
|
)
|
|
129
154
|
|
|
130
155
|
self._collect_vacancy_telemetry(telemetry_data, vacancies)
|
|
@@ -149,7 +174,7 @@ class Operation(BaseOperation):
|
|
|
149
174
|
|
|
150
175
|
try:
|
|
151
176
|
employer_id = vacancy["employer"]["id"]
|
|
152
|
-
except
|
|
177
|
+
except KeyError:
|
|
153
178
|
logger.warning(
|
|
154
179
|
f"Вакансия без работодателя: {vacancy['alternate_url']}"
|
|
155
180
|
)
|
|
@@ -173,13 +198,23 @@ class Operation(BaseOperation):
|
|
|
173
198
|
params = {
|
|
174
199
|
"resume_id": resume_id,
|
|
175
200
|
"vacancy_id": vacancy["id"],
|
|
176
|
-
"message":
|
|
177
|
-
random.choice(application_messages) % vacancy
|
|
178
|
-
if force_message or vacancy["response_letter_required"]
|
|
179
|
-
else ""
|
|
180
|
-
),
|
|
201
|
+
"message": "",
|
|
181
202
|
}
|
|
182
203
|
|
|
204
|
+
if vacancy.get("response_letter_required"):
|
|
205
|
+
message_template = random.choice(application_messages)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
params["message"] = message_template % vacancy
|
|
209
|
+
except TypeError as ex:
|
|
210
|
+
# TypeError: not enough arguments for format string
|
|
211
|
+
# API HH все кривое, иногда нет идентификатора работодателя, иногда у вакансии нет названия.
|
|
212
|
+
# И это типа рашн хайлоад, где из-за дрочки на аджайл слепили кривую говнину.
|
|
213
|
+
logger.error(
|
|
214
|
+
f"Ошибка форматирования шаблона сообщения {template_message!r} для {vacancy = }"
|
|
215
|
+
)
|
|
216
|
+
continue
|
|
217
|
+
|
|
183
218
|
res = api.post("/negotiations", params)
|
|
184
219
|
assert res == {}
|
|
185
220
|
print(
|
|
@@ -205,14 +240,20 @@ class Operation(BaseOperation):
|
|
|
205
240
|
page_min_interval: float,
|
|
206
241
|
page_max_interval: float,
|
|
207
242
|
per_page: int,
|
|
243
|
+
order_by: str,
|
|
244
|
+
search: str | None = None,
|
|
208
245
|
) -> list[VacancyItem]:
|
|
209
246
|
rv = []
|
|
210
247
|
for page in range(20):
|
|
248
|
+
params = {
|
|
249
|
+
"page": page,
|
|
250
|
+
"per_page": per_page,
|
|
251
|
+
"order_by": order_by,
|
|
252
|
+
}
|
|
253
|
+
if search:
|
|
254
|
+
params["text"] = search
|
|
211
255
|
res: ApiListResponse = api.get(
|
|
212
|
-
f"/resumes/{resume_id}/similar_vacancies",
|
|
213
|
-
page=page,
|
|
214
|
-
per_page=per_page,
|
|
215
|
-
order_by="relevance",
|
|
256
|
+
f"/resumes/{resume_id}/similar_vacancies", params
|
|
216
257
|
)
|
|
217
258
|
rv.extend(res["items"])
|
|
218
259
|
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/call_api.py
RENAMED
|
@@ -4,8 +4,8 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
from ..api import
|
|
8
|
-
from ..main import BaseOperation
|
|
7
|
+
from ..api import ApiError
|
|
8
|
+
from ..main import BaseOperation, get_api
|
|
9
9
|
from ..main import Namespace as BaseNamespace
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__package__)
|
|
@@ -34,11 +34,7 @@ class Operation(BaseOperation):
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
def run(self, args: Namespace) -> None:
|
|
37
|
-
|
|
38
|
-
api = ApiClient(
|
|
39
|
-
access_token=args.config["token"]["access_token"],
|
|
40
|
-
user_agent=args.config["user_agent"],
|
|
41
|
-
)
|
|
37
|
+
api = get_api(args)
|
|
42
38
|
params = dict(x.split("=", 1) for x in args.param)
|
|
43
39
|
try:
|
|
44
40
|
result = api.request(args.method, args.endpoint, params=params)
|
|
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
5
5
|
|
|
6
6
|
from ..api import ApiClient, ClientError
|
|
7
7
|
from ..constants import INVALID_ISO8601_FORMAT
|
|
8
|
-
from ..main import BaseOperation
|
|
8
|
+
from ..main import BaseOperation, get_api
|
|
9
9
|
from ..main import Namespace as BaseNamespace
|
|
10
10
|
from ..types import ApiListResponse
|
|
11
11
|
from ..utils import print_err, truncate_string
|
|
@@ -51,11 +51,7 @@ class Operation(BaseOperation):
|
|
|
51
51
|
return rv
|
|
52
52
|
|
|
53
53
|
def run(self, args: Namespace) -> None:
|
|
54
|
-
|
|
55
|
-
api = ApiClient(
|
|
56
|
-
access_token=args.config["token"]["access_token"],
|
|
57
|
-
user_agent=args.config["user_agent"],
|
|
58
|
-
)
|
|
54
|
+
api = get_api(args)
|
|
59
55
|
negotiations = self._get_active_negotiations(api)
|
|
60
56
|
print("Всего активных:", len(negotiations))
|
|
61
57
|
for item in negotiations:
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/list_resumes.py
RENAMED
|
@@ -5,7 +5,7 @@ import logging
|
|
|
5
5
|
from prettytable import PrettyTable
|
|
6
6
|
|
|
7
7
|
from ..api import ApiClient
|
|
8
|
-
from ..main import BaseOperation
|
|
8
|
+
from ..main import BaseOperation, get_api
|
|
9
9
|
from ..main import Namespace as BaseNamespace
|
|
10
10
|
from ..types import ApiListResponse
|
|
11
11
|
from ..utils import truncate_string
|
|
@@ -24,11 +24,7 @@ class Operation(BaseOperation):
|
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
26
|
def run(self, args: Namespace) -> None:
|
|
27
|
-
|
|
28
|
-
api = ApiClient(
|
|
29
|
-
access_token=args.config["token"]["access_token"],
|
|
30
|
-
user_agent=args.config["user_agent"],
|
|
31
|
-
)
|
|
27
|
+
api = get_api(args)
|
|
32
28
|
resumes: ApiListResponse = api.get("/resumes/mine")
|
|
33
29
|
t = PrettyTable(
|
|
34
30
|
field_names=["ID", "Название", "Статус"], align="l", valign="t"
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/update_resumes.py
RENAMED
|
@@ -3,7 +3,7 @@ import argparse
|
|
|
3
3
|
import logging
|
|
4
4
|
|
|
5
5
|
from ..api import ApiClient, ApiError
|
|
6
|
-
from ..main import BaseOperation
|
|
6
|
+
from ..main import BaseOperation, get_api
|
|
7
7
|
from ..main import Namespace as BaseNamespace
|
|
8
8
|
from ..types import ApiListResponse
|
|
9
9
|
from ..utils import print_err, truncate_string
|
|
@@ -22,11 +22,7 @@ class Operation(BaseOperation):
|
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
def run(self, args: Namespace) -> None:
|
|
25
|
-
|
|
26
|
-
api = ApiClient(
|
|
27
|
-
access_token=args.config["token"]["access_token"],
|
|
28
|
-
user_agent=args.config["user_agent"],
|
|
29
|
-
)
|
|
25
|
+
api = get_api(args)
|
|
30
26
|
resumes: ApiListResponse = api.get("/resumes/mine")
|
|
31
27
|
for resume in resumes["items"]:
|
|
32
28
|
try:
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
6
|
from ..api import ApiClient
|
|
7
|
-
from ..main import BaseOperation
|
|
7
|
+
from ..main import BaseOperation, get_api
|
|
8
8
|
from ..main import Namespace as BaseNamespace
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger(__package__)
|
|
@@ -21,10 +21,6 @@ class Operation(BaseOperation):
|
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
def run(self, args: Namespace) -> None:
|
|
24
|
-
|
|
25
|
-
api = ApiClient(
|
|
26
|
-
access_token=args.config["token"]["access_token"],
|
|
27
|
-
user_agent=args.config["user_agent"],
|
|
28
|
-
)
|
|
24
|
+
api = get_api(args)
|
|
29
25
|
result = api.get("/me")
|
|
30
26
|
print(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True))
|
|
@@ -51,8 +51,6 @@ class TelemetryClient:
|
|
|
51
51
|
:raises TelemetryError: Если произошла ошибка при отправке или декодировании JSON.
|
|
52
52
|
"""
|
|
53
53
|
url = urljoin(self.server_address, endpoint)
|
|
54
|
-
logger.debug(data)
|
|
55
|
-
|
|
56
54
|
try:
|
|
57
55
|
response = self.session.post(url, json=data)
|
|
58
56
|
# response.raise_for_status()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/__init__.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/authorize.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.3.4 → hh_applicant_tool-0.3.6}/hh_applicant_tool/operations/refresh_token.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|