hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.7__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 +23 -33
- hh_applicant_tool/api/client.py +50 -64
- hh_applicant_tool/api/errors.py +51 -7
- hh_applicant_tool/constants.py +0 -3
- hh_applicant_tool/datatypes.py +291 -0
- hh_applicant_tool/main.py +233 -111
- hh_applicant_tool/operations/apply_similar.py +266 -362
- hh_applicant_tool/operations/authorize.py +256 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_negotiations.py +102 -0
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +24 -10
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +120 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +148 -167
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +21 -10
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +4 -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/contact.py +16 -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 +119 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +176 -0
- hh_applicant_tool/storage/repositories/contacts.py +19 -0
- hh_applicant_tool/storage/repositories/employers.py +13 -0
- hh_applicant_tool/storage/repositories/negotiations.py +12 -0
- hh_applicant_tool/storage/repositories/resumes.py +14 -0
- hh_applicant_tool/storage/repositories/settings.py +34 -0
- hh_applicant_tool/storage/repositories/vacancies.py +8 -0
- hh_applicant_tool/storage/utils.py +49 -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/dateutil.py +19 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/jsonutil.py +61 -0
- hh_applicant_tool/utils/log.py +144 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +220 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +19 -0
- hh_applicant_tool/utils/user_agent.py +17 -0
- hh_applicant_tool-1.4.7.dist-info/METADATA +628 -0
- hh_applicant_tool-1.4.7.dist-info/RECORD +67 -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/clear_negotiations.py +0 -109
- 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.7.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.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
|
-
|
|
8
|
-
|
|
9
|
-
from ..
|
|
10
|
-
from ..
|
|
11
|
-
from ..main import
|
|
12
|
-
from ..
|
|
13
|
-
from ..utils import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .. import datatypes
|
|
9
|
+
from ..ai.base import AIError
|
|
10
|
+
from ..api import ApiError
|
|
11
|
+
from ..main import BaseNamespace, BaseOperation
|
|
12
|
+
from ..utils.dateutil 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,31 @@ 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
|
|
42
40
|
|
|
43
41
|
|
|
44
|
-
class Operation(BaseOperation
|
|
42
|
+
class Operation(BaseOperation):
|
|
45
43
|
"""Ответ всем работодателям."""
|
|
46
44
|
|
|
45
|
+
__aliases__ = ["reply-chats", "reply-all"]
|
|
46
|
+
|
|
47
47
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
48
|
-
# parser.add_argument(
|
|
49
|
-
# "reply_message",
|
|
50
|
-
# help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ. Если не передать, то его нужно будет вводить интерактивно.",
|
|
51
|
-
# )
|
|
52
48
|
parser.add_argument("--resume-id", help="Идентификатор резюме")
|
|
53
|
-
parser.add_argument(
|
|
54
|
-
"-i",
|
|
55
|
-
"--reply-interval",
|
|
56
|
-
help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
|
|
57
|
-
default="5-10",
|
|
58
|
-
type=parse_interval,
|
|
59
|
-
)
|
|
60
49
|
parser.add_argument(
|
|
61
50
|
"-m",
|
|
62
51
|
"--reply-message",
|
|
63
52
|
"--reply",
|
|
64
|
-
help="Отправить сообщение во все
|
|
53
|
+
help="Отправить сообщение во все чаты. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.", # noqa: E501
|
|
65
54
|
)
|
|
66
55
|
parser.add_argument(
|
|
67
56
|
"-p",
|
|
@@ -77,7 +66,6 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
77
66
|
default=False,
|
|
78
67
|
action=argparse.BooleanOptionalAction,
|
|
79
68
|
)
|
|
80
|
-
|
|
81
69
|
parser.add_argument(
|
|
82
70
|
"--dry-run",
|
|
83
71
|
"--dry",
|
|
@@ -85,39 +73,47 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
85
73
|
default=False,
|
|
86
74
|
action=argparse.BooleanOptionalAction,
|
|
87
75
|
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--use-ai",
|
|
78
|
+
"--ai",
|
|
79
|
+
help="Использовать AI для автоматической генерации ответов",
|
|
80
|
+
action=argparse.BooleanOptionalAction,
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--first-prompt",
|
|
84
|
+
help="Начальный промпт чата для AI",
|
|
85
|
+
default="Ты — соискатель на HeadHunter. Отвечай вежливо и кратко.",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--prompt",
|
|
89
|
+
help="Промпт для генерации сообщения",
|
|
90
|
+
default="Напиши короткий ответ работодателю на основе истории переписки.",
|
|
91
|
+
)
|
|
88
92
|
|
|
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` должен быть передан чеерез аргументы или настройки"
|
|
93
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
94
|
+
args: Namespace = tool.args
|
|
95
|
+
self.applicant_tool = tool
|
|
96
|
+
self.api_client = tool.api_client
|
|
97
|
+
self.resume_id = tool.first_resume_id()
|
|
98
|
+
self.reply_message = args.reply_message or tool.config.get(
|
|
99
|
+
"reply_message"
|
|
100
|
+
)
|
|
99
101
|
self.max_pages = args.max_pages
|
|
100
102
|
self.dry_run = args.dry_run
|
|
101
103
|
self.only_invitations = args.only_invitations
|
|
104
|
+
|
|
105
|
+
self.pre_prompt = args.prompt
|
|
106
|
+
self.openai_chat = (
|
|
107
|
+
tool.get_openai_chat(args.first_prompt) if args.use_ai else None
|
|
108
|
+
)
|
|
109
|
+
|
|
102
110
|
logger.debug(f"{self.reply_message = }")
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
def _get_blacklisted(self) -> list[str]:
|
|
106
|
-
rv = []
|
|
107
|
-
# В этом методе API страницы с 0 начинаются
|
|
108
|
-
for page in count(0):
|
|
109
|
-
r = self.api_client.get("/employers/blacklisted", page=page)
|
|
110
|
-
rv += [item["id"] for item in r["items"]]
|
|
111
|
-
if page + 1 >= r["pages"]:
|
|
112
|
-
break
|
|
113
|
-
return rv
|
|
114
|
-
|
|
115
|
-
def _reply_chats(self) -> None:
|
|
116
|
-
blacklisted = self._get_blacklisted()
|
|
117
|
-
logger.debug(f"{blacklisted = }")
|
|
118
|
-
me = self.me = self.api_client.get("/me")
|
|
111
|
+
self.reply_chats()
|
|
119
112
|
|
|
120
|
-
|
|
113
|
+
def reply_chats(self) -> None:
|
|
114
|
+
blacklisted = self.applicant_tool.get_blacklisted()
|
|
115
|
+
logger.debug(f"{blacklisted = }")
|
|
116
|
+
me: datatypes.User = self.applicant_tool.get_me()
|
|
121
117
|
|
|
122
118
|
basic_message_placeholders = {
|
|
123
119
|
"first_name": me.get("first_name", ""),
|
|
@@ -126,22 +122,20 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
126
122
|
"phone": me.get("phone", ""),
|
|
127
123
|
}
|
|
128
124
|
|
|
129
|
-
for negotiation in self.
|
|
125
|
+
for negotiation in self.applicant_tool.get_negotiations():
|
|
130
126
|
try:
|
|
131
|
-
|
|
127
|
+
self.applicant_tool.storage.negotiations.save(negotiation)
|
|
128
|
+
|
|
132
129
|
if self.resume_id != negotiation["resume"]["id"]:
|
|
133
130
|
continue
|
|
134
131
|
|
|
135
132
|
state_id = negotiation["state"]["id"]
|
|
136
|
-
|
|
137
|
-
# Пропускаем отказ
|
|
138
133
|
if state_id == "discard":
|
|
139
134
|
continue
|
|
140
135
|
|
|
141
136
|
if self.only_invitations and not state_id.startswith("inv"):
|
|
142
137
|
continue
|
|
143
138
|
|
|
144
|
-
logger.debug(negotiation)
|
|
145
139
|
nid = negotiation["id"]
|
|
146
140
|
vacancy = negotiation["vacancy"]
|
|
147
141
|
employer = vacancy.get("employer") or {}
|
|
@@ -169,156 +163,143 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
169
163
|
last_message: dict | None = None
|
|
170
164
|
message_history: list[str] = []
|
|
171
165
|
while True:
|
|
172
|
-
messages_res
|
|
166
|
+
messages_res: datatypes.PaginatedItems[
|
|
167
|
+
datatypes.Message
|
|
168
|
+
] = self.api_client.get(
|
|
173
169
|
f"/negotiations/{nid}/messages", page=page
|
|
174
170
|
)
|
|
171
|
+
if not messages_res["items"]:
|
|
172
|
+
break
|
|
175
173
|
|
|
176
174
|
last_message = messages_res["items"][-1]
|
|
177
|
-
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
for message in messages_res["items"]:
|
|
176
|
+
if not message.get("text"):
|
|
177
|
+
continue
|
|
178
|
+
author = (
|
|
179
|
+
"Работодатель"
|
|
180
|
+
if message["author"]["participant_type"]
|
|
181
|
+
== "employer"
|
|
182
|
+
else "Я"
|
|
182
183
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
message_date = parse_api_datetime(
|
|
185
|
+
message.get("created_at")
|
|
186
|
+
).isoformat()
|
|
187
|
+
message_history.append(
|
|
188
|
+
f"[ {message_date} ] {author}: {message['text']}"
|
|
189
|
+
)
|
|
190
|
+
|
|
188
191
|
if page + 1 >= messages_res["pages"]:
|
|
189
192
|
break
|
|
190
|
-
|
|
191
193
|
page = messages_res["pages"] - 1
|
|
192
194
|
|
|
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"]:
|
|
195
|
+
if not last_message:
|
|
217
196
|
continue
|
|
218
197
|
|
|
219
|
-
logger.debug(last_message)
|
|
220
|
-
|
|
221
198
|
is_employer_message = (
|
|
222
199
|
last_message["author"]["participant_type"] == "employer"
|
|
223
200
|
)
|
|
224
201
|
|
|
225
|
-
if is_employer_message or not negotiation.get(
|
|
202
|
+
if is_employer_message or not negotiation.get(
|
|
203
|
+
"viewed_by_opponent"
|
|
204
|
+
):
|
|
205
|
+
send_message = ""
|
|
226
206
|
if self.reply_message:
|
|
227
207
|
send_message = (
|
|
228
|
-
|
|
208
|
+
rand_text(self.reply_message) % message_placeholders
|
|
229
209
|
)
|
|
230
|
-
logger.debug(send_message)
|
|
210
|
+
logger.debug(f"Template message: {send_message}")
|
|
211
|
+
elif self.openai_chat:
|
|
212
|
+
try:
|
|
213
|
+
ai_query = (
|
|
214
|
+
f"Вакансия: {message_placeholders['vacancy_name']}\n"
|
|
215
|
+
f"История переписки:\n"
|
|
216
|
+
+ "\n".join(message_history[-10:])
|
|
217
|
+
+ f"\n\nИнструкция: {self.pre_prompt}"
|
|
218
|
+
)
|
|
219
|
+
send_message = self.openai_chat.send_message(
|
|
220
|
+
ai_query
|
|
221
|
+
)
|
|
222
|
+
logger.debug(f"AI message: {send_message}")
|
|
223
|
+
except AIError as ex:
|
|
224
|
+
logger.warning(
|
|
225
|
+
f"Ошибка OpenAI для чата {nid}: {ex}"
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
231
228
|
else:
|
|
232
|
-
print(
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
print(
|
|
230
|
+
"\n🏢",
|
|
231
|
+
message_placeholders["employer_name"],
|
|
232
|
+
"| 💼",
|
|
233
|
+
message_placeholders["vacancy_name"],
|
|
234
|
+
)
|
|
235
235
|
if salary:
|
|
236
|
-
salary_from = salary.get("from") or "-"
|
|
237
|
-
salary_to = salary.get("to") or "-"
|
|
238
|
-
salary_currency = salary.get("currency")
|
|
239
236
|
print(
|
|
240
|
-
"💵 от",
|
|
237
|
+
"💵 от",
|
|
238
|
+
salary.get("from") or salary.get("to") or 0,
|
|
239
|
+
"до",
|
|
240
|
+
salary.get("to") or salary.get("from") or 0,
|
|
241
|
+
salary.get("currency", "RUR"),
|
|
241
242
|
)
|
|
242
|
-
|
|
243
|
-
print("Последние
|
|
243
|
+
|
|
244
|
+
print("\nПоследние сообщения чата:")
|
|
245
|
+
print()
|
|
244
246
|
for msg in (
|
|
245
|
-
message_history[
|
|
247
|
+
message_history[-5:]
|
|
246
248
|
if len(message_history) > 5
|
|
247
249
|
else message_history
|
|
248
250
|
):
|
|
249
251
|
print(msg)
|
|
252
|
+
|
|
250
253
|
try:
|
|
251
254
|
print("-" * 10)
|
|
252
|
-
print()
|
|
253
255
|
print(
|
|
254
|
-
"
|
|
256
|
+
"Команды: /ban, /cancel <опционально сообщение для отмены>"
|
|
255
257
|
)
|
|
256
|
-
print("Заблокировать работодателя: /ban")
|
|
257
|
-
print()
|
|
258
258
|
send_message = input("Ваше сообщение: ").strip()
|
|
259
259
|
except EOFError:
|
|
260
260
|
continue
|
|
261
|
+
|
|
261
262
|
if not send_message:
|
|
262
263
|
print("🚶 Пропускаем чат")
|
|
263
264
|
continue
|
|
264
265
|
|
|
266
|
+
if send_message.startswith("/ban"):
|
|
267
|
+
self.applicant_tool.storage.employers.save(employer)
|
|
268
|
+
self.api_client.put(
|
|
269
|
+
f"/employers/blacklisted/{employer['id']}"
|
|
270
|
+
)
|
|
271
|
+
blacklisted.append(employer["id"])
|
|
272
|
+
print(
|
|
273
|
+
"🚫 Работодатель в ЧС",
|
|
274
|
+
employer.get("alternate_url"),
|
|
275
|
+
)
|
|
276
|
+
continue
|
|
277
|
+
elif send_message.startswith("/cancel"):
|
|
278
|
+
_, decline_msg = send_message.split("/cancel", 1)
|
|
279
|
+
self.api_client.delete(
|
|
280
|
+
f"/negotiations/active/{nid}",
|
|
281
|
+
with_decline_message=decline_msg.strip(),
|
|
282
|
+
)
|
|
283
|
+
print("❌ Отмена заявки", vacancy["alternate_url"])
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
# Финальная отправка текста
|
|
265
287
|
if self.dry_run:
|
|
266
|
-
logger.
|
|
267
|
-
"
|
|
288
|
+
logger.debug(
|
|
289
|
+
"dry-run: отклик на",
|
|
268
290
|
vacancy["alternate_url"],
|
|
269
291
|
send_message,
|
|
270
292
|
)
|
|
271
293
|
continue
|
|
272
294
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
295
|
+
self.api_client.post(
|
|
296
|
+
f"/negotiations/{nid}/messages",
|
|
297
|
+
message=send_message,
|
|
298
|
+
delay=random.uniform(1, 3),
|
|
278
299
|
)
|
|
300
|
+
print(f"📨 Отправлено для {vacancy['alternate_url']}")
|
|
279
301
|
|
|
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
302
|
except ApiError as ex:
|
|
304
303
|
logger.error(ex)
|
|
305
304
|
|
|
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
305
|
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 ..main import BaseNamespace, BaseOperation
|
|
11
|
+
from ..utils import jsonutil
|
|
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 jsonutil.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__")
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
# Этот модуль можно использовать как образец для других
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
2
4
|
import argparse
|
|
3
5
|
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..api import ApiError
|
|
9
|
+
from ..datatypes import PaginatedItems
|
|
10
|
+
from ..main import BaseNamespace, BaseOperation
|
|
11
|
+
from ..utils import print_err
|
|
12
|
+
from ..utils.string import shorten
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..main import HHApplicantTool
|
|
4
16
|
|
|
5
|
-
from ..api import ApiClient, ApiError
|
|
6
|
-
from ..main import BaseOperation
|
|
7
|
-
from ..main import Namespace as BaseNamespace
|
|
8
|
-
from ..types import ApiListResponse
|
|
9
|
-
from ..utils import print_err, truncate_string
|
|
10
17
|
|
|
11
18
|
logger = logging.getLogger(__package__)
|
|
12
19
|
|
|
@@ -18,15 +25,19 @@ class Namespace(BaseNamespace):
|
|
|
18
25
|
class Operation(BaseOperation):
|
|
19
26
|
"""Обновить все резюме"""
|
|
20
27
|
|
|
28
|
+
__aliases__ = ["update"]
|
|
29
|
+
|
|
21
30
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
31
|
pass
|
|
23
32
|
|
|
24
|
-
def run(self,
|
|
25
|
-
resumes:
|
|
33
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
34
|
+
resumes: PaginatedItems = tool.get_resumes()
|
|
26
35
|
for resume in resumes["items"]:
|
|
27
36
|
try:
|
|
28
|
-
res = api_client.post(
|
|
37
|
+
res = tool.api_client.post(
|
|
38
|
+
f"/resumes/{resume['id']}/publish",
|
|
39
|
+
)
|
|
29
40
|
assert res == {}
|
|
30
|
-
print("✅ Обновлено",
|
|
41
|
+
print("✅ Обновлено", shorten(resume["title"]))
|
|
31
42
|
except ApiError as ex:
|
|
32
|
-
print_err("❗
|
|
43
|
+
print_err("❗", ex)
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Этот модуль можно использовать как образец для других
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
2
4
|
import argparse
|
|
3
|
-
import json
|
|
4
5
|
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .. import datatypes
|
|
9
|
+
from ..main import BaseNamespace, BaseOperation
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..main import HHApplicantTool
|
|
5
13
|
|
|
6
|
-
from ..api import ApiClient
|
|
7
|
-
from ..main import BaseOperation
|
|
8
|
-
from ..main import Namespace as BaseNamespace
|
|
9
14
|
|
|
10
15
|
logger = logging.getLogger(__package__)
|
|
11
16
|
|
|
@@ -14,12 +19,40 @@ class Namespace(BaseNamespace):
|
|
|
14
19
|
pass
|
|
15
20
|
|
|
16
21
|
|
|
22
|
+
def fmt_plus(n: int) -> str:
|
|
23
|
+
assert n >= 0
|
|
24
|
+
return f"+{n}" if n else "0"
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
class Operation(BaseOperation):
|
|
18
28
|
"""Выведет текущего пользователя"""
|
|
19
29
|
|
|
30
|
+
__aliases__: list[str] = ["id"]
|
|
31
|
+
|
|
20
32
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
21
33
|
pass
|
|
22
34
|
|
|
23
|
-
def run(self,
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
36
|
+
api_client = tool.api_client
|
|
37
|
+
result: datatypes.User = api_client.get("me")
|
|
38
|
+
full_name = " ".join(
|
|
39
|
+
filter(
|
|
40
|
+
None,
|
|
41
|
+
[
|
|
42
|
+
result.get("last_name"),
|
|
43
|
+
result.get("first_name"),
|
|
44
|
+
result.get("middle_name"),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
with tool.storage.settings as s:
|
|
49
|
+
s.set_value("user.full_name", full_name)
|
|
50
|
+
s.set_value("user.email", result.get("email"))
|
|
51
|
+
s.set_value("user.phone", result.get("phone"))
|
|
52
|
+
counters = result["counters"]
|
|
53
|
+
print(
|
|
54
|
+
f"🆔 {result['id']} {full_name or 'Анонимный аккаунт'} "
|
|
55
|
+
f"[ 📄 {counters['resumes_count']} "
|
|
56
|
+
f"| 👁️ {fmt_plus(counters['new_resume_views'])} "
|
|
57
|
+
f"| ✉️ {fmt_plus(counters['unread_negotiations'])} ]"
|
|
58
|
+
)
|