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.
Files changed (36) hide show
  1. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/PKG-INFO +55 -30
  2. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/README.md +53 -26
  3. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/api/client.py +3 -1
  4. hh_applicant_tool-0.8.2/hh_applicant_tool/operations/authorize.py +151 -0
  5. hh_applicant_tool-0.8.2/hh_applicant_tool/operations/config.py +143 -0
  6. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/get_employer_contacts.py +31 -5
  7. hh_applicant_tool-0.8.2/hh_applicant_tool/operations/install.py +25 -0
  8. hh_applicant_tool-0.8.2/hh_applicant_tool/operations/uninstall.py +20 -0
  9. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/pyproject.toml +2 -6
  10. hh_applicant_tool-0.7.7/hh_applicant_tool/operations/authorize.py +0 -132
  11. hh_applicant_tool-0.7.7/hh_applicant_tool/operations/config.py +0 -49
  12. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/__init__.py +0 -0
  13. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/__main__.py +0 -0
  14. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/ai/__init__.py +0 -0
  15. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/ai/blackbox.py +0 -0
  16. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/ai/openai.py +0 -0
  17. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/api/__init__.py +0 -0
  18. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/api/errors.py +0 -0
  19. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/color_log.py +0 -0
  20. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/constants.py +0 -0
  21. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/jsonc.py +0 -0
  22. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/main.py +0 -0
  23. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/mixins.py +0 -0
  24. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/__init__.py +0 -0
  25. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/apply_similar.py +0 -0
  26. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/call_api.py +0 -0
  27. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
  28. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/delete_telemetry.py +0 -0
  29. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/list_resumes.py +0 -0
  30. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/refresh_token.py +0 -0
  31. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/reply_employers.py +0 -0
  32. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/update_resumes.py +0 -0
  33. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/operations/whoami.py +0 -0
  34. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/telemetry_client.py +0 -0
  35. {hh_applicant_tool-0.7.7 → hh_applicant_tool-0.8.2}/hh_applicant_tool/types.py +0 -0
  36. {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.7.7
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
- Provides-Extra: qt
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
- * [⚙️ Конфигурация](#пути-до-файла-configjson)
44
- * [🛠 Описание команд](#описание-команд)
45
- * [📝 Формат сообщений](#формат-текста-сообщений)
46
- * [🤖 Использование AI (ChatGPT)](#использование-ai-для-генерации-сопроводительного-письма)
47
- * [📡 Работа с API напрямую](#работа-с-api-напрямую)
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
- Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести конфиг на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
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 'hh-applicant-tool[qt]'
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[qt]
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 -vv authorize
157
+ $ hh-applicant-tool authorize '<ваш телефон или email>'
158
+ 📨 Код был отправлен. Проверьте почту или SMS.
159
+ 📩 Введите полученный код: 1387
160
+ 🔓 Авторизация прошла успешно!
149
161
  ```
150
162
 
151
- <img width="610" height="958" alt="image" src="https://github.com/user-attachments/assets/adf47113-df16-4500-aed5-a78d9a783d97" />
163
+ - Если вы ввели телефон, то код придет через SMS
164
+ - Если был введен Email, то проверьте почту
152
165
 
153
- > Пусть надпись "Поиск содрудников" не вводит в заблуждение это форма авторизации соискателей. Просто есть долбоебы, которые в этом окне начинают лихорадочно тыкать по ссылкам, не понимая, что форма авторизации у них была перед носом.
166
+ Если вы пропустили пункт про установку зависимостей, то увидите такую ошибку:
154
167
 
155
- > В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
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
- * [⚙️ Конфигурация](#пути-до-файла-configjson)
22
- * [🛠 Описание команд](#описание-команд)
23
- * [📝 Формат сообщений](#формат-текста-сообщений)
24
- * [🤖 Использование AI (ChatGPT)](#использование-ai-для-генерации-сопроводительного-письма)
25
- * [📡 Работа с API напрямую](#работа-с-api-напрямую)
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
- Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести конфиг на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
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 'hh-applicant-tool[qt]'
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[qt]
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 -vv authorize
137
+ $ hh-applicant-tool authorize '<ваш телефон или email>'
138
+ 📨 Код был отправлен. Проверьте почту или SMS.
139
+ 📩 Введите полученный код: 1387
140
+ 🔓 Авторизация прошла успешно!
127
141
  ```
128
142
 
129
- <img width="610" height="958" alt="image" src="https://github.com/user-attachments/assets/adf47113-df16-4500-aed5-a78d9a783d97" />
143
+ - Если вы ввели телефон, то код придет через SMS
144
+ - Если был введен Email, то проверьте почту
130
145
 
131
- > Пусть надпись "Поиск содрудников" не вводит в заблуждение это форма авторизации соискателей. Просто есть долбоебы, которые в этом окне начинают лихорадочно тыкать по ссылкам, не понимая, что форма авторизации у них была перед носом.
146
+ Если вы пропустили пункт про установку зависимостей, то увидите такую ошибку:
132
147
 
133
- > В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
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 = }, proxies = {self.proxies}, params = {repr(params)[:255]}")
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('is_scam'):
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.7.7"
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
- pyqt6 = { version = "6.7.0", optional = true }
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])