hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.12__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.
- hh_applicant_tool/__init__.py +1 -0
- hh_applicant_tool/__main__.py +1 -1
- hh_applicant_tool/ai/base.py +2 -0
- hh_applicant_tool/ai/openai.py +25 -35
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +65 -68
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
- hh_applicant_tool/api/datatypes.py +293 -0
- hh_applicant_tool/api/errors.py +57 -7
- hh_applicant_tool/api/user_agent.py +17 -0
- hh_applicant_tool/main.py +234 -113
- hh_applicant_tool/operations/apply_similar.py +353 -371
- hh_applicant_tool/operations/authorize.py +313 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/clear_negotiations.py +90 -82
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +23 -11
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +122 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +201 -180
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +23 -11
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +8 -0
- hh_applicant_tool/storage/facade.py +24 -0
- hh_applicant_tool/storage/models/__init__.py +0 -0
- hh_applicant_tool/storage/models/base.py +169 -0
- hh_applicant_tool/storage/models/contacts.py +28 -0
- hh_applicant_tool/storage/models/employer.py +12 -0
- hh_applicant_tool/storage/models/negotiation.py +16 -0
- hh_applicant_tool/storage/models/resume.py +19 -0
- hh_applicant_tool/storage/models/setting.py +6 -0
- hh_applicant_tool/storage/models/vacancy.py +36 -0
- hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
- hh_applicant_tool/storage/queries/schema.sql +132 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +230 -0
- hh_applicant_tool/storage/repositories/contacts.py +14 -0
- hh_applicant_tool/storage/repositories/employers.py +14 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +13 -0
- hh_applicant_tool/storage/repositories/resumes.py +9 -0
- hh_applicant_tool/storage/repositories/settings.py +35 -0
- hh_applicant_tool/storage/repositories/vacancies.py +9 -0
- hh_applicant_tool/storage/utils.py +40 -0
- hh_applicant_tool/utils/__init__.py +31 -0
- hh_applicant_tool/utils/attrdict.py +6 -0
- hh_applicant_tool/utils/binpack.py +167 -0
- hh_applicant_tool/utils/config.py +55 -0
- hh_applicant_tool/utils/date.py +19 -0
- hh_applicant_tool/utils/json.py +61 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/log.py +147 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +221 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +32 -0
- hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
- hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
- hh_applicant_tool/ai/blackbox.py +0 -55
- hh_applicant_tool/color_log.py +0 -47
- hh_applicant_tool/mixins.py +0 -13
- hh_applicant_tool/operations/delete_telemetry.py +0 -30
- hh_applicant_tool/operations/get_employer_contacts.py +0 -348
- hh_applicant_tool/telemetry_client.py +0 -106
- hh_applicant_tool/types.py +0 -45
- hh_applicant_tool/utils.py +0 -119
- hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
- hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import argparse
|
|
4
4
|
import logging
|
|
5
5
|
import random
|
|
6
|
-
import
|
|
7
|
-
from typing import
|
|
8
|
-
|
|
9
|
-
from ..
|
|
10
|
-
from ..
|
|
11
|
-
from ..main import
|
|
12
|
-
from ..
|
|
13
|
-
from ..utils import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from ..ai.base import AIError
|
|
10
|
+
from ..api import ApiError, datatypes
|
|
11
|
+
from ..main import BaseNamespace, BaseOperation
|
|
12
|
+
from ..utils.date import parse_api_datetime
|
|
13
|
+
from ..utils.string import rand_text
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..main import HHApplicantTool
|
|
17
|
+
|
|
17
18
|
|
|
18
19
|
try:
|
|
19
20
|
import readline
|
|
@@ -25,43 +26,40 @@ except ImportError:
|
|
|
25
26
|
pass
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
GOOGLE_DOCS_RE = re.compile(
|
|
29
|
-
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",
|
|
30
|
-
re.I,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
29
|
logger = logging.getLogger(__package__)
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
class Namespace(BaseNamespace):
|
|
37
33
|
reply_message: str
|
|
38
|
-
reply_interval: Tuple[float, float]
|
|
39
34
|
max_pages: int
|
|
40
35
|
only_invitations: bool
|
|
41
36
|
dry_run: bool
|
|
37
|
+
use_ai: bool
|
|
38
|
+
first_prompt: str
|
|
39
|
+
prompt: str
|
|
40
|
+
period: int
|
|
42
41
|
|
|
43
42
|
|
|
44
|
-
class Operation(BaseOperation
|
|
43
|
+
class Operation(BaseOperation):
|
|
45
44
|
"""Ответ всем работодателям."""
|
|
46
45
|
|
|
46
|
+
__aliases__ = ["reply-empls", "reply-chats", "reall"]
|
|
47
|
+
|
|
47
48
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
48
|
-
# parser.add_argument(
|
|
49
|
-
# "reply_message",
|
|
50
|
-
# help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ. Если не передать, то его нужно будет вводить интерактивно.",
|
|
51
|
-
# )
|
|
52
|
-
parser.add_argument("--resume-id", help="Идентификатор резюме")
|
|
53
49
|
parser.add_argument(
|
|
54
|
-
"-
|
|
55
|
-
"
|
|
56
|
-
help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
|
|
57
|
-
default="5-10",
|
|
58
|
-
type=parse_interval,
|
|
50
|
+
"--resume-id",
|
|
51
|
+
help="Идентификатор резюме. Если не указан, то просматриваем чаты для всех резюме",
|
|
59
52
|
)
|
|
60
53
|
parser.add_argument(
|
|
61
54
|
"-m",
|
|
62
55
|
"--reply-message",
|
|
63
56
|
"--reply",
|
|
64
|
-
help="Отправить сообщение во все
|
|
57
|
+
help="Отправить сообщение во все чаты. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.", # noqa: E501
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--period",
|
|
61
|
+
type=int,
|
|
62
|
+
help="Игнорировать отклики, которые не обновлялись больше N дней",
|
|
65
63
|
)
|
|
66
64
|
parser.add_argument(
|
|
67
65
|
"-p",
|
|
@@ -77,7 +75,6 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
77
75
|
default=False,
|
|
78
76
|
action=argparse.BooleanOptionalAction,
|
|
79
77
|
)
|
|
80
|
-
|
|
81
78
|
parser.add_argument(
|
|
82
79
|
"--dry-run",
|
|
83
80
|
"--dry",
|
|
@@ -85,240 +82,264 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
85
82
|
default=False,
|
|
86
83
|
action=argparse.BooleanOptionalAction,
|
|
87
84
|
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--use-ai",
|
|
87
|
+
"--ai",
|
|
88
|
+
help="Использовать AI для автоматической генерации ответов",
|
|
89
|
+
action=argparse.BooleanOptionalAction,
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--first-prompt",
|
|
93
|
+
help="Начальный промпт чата для AI",
|
|
94
|
+
default="Ты — соискатель на HeadHunter. Отвечай вежливо и кратко.",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--prompt",
|
|
98
|
+
help="Промпт для генерации сообщения",
|
|
99
|
+
default="Напиши короткий ответ работодателю на основе истории переписки.",
|
|
100
|
+
)
|
|
88
101
|
|
|
89
|
-
def run(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
self.api_client = api_client
|
|
93
|
-
self.
|
|
94
|
-
self.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
self.reply_message = args.reply_message or args.config["reply_message"]
|
|
98
|
-
# assert self.reply_message, "`reply_message` должен быть передан чеерез аргументы или настройки"
|
|
102
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
103
|
+
args: Namespace = tool.args
|
|
104
|
+
self.tool = tool
|
|
105
|
+
self.api_client = tool.api_client
|
|
106
|
+
self.resume_id = tool.first_resume_id()
|
|
107
|
+
self.reply_message = args.reply_message or tool.config.get(
|
|
108
|
+
"reply_message"
|
|
109
|
+
)
|
|
99
110
|
self.max_pages = args.max_pages
|
|
100
111
|
self.dry_run = args.dry_run
|
|
101
112
|
self.only_invitations = args.only_invitations
|
|
113
|
+
|
|
114
|
+
self.pre_prompt = args.prompt
|
|
115
|
+
self.openai_chat = (
|
|
116
|
+
tool.get_openai_chat(args.first_prompt) if args.use_ai else None
|
|
117
|
+
)
|
|
118
|
+
self.period = args.period
|
|
119
|
+
|
|
102
120
|
logger.debug(f"{self.reply_message = }")
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
self.reply_employers()
|
|
122
|
+
|
|
123
|
+
def reply_employers(self):
|
|
124
|
+
blacklist = set(self.tool.get_blacklisted())
|
|
125
|
+
me: datatypes.User = self.tool.get_me()
|
|
126
|
+
resumes = self.tool.get_resumes()
|
|
127
|
+
resumes = (
|
|
128
|
+
list(filter(lambda x: x["id"] == self.resume_id, resumes))
|
|
129
|
+
if self.resume_id
|
|
130
|
+
else resumes
|
|
131
|
+
)
|
|
132
|
+
resumes = list(
|
|
133
|
+
filter(
|
|
134
|
+
lambda resume: resume["status"]["id"] == "published", resumes
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
self._reply_chats(user=me, resumes=resumes, blacklist=blacklist)
|
|
138
|
+
|
|
139
|
+
def _reply_chats(
|
|
140
|
+
self,
|
|
141
|
+
user: datatypes.User,
|
|
142
|
+
resumes: list[datatypes.Resume],
|
|
143
|
+
blacklist: set[str],
|
|
144
|
+
) -> None:
|
|
145
|
+
resume_map = {r["id"]: r for r in resumes}
|
|
146
|
+
|
|
147
|
+
base_placeholders = {
|
|
148
|
+
"first_name": user.get("first_name") or "",
|
|
149
|
+
"last_name": user.get("last_name") or "",
|
|
150
|
+
"email": user.get("email") or "",
|
|
151
|
+
"phone": user.get("phone") or "",
|
|
127
152
|
}
|
|
128
153
|
|
|
129
|
-
for negotiation in self.
|
|
154
|
+
for negotiation in self.tool.get_negotiations():
|
|
130
155
|
try:
|
|
131
|
-
#
|
|
132
|
-
|
|
156
|
+
# try:
|
|
157
|
+
# self.tool.storage.negotiations.save(negotiation)
|
|
158
|
+
# except RepositoryError as e:
|
|
159
|
+
# logger.exception(e)
|
|
160
|
+
|
|
161
|
+
if not (resume := resume_map.get(negotiation["resume"]["id"])):
|
|
133
162
|
continue
|
|
134
163
|
|
|
135
|
-
|
|
164
|
+
updated_at = parse_api_datetime(negotiation["updated_at"])
|
|
165
|
+
|
|
166
|
+
# Пропуск откликов, которые не обновлялись более N дней (при просмотре они обновляются вроде)
|
|
167
|
+
if (
|
|
168
|
+
self.period
|
|
169
|
+
and (datetime().now(updated_at.tzinfo) - updated_at).days
|
|
170
|
+
> self.period
|
|
171
|
+
):
|
|
172
|
+
continue
|
|
136
173
|
|
|
137
|
-
|
|
174
|
+
state_id = negotiation["state"]["id"]
|
|
138
175
|
if state_id == "discard":
|
|
139
176
|
continue
|
|
140
177
|
|
|
141
178
|
if self.only_invitations and not state_id.startswith("inv"):
|
|
142
179
|
continue
|
|
143
180
|
|
|
144
|
-
logger.debug(negotiation)
|
|
145
181
|
nid = negotiation["id"]
|
|
146
182
|
vacancy = negotiation["vacancy"]
|
|
147
183
|
employer = vacancy.get("employer") or {}
|
|
148
184
|
salary = vacancy.get("salary") or {}
|
|
149
185
|
|
|
150
|
-
if employer.get("id") in
|
|
186
|
+
if employer.get("id") in blacklist:
|
|
151
187
|
print(
|
|
152
188
|
"🚫 Пропускаем заблокированного работодателя",
|
|
153
189
|
employer.get("alternate_url"),
|
|
154
190
|
)
|
|
155
191
|
continue
|
|
156
192
|
|
|
157
|
-
|
|
193
|
+
placeholders = {
|
|
158
194
|
"vacancy_name": vacancy.get("name", ""),
|
|
159
195
|
"employer_name": employer.get("name", ""),
|
|
160
|
-
|
|
196
|
+
"resume_title": resume.get("title") or "",
|
|
197
|
+
**base_placeholders,
|
|
161
198
|
}
|
|
162
199
|
|
|
163
200
|
logger.debug(
|
|
164
201
|
"Вакансия %(vacancy_name)s от %(employer_name)s"
|
|
165
|
-
%
|
|
202
|
+
% placeholders
|
|
166
203
|
)
|
|
167
204
|
|
|
168
205
|
page: int = 0
|
|
169
|
-
last_message:
|
|
206
|
+
last_message: datatypes.Message | None = None
|
|
170
207
|
message_history: list[str] = []
|
|
171
208
|
while True:
|
|
172
|
-
messages_res
|
|
209
|
+
messages_res: datatypes.PaginatedItems[
|
|
210
|
+
datatypes.Message
|
|
211
|
+
] = self.api_client.get(
|
|
173
212
|
f"/negotiations/{nid}/messages", page=page
|
|
174
213
|
)
|
|
214
|
+
if not messages_res["items"]:
|
|
215
|
+
break
|
|
175
216
|
|
|
176
217
|
last_message = messages_res["items"][-1]
|
|
177
|
-
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
for message in messages_res["items"]:
|
|
219
|
+
if not message.get("text"):
|
|
220
|
+
continue
|
|
221
|
+
author = (
|
|
222
|
+
"Работодатель"
|
|
223
|
+
if message["author"]["participant_type"]
|
|
224
|
+
== "employer"
|
|
225
|
+
else "Я"
|
|
182
226
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
227
|
+
message_date = parse_api_datetime(
|
|
228
|
+
message.get("created_at")
|
|
229
|
+
).strftime("%d.%m.%Y %H:%M:%S")
|
|
230
|
+
|
|
231
|
+
message_history.append(
|
|
232
|
+
f"[ {message_date} ] {author}: {message['text']}"
|
|
233
|
+
)
|
|
234
|
+
|
|
188
235
|
if page + 1 >= messages_res["pages"]:
|
|
189
236
|
break
|
|
190
|
-
|
|
191
237
|
page = messages_res["pages"] - 1
|
|
192
238
|
|
|
193
|
-
if
|
|
194
|
-
# Собираем ссылки на тестовые задания
|
|
195
|
-
for message in message_history:
|
|
196
|
-
if message.startswith("-> "):
|
|
197
|
-
continue
|
|
198
|
-
# Тестовые задания и тп
|
|
199
|
-
for link in GOOGLE_DOCS_RE.findall(message):
|
|
200
|
-
document_data = {
|
|
201
|
-
"vacancy_url": vacancy.get("alternate_url"),
|
|
202
|
-
"vacancy_name": vacancy.get("name"),
|
|
203
|
-
"salary": (
|
|
204
|
-
f"{salary.get('from', '...')}-{salary.get('to', '...')} {salary.get('currency', 'RUR')}" # noqa: E501
|
|
205
|
-
if salary
|
|
206
|
-
else None
|
|
207
|
-
),
|
|
208
|
-
"employer_url": vacancy.get("employer", {}).get(
|
|
209
|
-
"alternate_url"
|
|
210
|
-
),
|
|
211
|
-
"link": link,
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
telemetry_data["links"].append(document_data)
|
|
215
|
-
|
|
216
|
-
if os.getenv("TEST_SEND_TELEMETRY") in ["1", "y", "Y"]:
|
|
239
|
+
if not last_message:
|
|
217
240
|
continue
|
|
218
241
|
|
|
219
|
-
logger.debug(last_message)
|
|
220
|
-
|
|
221
242
|
is_employer_message = (
|
|
222
243
|
last_message["author"]["participant_type"] == "employer"
|
|
223
244
|
)
|
|
224
245
|
|
|
225
|
-
if is_employer_message or not negotiation.get(
|
|
246
|
+
if is_employer_message or not negotiation.get(
|
|
247
|
+
"viewed_by_opponent"
|
|
248
|
+
):
|
|
249
|
+
send_message = ""
|
|
226
250
|
if self.reply_message:
|
|
227
251
|
send_message = (
|
|
228
|
-
|
|
252
|
+
rand_text(self.reply_message) % placeholders
|
|
229
253
|
)
|
|
230
|
-
logger.debug(send_message)
|
|
254
|
+
logger.debug(f"Template message: {send_message}")
|
|
255
|
+
elif self.openai_chat:
|
|
256
|
+
try:
|
|
257
|
+
ai_query = (
|
|
258
|
+
f"Вакансия: {placeholders['vacancy_name']}\n"
|
|
259
|
+
f"История переписки:\n"
|
|
260
|
+
+ "\n".join(message_history[-10:])
|
|
261
|
+
+ f"\n\nИнструкция: {self.pre_prompt}"
|
|
262
|
+
)
|
|
263
|
+
send_message = self.openai_chat.send_message(
|
|
264
|
+
ai_query
|
|
265
|
+
)
|
|
266
|
+
logger.debug(f"AI message: {send_message}")
|
|
267
|
+
except AIError as ex:
|
|
268
|
+
logger.warning(
|
|
269
|
+
f"Ошибка OpenAI для чата {nid}: {ex}"
|
|
270
|
+
)
|
|
271
|
+
continue
|
|
231
272
|
else:
|
|
232
|
-
print("🏢",
|
|
233
|
-
print("💼",
|
|
234
|
-
print("📅", vacancy["created_at"])
|
|
273
|
+
print("🏢", placeholders["employer_name"])
|
|
274
|
+
print("💼", placeholders["vacancy_name"])
|
|
235
275
|
if salary:
|
|
236
|
-
salary_from = salary.get("from") or "-"
|
|
237
|
-
salary_to = salary.get("to") or "-"
|
|
238
|
-
salary_currency = salary.get("currency")
|
|
239
276
|
print(
|
|
240
|
-
"💵 от",
|
|
277
|
+
"💵 от",
|
|
278
|
+
salary.get("from") or salary.get("to") or 0,
|
|
279
|
+
"до",
|
|
280
|
+
salary.get("to") or salary.get("from") or 0,
|
|
281
|
+
salary.get("currency", "RUR"),
|
|
241
282
|
)
|
|
242
|
-
|
|
243
|
-
print("Последние
|
|
283
|
+
|
|
284
|
+
print("\nПоследние сообщения чата:")
|
|
285
|
+
print()
|
|
244
286
|
for msg in (
|
|
245
|
-
message_history[
|
|
287
|
+
message_history[-5:]
|
|
246
288
|
if len(message_history) > 5
|
|
247
289
|
else message_history
|
|
248
290
|
):
|
|
249
291
|
print(msg)
|
|
292
|
+
|
|
250
293
|
try:
|
|
251
|
-
print("-" *
|
|
252
|
-
print()
|
|
294
|
+
print("-" * 40)
|
|
295
|
+
print("Активное резюме:", resume.get("title") or "")
|
|
253
296
|
print(
|
|
254
|
-
"
|
|
297
|
+
"/ban, /cancel необязательное сообщение для отмены"
|
|
255
298
|
)
|
|
256
|
-
print("Заблокировать работодателя: /ban")
|
|
257
|
-
print()
|
|
258
299
|
send_message = input("Ваше сообщение: ").strip()
|
|
259
300
|
except EOFError:
|
|
260
301
|
continue
|
|
302
|
+
|
|
261
303
|
if not send_message:
|
|
262
304
|
print("🚶 Пропускаем чат")
|
|
263
305
|
continue
|
|
264
306
|
|
|
307
|
+
if send_message.startswith("/ban"):
|
|
308
|
+
self.api_client.put(
|
|
309
|
+
f"/employers/blacklisted/{employer['id']}"
|
|
310
|
+
)
|
|
311
|
+
blacklist.add(employer["id"])
|
|
312
|
+
print(
|
|
313
|
+
"🚫 Работодатель заблокирован",
|
|
314
|
+
employer.get("alternate_url"),
|
|
315
|
+
)
|
|
316
|
+
continue
|
|
317
|
+
elif send_message.startswith("/cancel"):
|
|
318
|
+
_, decline_msg = send_message.split("/cancel", 1)
|
|
319
|
+
self.api_client.delete(
|
|
320
|
+
f"/negotiations/active/{nid}",
|
|
321
|
+
with_decline_message=decline_msg.strip(),
|
|
322
|
+
)
|
|
323
|
+
print("❌ Отмена заявки", vacancy["alternate_url"])
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
# Финальная отправка текста
|
|
265
327
|
if self.dry_run:
|
|
266
|
-
logger.
|
|
267
|
-
"
|
|
328
|
+
logger.debug(
|
|
329
|
+
"dry-run: отклик на",
|
|
268
330
|
vacancy["alternate_url"],
|
|
269
331
|
send_message,
|
|
270
332
|
)
|
|
271
333
|
continue
|
|
272
334
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
335
|
+
self.api_client.post(
|
|
336
|
+
f"/negotiations/{nid}/messages",
|
|
337
|
+
message=send_message,
|
|
338
|
+
delay=random.uniform(1, 3),
|
|
278
339
|
)
|
|
340
|
+
print(f"📨 Отправлено для {vacancy['alternate_url']}")
|
|
279
341
|
|
|
280
|
-
if send_message.startswith("/ban"):
|
|
281
|
-
self.api_client.put(f"/employers/blacklisted/{employer['id']}")
|
|
282
|
-
blacklisted.append(employer["id"])
|
|
283
|
-
print(
|
|
284
|
-
"🚫 Работодатель добавлен в черный список",
|
|
285
|
-
employer.get("alternate_url"),
|
|
286
|
-
)
|
|
287
|
-
elif send_message.startswith("/cancel"):
|
|
288
|
-
_, decline_allowed = send_message.split("/cancel", 1)
|
|
289
|
-
self.api_client.delete(
|
|
290
|
-
f"/negotiations/active/{negotiation['id']}",
|
|
291
|
-
with_decline_message=decline_allowed.strip(),
|
|
292
|
-
)
|
|
293
|
-
print("❌ Отменили заявку", vacancy["alternate_url"])
|
|
294
|
-
else:
|
|
295
|
-
self.api_client.post(
|
|
296
|
-
f"/negotiations/{nid}/messages",
|
|
297
|
-
message=send_message,
|
|
298
|
-
)
|
|
299
|
-
print(
|
|
300
|
-
"📨 Отправили сообщение для",
|
|
301
|
-
vacancy["alternate_url"],
|
|
302
|
-
)
|
|
303
342
|
except ApiError as ex:
|
|
304
343
|
logger.error(ex)
|
|
305
344
|
|
|
306
|
-
if self.enable_telemetry and len(telemetry_data["links"]) > 0:
|
|
307
|
-
logger.debug(telemetry_data)
|
|
308
|
-
try:
|
|
309
|
-
self.telemetry_client.send_telemetry("/docs", telemetry_data)
|
|
310
|
-
except TelemetryError as ex:
|
|
311
|
-
logger.warning(ex, exc_info=True)
|
|
312
|
-
|
|
313
345
|
print("📝 Сообщения разосланы!")
|
|
314
|
-
|
|
315
|
-
def _get_negotiations(self) -> list[dict]:
|
|
316
|
-
rv = []
|
|
317
|
-
for page in range(self.max_pages):
|
|
318
|
-
res = self.api_client.get("/negotiations", page=page, status="active")
|
|
319
|
-
rv.extend(res["items"])
|
|
320
|
-
if page >= res["pages"] - 1:
|
|
321
|
-
break
|
|
322
|
-
page += 1
|
|
323
|
-
|
|
324
|
-
return rv
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from prettytable import PrettyTable
|
|
9
|
+
|
|
10
|
+
from .. import utils
|
|
11
|
+
from ..main import BaseNamespace, BaseOperation
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..main import HHApplicantTool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MISSING = type("Missing", (), {"__str__": lambda self: "Не установлено"})()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__package__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Namespace(BaseNamespace):
|
|
24
|
+
key: str | None
|
|
25
|
+
value: str | None
|
|
26
|
+
delete: bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_value(v):
|
|
30
|
+
try:
|
|
31
|
+
return utils.json.loads(v)
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
return v
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Operation(BaseOperation):
|
|
37
|
+
"""Просмотр и управление настройками"""
|
|
38
|
+
|
|
39
|
+
__aliases__: list[str] = ["setting"]
|
|
40
|
+
|
|
41
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-d",
|
|
44
|
+
"--delete",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Удалить настройку по ключу либо удалить все насйтроки, если ключ не передан",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"key", nargs="?", help="Ключ настройки", default=MISSING
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"value",
|
|
53
|
+
nargs="?",
|
|
54
|
+
type=parse_value,
|
|
55
|
+
help="Значение настройки",
|
|
56
|
+
default=MISSING,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
60
|
+
args: Namespace = tool.args
|
|
61
|
+
settings = tool.storage.settings
|
|
62
|
+
|
|
63
|
+
if args.delete:
|
|
64
|
+
if args.key is not MISSING:
|
|
65
|
+
# Delete value
|
|
66
|
+
settings.delete_value(args.key)
|
|
67
|
+
print(f"🗑️ Настройка '{args.key}' удалена")
|
|
68
|
+
else:
|
|
69
|
+
settings.clear()
|
|
70
|
+
elif args.key is not MISSING and args.value is not MISSING:
|
|
71
|
+
settings.set_value(args.key, args.value)
|
|
72
|
+
print(f"✅ Установлено значение для '{args.key}'")
|
|
73
|
+
elif args.key is not MISSING:
|
|
74
|
+
# Get value
|
|
75
|
+
value = settings.get_value(args.key, MISSING)
|
|
76
|
+
if value is not MISSING:
|
|
77
|
+
# print(type(value).__name__, value)
|
|
78
|
+
print(value)
|
|
79
|
+
else:
|
|
80
|
+
print(f"⚠️ Настройка '{args.key}' не найдена")
|
|
81
|
+
else:
|
|
82
|
+
# List all settings
|
|
83
|
+
settings = settings.find()
|
|
84
|
+
t = PrettyTable(field_names=["Ключ", "Тип", "Значение"], align="l")
|
|
85
|
+
for setting in settings:
|
|
86
|
+
if setting.key.startswith("_"):
|
|
87
|
+
continue
|
|
88
|
+
t.add_row(
|
|
89
|
+
[
|
|
90
|
+
setting.key,
|
|
91
|
+
type(setting.value).__name__,
|
|
92
|
+
setting.value,
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
print(t)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from runpy import run_module
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from ..main import BaseOperation
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..main import HHApplicantTool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__package__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Operation(BaseOperation):
|
|
19
|
+
"""Удалит Chromium и другие зависимости"""
|
|
20
|
+
|
|
21
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
25
|
+
sys.argv = ["playwright", "uninstall", "chromium"]
|
|
26
|
+
run_module("playwright", run_name="__main__")
|