hh-applicant-tool 0.5.9__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hh-applicant-tool might be problematic. Click here for more details.
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/PKG-INFO +12 -8
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/README.md +11 -7
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/main.py +4 -2
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/apply_similar.py +7 -22
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/call_api.py +1 -1
- hh_applicant_tool-0.6.1/hh_applicant_tool/operations/get_employer_contacts.py +293 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/reply_employers.py +90 -14
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/telemetry_client.py +1 -1
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/pyproject.toml +1 -1
- hh_applicant_tool-0.5.9/hh_applicant_tool/operations/get_employer_contacts.py +0 -87
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/__init__.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/__main__.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/ai/__init__.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/ai/blackbox.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/api/__init__.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/api/client.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/api/errors.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/color_log.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/constants.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/jsonc.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/mixins.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/__init__.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/authorize.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/config.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/delete_telemetry.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/list_resumes.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/refresh_token.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/update_resumes.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/whoami.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/types.py +0 -0
- {hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -30,11 +30,6 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
<img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
### Внимание!!!
|
|
34
|
-
|
|
35
|
-
Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/).
|
|
36
|
-
|
|
37
|
-
|
|
38
33
|
### Описание
|
|
39
34
|
|
|
40
35
|
> Утилита для генерации сопроводительного письма может использовать AI
|
|
@@ -53,6 +48,10 @@ asdf/pyenv/conda и что-то еще. В школотронской Manjaro и
|
|
|
53
48
|
|
|
54
49
|
> Если в веб-интерфейсе выставить фильтры, то они будут применяться в скрипте при отклике на подходящие
|
|
55
50
|
|
|
51
|
+
### Внимание!!!
|
|
52
|
+
|
|
53
|
+
Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/). Оно обладает минимальным функционалом: обновление резюме (одного) и рассылка откликов (чистить их и тп нельзя).
|
|
54
|
+
|
|
56
55
|
### Предыстория
|
|
57
56
|
|
|
58
57
|
Долгое время я делал массовые заявки с помощью консоли браузера:
|
|
@@ -184,7 +183,7 @@ hh-applicant-tool config -p
|
|
|
184
183
|
|
|
185
184
|
| Имя атрибута | Описание |
|
|
186
185
|
| --- | --- |
|
|
187
|
-
| `user_agent` | Кастомный юзерагент, передаваемый при кажом
|
|
186
|
+
| `user_agent` | Кастомный юзерагент, передаваемый при кажом запросе. |
|
|
188
187
|
| `proxy_url` | Прокси, используемый для всех запросов, например, `socks5h://127.0.0.1:9050` |
|
|
189
188
|
| `reply_message` | Сообщение для ответа работодателю при отклике на вакансии, см. формат сообщений |
|
|
190
189
|
|
|
@@ -210,6 +209,10 @@ $ hh-applicant-tool update-resumes
|
|
|
210
209
|
|
|
211
210
|
# Чистим заявки и баним за отказы говноконторы
|
|
212
211
|
$ hh-applicant-tool clear-negotiations --blacklist-discard
|
|
212
|
+
|
|
213
|
+
# Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
|
|
214
|
+
# приглашение
|
|
215
|
+
$ hh-applicant-tool get-employer-contacts --export -f html > report.html
|
|
213
216
|
```
|
|
214
217
|
|
|
215
218
|
Можно вызвать любой метод API:
|
|
@@ -257,7 +260,8 @@ https://hh.ru/employer/1918903
|
|
|
257
260
|
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
258
261
|
| **refresh-token** | Обновляет access_token. |
|
|
259
262
|
| **config** | Редактировать конфигурационный файл. |
|
|
260
|
-
| **get-employer-contacts** | Получить список контактов
|
|
263
|
+
| **get-employer-contacts** | Получить список полученных вами контактов работодателей. Поддерживается так же экспорт в html/jsonl. Если хотите собирать контакты с нескольких акков, то укажите им одинаковый `client_telemetry_id` в конфигах. |
|
|
264
|
+
| **delete-telemetry** | Удадяет телеметрию, если та была включена. |
|
|
261
265
|
|
|
262
266
|
### Формат текста сообщений
|
|
263
267
|
|
|
@@ -11,11 +11,6 @@
|
|
|
11
11
|
<img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
### Внимание!!!
|
|
15
|
-
|
|
16
|
-
Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/).
|
|
17
|
-
|
|
18
|
-
|
|
19
14
|
### Описание
|
|
20
15
|
|
|
21
16
|
> Утилита для генерации сопроводительного письма может использовать AI
|
|
@@ -34,6 +29,10 @@ asdf/pyenv/conda и что-то еще. В школотронской Manjaro и
|
|
|
34
29
|
|
|
35
30
|
> Если в веб-интерфейсе выставить фильтры, то они будут применяться в скрипте при отклике на подходящие
|
|
36
31
|
|
|
32
|
+
### Внимание!!!
|
|
33
|
+
|
|
34
|
+
Если для Вас проблема установить данную утилиту, лень разбираться с ее настройкой, то Вы можете установить мое приложение под **Android** [HH Resume Automate](https://github.com/s3rgeym/hh-resume-automate/). Оно обладает минимальным функционалом: обновление резюме (одного) и рассылка откликов (чистить их и тп нельзя).
|
|
35
|
+
|
|
37
36
|
### Предыстория
|
|
38
37
|
|
|
39
38
|
Долгое время я делал массовые заявки с помощью консоли браузера:
|
|
@@ -165,7 +164,7 @@ hh-applicant-tool config -p
|
|
|
165
164
|
|
|
166
165
|
| Имя атрибута | Описание |
|
|
167
166
|
| --- | --- |
|
|
168
|
-
| `user_agent` | Кастомный юзерагент, передаваемый при кажом
|
|
167
|
+
| `user_agent` | Кастомный юзерагент, передаваемый при кажом запросе. |
|
|
169
168
|
| `proxy_url` | Прокси, используемый для всех запросов, например, `socks5h://127.0.0.1:9050` |
|
|
170
169
|
| `reply_message` | Сообщение для ответа работодателю при отклике на вакансии, см. формат сообщений |
|
|
171
170
|
|
|
@@ -191,6 +190,10 @@ $ hh-applicant-tool update-resumes
|
|
|
191
190
|
|
|
192
191
|
# Чистим заявки и баним за отказы говноконторы
|
|
193
192
|
$ hh-applicant-tool clear-negotiations --blacklist-discard
|
|
193
|
+
|
|
194
|
+
# Экспортировать в HTML, контакты работодателей, которые когда-либо высылали вам
|
|
195
|
+
# приглашение
|
|
196
|
+
$ hh-applicant-tool get-employer-contacts --export -f html > report.html
|
|
194
197
|
```
|
|
195
198
|
|
|
196
199
|
Можно вызвать любой метод API:
|
|
@@ -238,7 +241,8 @@ https://hh.ru/employer/1918903
|
|
|
238
241
|
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
239
242
|
| **refresh-token** | Обновляет access_token. |
|
|
240
243
|
| **config** | Редактировать конфигурационный файл. |
|
|
241
|
-
| **get-employer-contacts** | Получить список контактов
|
|
244
|
+
| **get-employer-contacts** | Получить список полученных вами контактов работодателей. Поддерживается так же экспорт в html/jsonl. Если хотите собирать контакты с нескольких акков, то укажите им одинаковый `client_telemetry_id` в конфигах. |
|
|
245
|
+
| **delete-telemetry** | Удадяет телеметрию, если та была включена. |
|
|
242
246
|
|
|
243
247
|
### Формат текста сообщений
|
|
244
248
|
|
|
@@ -11,9 +11,8 @@ from typing import Literal, Sequence
|
|
|
11
11
|
|
|
12
12
|
from .api import ApiClient
|
|
13
13
|
from .color_log import ColorHandler
|
|
14
|
-
from .utils import Config, get_config_path
|
|
15
14
|
from .telemetry_client import TelemetryClient
|
|
16
|
-
|
|
15
|
+
from .utils import Config, get_config_path
|
|
17
16
|
|
|
18
17
|
DEFAULT_CONFIG_PATH = (
|
|
19
18
|
get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
|
|
@@ -151,6 +150,9 @@ class HHApplicantTool:
|
|
|
151
150
|
if (token := api_client.get_access_token()) != args.config["token"]:
|
|
152
151
|
args.config.save(token=token)
|
|
153
152
|
return res
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
logger.warning("Interrupted by user")
|
|
155
|
+
return 1
|
|
154
156
|
except Exception as e:
|
|
155
157
|
logger.exception(e)
|
|
156
158
|
return 1
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/apply_similar.py
RENAMED
|
@@ -255,28 +255,13 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
255
255
|
"area": employer.get("area", {}).get("name"), # город
|
|
256
256
|
}
|
|
257
257
|
if "got_rejection" in relations:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
employer_data,
|
|
266
|
-
)
|
|
267
|
-
if "topic_url" in response:
|
|
268
|
-
print(
|
|
269
|
-
"Ссылка на обсуждение работодателя:",
|
|
270
|
-
response["topic_url"],
|
|
271
|
-
)
|
|
272
|
-
else:
|
|
273
|
-
# print(
|
|
274
|
-
# "Создание темы для обсуждения работодателя добавлено в очередь..."
|
|
275
|
-
# )
|
|
276
|
-
...
|
|
277
|
-
complained_employers.add(employer_id)
|
|
278
|
-
except TelemetryError as ex:
|
|
279
|
-
logger.error(ex)
|
|
258
|
+
print(
|
|
259
|
+
"🚨 Вы получили отказ от https://hh.ru/employer/%s"
|
|
260
|
+
% employer_id
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
complained_employers.add(employer_id)
|
|
264
|
+
|
|
280
265
|
elif do_apply:
|
|
281
266
|
telemetry_data["employers"][employer_id] = employer_data
|
|
282
267
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
from os import getenv
|
|
4
|
+
import pathlib
|
|
5
|
+
from ..main import BaseOperation
|
|
6
|
+
from ..main import Namespace as BaseNamespace
|
|
7
|
+
from ..telemetry_client import TelemetryClient
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__package__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Namespace(BaseNamespace):
|
|
13
|
+
username: str | None
|
|
14
|
+
password: str | None
|
|
15
|
+
search: str | None
|
|
16
|
+
export: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Operation(BaseOperation):
|
|
20
|
+
"""Выведет контакты работодателей, которые высылали вам приглашения"""
|
|
21
|
+
|
|
22
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
23
|
+
# parser.add_argument(
|
|
24
|
+
# "-u",
|
|
25
|
+
# "--username",
|
|
26
|
+
# type=str,
|
|
27
|
+
# help="Имя пользователя для аутентификации",
|
|
28
|
+
# default=getenv("AUTH_USERNAME"),
|
|
29
|
+
# )
|
|
30
|
+
# parser.add_argument(
|
|
31
|
+
# "-P",
|
|
32
|
+
# "--password",
|
|
33
|
+
# type=str,
|
|
34
|
+
# help="Пароль для аутентификации",
|
|
35
|
+
# default=getenv("AUTH_PASSWORD"),
|
|
36
|
+
# )
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-s",
|
|
39
|
+
"--search",
|
|
40
|
+
type=str,
|
|
41
|
+
default="",
|
|
42
|
+
help="Строка поиска контактов работодателя (email, имя, название компании)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"-p",
|
|
46
|
+
"--page",
|
|
47
|
+
default=1,
|
|
48
|
+
help="Номер страницы в выдаче. Игнорируется при экспорте.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--export",
|
|
52
|
+
action=argparse.BooleanOptionalAction,
|
|
53
|
+
default=False,
|
|
54
|
+
help="Экспортировать контакты работодателей.",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"-f",
|
|
58
|
+
"--format",
|
|
59
|
+
default="html",
|
|
60
|
+
choices=["html", "jsonl"],
|
|
61
|
+
help="Формат вывода",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def run(self, args: Namespace, _, telemetry_client: TelemetryClient) -> None:
|
|
65
|
+
if args.export:
|
|
66
|
+
contact_persons = []
|
|
67
|
+
page = 1
|
|
68
|
+
per_page = 100
|
|
69
|
+
while True:
|
|
70
|
+
res = telemetry_client.get_telemetry(
|
|
71
|
+
"/contact/persons",
|
|
72
|
+
{"search": args.search, "per_page": per_page, "page": page},
|
|
73
|
+
)
|
|
74
|
+
assert "contact_persons" in res
|
|
75
|
+
contact_persons += res["contact_persons"]
|
|
76
|
+
if per_page * page >= res["total"]:
|
|
77
|
+
break
|
|
78
|
+
page += 1
|
|
79
|
+
if args.format == "jsonl":
|
|
80
|
+
import json, sys
|
|
81
|
+
|
|
82
|
+
for contact in contact_persons:
|
|
83
|
+
json.dump(contact, sys.stdout, ensure_ascii=False)
|
|
84
|
+
sys.stdout.write("\n")
|
|
85
|
+
sys.stdout.flush()
|
|
86
|
+
else:
|
|
87
|
+
print(generate_html_report(contact_persons))
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
res = telemetry_client.get_telemetry(
|
|
91
|
+
"/contact/persons",
|
|
92
|
+
{"search": args.search, "per_page": 10, "page": args.page},
|
|
93
|
+
)
|
|
94
|
+
if "contact_persons" not in res:
|
|
95
|
+
print("❌", res)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
print(
|
|
99
|
+
"Тут отображаются только данные, собранные с вашего telemetry_client_id. Вы так же можете их удалить с помощью команды delete-telemetry."
|
|
100
|
+
)
|
|
101
|
+
print()
|
|
102
|
+
|
|
103
|
+
print_contacts(res)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def generate_html_report(data: list[dict]) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Генерирует HTML-отчет на основе предоставленных данных.
|
|
109
|
+
"""
|
|
110
|
+
html_content = """\
|
|
111
|
+
<!DOCTYPE html>
|
|
112
|
+
<html lang="ru">
|
|
113
|
+
<head>
|
|
114
|
+
<meta charset="UTF-8">
|
|
115
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
116
|
+
<title>Контакты работодателей</title>
|
|
117
|
+
<style>
|
|
118
|
+
body {
|
|
119
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
120
|
+
margin: 20px;
|
|
121
|
+
background-color: #f4f7f6;
|
|
122
|
+
color: #333;
|
|
123
|
+
}
|
|
124
|
+
.container {
|
|
125
|
+
max-width: 900px;
|
|
126
|
+
margin: 20px auto;
|
|
127
|
+
background-color: #ffffff;
|
|
128
|
+
padding: 30px;
|
|
129
|
+
border-radius: 10px;
|
|
130
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
131
|
+
}
|
|
132
|
+
h1 {
|
|
133
|
+
color: #0056b3;
|
|
134
|
+
text-align: center;
|
|
135
|
+
margin-bottom: 30px;
|
|
136
|
+
}
|
|
137
|
+
.person-card {
|
|
138
|
+
background-color: #e9f0f8;
|
|
139
|
+
border: 1px solid #cce5ff;
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
padding: 20px;
|
|
142
|
+
margin-bottom: 25px;
|
|
143
|
+
transition: transform 0.2s ease-in-out;
|
|
144
|
+
}
|
|
145
|
+
.person-card:hover {
|
|
146
|
+
transform: translateY(-5px);
|
|
147
|
+
}
|
|
148
|
+
.person-card h2 {
|
|
149
|
+
color: #004085;
|
|
150
|
+
margin-top: 0;
|
|
151
|
+
margin-bottom: 10px;
|
|
152
|
+
border-bottom: 2px solid #0056b3;
|
|
153
|
+
padding-bottom: 5px;
|
|
154
|
+
}
|
|
155
|
+
.person-card p {
|
|
156
|
+
margin: 5px 0;
|
|
157
|
+
}
|
|
158
|
+
.person-card strong {
|
|
159
|
+
color: #004085;
|
|
160
|
+
}
|
|
161
|
+
.employer-info {
|
|
162
|
+
background-color: #d1ecf1;
|
|
163
|
+
border-left: 5px solid #007bff;
|
|
164
|
+
padding: 15px;
|
|
165
|
+
margin-top: 15px;
|
|
166
|
+
border-radius: 5px;
|
|
167
|
+
}
|
|
168
|
+
.employer-info h3 {
|
|
169
|
+
color: #0056b3;
|
|
170
|
+
margin-top: 0;
|
|
171
|
+
margin-bottom: 10px;
|
|
172
|
+
}
|
|
173
|
+
ul {
|
|
174
|
+
list-style-type: none;
|
|
175
|
+
padding: 0;
|
|
176
|
+
}
|
|
177
|
+
ul li {
|
|
178
|
+
background-color: #f8fafd;
|
|
179
|
+
padding: 8px 12px;
|
|
180
|
+
margin-bottom: 5px;
|
|
181
|
+
border-radius: 4px;
|
|
182
|
+
border: 1px solid #e0e9f1;
|
|
183
|
+
}
|
|
184
|
+
a {
|
|
185
|
+
color: #007bff;
|
|
186
|
+
text-decoration: none;
|
|
187
|
+
}
|
|
188
|
+
a:hover {
|
|
189
|
+
text-decoration: underline;
|
|
190
|
+
}
|
|
191
|
+
.no-data {
|
|
192
|
+
color: #6c757d;
|
|
193
|
+
font-style: italic;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
<body>
|
|
198
|
+
<div class="container">
|
|
199
|
+
<h1>Полученные контакты</h1>
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
for item in data:
|
|
203
|
+
name = item.get("name", "N/A")
|
|
204
|
+
email = item.get("email", "N/A")
|
|
205
|
+
employer = item.get("employer") or {}
|
|
206
|
+
|
|
207
|
+
employer_name = employer.get("name", "N/A")
|
|
208
|
+
employer_area = employer.get("area", "N/A")
|
|
209
|
+
employer_site_url = employer.get("site_url", "")
|
|
210
|
+
|
|
211
|
+
phone_numbers = [
|
|
212
|
+
pn["phone_number"]
|
|
213
|
+
for pn in item.get("phone_numbers", [])
|
|
214
|
+
if "phone_number" in pn
|
|
215
|
+
]
|
|
216
|
+
telegram_usernames = [
|
|
217
|
+
tu["username"]
|
|
218
|
+
for tu in item.get("telegram_usernames", [])
|
|
219
|
+
if "username" in tu
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
html_content += f"""\
|
|
223
|
+
<div class="person-card">
|
|
224
|
+
<h2>{name}</h2>
|
|
225
|
+
<p><strong>Email:</strong> <a href="mailto:{email}">{email}</a></p>
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
if employer_name != "N/A":
|
|
229
|
+
html_content += f"""\
|
|
230
|
+
<div class="employer-info">
|
|
231
|
+
<h3>Работодатель: {employer_name}</h3>
|
|
232
|
+
<p><strong>Город:</strong> {employer_area}</p>
|
|
233
|
+
"""
|
|
234
|
+
if employer_site_url:
|
|
235
|
+
html_content += f"""\
|
|
236
|
+
<p><strong>Сайт:</strong> <a href="{employer_site_url}" target="_blank">{employer_site_url}</a></p>
|
|
237
|
+
"""
|
|
238
|
+
html_content += "</div>" # Закрываем employer-info
|
|
239
|
+
else:
|
|
240
|
+
html_content += (
|
|
241
|
+
'<p class="no-data">Информация о работодателе отсутствует.</p>'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if phone_numbers:
|
|
245
|
+
html_content += "<p><strong>Номера телефонов:</strong></p><ul>"
|
|
246
|
+
for phone in phone_numbers:
|
|
247
|
+
html_content += f"<li><a href='tel:{phone}'>{phone}</a></li>"
|
|
248
|
+
html_content += "</ul>"
|
|
249
|
+
else:
|
|
250
|
+
html_content += '<p class="no-data">Номера телефонов отсутствуют.</p>'
|
|
251
|
+
|
|
252
|
+
if telegram_usernames:
|
|
253
|
+
html_content += "<p><strong>Имена пользователей Telegram:</strong></p><ul>"
|
|
254
|
+
for username in telegram_usernames:
|
|
255
|
+
html_content += f"<li><a href='https://t.me/{username}' target='_blank'>@{username}</a></li>"
|
|
256
|
+
html_content += "</ul>"
|
|
257
|
+
else:
|
|
258
|
+
html_content += (
|
|
259
|
+
'<p class="no-data">Имена пользователей Telegram отсутствуют.</p>'
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
html_content += "</div>" # Закрываем person-card
|
|
263
|
+
|
|
264
|
+
html_content += """\
|
|
265
|
+
</div>
|
|
266
|
+
</body>
|
|
267
|
+
</html>"""
|
|
268
|
+
return html_content
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def print_contacts(data: dict) -> None:
|
|
272
|
+
"""Вывод всех контактов в древовидной структуре."""
|
|
273
|
+
page = data["page"]
|
|
274
|
+
pages = (data["total"] // data["per_page"]) + 1
|
|
275
|
+
print(f"Страница {page}/{pages}:")
|
|
276
|
+
contacts = data.get("contact_persons", [])
|
|
277
|
+
for idx, contact in enumerate(contacts):
|
|
278
|
+
is_last_contact = idx == len(contacts) - 1
|
|
279
|
+
print_contact(contact, is_last_contact)
|
|
280
|
+
print()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def print_contact(contact: dict, is_last_contact: bool) -> None:
|
|
284
|
+
"""Вывод информации о конкретном контакте."""
|
|
285
|
+
prefix = "└──" if is_last_contact else "├──"
|
|
286
|
+
print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
|
|
287
|
+
prefix2 = " " if is_last_contact else " │ "
|
|
288
|
+
print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
|
|
289
|
+
employer = contact.get("employer") or {}
|
|
290
|
+
print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
|
|
291
|
+
print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
|
|
292
|
+
print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
|
|
293
|
+
print(prefix2)
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/reply_employers.py
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
1
3
|
import argparse
|
|
2
4
|
import logging
|
|
3
5
|
import random
|
|
@@ -9,6 +11,22 @@ from ..main import BaseOperation
|
|
|
9
11
|
from ..main import Namespace as BaseNamespace
|
|
10
12
|
from ..mixins import GetResumeIdMixin
|
|
11
13
|
from ..utils import parse_interval, random_text
|
|
14
|
+
from ..telemetry_client import TelemetryClient, TelemetryError
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import readline
|
|
19
|
+
|
|
20
|
+
readline.add_history("/cancel ")
|
|
21
|
+
readline.set_history_length(10_000)
|
|
22
|
+
except ImportError:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
GOOGLE_DOCS_RE = re.compile(
|
|
27
|
+
r"\b(?:https?:\/\/)?(?:docs|forms|sheets|slides|drive)\.google\.com\/(?:document|spreadsheets|presentation|forms|file)\/(?:d|u)\/[a-zA-Z0-9_\-]+(?:\/[a-zA-Z0-9_\-]+)?\/?(?:[?#].*)?\b|\b(?:https?:\/\/)?(?:goo\.gl|forms\.gle)\/[a-zA-Z0-9]+\b",
|
|
28
|
+
re.I,
|
|
29
|
+
)
|
|
12
30
|
|
|
13
31
|
logger = logging.getLogger(__package__)
|
|
14
32
|
|
|
@@ -66,8 +84,12 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
66
84
|
action=argparse.BooleanOptionalAction,
|
|
67
85
|
)
|
|
68
86
|
|
|
69
|
-
def run(
|
|
87
|
+
def run(
|
|
88
|
+
self, args: Namespace, api_client: ApiClient, telemetry_client: TelemetryClient
|
|
89
|
+
) -> None:
|
|
70
90
|
self.api_client = api_client
|
|
91
|
+
self.telemetry_client = telemetry_client
|
|
92
|
+
self.enable_telemetry = not args.disable_telemetry
|
|
71
93
|
self.resume_id = self._get_resume_id()
|
|
72
94
|
self.reply_min_interval, self.reply_max_interval = args.reply_interval
|
|
73
95
|
self.reply_message = args.reply_message or args.config["reply_message"]
|
|
@@ -81,6 +103,8 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
81
103
|
def _reply_chats(self) -> None:
|
|
82
104
|
me = self.me = self.api_client.get("/me")
|
|
83
105
|
|
|
106
|
+
telemetry_data = {"links": []}
|
|
107
|
+
|
|
84
108
|
basic_message_placeholders = {
|
|
85
109
|
"first_name": me.get("first_name", ""),
|
|
86
110
|
"last_name": me.get("last_name", ""),
|
|
@@ -144,6 +168,32 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
144
168
|
|
|
145
169
|
page = messages_res["pages"] - 1
|
|
146
170
|
|
|
171
|
+
if self.enable_telemetry:
|
|
172
|
+
# Собираем ссылки на тестовые задания
|
|
173
|
+
for message in message_history:
|
|
174
|
+
if message.startswith("-> "):
|
|
175
|
+
continue
|
|
176
|
+
# Тестовые задания и тп
|
|
177
|
+
for link in GOOGLE_DOCS_RE.findall(message):
|
|
178
|
+
document_data = {
|
|
179
|
+
"vacancy_url": vacancy.get("alternate_url"),
|
|
180
|
+
"vacancy_name": vacancy.get("name"),
|
|
181
|
+
"salary": (
|
|
182
|
+
f"{salary.get('from', '...')}-{salary.get('to', '...')} {salary.get('currency', 'RUR')}" # noqa: E501
|
|
183
|
+
if salary
|
|
184
|
+
else None
|
|
185
|
+
),
|
|
186
|
+
"employer_url": vacancy.get("employer", {}).get(
|
|
187
|
+
"alternate_url"
|
|
188
|
+
),
|
|
189
|
+
"link": link,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
telemetry_data["links"].append(document_data)
|
|
193
|
+
|
|
194
|
+
if os.getenv("TEST_SEND_TELEMETRY") in ["1", "y", "Y"]:
|
|
195
|
+
continue
|
|
196
|
+
|
|
147
197
|
logger.debug(last_message)
|
|
148
198
|
|
|
149
199
|
is_employer_message = (
|
|
@@ -152,8 +202,10 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
152
202
|
|
|
153
203
|
if is_employer_message or not negotiation.get("viewed_by_opponent"):
|
|
154
204
|
if self.reply_message:
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
send_message = (
|
|
206
|
+
random_text(self.reply_message) % message_placeholders
|
|
207
|
+
)
|
|
208
|
+
logger.debug(send_message)
|
|
157
209
|
else:
|
|
158
210
|
print("🏢", message_placeholders["employer_name"])
|
|
159
211
|
print("💼", message_placeholders["vacancy_name"])
|
|
@@ -173,8 +225,16 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
173
225
|
else message_history
|
|
174
226
|
):
|
|
175
227
|
print(msg)
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
try:
|
|
229
|
+
print("-" * 10)
|
|
230
|
+
print()
|
|
231
|
+
print(
|
|
232
|
+
"Чтобы отменить отклик введите /cancel <необязательное сообщение для отказа>"
|
|
233
|
+
)
|
|
234
|
+
print()
|
|
235
|
+
send_message = input("Ваше сообщение: ").strip()
|
|
236
|
+
except EOFError:
|
|
237
|
+
continue
|
|
178
238
|
if not message:
|
|
179
239
|
print("🚶 Пропускаем чат")
|
|
180
240
|
continue
|
|
@@ -183,7 +243,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
183
243
|
logger.info(
|
|
184
244
|
"Dry Run: Отправка сообщения в чат по вакансии %s: %s",
|
|
185
245
|
vacancy["alternate_url"],
|
|
186
|
-
|
|
246
|
+
send_message,
|
|
187
247
|
)
|
|
188
248
|
continue
|
|
189
249
|
|
|
@@ -193,17 +253,33 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
193
253
|
self.reply_max_interval,
|
|
194
254
|
)
|
|
195
255
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
256
|
+
|
|
257
|
+
if send_message.startswith("/cancel"):
|
|
258
|
+
_, decline_allowed = send_message.split("/cancel", 1)
|
|
259
|
+
self.api_client.delete(
|
|
260
|
+
f"/negotiations/active/{negotiation['id']}",
|
|
261
|
+
with_decline_message=decline_allowed.strip(),
|
|
262
|
+
)
|
|
263
|
+
print("Отменили заявку", vacancy["alternate_url"])
|
|
264
|
+
else:
|
|
265
|
+
self.api_client.post(
|
|
266
|
+
f"/negotiations/{nid}/messages",
|
|
267
|
+
message=send_message,
|
|
268
|
+
)
|
|
269
|
+
print(
|
|
270
|
+
"📨 Отправили сообщение для",
|
|
271
|
+
vacancy["alternate_url"],
|
|
272
|
+
)
|
|
204
273
|
except ApiError as ex:
|
|
205
274
|
logger.error(ex)
|
|
206
275
|
|
|
276
|
+
if self.enable_telemetry and len(telemetry_data["links"]) > 0:
|
|
277
|
+
logger.debug(telemetry_data)
|
|
278
|
+
try:
|
|
279
|
+
self.telemetry_client.send_telemetry("/docs", telemetry_data)
|
|
280
|
+
except TelemetryError as ex:
|
|
281
|
+
logger.warning(ex, exc_info=True)
|
|
282
|
+
|
|
207
283
|
print("📝 Сообщения разосланы!")
|
|
208
284
|
|
|
209
285
|
def _get_negotiations(self) -> list[dict]:
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import logging
|
|
3
|
-
from os import getenv
|
|
4
|
-
|
|
5
|
-
from ..main import BaseOperation
|
|
6
|
-
from ..main import Namespace as BaseNamespace
|
|
7
|
-
from ..telemetry_client import TelemetryClient
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__package__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class Namespace(BaseNamespace):
|
|
13
|
-
username: str | None = None
|
|
14
|
-
password: str | None = None
|
|
15
|
-
search: str | None = None
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Operation(BaseOperation):
|
|
19
|
-
"""Выведет контакты работодателя по заданной строке поиска"""
|
|
20
|
-
|
|
21
|
-
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
|
-
parser.add_argument(
|
|
23
|
-
"-u",
|
|
24
|
-
"--username",
|
|
25
|
-
type=str,
|
|
26
|
-
help="Имя пользователя для аутентификации",
|
|
27
|
-
default=getenv("AUTH_USERNAME"),
|
|
28
|
-
)
|
|
29
|
-
parser.add_argument(
|
|
30
|
-
"-P",
|
|
31
|
-
"--password",
|
|
32
|
-
type=str,
|
|
33
|
-
help="Пароль для аутентификации",
|
|
34
|
-
default=getenv("AUTH_PASSWORD"),
|
|
35
|
-
)
|
|
36
|
-
parser.add_argument(
|
|
37
|
-
"-s",
|
|
38
|
-
"--search",
|
|
39
|
-
type=str,
|
|
40
|
-
default="",
|
|
41
|
-
help="Строка поиска для контактов работодателя",
|
|
42
|
-
)
|
|
43
|
-
parser.add_argument(
|
|
44
|
-
"-p",
|
|
45
|
-
"--page",
|
|
46
|
-
default=1,
|
|
47
|
-
help="Номер страницы в выдаче",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
def run(self, args: Namespace, _, telemetry_client: TelemetryClient) -> None:
|
|
51
|
-
results = telemetry_client.get_telemetry(
|
|
52
|
-
"/contact/persons",
|
|
53
|
-
{"search": args.search, "per_page": 10, "page": args.page},
|
|
54
|
-
)
|
|
55
|
-
if "contact_persons" not in results:
|
|
56
|
-
print("❌", results)
|
|
57
|
-
return 1
|
|
58
|
-
|
|
59
|
-
print(
|
|
60
|
-
"Тут отображаются только данные, собранные с вашего telemetry_client_id. Вы так же можете их удалить с помощью команды delete-telemetry."
|
|
61
|
-
)
|
|
62
|
-
print()
|
|
63
|
-
|
|
64
|
-
self._print_contacts(results)
|
|
65
|
-
|
|
66
|
-
def _print_contacts(self, data: dict) -> None:
|
|
67
|
-
"""Вывод всех контактов в древовидной структуре."""
|
|
68
|
-
page = data["page"]
|
|
69
|
-
pages = (data["total"] // data["per_page"]) + 1
|
|
70
|
-
print(f"Страница {page}/{pages}:")
|
|
71
|
-
contacts = data.get("contact_persons", [])
|
|
72
|
-
for idx, contact in enumerate(contacts):
|
|
73
|
-
is_last_contact = idx == len(contacts) - 1
|
|
74
|
-
self._print_contact(contact, is_last_contact)
|
|
75
|
-
print()
|
|
76
|
-
|
|
77
|
-
def _print_contact(self, contact: dict, is_last_contact: bool) -> None:
|
|
78
|
-
"""Вывод информации о конкретном контакте."""
|
|
79
|
-
prefix = "└──" if is_last_contact else "├──"
|
|
80
|
-
print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
|
|
81
|
-
prefix2 = " " if is_last_contact else " │ "
|
|
82
|
-
print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
|
|
83
|
-
employer = contact.get("employer") or {}
|
|
84
|
-
print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
|
|
85
|
-
print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
|
|
86
|
-
print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
|
|
87
|
-
print(prefix2)
|
|
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.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/__init__.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/authorize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/delete_telemetry.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/list_resumes.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/refresh_token.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.5.9 → hh_applicant_tool-0.6.1}/hh_applicant_tool/operations/update_resumes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|