hh-applicant-tool 0.5.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
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/operations/reply_employers.py +91 -28
- hh_applicant_tool/utils.py +2 -2
- {hh_applicant_tool-0.5.0.dist-info → hh_applicant_tool-0.5.1.dist-info}/METADATA +3 -3
- {hh_applicant_tool-0.5.0.dist-info → hh_applicant_tool-0.5.1.dist-info}/RECORD +6 -6
- {hh_applicant_tool-0.5.0.dist-info → hh_applicant_tool-0.5.1.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.5.0.dist-info → hh_applicant_tool-0.5.1.dist-info}/entry_points.txt +0 -0
|
@@ -6,9 +6,10 @@ from typing import Tuple
|
|
|
6
6
|
|
|
7
7
|
from ..api import ApiError
|
|
8
8
|
from ..main import BaseOperation
|
|
9
|
-
from ..main import Namespace as BaseNamespace
|
|
10
|
-
from ..
|
|
9
|
+
from ..main import Namespace as BaseNamespace
|
|
10
|
+
from ..main import get_api
|
|
11
11
|
from ..mixins import GetResumeIdMixin
|
|
12
|
+
from ..utils import parse_interval, random_text
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__package__)
|
|
14
15
|
|
|
@@ -17,6 +18,7 @@ class Namespace(BaseNamespace):
|
|
|
17
18
|
reply_message: str
|
|
18
19
|
reply_interval: Tuple[float, float]
|
|
19
20
|
max_pages: int
|
|
21
|
+
only_invitations: bool
|
|
20
22
|
dry_run: bool
|
|
21
23
|
|
|
22
24
|
|
|
@@ -24,25 +26,42 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
24
26
|
"""Ответ всем работодателям."""
|
|
25
27
|
|
|
26
28
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
29
|
+
# parser.add_argument(
|
|
30
|
+
# "reply_message",
|
|
31
|
+
# help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ. Если не передать, то его нужно будет вводить интерактивно.",
|
|
32
|
+
# )
|
|
33
|
+
parser.add_argument("--resume-id", help="Идентификатор резюме")
|
|
27
34
|
parser.add_argument(
|
|
28
|
-
"
|
|
29
|
-
help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ",
|
|
30
|
-
)
|
|
31
|
-
parser.add_argument('--resume-id', help="Идентификатор резюме")
|
|
32
|
-
parser.add_argument(
|
|
35
|
+
"-i",
|
|
33
36
|
"--reply-interval",
|
|
34
37
|
help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
|
|
35
38
|
default="5-10",
|
|
36
39
|
type=parse_interval,
|
|
37
40
|
)
|
|
38
41
|
parser.add_argument(
|
|
42
|
+
"-m",
|
|
39
43
|
"--reply-message",
|
|
40
44
|
"--reply",
|
|
41
|
-
help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали
|
|
45
|
+
help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали ответ. Еслм не передать сообщение, то нужно будет вводить его в интерактивном режиме.",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-p",
|
|
49
|
+
"--max-pages",
|
|
50
|
+
type=int,
|
|
51
|
+
default=25,
|
|
52
|
+
help="Максимальное количество страниц для проверки",
|
|
42
53
|
)
|
|
43
|
-
parser.add_argument(
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"-oi",
|
|
56
|
+
"--only-invitations",
|
|
57
|
+
help="Отвечать только на приглашения",
|
|
58
|
+
default=False,
|
|
59
|
+
action=argparse.BooleanOptionalAction,
|
|
60
|
+
)
|
|
61
|
+
|
|
44
62
|
parser.add_argument(
|
|
45
63
|
"--dry-run",
|
|
64
|
+
"--dry",
|
|
46
65
|
help="Не отправлять сообщения, а только выводить параметры запроса",
|
|
47
66
|
default=False,
|
|
48
67
|
action=argparse.BooleanOptionalAction,
|
|
@@ -52,14 +71,16 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
52
71
|
self.api = get_api(args)
|
|
53
72
|
self.resume_id = self._get_resume_id()
|
|
54
73
|
self.reply_min_interval, self.reply_max_interval = args.reply_interval
|
|
55
|
-
self.reply_message = args.reply_message
|
|
74
|
+
self.reply_message = args.reply_message or args.config["reply_message"]
|
|
75
|
+
# assert self.reply_message, "`reply_message` должен быть передан чеерез аргументы или настройки"
|
|
56
76
|
self.max_pages = args.max_pages
|
|
57
77
|
self.dry_run = args.dry_run
|
|
58
|
-
|
|
78
|
+
self.only_invitations = args.only_invitations
|
|
79
|
+
logger.debug(f"{self.reply_message = }")
|
|
59
80
|
self._reply_chats()
|
|
60
81
|
|
|
61
82
|
def _reply_chats(self) -> None:
|
|
62
|
-
me =self.me= self.api.get("/me")
|
|
83
|
+
me = self.me = self.api.get("/me")
|
|
63
84
|
|
|
64
85
|
basic_message_placeholders = {
|
|
65
86
|
"first_name": me.get("first_name", ""),
|
|
@@ -71,17 +92,26 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
71
92
|
for negotiation in self._get_negotiations():
|
|
72
93
|
try:
|
|
73
94
|
# Пропускаем другие резюме
|
|
74
|
-
if self.resume_id != negotiation[
|
|
95
|
+
if self.resume_id != negotiation["resume"]["id"]:
|
|
75
96
|
continue
|
|
76
97
|
|
|
98
|
+
state_id = negotiation["state"]["id"]
|
|
99
|
+
|
|
100
|
+
# Пропускаем отказ
|
|
101
|
+
if state_id == "discard":
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
if self.only_invitations and not state_id.startswith("inv"):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
logger.debug(negotiation)
|
|
77
108
|
nid = negotiation["id"]
|
|
78
109
|
vacancy = negotiation["vacancy"]
|
|
110
|
+
salary = vacancy.get("salary") or {}
|
|
79
111
|
|
|
80
112
|
message_placeholders = {
|
|
81
113
|
"vacancy_name": vacancy.get("name", ""),
|
|
82
|
-
"employer_name": vacancy.get("employer", {}).get(
|
|
83
|
-
"name", ""
|
|
84
|
-
),
|
|
114
|
+
"employer_name": vacancy.get("employer", {}).get("name", ""),
|
|
85
115
|
**basic_message_placeholders,
|
|
86
116
|
}
|
|
87
117
|
|
|
@@ -92,28 +122,61 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
92
122
|
|
|
93
123
|
page: int = 0
|
|
94
124
|
last_message: dict | None = None
|
|
125
|
+
message_history: list[str] = []
|
|
95
126
|
while True:
|
|
96
127
|
messages_res = self.api.get(
|
|
97
128
|
f"/negotiations/{nid}/messages", page=page
|
|
98
129
|
)
|
|
130
|
+
|
|
99
131
|
last_message = messages_res["items"][-1]
|
|
132
|
+
message_history.extend(
|
|
133
|
+
(
|
|
134
|
+
"<-"
|
|
135
|
+
if item["author"]["participant_type"] == "employer"
|
|
136
|
+
else "->"
|
|
137
|
+
)
|
|
138
|
+
+ " "
|
|
139
|
+
+ item["text"]
|
|
140
|
+
for item in messages_res["items"]
|
|
141
|
+
if item.get("text")
|
|
142
|
+
)
|
|
100
143
|
if page + 1 >= messages_res["pages"]:
|
|
101
144
|
break
|
|
102
145
|
|
|
103
146
|
page = messages_res["pages"] - 1
|
|
104
147
|
|
|
105
|
-
logger.debug(last_message
|
|
148
|
+
logger.debug(last_message)
|
|
106
149
|
|
|
107
|
-
|
|
108
|
-
"participant_type"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
):
|
|
112
|
-
|
|
113
|
-
random_text(self.reply_message)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
is_employer_message = (
|
|
151
|
+
last_message["author"]["participant_type"] == "employer"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if is_employer_message or not negotiation.get("viewed_by_opponent"):
|
|
155
|
+
if self.reply_message:
|
|
156
|
+
message = random_text(self.reply_message) % message_placeholders
|
|
157
|
+
logger.debug(message)
|
|
158
|
+
else:
|
|
159
|
+
print("🏢", message_placeholders["employer_name"])
|
|
160
|
+
print("💼", message_placeholders["vacancy_name"])
|
|
161
|
+
print("📅", vacancy["created_at"])
|
|
162
|
+
if salary:
|
|
163
|
+
salary_from = salary.get("from", "-")
|
|
164
|
+
salary_to = salary.get("to", "-")
|
|
165
|
+
salary_currency = salary.get("currency")
|
|
166
|
+
print("💵 от", salary_from, "до", salary_to, salary_currency)
|
|
167
|
+
print("")
|
|
168
|
+
print("Последние сообщения:")
|
|
169
|
+
for msg in (
|
|
170
|
+
message_history[:1] + ["..."] + message_history[-2:]
|
|
171
|
+
if len(message_history) > 3
|
|
172
|
+
else message_history
|
|
173
|
+
):
|
|
174
|
+
print(msg)
|
|
175
|
+
print("-" * 10)
|
|
176
|
+
message = input("Ваше сообщение: ").strip()
|
|
177
|
+
if not message:
|
|
178
|
+
print("🚶 Пропускаем чат")
|
|
179
|
+
continue
|
|
117
180
|
|
|
118
181
|
if self.dry_run:
|
|
119
182
|
logger.info(
|
|
@@ -145,7 +208,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
145
208
|
def _get_negotiations(self) -> list[dict]:
|
|
146
209
|
rv = []
|
|
147
210
|
for page in range(self.max_pages):
|
|
148
|
-
res = self.api.get("/negotiations", page=page, status=
|
|
211
|
+
res = self.api.get("/negotiations", page=page, status="active")
|
|
149
212
|
rv.extend(res["items"])
|
|
150
213
|
if page >= res["pages"] - 1:
|
|
151
214
|
break
|
hh_applicant_tool/utils.py
CHANGED
|
@@ -85,7 +85,7 @@ def fix_datetime(dt: str | None) -> str | None:
|
|
|
85
85
|
|
|
86
86
|
def random_text(s: str) -> str:
|
|
87
87
|
while (
|
|
88
|
-
|
|
88
|
+
temp := re.sub(
|
|
89
89
|
r"{([^{}]+)}",
|
|
90
90
|
lambda m: random.choice(
|
|
91
91
|
m.group(1).split("|"),
|
|
@@ -93,7 +93,7 @@ def random_text(s: str) -> str:
|
|
|
93
93
|
s,
|
|
94
94
|
)
|
|
95
95
|
) != s:
|
|
96
|
-
s =
|
|
96
|
+
s = temp
|
|
97
97
|
return s
|
|
98
98
|
|
|
99
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
### Описание
|
|
34
34
|
|
|
35
|
-
Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие.
|
|
35
|
+
Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Для этого собираются данные о работодателях и их вакансиях (персональные данные пользователя не передаются ни в каком виде). Отправку данных на сервер разработчика можно отключить, но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги или после угроз судом удаляют отзывы. Единственное место где можно написать отзыв — это **Telegram**.
|
|
36
36
|
|
|
37
37
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
38
38
|
asdf/pyenv/conda и что-то еще.
|
|
@@ -41,7 +41,7 @@ asdf/pyenv/conda и что-то еще.
|
|
|
41
41
|
|
|
42
42
|
Пример работы:
|
|
43
43
|
|
|
44
|
-

|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
### Предыстория
|
|
@@ -15,13 +15,13 @@ hh_applicant_tool/operations/clear_negotiations.py,sha256=98Yuw9xP4dN5sUnUKlZxqf
|
|
|
15
15
|
hh_applicant_tool/operations/get_employer_contacts.py,sha256=7BEzEnNAp87RlOP6HX0LR6cbtud2FuKCKK5sq6fINq8,4066
|
|
16
16
|
hh_applicant_tool/operations/list_resumes.py,sha256=XBrVFTnl45BUtuvjVm70h_CXZrOvAULnLkLkyUh7gxw,1134
|
|
17
17
|
hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
|
|
18
|
-
hh_applicant_tool/operations/reply_employers.py,sha256=
|
|
18
|
+
hh_applicant_tool/operations/reply_employers.py,sha256=pcD1Uxmaixw7LSebDE2bUot67HRxbgghR9mvuW2MBUk,8660
|
|
19
19
|
hh_applicant_tool/operations/update_resumes.py,sha256=gGxMYMoT9GqJjwn4AgrOAEJCZu4sdhaV0VmPr3jG-q8,1049
|
|
20
20
|
hh_applicant_tool/operations/whoami.py,sha256=sg0r7m6oKkpMEmGt4QdtYdS-1gf-1KKdnk32yblbRJs,714
|
|
21
21
|
hh_applicant_tool/telemetry_client.py,sha256=wYLbKnx3sOmESFHqjLt-0Gww1O3lJiXFYdWnsorIhK8,3261
|
|
22
22
|
hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
|
|
23
|
-
hh_applicant_tool/utils.py,sha256=
|
|
24
|
-
hh_applicant_tool-0.5.
|
|
25
|
-
hh_applicant_tool-0.5.
|
|
26
|
-
hh_applicant_tool-0.5.
|
|
27
|
-
hh_applicant_tool-0.5.
|
|
23
|
+
hh_applicant_tool/utils.py,sha256=OsdjzUa76VC8vihHu27rD5df9DWqRzpvJr3PsdfJzNE,3071
|
|
24
|
+
hh_applicant_tool-0.5.1.dist-info/METADATA,sha256=-xswxfCHwrU43KuTRfJHFHLz4TRqRn3AqaCPXKmNrQk,19553
|
|
25
|
+
hh_applicant_tool-0.5.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
26
|
+
hh_applicant_tool-0.5.1.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
|
|
27
|
+
hh_applicant_tool-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|