hh-applicant-tool 0.7.7__tar.gz → 0.8.2__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.
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/PKG-INFO +55 -30
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/README.md +53 -26
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/api/client.py +3 -1
- hh_applicant_tool-0.8.2/hh_applicant_tool/operations/authorize.py +151 -0
- hh_applicant_tool-0.8.2/hh_applicant_tool/operations/config.py +143 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/get_employer_contacts.py +31 -5
- hh_applicant_tool-0.8.2/hh_applicant_tool/operations/install.py +25 -0
- hh_applicant_tool-0.8.2/hh_applicant_tool/operations/uninstall.py +20 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/pyproject.toml +2 -6
- hh_applicant_tool-0.7.7/hh_applicant_tool/operations/authorize.py +0 -132
- hh_applicant_tool-0.7.7/hh_applicant_tool/operations/config.py +0 -49
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/__init__.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/__main__.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/ai/__init__.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/ai/blackbox.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/ai/openai.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/api/__init__.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/api/errors.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/color_log.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/constants.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/jsonc.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/main.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/mixins.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/__init__.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/apply_similar.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/call_api.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/delete_telemetry.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/list_resumes.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/refresh_token.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/reply_employers.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/update_resumes.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/whoami.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/telemetry_client.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/types.py +0 -0
- {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: HH-Applicant-Tool: An automation utility for HeadHunter (hh.ru) designed to streamline the job search process by auto-applying to relevant vacancies and periodically refreshing resumes to stay at the top of recruiter searches.
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -11,10 +11,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
|
|
14
|
+
Requires-Dist: playwright (>=1.57.0,<2.0.0)
|
|
15
15
|
Requires-Dist: prettytable (>=3.6.0,<4.0.0)
|
|
16
|
-
Requires-Dist: pyqt6 (==6.7.0) ; extra == "qt"
|
|
17
|
-
Requires-Dist: pyqt6-webengine (==6.7.0) ; extra == "qt"
|
|
18
16
|
Requires-Dist: requests[socks] (>=2.32.3,<3.0.0)
|
|
19
17
|
Project-URL: Homepage, https://github.com/s3rgeym/hh-applicant-tool
|
|
20
18
|
Project-URL: Repository, https://github.com/s3rgeym/hh-applicant-tool
|
|
@@ -35,23 +33,25 @@ Description-Content-Type: text/markdown
|
|
|
35
33
|
|
|
36
34
|
### Содержание
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
- [🚀 Описание](#описание)
|
|
37
|
+
- [⚠️ Внимание](#внимание)
|
|
38
|
+
- [📜 Предыстория](#предыстория)
|
|
39
|
+
- [📦 Установка](#установка)
|
|
40
|
+
- [Установка утилиты](#установка-утилиты)
|
|
41
|
+
- [Установка зависимостей](#установка-зависимостей)
|
|
42
|
+
- [🔑 Авторизация](#авторизация)
|
|
43
|
+
- [⚙️ Конфигурация](#пути-до-файла-configjson)
|
|
44
|
+
- [🛠 Описание команд](#описание-команд)
|
|
45
|
+
- [📝 Формат сообщений](#формат-текста-сообщений)
|
|
46
|
+
- [🤖 Использование AI (ChatGPT)](#использование-ai-для-генерации-сопроводительного-письма)
|
|
47
|
+
- [📡 Работа с API напрямую](#работа-с-api-напрямую)
|
|
48
|
+
- [🔌 Добавление своих команд](#добавление-своих-команд)
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
52
|
### Описание
|
|
53
53
|
|
|
54
|
-
> Данной утилите похуй на "запрет" доступа к API HH сторонним приложениям, так как она прикидывается официальным приложением под Android
|
|
54
|
+
> Данной утилите похуй на "запрет" доступа к API HH сторонним приложениям, так как она прикидывается официальным приложением под Android
|
|
55
55
|
|
|
56
56
|
> Утилита для генерации сопроводительного письма может использовать AI в тч ChatGPT. Подробное описание ниже
|
|
57
57
|
|
|
@@ -61,12 +61,11 @@ Description-Content-Type: text/markdown
|
|
|
61
61
|
|
|
62
62
|
> Утилита предупреждает о подозрительных работодателях, с высокой вероятностью являющихся мошенниками
|
|
63
63
|
|
|
64
|
-
|
|
65
64
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
66
65
|
asdf/pyenv/conda и что-то еще. В школотронской Manjaro и даже в последних Ubuntu
|
|
67
66
|
версия Python новее.
|
|
68
67
|
|
|
69
|
-
Данная утилита
|
|
68
|
+
Данная утилита кроссплатформенна. Она гарантировано работает на Linux, Mac и Windows в тч WSL. При наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
|
|
70
69
|
|
|
71
70
|
Пример работы:
|
|
72
71
|
|
|
@@ -74,6 +73,8 @@ asdf/pyenv/conda и что-то еще. В школотронской Manjaro и
|
|
|
74
73
|
|
|
75
74
|
> Если в веб-интерфейсе выставить фильтры, то они будут применяться в скрипте при отклике на подходящие
|
|
76
75
|
|
|
76
|
+
> Утилита автоматически подхватывает прокси из переменных окружения типа http_proxy или HTTPS_PROXY
|
|
77
|
+
|
|
77
78
|
### Внимание!!!
|
|
78
79
|
|
|
79
80
|
Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/). Оно обладает минимальным функционалом: обновление резюме (одного) и рассылка откликов (чистить их и тп нельзя).
|
|
@@ -90,12 +91,12 @@ $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
|
|
|
90
91
|
|
|
91
92
|
### Установка
|
|
92
93
|
|
|
94
|
+
#### Установка утилиты
|
|
95
|
+
|
|
93
96
|
Универсальный с использованием pipx (требует пакета `python-pipx` в Arch):
|
|
94
97
|
|
|
95
98
|
```bash
|
|
96
|
-
|
|
97
|
-
# Можно использовать обычный pip
|
|
98
|
-
$ pipx install 'hh-applicant-tool[qt]'
|
|
99
|
+
$ pipx install 'hh-applicant-tool'
|
|
99
100
|
|
|
100
101
|
# Если хочется использовать самую последнюю версию, то можно установить ее через git
|
|
101
102
|
$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
|
|
@@ -113,7 +114,7 @@ mkdir -p ~/.venvs
|
|
|
113
114
|
python -m venv ~/.venvs/hh-applicant-tool
|
|
114
115
|
# Это придется делать постоянно, чтобы команда hh-applicant-tool стала доступна
|
|
115
116
|
. ~/.venvs/hh-applicant-tool/bin/activate
|
|
116
|
-
pip install
|
|
117
|
+
pip install hh-applicant-tool
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
Отдельно я распишу процесс установки в **Windows** в подробностях:
|
|
@@ -133,7 +134,7 @@ pip install 'hh-applicant-tool[qt]'
|
|
|
133
134
|
|
|
134
135
|
- Поставьте все пакеты в виртуальное окружение `hh-applicant-venv`:
|
|
135
136
|
```ps
|
|
136
|
-
(hh-applicant-venv) PS> pip install hh-applicant-tool
|
|
137
|
+
(hh-applicant-venv) PS> pip install hh-applicant-tool
|
|
137
138
|
```
|
|
138
139
|
- Проверьте работает ли оно:
|
|
139
140
|
```ps
|
|
@@ -142,17 +143,31 @@ pip install 'hh-applicant-tool[qt]'
|
|
|
142
143
|
- В случае неудачи вернитесь к первому шагу.
|
|
143
144
|
- Для последующих запусков сначала активируйте виртуальное окружение.
|
|
144
145
|
|
|
146
|
+
#### Установка зависимостей
|
|
147
|
+
|
|
148
|
+
После вышеописанного нужно установить зависимости:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
$ hh-applicant-tool install
|
|
152
|
+
```
|
|
153
|
+
|
|
145
154
|
### Авторизация
|
|
146
155
|
|
|
147
156
|
```bash
|
|
148
|
-
$ hh-applicant-tool
|
|
157
|
+
$ hh-applicant-tool authorize '<ваш телефон или email>'
|
|
158
|
+
📨 Код был отправлен. Проверьте почту или SMS.
|
|
159
|
+
📩 Введите полученный код: 1387
|
|
160
|
+
🔓 Авторизация прошла успешно!
|
|
149
161
|
```
|
|
150
162
|
|
|
151
|
-
|
|
163
|
+
- Если вы ввели телефон, то код придет через SMS
|
|
164
|
+
- Если был введен Email, то проверьте почту
|
|
152
165
|
|
|
153
|
-
|
|
166
|
+
Если вы пропустили пункт про установку зависимостей, то увидите такую ошибку:
|
|
154
167
|
|
|
155
|
-
|
|
168
|
+
```sh
|
|
169
|
+
[E] BrowserType.launch: Executable doesn't exist at...
|
|
170
|
+
```
|
|
156
171
|
|
|
157
172
|
Проверка авторизации:
|
|
158
173
|
|
|
@@ -259,8 +274,17 @@ $ hh-applicant-tool update-resumes
|
|
|
259
274
|
$ hh-applicant-tool clear-negotiations --blacklist-discard
|
|
260
275
|
|
|
261
276
|
# Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
|
|
262
|
-
# приглашение
|
|
277
|
+
# приглашение (видны изменения номеров)
|
|
263
278
|
$ hh-applicant-tool get-employer-contacts --export -f html > report.html
|
|
279
|
+
|
|
280
|
+
# Редактировать конфиг в стандартном редакторе
|
|
281
|
+
$ hh-applicant-tool config
|
|
282
|
+
|
|
283
|
+
# Вывести значение из конфига
|
|
284
|
+
$ hh-applicant-tool config -k token.access_token
|
|
285
|
+
|
|
286
|
+
# Установить значение в конфиге
|
|
287
|
+
hh-applicant-tool config --set openai.model gpt-4o
|
|
264
288
|
```
|
|
265
289
|
|
|
266
290
|
Можно вызвать любой метод API:
|
|
@@ -308,6 +332,8 @@ https://hh.ru/employer/1918903
|
|
|
308
332
|
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
309
333
|
| **refresh-token** | Обновляет access_token. |
|
|
310
334
|
| **config** | Редактировать конфигурационный файл. |
|
|
335
|
+
| **install** | Устанавливает зависимости, такие как браузер Chromium, необходимые для авторизации. |
|
|
336
|
+
| **uninstall** | Удаляет браузер Chromium, используемый для авторизации. |
|
|
311
337
|
| **get-employer-contacts** | Получить список полученных вами контактов работодателей. Поддерживается так же экспорт в html/jsonl. Если хотите собирать контакты с нескольких акков, то укажите им одинаковый `client_telemetry_id` в конфигах. |
|
|
312
338
|
| **delete-telemetry** | Удадяет телеметрию (контакты работодателей, которые вас пригласили), если та была включена. |
|
|
313
339
|
|
|
@@ -404,7 +430,7 @@ npx @redocly/cli preview -d docs/hhapi
|
|
|
404
430
|
|
|
405
431
|
> Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
|
|
406
432
|
|
|
407
|
-
Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате. По-сути, никакие дополнительные команды кроме имеющихся не нужны. Вы можете сделать что угодно с помощью `call-api`.
|
|
433
|
+
Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате. По-сути, никакие дополнительные команды кроме имеющихся не нужны. Вы можете сделать что угодно с помощью `call-api`.
|
|
408
434
|
|
|
409
435
|
Синтаксис `call-api` немного похож на `httpie` или `curlie`:
|
|
410
436
|
|
|
@@ -433,4 +459,3 @@ done
|
|
|
433
459
|
|
|
434
460
|
Утилита использует систему плагинов. Все они лежат в [operations](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations). Модули расположенные там автоматически добавляются как доступные команды. За основу для своего плагина можно взять [whoami.py](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations/whoami.py).
|
|
435
461
|
|
|
436
|
-
|
|
@@ -13,23 +13,25 @@
|
|
|
13
13
|
|
|
14
14
|
### Содержание
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
- [🚀 Описание](#описание)
|
|
17
|
+
- [⚠️ Внимание](#внимание)
|
|
18
|
+
- [📜 Предыстория](#предыстория)
|
|
19
|
+
- [📦 Установка](#установка)
|
|
20
|
+
- [Установка утилиты](#установка-утилиты)
|
|
21
|
+
- [Установка зависимостей](#установка-зависимостей)
|
|
22
|
+
- [🔑 Авторизация](#авторизация)
|
|
23
|
+
- [⚙️ Конфигурация](#пути-до-файла-configjson)
|
|
24
|
+
- [🛠 Описание команд](#описание-команд)
|
|
25
|
+
- [📝 Формат сообщений](#формат-текста-сообщений)
|
|
26
|
+
- [🤖 Использование AI (ChatGPT)](#использование-ai-для-генерации-сопроводительного-письма)
|
|
27
|
+
- [📡 Работа с API напрямую](#работа-с-api-напрямую)
|
|
28
|
+
- [🔌 Добавление своих команд](#добавление-своих-команд)
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
### Описание
|
|
31
33
|
|
|
32
|
-
> Данной утилите похуй на "запрет" доступа к API HH сторонним приложениям, так как она прикидывается официальным приложением под Android
|
|
34
|
+
> Данной утилите похуй на "запрет" доступа к API HH сторонним приложениям, так как она прикидывается официальным приложением под Android
|
|
33
35
|
|
|
34
36
|
> Утилита для генерации сопроводительного письма может использовать AI в тч ChatGPT. Подробное описание ниже
|
|
35
37
|
|
|
@@ -39,12 +41,11 @@
|
|
|
39
41
|
|
|
40
42
|
> Утилита предупреждает о подозрительных работодателях, с высокой вероятностью являющихся мошенниками
|
|
41
43
|
|
|
42
|
-
|
|
43
44
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
44
45
|
asdf/pyenv/conda и что-то еще. В школотронской Manjaro и даже в последних Ubuntu
|
|
45
46
|
версия Python новее.
|
|
46
47
|
|
|
47
|
-
Данная утилита
|
|
48
|
+
Данная утилита кроссплатформенна. Она гарантировано работает на Linux, Mac и Windows в тч WSL. При наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
|
|
48
49
|
|
|
49
50
|
Пример работы:
|
|
50
51
|
|
|
@@ -52,6 +53,8 @@ asdf/pyenv/conda и что-то еще. В школотронской Manjaro и
|
|
|
52
53
|
|
|
53
54
|
> Если в веб-интерфейсе выставить фильтры, то они будут применяться в скрипте при отклике на подходящие
|
|
54
55
|
|
|
56
|
+
> Утилита автоматически подхватывает прокси из переменных окружения типа http_proxy или HTTPS_PROXY
|
|
57
|
+
|
|
55
58
|
### Внимание!!!
|
|
56
59
|
|
|
57
60
|
Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/). Оно обладает минимальным функционалом: обновление резюме (одного) и рассылка откликов (чистить их и тп нельзя).
|
|
@@ -68,12 +71,12 @@ $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
|
|
|
68
71
|
|
|
69
72
|
### Установка
|
|
70
73
|
|
|
74
|
+
#### Установка утилиты
|
|
75
|
+
|
|
71
76
|
Универсальный с использованием pipx (требует пакета `python-pipx` в Arch):
|
|
72
77
|
|
|
73
78
|
```bash
|
|
74
|
-
|
|
75
|
-
# Можно использовать обычный pip
|
|
76
|
-
$ pipx install 'hh-applicant-tool[qt]'
|
|
79
|
+
$ pipx install 'hh-applicant-tool'
|
|
77
80
|
|
|
78
81
|
# Если хочется использовать самую последнюю версию, то можно установить ее через git
|
|
79
82
|
$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
|
|
@@ -91,7 +94,7 @@ mkdir -p ~/.venvs
|
|
|
91
94
|
python -m venv ~/.venvs/hh-applicant-tool
|
|
92
95
|
# Это придется делать постоянно, чтобы команда hh-applicant-tool стала доступна
|
|
93
96
|
. ~/.venvs/hh-applicant-tool/bin/activate
|
|
94
|
-
pip install
|
|
97
|
+
pip install hh-applicant-tool
|
|
95
98
|
```
|
|
96
99
|
|
|
97
100
|
Отдельно я распишу процесс установки в **Windows** в подробностях:
|
|
@@ -111,7 +114,7 @@ pip install 'hh-applicant-tool[qt]'
|
|
|
111
114
|
|
|
112
115
|
- Поставьте все пакеты в виртуальное окружение `hh-applicant-venv`:
|
|
113
116
|
```ps
|
|
114
|
-
(hh-applicant-venv) PS> pip install hh-applicant-tool
|
|
117
|
+
(hh-applicant-venv) PS> pip install hh-applicant-tool
|
|
115
118
|
```
|
|
116
119
|
- Проверьте работает ли оно:
|
|
117
120
|
```ps
|
|
@@ -120,17 +123,31 @@ pip install 'hh-applicant-tool[qt]'
|
|
|
120
123
|
- В случае неудачи вернитесь к первому шагу.
|
|
121
124
|
- Для последующих запусков сначала активируйте виртуальное окружение.
|
|
122
125
|
|
|
126
|
+
#### Установка зависимостей
|
|
127
|
+
|
|
128
|
+
После вышеописанного нужно установить зависимости:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
$ hh-applicant-tool install
|
|
132
|
+
```
|
|
133
|
+
|
|
123
134
|
### Авторизация
|
|
124
135
|
|
|
125
136
|
```bash
|
|
126
|
-
$ hh-applicant-tool
|
|
137
|
+
$ hh-applicant-tool authorize '<ваш телефон или email>'
|
|
138
|
+
📨 Код был отправлен. Проверьте почту или SMS.
|
|
139
|
+
📩 Введите полученный код: 1387
|
|
140
|
+
🔓 Авторизация прошла успешно!
|
|
127
141
|
```
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
- Если вы ввели телефон, то код придет через SMS
|
|
144
|
+
- Если был введен Email, то проверьте почту
|
|
130
145
|
|
|
131
|
-
|
|
146
|
+
Если вы пропустили пункт про установку зависимостей, то увидите такую ошибку:
|
|
132
147
|
|
|
133
|
-
|
|
148
|
+
```sh
|
|
149
|
+
[E] BrowserType.launch: Executable doesn't exist at...
|
|
150
|
+
```
|
|
134
151
|
|
|
135
152
|
Проверка авторизации:
|
|
136
153
|
|
|
@@ -237,8 +254,17 @@ $ hh-applicant-tool update-resumes
|
|
|
237
254
|
$ hh-applicant-tool clear-negotiations --blacklist-discard
|
|
238
255
|
|
|
239
256
|
# Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
|
|
240
|
-
# приглашение
|
|
257
|
+
# приглашение (видны изменения номеров)
|
|
241
258
|
$ hh-applicant-tool get-employer-contacts --export -f html > report.html
|
|
259
|
+
|
|
260
|
+
# Редактировать конфиг в стандартном редакторе
|
|
261
|
+
$ hh-applicant-tool config
|
|
262
|
+
|
|
263
|
+
# Вывести значение из конфига
|
|
264
|
+
$ hh-applicant-tool config -k token.access_token
|
|
265
|
+
|
|
266
|
+
# Установить значение в конфиге
|
|
267
|
+
hh-applicant-tool config --set openai.model gpt-4o
|
|
242
268
|
```
|
|
243
269
|
|
|
244
270
|
Можно вызвать любой метод API:
|
|
@@ -286,6 +312,8 @@ https://hh.ru/employer/1918903
|
|
|
286
312
|
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
287
313
|
| **refresh-token** | Обновляет access_token. |
|
|
288
314
|
| **config** | Редактировать конфигурационный файл. |
|
|
315
|
+
| **install** | Устанавливает зависимости, такие как браузер Chromium, необходимые для авторизации. |
|
|
316
|
+
| **uninstall** | Удаляет браузер Chromium, используемый для авторизации. |
|
|
289
317
|
| **get-employer-contacts** | Получить список полученных вами контактов работодателей. Поддерживается так же экспорт в html/jsonl. Если хотите собирать контакты с нескольких акков, то укажите им одинаковый `client_telemetry_id` в конфигах. |
|
|
290
318
|
| **delete-telemetry** | Удадяет телеметрию (контакты работодателей, которые вас пригласили), если та была включена. |
|
|
291
319
|
|
|
@@ -382,7 +410,7 @@ npx @redocly/cli preview -d docs/hhapi
|
|
|
382
410
|
|
|
383
411
|
> Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
|
|
384
412
|
|
|
385
|
-
Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате. По-сути, никакие дополнительные команды кроме имеющихся не нужны. Вы можете сделать что угодно с помощью `call-api`.
|
|
413
|
+
Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате. По-сути, никакие дополнительные команды кроме имеющихся не нужны. Вы можете сделать что угодно с помощью `call-api`.
|
|
386
414
|
|
|
387
415
|
Синтаксис `call-api` немного похож на `httpie` или `curlie`:
|
|
388
416
|
|
|
@@ -410,4 +438,3 @@ done
|
|
|
410
438
|
### Добавление своих команд
|
|
411
439
|
|
|
412
440
|
Утилита использует систему плагинов. Все они лежат в [operations](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations). Модули расположенные там автоматически добавляются как доступные команды. За основу для своего плагина можно взять [whoami.py](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations/whoami.py).
|
|
413
|
-
|
|
@@ -39,6 +39,8 @@ class BaseClient:
|
|
|
39
39
|
self.lock = Lock()
|
|
40
40
|
if not self.session:
|
|
41
41
|
self.session = requests.session()
|
|
42
|
+
if self.proxies:
|
|
43
|
+
logger.debug(f"client proxies: {self.proxies}")
|
|
42
44
|
|
|
43
45
|
def default_headers(self) -> dict[str, str]:
|
|
44
46
|
return {
|
|
@@ -76,7 +78,7 @@ class BaseClient:
|
|
|
76
78
|
has_body = method in ["POST", "PUT"]
|
|
77
79
|
payload = {"data" if has_body else "params": params}
|
|
78
80
|
headers = self.default_headers() | self.additional_headers()
|
|
79
|
-
logger.debug(f"request info: {method = }, {url = }, {headers = },
|
|
81
|
+
logger.debug(f"request info: {method = }, {url = }, {headers = }, params = {repr(params)[:255]}")
|
|
80
82
|
response = self.session.request(
|
|
81
83
|
method,
|
|
82
84
|
url,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from urllib.parse import parse_qs, urlsplit
|
|
5
|
+
|
|
6
|
+
from playwright.async_api import async_playwright
|
|
7
|
+
|
|
8
|
+
from ..api import ApiClient
|
|
9
|
+
from ..main import BaseOperation, Namespace
|
|
10
|
+
|
|
11
|
+
HH_ANDROID_SCHEME = "hhandroid"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Operation(BaseOperation):
|
|
17
|
+
"""Авторизация через Playwright"""
|
|
18
|
+
|
|
19
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"username",
|
|
22
|
+
nargs="?",
|
|
23
|
+
help="Email или телефон",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--no-headless",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="Показать окно браузера для отладки (отключает headless режим).",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def run(self, args: Namespace, api_client: ApiClient, *_):
|
|
32
|
+
asyncio.run(self._main(args, api_client))
|
|
33
|
+
|
|
34
|
+
async def _main(self, args: Namespace, api_client: ApiClient):
|
|
35
|
+
username_prompt = "👤 Введите email или телефон: "
|
|
36
|
+
username = (
|
|
37
|
+
args.username or (await asyncio.to_thread(input, username_prompt))
|
|
38
|
+
).strip()
|
|
39
|
+
|
|
40
|
+
if not username:
|
|
41
|
+
raise RuntimeError("Empty username")
|
|
42
|
+
|
|
43
|
+
proxies = api_client.proxies
|
|
44
|
+
proxy_url = proxies.get("https")
|
|
45
|
+
|
|
46
|
+
chromium_args: list[str] = []
|
|
47
|
+
if proxy_url:
|
|
48
|
+
chromium_args.append(f"--proxy-server={proxy_url}")
|
|
49
|
+
logger.debug("Используется proxy: %s", proxy_url)
|
|
50
|
+
|
|
51
|
+
is_headless = not args.no_headless
|
|
52
|
+
if is_headless:
|
|
53
|
+
logger.info("Включен headless-режим с серверными флагами.")
|
|
54
|
+
chromium_args.extend(
|
|
55
|
+
[
|
|
56
|
+
"--no-sandbox",
|
|
57
|
+
"--disable-setuid-sandbox",
|
|
58
|
+
"--disable-dev-shm-usage",
|
|
59
|
+
"--disable-gpu",
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
oauth_url = api_client.oauth_client.authorize_url
|
|
64
|
+
logger.debug("OAuth URL: %s", oauth_url)
|
|
65
|
+
|
|
66
|
+
async with async_playwright() as pw:
|
|
67
|
+
browser = await pw.chromium.launch(
|
|
68
|
+
headless=is_headless,
|
|
69
|
+
args=chromium_args,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
context = await browser.new_context()
|
|
74
|
+
page = await context.new_page()
|
|
75
|
+
|
|
76
|
+
code_future: asyncio.Future[str | None] = asyncio.Future()
|
|
77
|
+
|
|
78
|
+
def handle_request(request):
|
|
79
|
+
url = request.url
|
|
80
|
+
|
|
81
|
+
if url.startswith(f"{HH_ANDROID_SCHEME}://"):
|
|
82
|
+
logger.info("Перехвачен redirect на: %s", url)
|
|
83
|
+
|
|
84
|
+
if not code_future.done():
|
|
85
|
+
sp = urlsplit(url)
|
|
86
|
+
code = parse_qs(sp.query).get("code", [None])[0]
|
|
87
|
+
code_future.set_result(code)
|
|
88
|
+
|
|
89
|
+
page.on("request", handle_request)
|
|
90
|
+
|
|
91
|
+
logger.info("Открываем страницу авторизации")
|
|
92
|
+
await page.goto(oauth_url, wait_until="load")
|
|
93
|
+
|
|
94
|
+
await self._login_step(page, username)
|
|
95
|
+
await self._code_step(page)
|
|
96
|
+
|
|
97
|
+
logger.info(f"Ожидание перенаправления на {HH_ANDROID_SCHEME}://")
|
|
98
|
+
|
|
99
|
+
code = await code_future
|
|
100
|
+
|
|
101
|
+
page.remove_listener("request", handle_request)
|
|
102
|
+
|
|
103
|
+
assert code
|
|
104
|
+
|
|
105
|
+
logger.debug("OAuth code: %s", code)
|
|
106
|
+
|
|
107
|
+
token = await asyncio.to_thread(
|
|
108
|
+
api_client.oauth_client.authenticate,
|
|
109
|
+
code,
|
|
110
|
+
)
|
|
111
|
+
api_client.handle_access_token(token)
|
|
112
|
+
|
|
113
|
+
print("🔓 Авторизация прошла успешно!")
|
|
114
|
+
|
|
115
|
+
finally:
|
|
116
|
+
await browser.close()
|
|
117
|
+
|
|
118
|
+
async def _login_step(self, page, username: str) -> None:
|
|
119
|
+
logger.info("Ожидание поля ввода логина")
|
|
120
|
+
|
|
121
|
+
login_input_selector = 'input[data-qa="login-input-username"]'
|
|
122
|
+
|
|
123
|
+
await page.wait_for_selector(login_input_selector)
|
|
124
|
+
|
|
125
|
+
logger.debug("Ввод username: %s", username)
|
|
126
|
+
await page.fill(login_input_selector, username)
|
|
127
|
+
|
|
128
|
+
logger.debug("Отправка формы по Enter")
|
|
129
|
+
await page.press(login_input_selector, "Enter")
|
|
130
|
+
|
|
131
|
+
async def _code_step(self, page) -> None:
|
|
132
|
+
logger.info("Ожидание поля ввода кода")
|
|
133
|
+
|
|
134
|
+
await page.wait_for_selector('div[data-qa="account-login-code-input"]')
|
|
135
|
+
|
|
136
|
+
print("📨 Код был отправлен. Проверьте почту или SMS.")
|
|
137
|
+
|
|
138
|
+
code_prompt = "📩 Введите полученный код: "
|
|
139
|
+
code = (await asyncio.to_thread(input, code_prompt)).strip()
|
|
140
|
+
|
|
141
|
+
if not code:
|
|
142
|
+
raise RuntimeError("Empty confirmation code")
|
|
143
|
+
|
|
144
|
+
logger.debug("Ввод кода")
|
|
145
|
+
|
|
146
|
+
code_input_selector = 'input[data-qa="magritte-pincode-input-field"]'
|
|
147
|
+
await page.focus(code_input_selector)
|
|
148
|
+
await page.fill(code_input_selector, code)
|
|
149
|
+
|
|
150
|
+
logger.debug("Подтверждаем код по Enter")
|
|
151
|
+
await page.press(code_input_selector, "Enter")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import ast
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..main import BaseOperation
|
|
11
|
+
from ..main import Namespace as BaseNamespace
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__package__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Namespace(BaseNamespace):
|
|
17
|
+
show_path: bool
|
|
18
|
+
key: str
|
|
19
|
+
set: list[str]
|
|
20
|
+
view: bool
|
|
21
|
+
unset: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_value(data: dict[str, Any], path: str) -> Any:
|
|
25
|
+
for key in path.split("."):
|
|
26
|
+
if isinstance(data, dict) and key in data:
|
|
27
|
+
data = data[key]
|
|
28
|
+
else:
|
|
29
|
+
return None
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_value(data: dict[str, Any], path: str, value: Any) -> None:
|
|
34
|
+
"""Устанавливает значение во вложенном словаре по ключу в виде строки."""
|
|
35
|
+
keys = path.split(".")
|
|
36
|
+
for key in keys[:-1]:
|
|
37
|
+
data = data.setdefault(key, {})
|
|
38
|
+
data[keys[-1]] = value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def del_value(data: dict[str, Any], path: str) -> bool:
|
|
42
|
+
"""Удаляет значение из вложенного словаря по ключу в виде строки."""
|
|
43
|
+
keys = path.split(".")
|
|
44
|
+
for key in keys[:-1]:
|
|
45
|
+
if isinstance(data, dict) and key in data:
|
|
46
|
+
data = data[key]
|
|
47
|
+
else:
|
|
48
|
+
return False # Key path does not exist
|
|
49
|
+
|
|
50
|
+
final_key = keys[-1]
|
|
51
|
+
if isinstance(data, dict) and final_key in data:
|
|
52
|
+
del data[final_key]
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Operation(BaseOperation):
|
|
58
|
+
"""Операции с конфигурационным файлом"""
|
|
59
|
+
|
|
60
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
61
|
+
group = parser.add_mutually_exclusive_group()
|
|
62
|
+
group.add_argument(
|
|
63
|
+
"-p",
|
|
64
|
+
"--show-path",
|
|
65
|
+
"--path",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="Вывести полный путь к конфигу",
|
|
68
|
+
)
|
|
69
|
+
group.add_argument("-k", "--key", help="Вывести отдельное значение из конфига")
|
|
70
|
+
group.add_argument(
|
|
71
|
+
"-s",
|
|
72
|
+
"--set",
|
|
73
|
+
nargs=2,
|
|
74
|
+
metavar=("KEY", "VALUE"),
|
|
75
|
+
help="Установить значение в конфиге (например, --set openai.model gpt-4o)",
|
|
76
|
+
)
|
|
77
|
+
group.add_argument(
|
|
78
|
+
"-u", "--unset", metavar="KEY", help="Удалить ключ из конфига"
|
|
79
|
+
)
|
|
80
|
+
group.add_argument(
|
|
81
|
+
"-V",
|
|
82
|
+
"--view",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Вывести содержимое конфига в консоль",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def run(self, args: Namespace, *_) -> None:
|
|
88
|
+
if args.view:
|
|
89
|
+
print(json.dumps(args.config, indent=2, ensure_ascii=False))
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if args.set:
|
|
93
|
+
key, value_str = args.set
|
|
94
|
+
try:
|
|
95
|
+
# Пытаемся преобразовать значение в Python-объект (число, bool, etc)
|
|
96
|
+
value = ast.literal_eval(value_str)
|
|
97
|
+
except (ValueError, SyntaxError):
|
|
98
|
+
# Если не получилось, оставляем как есть (строка)
|
|
99
|
+
value = value_str
|
|
100
|
+
|
|
101
|
+
set_value(args.config, key, value)
|
|
102
|
+
args.config.save()
|
|
103
|
+
logger.info("Значение '%s' для ключа '%s' сохранено.", value, key)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if args.unset:
|
|
107
|
+
key = args.unset
|
|
108
|
+
if del_value(args.config, key):
|
|
109
|
+
args.config.save()
|
|
110
|
+
logger.info("Ключ '%s' удален из конфига.", key)
|
|
111
|
+
else:
|
|
112
|
+
logger.warning("Ключ '%s' не найден в конфиге.", key)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if args.key:
|
|
116
|
+
value = get_value(args.config, args.key)
|
|
117
|
+
if value is not None:
|
|
118
|
+
print(value)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
config_path = str(args.config._config_path)
|
|
122
|
+
if args.show_path:
|
|
123
|
+
print(config_path)
|
|
124
|
+
else:
|
|
125
|
+
self._open_editor(config_path)
|
|
126
|
+
|
|
127
|
+
def _open_editor(self, filepath: str) -> None:
|
|
128
|
+
"""Открывает файл в редакторе по умолчанию в зависимости от ОС."""
|
|
129
|
+
system = platform.system()
|
|
130
|
+
try:
|
|
131
|
+
if system == "Windows":
|
|
132
|
+
os.startfile(filepath)
|
|
133
|
+
elif system == "Darwin": # macOS
|
|
134
|
+
subprocess.run(["open", filepath], check=True)
|
|
135
|
+
else: # Linux and other Unix-like
|
|
136
|
+
editor = os.getenv("EDITOR")
|
|
137
|
+
if editor:
|
|
138
|
+
subprocess.run([editor, filepath], check=True)
|
|
139
|
+
else:
|
|
140
|
+
subprocess.run(["xdg-open", filepath], check=True)
|
|
141
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
142
|
+
logger.error("Не удалось открыть редактор. Ошибка: %s", e)
|
|
143
|
+
logger.info("Пожалуйста, откройте файл вручную: %s", filepath)
|
|
@@ -78,21 +78,22 @@ class Operation(BaseOperation):
|
|
|
78
78
|
page += 1
|
|
79
79
|
if args.format.startswith("json"):
|
|
80
80
|
import json, sys
|
|
81
|
+
|
|
81
82
|
is_json = args.format == "json"
|
|
82
83
|
total_contacts = len(contact_persons)
|
|
83
|
-
|
|
84
|
+
|
|
84
85
|
if is_json:
|
|
85
86
|
sys.stdout.write("[")
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
for index, contact in enumerate(contact_persons):
|
|
88
89
|
if is_json and index > 0:
|
|
89
90
|
sys.stdout.write(",")
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
json.dump(contact, sys.stdout, ensure_ascii=False)
|
|
92
93
|
|
|
93
94
|
if not is_json:
|
|
94
95
|
sys.stdout.write("\n")
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
if is_json:
|
|
97
98
|
sys.stdout.write("]\n")
|
|
98
99
|
else:
|
|
@@ -244,7 +245,7 @@ def generate_html_report(data: list[dict]) -> str:
|
|
|
244
245
|
|
|
245
246
|
html_content += '<div class="person-card">'
|
|
246
247
|
|
|
247
|
-
if item.get(
|
|
248
|
+
if item.get("is_scam"):
|
|
248
249
|
html_content += '<div class="scam-warning">⚠️ ВНИМАНИЕ: Подозрение на мошенничество!</div>'
|
|
249
250
|
|
|
250
251
|
html_content += f"""\
|
|
@@ -312,11 +313,36 @@ def print_contact(contact: dict, is_last_contact: bool) -> None:
|
|
|
312
313
|
is_scam = contact.get("is_scam", False)
|
|
313
314
|
prefix = "└──" if is_last_contact else "├──"
|
|
314
315
|
scam_label = " ⚠️ [МОШЕННИК]" if is_scam else ""
|
|
316
|
+
|
|
315
317
|
print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}{scam_label}")
|
|
318
|
+
|
|
316
319
|
prefix2 = " " if is_last_contact else " │ "
|
|
320
|
+
|
|
317
321
|
print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
|
|
322
|
+
|
|
323
|
+
# 📞 Телефоны (вложенный список)
|
|
324
|
+
phones = contact.get("phone_numbers") or []
|
|
325
|
+
print(f"{prefix2}├── 📞 Телефоны:")
|
|
326
|
+
if phones:
|
|
327
|
+
for i, phone in enumerate(phones):
|
|
328
|
+
p = "└──" if i == len(phones) - 1 else "├──"
|
|
329
|
+
print(f"{prefix2}│ {p} {phone['phone_number']}")
|
|
330
|
+
else:
|
|
331
|
+
print(f"{prefix2}│ └── н/д")
|
|
332
|
+
|
|
333
|
+
# 💬 Telegram (вложенный список)
|
|
334
|
+
telegram_usernames = contact.get("telegram_usernames") or []
|
|
335
|
+
print(f"{prefix2}├── 💬 Telegram:")
|
|
336
|
+
if telegram_usernames:
|
|
337
|
+
for i, tg in enumerate(telegram_usernames):
|
|
338
|
+
p = "└──" if i == len(telegram_usernames) - 1 else "├──"
|
|
339
|
+
print(f"{prefix2}│ {p} {tg['username']}")
|
|
340
|
+
else:
|
|
341
|
+
print(f"{prefix2}│ └── н/д")
|
|
342
|
+
|
|
318
343
|
employer = contact.get("employer") or {}
|
|
319
344
|
print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
|
|
320
345
|
print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
|
|
321
346
|
print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
|
|
347
|
+
|
|
322
348
|
print(prefix2)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from runpy import run_module
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..main import BaseOperation
|
|
8
|
+
from ..main import Namespace as BaseNamespace
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__package__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Namespace(BaseNamespace):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Operation(BaseOperation):
|
|
18
|
+
"""Установит Chromium"""
|
|
19
|
+
|
|
20
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def run(self, *args: Any, **kwargs: Any) -> None:
|
|
24
|
+
sys.argv = ["playwright", "install", "chromium"]
|
|
25
|
+
run_module("playwright", run_name="__main__")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from runpy import run_module
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..main import BaseOperation
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__package__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Operation(BaseOperation):
|
|
13
|
+
"""Удалит Chromium"""
|
|
14
|
+
|
|
15
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def run(self, *args: Any, **kwargs: Any) -> None:
|
|
19
|
+
sys.argv = ["playwright", "uninstall", "chromium"]
|
|
20
|
+
run_module("playwright", run_name="__main__")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "hh-applicant-tool"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.2"
|
|
4
4
|
description = "HH-Applicant-Tool: An automation utility for HeadHunter (hh.ru) designed to streamline the job search process by auto-applying to relevant vacancies and periodically refreshing resumes to stay at the top of recruiter searches."
|
|
5
5
|
homepage = "https://github.com/s3rgeym/hh-applicant-tool"
|
|
6
6
|
repository = "https://github.com/s3rgeym/hh-applicant-tool"
|
|
@@ -13,11 +13,7 @@ packages = [{include = "hh_applicant_tool"}]
|
|
|
13
13
|
python = "^3.10"
|
|
14
14
|
requests = {extras = ["socks"], version = "^2.32.3"}
|
|
15
15
|
prettytable = "^3.6.0"
|
|
16
|
-
|
|
17
|
-
pyqt6-webengine = { version = "6.7.0", optional = true }
|
|
18
|
-
|
|
19
|
-
[tool.poetry.extras]
|
|
20
|
-
qt = ["pyqt6", "pyqt6-webengine"]
|
|
16
|
+
playwright = "^1.57.0"
|
|
21
17
|
|
|
22
18
|
[tool.poetry.group.dev.dependencies]
|
|
23
19
|
isort = "^5.12.0"
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import logging
|
|
3
|
-
from urllib.parse import parse_qs, urlsplit
|
|
4
|
-
import sys
|
|
5
|
-
from typing import Any
|
|
6
|
-
from ..utils import print_err
|
|
7
|
-
|
|
8
|
-
from ..api import ApiClient # noqa: E402
|
|
9
|
-
from ..main import BaseOperation, Namespace # noqa: E402
|
|
10
|
-
|
|
11
|
-
HH_ANDROID_SCHEME = "hhandroid"
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__package__)
|
|
14
|
-
|
|
15
|
-
QT_IMPORTED = False
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
from PyQt6.QtCore import QUrl
|
|
19
|
-
from PyQt6.QtWidgets import QApplication, QMainWindow
|
|
20
|
-
from PyQt6.QtWebEngineCore import QWebEngineUrlScheme
|
|
21
|
-
from PyQt6.QtWebEngineCore import QWebEngineUrlSchemeHandler
|
|
22
|
-
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
23
|
-
|
|
24
|
-
QT_IMPORTED = True
|
|
25
|
-
except ImportError as ex:
|
|
26
|
-
logger.debug(ex)
|
|
27
|
-
# Заглушки чтобы на сервере не нужно было ставить сотни мегабайт qt-говна
|
|
28
|
-
|
|
29
|
-
class QUrl:
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
class QApplication:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
class QMainWindow:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
class QWebEngineUrlSchemeHandler:
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
class QWebEngineView:
|
|
42
|
-
pass
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class HHAndroidUrlSchemeHandler(QWebEngineUrlSchemeHandler):
|
|
47
|
-
def __init__(self, parent: "WebViewWindow") -> None:
|
|
48
|
-
super().__init__()
|
|
49
|
-
self.parent = parent
|
|
50
|
-
self._register_hhandroid_scheme()
|
|
51
|
-
|
|
52
|
-
def requestStarted(self, info: Any) -> None:
|
|
53
|
-
url = info.requestUrl().toString()
|
|
54
|
-
if url.startswith(f"{HH_ANDROID_SCHEME}://"):
|
|
55
|
-
self.parent.handle_redirect_uri(url)
|
|
56
|
-
|
|
57
|
-
def _register_hhandroid_scheme(self) -> None:
|
|
58
|
-
scheme = QWebEngineUrlScheme(HH_ANDROID_SCHEME.encode())
|
|
59
|
-
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Path)
|
|
60
|
-
scheme.setFlags(
|
|
61
|
-
QWebEngineUrlScheme.Flag.SecureScheme |
|
|
62
|
-
QWebEngineUrlScheme.Flag.LocalScheme |
|
|
63
|
-
QWebEngineUrlScheme.Flag.LocalAccessAllowed |
|
|
64
|
-
QWebEngineUrlScheme.Flag.CorsEnabled
|
|
65
|
-
)
|
|
66
|
-
QWebEngineUrlScheme.registerScheme(scheme)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class WebViewWindow(QMainWindow):
|
|
70
|
-
def __init__(self, api_client: ApiClient) -> None:
|
|
71
|
-
super().__init__()
|
|
72
|
-
self.api_client = api_client
|
|
73
|
-
|
|
74
|
-
self.web_view = QWebEngineView()
|
|
75
|
-
self.setCentralWidget(self.web_view)
|
|
76
|
-
self.setWindowTitle("Авторизация на HH.RU")
|
|
77
|
-
self.hhandroid_handler = HHAndroidUrlSchemeHandler(self)
|
|
78
|
-
|
|
79
|
-
profile = self.web_view.page().profile()
|
|
80
|
-
profile.installUrlSchemeHandler(HH_ANDROID_SCHEME.encode(), self.hhandroid_handler)
|
|
81
|
-
|
|
82
|
-
self.web_view.page().acceptNavigationRequest = self._filter_http_requests
|
|
83
|
-
|
|
84
|
-
self.resize(480, 800)
|
|
85
|
-
oauth_url = api_client.oauth_client.authorize_url
|
|
86
|
-
logger.debug(f"{oauth_url = }")
|
|
87
|
-
self.web_view.setUrl(QUrl(oauth_url))
|
|
88
|
-
|
|
89
|
-
def _filter_http_requests(self, url: QUrl, _type, is_main_frame):
|
|
90
|
-
"""Блокирует любые переходы по протоколу HTTP"""
|
|
91
|
-
if url.scheme().lower() == "http":
|
|
92
|
-
logger.warning(f"🚫 Заблокирован небезопасный запрос: {url.toString()}")
|
|
93
|
-
return False
|
|
94
|
-
return True
|
|
95
|
-
|
|
96
|
-
def handle_redirect_uri(self, redirect_uri: str) -> None:
|
|
97
|
-
logger.debug(f"handle redirect uri: {redirect_uri}")
|
|
98
|
-
sp = urlsplit(redirect_uri)
|
|
99
|
-
code = parse_qs(sp.query).get("code", [None])[0]
|
|
100
|
-
if code:
|
|
101
|
-
token = self.api_client.oauth_client.authenticate(code)
|
|
102
|
-
self.api_client.handle_access_token(token)
|
|
103
|
-
print("🔓 Авторизация прошла успешно!")
|
|
104
|
-
self.close()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class Operation(BaseOperation):
|
|
108
|
-
"""Авторизоваться на сайте"""
|
|
109
|
-
|
|
110
|
-
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
114
|
-
if not QT_IMPORTED:
|
|
115
|
-
print_err(
|
|
116
|
-
"❗Ошибка: PyQt6 не был импортирован, возможно, вы забыли его установить, либо же это ошибка самой библиотеки."
|
|
117
|
-
)
|
|
118
|
-
sys.exit(1)
|
|
119
|
-
|
|
120
|
-
proxies = api_client.proxies
|
|
121
|
-
if proxy_url := proxies.get("https"):
|
|
122
|
-
import os
|
|
123
|
-
|
|
124
|
-
qtwebengine_chromium_flags = f"--proxy-server={proxy_url}"
|
|
125
|
-
logger.debug(f"set {qtwebengine_chromium_flags = }")
|
|
126
|
-
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = qtwebengine_chromium_flags
|
|
127
|
-
|
|
128
|
-
app = QApplication(sys.argv)
|
|
129
|
-
window = WebViewWindow(api_client=api_client)
|
|
130
|
-
window.show()
|
|
131
|
-
|
|
132
|
-
app.exec()
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from ..main import BaseOperation
|
|
8
|
-
from ..main import Namespace as BaseNamespace
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__package__)
|
|
11
|
-
|
|
12
|
-
EDITOR = os.getenv("EDITOR", "nano")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Namespace(BaseNamespace):
|
|
16
|
-
show_path: bool
|
|
17
|
-
key: str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def get_value(data: dict[str, Any], path: str) -> Any:
|
|
21
|
-
for key in path.split("."):
|
|
22
|
-
if key not in data:
|
|
23
|
-
return None
|
|
24
|
-
data = data[key]
|
|
25
|
-
return data
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class Operation(BaseOperation):
|
|
29
|
-
"""Операции с конфигурационным файлом"""
|
|
30
|
-
|
|
31
|
-
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
32
|
-
parser.add_argument(
|
|
33
|
-
"-p",
|
|
34
|
-
"--show-path",
|
|
35
|
-
"--path",
|
|
36
|
-
action=argparse.BooleanOptionalAction,
|
|
37
|
-
help="Вывести полный путь к конфигу",
|
|
38
|
-
)
|
|
39
|
-
parser.add_argument("-k", "--key", help="Вывести отдельное значение из конфига")
|
|
40
|
-
|
|
41
|
-
def run(self, args: Namespace, *_) -> None:
|
|
42
|
-
if args.key:
|
|
43
|
-
print(get_value(args.config, args.key))
|
|
44
|
-
return
|
|
45
|
-
config_path = str(args.config._config_path)
|
|
46
|
-
if args.show_path:
|
|
47
|
-
print(config_path)
|
|
48
|
-
else:
|
|
49
|
-
subprocess.call([EDITOR, config_path])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/__init__.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/apply_similar.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/call_api.py
RENAMED
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/delete_telemetry.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/list_resumes.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/refresh_token.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/reply_employers.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/update_resumes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|