hh-applicant-tool 0.3.0__tar.gz → 0.3.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.

Potentially problematic release.


This version of hh-applicant-tool might be problematic. Click here for more details.

Files changed (24) hide show
  1. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/PKG-INFO +35 -13
  2. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/README.md +31 -12
  3. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/main.py +3 -3
  4. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/apply_similar.py +61 -1
  5. hh_applicant_tool-0.3.2/hh_applicant_tool/telemetry_client.py +67 -0
  6. hh_applicant_tool-0.3.2/hh_applicant_tool/utils.py +74 -0
  7. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/pyproject.toml +5 -4
  8. hh_applicant_tool-0.3.0/hh_applicant_tool/utils.py +0 -48
  9. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/__init__.py +0 -0
  10. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/__main__.py +0 -0
  11. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/api/__init__.py +0 -0
  12. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/api/client.py +0 -0
  13. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/api/errors.py +0 -0
  14. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/color_log.py +0 -0
  15. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/constants.py +0 -0
  16. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/__init__.py +0 -0
  17. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/authorize.py +0 -0
  18. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/call_api.py +0 -0
  19. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
  20. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/list_resumes.py +0 -0
  21. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/refresh_token.py +0 -0
  22. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/update_resumes.py +0 -0
  23. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/operations/whoami.py +0 -0
  24. {hh_applicant_tool-0.3.0 → hh_applicant_tool-0.3.2}/hh_applicant_tool/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -10,11 +10,14 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Provides-Extra: qt
13
14
  Requires-Dist: prettytable (>=3.6.0,<4.0.0)
15
+ Requires-Dist: pyqt6 (>=6.7.1,<7.0.0) ; extra == "qt"
16
+ Requires-Dist: pyqt6-webengine (>=6.7.0,<7.0.0) ; extra == "qt"
14
17
  Requires-Dist: requests (>=2.28.2,<3.0.0)
15
18
  Description-Content-Type: text/markdown
16
19
 
17
- # HH Applicant Tool
20
+ ## HH Applicant Tool
18
21
 
19
22
  > ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
20
23
 
@@ -26,10 +29,10 @@ Description-Content-Type: text/markdown
26
29
  [![Total Downloads](https://static.pepy.tech/badge/hh-applicant-tool)]()
27
30
 
28
31
  <div align="center">
29
- <img src="https://github.com/user-attachments/assets/9bfce763-1359-471f-8b0b-ad0b7d21bd1c" width="500">
32
+ <img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
30
33
  </div>
31
34
 
32
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/vaitishniki (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
35
+ Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/+aSjr8qM_AP85ZDBi (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
33
36
 
34
37
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
35
38
  asdf/pyenv/conda и что-то еще...
@@ -39,11 +42,11 @@ asdf/pyenv/conda и что-то еще...
39
42
  ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
40
43
 
41
44
 
42
- Данная утилита написана для Linux, но будет работать и в Windows, но в WSL она не работает, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
45
+ Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, в Windows, но в WSL она не работает, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
43
46
 
44
47
  Предыстория.
45
48
 
46
- Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отличить Java от JavaScript, но я думаю, что <s>в значительном числе случаев, тут имеют место такие вот рассылки</s> они просто идиотки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу.
49
+ Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отличить Java от JavaScript, но я думаю, что <s>в значительном числе случаев, тут имеют место такие вот рассылки</s> они просто идиотки... И я тупо стал спамить как они. Мне уже было просто лень читать весь этот бред, что пишут долбоебы в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое.
47
50
 
48
51
  Долгое время я делал массовые заявки с помощью консоли браузера:
49
52
 
@@ -56,7 +59,7 @@ $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
56
59
  Установка:
57
60
 
58
61
  ```bash
59
- # Версия с поддержкой авторизации через всплывающее окно
62
+ # Версия с поддержкой авторизации через запуск окна с браузером (эта версия очень много весит)
60
63
  $ pipx install 'hh-applicant-tool[qt]'
61
64
 
62
65
  # Если хочется использовать самую последнюю версию, то можно установить ее через git
@@ -196,8 +199,6 @@ $ hh-applicant-tool whoami
196
199
  }
197
200
  ```
198
201
 
199
- Далее идут заметки для разработчиков...
200
-
201
202
  Токен выдается на две недели:
202
203
 
203
204
  ```python
@@ -209,11 +210,11 @@ datetime.datetime(2023, 3, 23, 6, 36, 15, 596290)
209
210
  >>>
210
211
  ```
211
212
 
212
- После нужно вызвать `refresh-token`.
213
-
214
- ![](https://user-images.githubusercontent.com/12753171/222870516-b29f2417-d11a-4122-8291-7d440a422a31.png)
213
+ После нужно вызвать `refresh-token`:
215
214
 
216
- При авторизации можно указать `redirect_uri`, но любые адреса кроме того, что с протоколом `hhandroid`, будут приводить к ошибке. Поэтому и нужно добавление обработчика кастомного протокола. Через расширения браузера это не сделать, но можно программно перехватить редирект... Использование АВТОРИЗАЦИИ ДЛЯ САЙТОВ в мобильном приложении выглядит странной, так как десктопные и мобильные приложения обычно авторизуются напрямую, но у чуваков свое понимание не только протокола OAuth...
215
+ ```bash
216
+ $ hh-applicant-tool refresh-token
217
+ ```
217
218
 
218
219
  Удаление хвостов:
219
220
 
@@ -228,3 +229,24 @@ rm -f ~/.local/share/applications/hhandroid.desktop
228
229
 
229
230
  Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
230
231
 
232
+ Утилита собирает и передает на сервер разработчика следующую ифнормацию:
233
+
234
+ Утилита собирает следующую информацию:
235
+
236
+ 1. Название вакансии.
237
+ 1. Тип вакансии (открытая/закрытая).
238
+ 1. Город, в котором размещена вакансия.
239
+ 1. Информация о зарплате (минимальная, максимальная, валюта, указана ли зарплата до вычета налогов).
240
+ 1. Прямая ссылка на вакансию.
241
+ 1. Дата создания вакансии.
242
+ 1. Дата публикации вакансии.
243
+ 1. Контактная информация хрюши (ее телефон, email и тп).
244
+ 1. Название компании.
245
+ 1. Тип компании.
246
+ 1. Описание компании.
247
+ 1. Ссылка на сайт компании.
248
+ 1. Город, в котором находится компания.
249
+
250
+ !!! УТИЛИТА НЕ СОБИРАЕТ НИКАКИХ ПЕРСОНАЛЬНЫХ ДАННЫХ ПОЛЬЗОВАТЕЛЕЙ — ТОЛЬКО ВСЯКИХ РАБОТАДАТЕЛЕЙ И ИХ ОВЧАРОК. ТАК ЖЕ Я ОБЕЩАЮ, ЧТО УТИЛИТА ВСЕГДА БУДЕТ БЕСПЛАТНОЙ, ВСЕ КТО ЕЮ ПЫТАЮТСЯ ТОРГОВАТЬ — УЕБКИ И У НИХ ПОЧЕРНЕЕТ И ОТВАЛИТСЯ ХУЙ
251
+
252
+
@@ -1,4 +1,4 @@
1
- # HH Applicant Tool
1
+ ## HH Applicant Tool
2
2
 
3
3
  > ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
4
4
 
@@ -10,10 +10,10 @@
10
10
  [![Total Downloads](https://static.pepy.tech/badge/hh-applicant-tool)]()
11
11
 
12
12
  <div align="center">
13
- <img src="https://github.com/user-attachments/assets/9bfce763-1359-471f-8b0b-ad0b7d21bd1c" width="500">
13
+ <img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
14
14
  </div>
15
15
 
16
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/vaitishniki (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
16
+ Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/+aSjr8qM_AP85ZDBi (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
17
17
 
18
18
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
19
19
  asdf/pyenv/conda и что-то еще...
@@ -23,11 +23,11 @@ asdf/pyenv/conda и что-то еще...
23
23
  ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
24
24
 
25
25
 
26
- Данная утилита написана для Linux, но будет работать и в Windows, но в WSL она не работает, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
26
+ Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, в Windows, но в WSL она не работает, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
27
27
 
28
28
  Предыстория.
29
29
 
30
- Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отличить Java от JavaScript, но я думаю, что <s>в значительном числе случаев, тут имеют место такие вот рассылки</s> они просто идиотки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу.
30
+ Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отличить Java от JavaScript, но я думаю, что <s>в значительном числе случаев, тут имеют место такие вот рассылки</s> они просто идиотки... И я тупо стал спамить как они. Мне уже было просто лень читать весь этот бред, что пишут долбоебы в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое.
31
31
 
32
32
  Долгое время я делал массовые заявки с помощью консоли браузера:
33
33
 
@@ -40,7 +40,7 @@ $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
40
40
  Установка:
41
41
 
42
42
  ```bash
43
- # Версия с поддержкой авторизации через всплывающее окно
43
+ # Версия с поддержкой авторизации через запуск окна с браузером (эта версия очень много весит)
44
44
  $ pipx install 'hh-applicant-tool[qt]'
45
45
 
46
46
  # Если хочется использовать самую последнюю версию, то можно установить ее через git
@@ -180,8 +180,6 @@ $ hh-applicant-tool whoami
180
180
  }
181
181
  ```
182
182
 
183
- Далее идут заметки для разработчиков...
184
-
185
183
  Токен выдается на две недели:
186
184
 
187
185
  ```python
@@ -193,11 +191,11 @@ datetime.datetime(2023, 3, 23, 6, 36, 15, 596290)
193
191
  >>>
194
192
  ```
195
193
 
196
- После нужно вызвать `refresh-token`.
197
-
198
- ![](https://user-images.githubusercontent.com/12753171/222870516-b29f2417-d11a-4122-8291-7d440a422a31.png)
194
+ После нужно вызвать `refresh-token`:
199
195
 
200
- При авторизации можно указать `redirect_uri`, но любые адреса кроме того, что с протоколом `hhandroid`, будут приводить к ошибке. Поэтому и нужно добавление обработчика кастомного протокола. Через расширения браузера это не сделать, но можно программно перехватить редирект... Использование АВТОРИЗАЦИИ ДЛЯ САЙТОВ в мобильном приложении выглядит странной, так как десктопные и мобильные приложения обычно авторизуются напрямую, но у чуваков свое понимание не только протокола OAuth...
196
+ ```bash
197
+ $ hh-applicant-tool refresh-token
198
+ ```
201
199
 
202
200
  Удаление хвостов:
203
201
 
@@ -211,3 +209,24 @@ rm -f ~/.local/share/applications/hhandroid.desktop
211
209
  Утилита использует систему плагинов. Все они лежат в [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).
212
210
 
213
211
  Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
212
+
213
+ Утилита собирает и передает на сервер разработчика следующую ифнормацию:
214
+
215
+ Утилита собирает следующую информацию:
216
+
217
+ 1. Название вакансии.
218
+ 1. Тип вакансии (открытая/закрытая).
219
+ 1. Город, в котором размещена вакансия.
220
+ 1. Информация о зарплате (минимальная, максимальная, валюта, указана ли зарплата до вычета налогов).
221
+ 1. Прямая ссылка на вакансию.
222
+ 1. Дата создания вакансии.
223
+ 1. Дата публикации вакансии.
224
+ 1. Контактная информация хрюши (ее телефон, email и тп).
225
+ 1. Название компании.
226
+ 1. Тип компании.
227
+ 1. Описание компании.
228
+ 1. Ссылка на сайт компании.
229
+ 1. Город, в котором находится компания.
230
+
231
+ !!! УТИЛИТА НЕ СОБИРАЕТ НИКАКИХ ПЕРСОНАЛЬНЫХ ДАННЫХ ПОЛЬЗОВАТЕЛЕЙ — ТОЛЬКО ВСЯКИХ РАБОТАДАТЕЛЕЙ И ИХ ОВЧАРОК. ТАК ЖЕ Я ОБЕЩАЮ, ЧТО УТИЛИТА ВСЕГДА БУДЕТ БЕСПЛАТНОЙ, ВСЕ КТО ЕЮ ПЫТАЮТСЯ ТОРГОВАТЬ — УЕБКИ И У НИХ ПОЧЕРНЕЕТ И ОТВАЛИТСЯ ХУЙ
232
+
@@ -11,10 +11,10 @@ from pkgutil import iter_modules
11
11
  from typing import Sequence
12
12
 
13
13
  from .color_log import ColorHandler
14
- from .utils import Config
14
+ from .utils import Config, get_config_path
15
15
 
16
16
  DEFAULT_CONFIG_PATH = (
17
- Path(getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
17
+ get_config_path()
18
18
  / __package__.replace("_", "-")
19
19
  / "config.json"
20
20
  )
@@ -42,7 +42,7 @@ class HHApplicantTool:
42
42
 
43
43
  Исходники и предложения: <https://github.com/s3rgeym/hh-applicant-tool>
44
44
 
45
- Группа поддержки: <https://t.me/vaitishniki>
45
+ Группа поддержки: <https://t.me/+aSjr8qM_AP85ZDBi>
46
46
  """
47
47
 
48
48
  def create_parser(self) -> argparse.ArgumentParser:
@@ -2,13 +2,19 @@ import argparse
2
2
  import logging
3
3
  import random
4
4
  import time
5
+ from collections import defaultdict
6
+ from os import getenv
5
7
  from typing import TextIO, Tuple
6
8
 
7
9
  from ..api import ApiClient, ApiError, BadRequest
8
10
  from ..main import BaseOperation
9
11
  from ..main import Namespace as BaseNamespace
10
12
  from ..types import ApiListResponse, VacancyItem
11
- from ..utils import print_err, truncate_string
13
+ from ..utils import print_err, truncate_string, fix_datetime
14
+ from ..telemetry_client import (
15
+ get_client as get_telemetry_client,
16
+ TelemetryError,
17
+ )
12
18
 
13
19
  logger = logging.getLogger(__package__)
14
20
 
@@ -114,6 +120,10 @@ class Operation(BaseOperation):
114
120
  order_by="relevance",
115
121
  )
116
122
  rv.extend(res["items"])
123
+
124
+ if getenv("TEST_TELEMETRY"):
125
+ break
126
+
117
127
  if page >= res["pages"] - 1:
118
128
  break
119
129
 
@@ -136,10 +146,54 @@ class Operation(BaseOperation):
136
146
  page_max_interval: float,
137
147
  ) -> None:
138
148
  item: VacancyItem
149
+
150
+ # Телеметрия не включает ваши персональные данные, она нужна для сбора информации о работодателях и их вакансиях
151
+ telemetry_client = get_telemetry_client()
152
+ telemetry_data = defaultdict(dict)
153
+
139
154
  for item in self._get_vacancies(
140
155
  api, resume_id, page_min_interval, page_max_interval
141
156
  ):
142
157
  try:
158
+ # Информация о вакансии
159
+ vacancy_id = item["id"]
160
+
161
+ telemetry_data["vacancies"][vacancy_id] = {
162
+ "name": item.get("name"),
163
+ "type": item.get("type", {}).get("id"), # open/closed
164
+ "area": item.get("area", {}).get("name"), # город
165
+ "salary": item.get("salary"), # from, to, currency, gross
166
+ "direct_url": item.get(
167
+ "alternate_url"
168
+ ), # ссылка на вакансию
169
+ "created_at": fix_datetime(
170
+ item.get("created_at")
171
+ ), # будем вычислять говно-вакансии, которые по полгода висят
172
+ "published_at": fix_datetime(item.get("published_at")),
173
+ "contacts": item.get(
174
+ "contacts"
175
+ ), # пиздорванки там телеграм для связи указывают
176
+ # Остальное неинтересно
177
+ }
178
+
179
+ employer_id = item["employer"][
180
+ "id"
181
+ ] # меня интересуют только название и ссылка на сайт
182
+
183
+ # так еще эмулируем какое-то иное действие нежели набор однотипных
184
+ employer = api.get(f"/employers/{employer_id}")
185
+
186
+ telemetry_data["employers"][employer_id] = {
187
+ "name": employer.get("name"),
188
+ "type": employer.get("type"),
189
+ "description": employer.get("description"),
190
+ "site_url": employer.get("site_url"),
191
+ "area": employer.get("area", {}).get("name"), # город
192
+ }
193
+
194
+ if getenv("TEST_TELEMETRY"):
195
+ break
196
+
143
197
  if item["has_test"]:
144
198
  print("Пропускаем тест", item["alternate_url"])
145
199
  continue
@@ -190,3 +244,9 @@ class Operation(BaseOperation):
190
244
  break
191
245
 
192
246
  print("📝 Отклики на вакансии разосланы!")
247
+
248
+ # Отправляем telemetry_data
249
+ try:
250
+ telemetry_client.send_telemetry("/collect", dict(telemetry_data))
251
+ except TelemetryError as err:
252
+ logger.error("Не могу отправить телеметрию")
@@ -0,0 +1,67 @@
1
+ import os
2
+ import json
3
+ from urllib.parse import urljoin
4
+ import requests
5
+ from typing import Optional, Dict, Any
6
+ from functools import cache
7
+ import logging
8
+ import base64
9
+
10
+ logger = logging.getLogger(__package__)
11
+
12
+
13
+ class TelemetryError(Exception):
14
+ """Исключение, возникающее при ошибках в работе TelemetryClient."""
15
+
16
+ pass
17
+
18
+
19
+ class TelemetryClient:
20
+ """Клиент для отправки телеметрии на сервер."""
21
+
22
+ server_address = base64.b64decode('aHR0cDovLzMxLjEzMS4yNTEuMTA3OjU0MTU2').decode()
23
+
24
+ def __init__(
25
+ self,
26
+ server_address: Optional[str] = None,
27
+ session: Optional[requests.Session] = None,
28
+ ) -> None:
29
+ """
30
+ Инициализация клиента.
31
+
32
+ :param server_address: Адрес сервера для отправки телеметрии.
33
+ :param session: Сессия для повторного использования соединения.
34
+ """
35
+ self.session = session or requests.Session()
36
+ self.server_address = os.getenv(
37
+ "TELEMETRY_SERVER", server_address or self.server_address
38
+ )
39
+
40
+ def send_telemetry(
41
+ self, endpoint: str, data: Dict[str, Any]
42
+ ) -> Dict[str, Any]:
43
+ """
44
+ Отправка телеметрии на сервер.
45
+
46
+ :param endpoint: Конечная точка на сервере.
47
+ :param data: Данные для отправки.
48
+ :return: Ответ сервера в формате JSON.
49
+ :raises TelemetryError: Если произошла ошибка при отправке или декодировании JSON.
50
+ """
51
+ url = urljoin(self.server_address, endpoint)
52
+ logger.debug(data)
53
+
54
+ try:
55
+ response = self.session.post(url, json=data)
56
+ response.raise_for_status()
57
+ return response.json()
58
+ except (
59
+ requests.exceptions.RequestException,
60
+ json.JSONDecodeError,
61
+ ) as ex:
62
+ raise TelemetryError(str(ex)) from ex
63
+
64
+
65
+ @cache
66
+ def get_client() -> TelemetryClient:
67
+ return TelemetryClient()
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ from datetime import datetime
3
+ import hashlib
4
+ import json
5
+ import platform
6
+ import sys
7
+ from functools import partial
8
+ from pathlib import Path
9
+ from threading import Lock
10
+ from typing import Any
11
+ from os import getenv
12
+ from .constants import INVALID_ISO8601_FORMAT
13
+
14
+ print_err = partial(print, file=sys.stderr, flush=True)
15
+
16
+
17
+ def get_config_path() -> Path:
18
+ match platform.system():
19
+ case "Windows":
20
+ return Path(getenv("APPDATA", Path.home() / "AppData" / "Roaming"))
21
+ case "Darwin": # macOS
22
+ return Path.home() / "Library" / "Application Support"
23
+ case _: # Linux and etc
24
+ return Path(getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
25
+
26
+
27
+ class AttrDict(dict):
28
+ __getattr__ = dict.get
29
+ __setattr__ = dict.__setitem__
30
+ __delattr__ = dict.__delitem__
31
+
32
+
33
+ # TODO: добавить defaults
34
+ class Config(dict):
35
+ def __init__(self, config_path: str | Path | None = None):
36
+ self._config_path = Path(config_path or get_config_path())
37
+ self._lock = Lock()
38
+ self.load()
39
+
40
+ def load(self) -> None:
41
+ if self._config_path.exists():
42
+ with self._lock:
43
+ with self._config_path.open() as f:
44
+ try:
45
+ self.update(json.load(f))
46
+ except ValueError:
47
+ pass
48
+
49
+ def save(self, *args: Any, **kwargs: Any) -> None:
50
+ self.update(*args, **kwargs)
51
+ self._config_path.parent.mkdir(exist_ok=True, parents=True)
52
+ with self._lock:
53
+ with self._config_path.open("w+") as fp:
54
+ json.dump(self, fp, ensure_ascii=True, indent=2, sort_keys=True)
55
+
56
+ __getitem__ = dict.get
57
+
58
+
59
+ def truncate_string(s: str, limit: int = 75, ellipsis: str = "…") -> str:
60
+ return s[:limit] + bool(s[limit:]) * ellipsis
61
+
62
+
63
+ def hash_with_salt(data: str, salt: str = "HorsePenis") -> str:
64
+ # Объединяем данные и соль
65
+ salted_data = data + salt
66
+ # Вычисляем хеш SHA-256
67
+ hashed_data = hashlib.sha256(salted_data.encode()).hexdigest()
68
+ return hashed_data
69
+
70
+
71
+ def fix_datetime(dt: str | None) -> str | None:
72
+ if dt is None:
73
+ return None
74
+ return datetime.strptime(dt, INVALID_ISO8601_FORMAT).isoformat()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hh-applicant-tool"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = ""
5
5
  authors = ["Senior YAML Developer <yamldeveloper@proton.me>"]
6
6
  readme = "README.md"
@@ -10,10 +10,11 @@ packages = [{include = "hh_applicant_tool"}]
10
10
  python = "^3.10"
11
11
  requests = "^2.28.2"
12
12
  prettytable = "^3.6.0"
13
+ pyqt6 = { version = "^6.7.1", optional = true }
14
+ pyqt6-webengine = { version = "^6.7.0", optional = true }
13
15
 
14
- [tool.poetry.group.qt.dependencies]
15
- pyqt6 = "^6.7.1"
16
- pyqt6-webengine = "^6.7.0"
16
+ [tool.poetry.extras]
17
+ qt = ["pyqt6", "pyqt6-webengine"]
17
18
 
18
19
  [tool.poetry.group.dev.dependencies]
19
20
  black = "^23.1.0"
@@ -1,48 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import sys
5
- from functools import partial
6
- from pathlib import Path
7
- from threading import Lock
8
- from typing import Any
9
-
10
- print_err = partial(print, file=sys.stderr, flush=True)
11
-
12
-
13
- class AttrDict(dict):
14
- __getattr__ = dict.get
15
- __setattr__ = dict.__setitem__
16
- __delattr__ = dict.__delitem__
17
-
18
-
19
- # TODO: добавить defaults
20
- class Config(dict):
21
- def __init__(self, config_path: str | Path):
22
- self._config_path = Path(config_path)
23
- self._lock = Lock()
24
- self.load()
25
-
26
- def load(self) -> None:
27
- if self._config_path.exists():
28
- with self._lock:
29
- with self._config_path.open() as f:
30
- try:
31
- self.update(json.load(f))
32
- except ValueError:
33
- pass
34
-
35
- def save(self, *args: Any, **kwargs: Any) -> None:
36
- self.update(*args, **kwargs)
37
- self._config_path.parent.mkdir(exist_ok=True, parents=True)
38
- with self._lock:
39
- with self._config_path.open("w+") as fp:
40
- json.dump(
41
- self, fp, ensure_ascii=True, indent=2, sort_keys=True
42
- )
43
-
44
- __getitem__ = dict.get
45
-
46
-
47
- def truncate_string(s: str, limit: int = 75, ellipsis: str = "…") -> str:
48
- return s[:limit] + bool(s[limit:]) * ellipsis