hh-applicant-tool 1.4.7__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/__main__.py +1 -1
- hh_applicant_tool/ai/openai.py +2 -2
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +23 -12
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -3
- hh_applicant_tool/{datatypes.py → api/datatypes.py} +2 -0
- hh_applicant_tool/api/errors.py +8 -2
- hh_applicant_tool/{utils → api}/user_agent.py +1 -1
- hh_applicant_tool/main.py +12 -13
- hh_applicant_tool/operations/apply_similar.py +125 -47
- hh_applicant_tool/operations/authorize.py +82 -25
- hh_applicant_tool/operations/call_api.py +3 -3
- hh_applicant_tool/operations/{check_negotiations.py → clear_negotiations.py} +40 -25
- hh_applicant_tool/operations/list_resumes.py +5 -7
- hh_applicant_tool/operations/query.py +3 -1
- hh_applicant_tool/operations/reply_employers.py +80 -40
- hh_applicant_tool/operations/settings.py +2 -2
- hh_applicant_tool/operations/update_resumes.py +5 -4
- hh_applicant_tool/operations/whoami.py +1 -1
- hh_applicant_tool/storage/__init__.py +5 -1
- hh_applicant_tool/storage/facade.py +2 -2
- hh_applicant_tool/storage/models/base.py +4 -4
- hh_applicant_tool/storage/models/contacts.py +28 -0
- hh_applicant_tool/storage/queries/schema.sql +22 -9
- hh_applicant_tool/storage/repositories/base.py +69 -15
- hh_applicant_tool/storage/repositories/contacts.py +5 -10
- hh_applicant_tool/storage/repositories/employers.py +1 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +1 -0
- hh_applicant_tool/storage/repositories/resumes.py +2 -7
- hh_applicant_tool/storage/repositories/settings.py +1 -0
- hh_applicant_tool/storage/repositories/vacancies.py +1 -0
- hh_applicant_tool/storage/utils.py +6 -15
- hh_applicant_tool/utils/__init__.py +3 -3
- hh_applicant_tool/utils/config.py +1 -1
- hh_applicant_tool/utils/log.py +4 -1
- hh_applicant_tool/utils/mixins.py +20 -19
- hh_applicant_tool/utils/terminal.py +13 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.4.12.dist-info}/METADATA +197 -140
- hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
- hh_applicant_tool/storage/models/contact.py +0 -16
- hh_applicant_tool-1.4.7.dist-info/RECORD +0 -67
- /hh_applicant_tool/utils/{dateutil.py → date.py} +0 -0
- /hh_applicant_tool/utils/{jsonutil.py → json.py} +0 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
hh_applicant_tool/__main__.py
CHANGED
hh_applicant_tool/ai/openai.py
CHANGED
|
@@ -24,7 +24,7 @@ class ChatOpenAI:
|
|
|
24
24
|
max_completion_tokens: int = 1000
|
|
25
25
|
session: requests.Session = field(default_factory=requests.Session)
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def _default_headers(self) -> dict[str, str]:
|
|
28
28
|
return {
|
|
29
29
|
"Authorization": f"Bearer {self.token}",
|
|
30
30
|
}
|
|
@@ -44,7 +44,7 @@ class ChatOpenAI:
|
|
|
44
44
|
response = self.session.post(
|
|
45
45
|
self.chat_endpoint,
|
|
46
46
|
json=payload,
|
|
47
|
-
headers=self.
|
|
47
|
+
headers=self._default_headers(),
|
|
48
48
|
timeout=30,
|
|
49
49
|
)
|
|
50
50
|
response.raise_for_status()
|
hh_applicant_tool/api/client.py
CHANGED
|
@@ -13,11 +13,18 @@ from urllib.parse import urlencode, urljoin
|
|
|
13
13
|
import requests
|
|
14
14
|
from requests import Session
|
|
15
15
|
|
|
16
|
-
from ..datatypes import AccessToken
|
|
17
16
|
from . import errors
|
|
17
|
+
from .client_keys import (
|
|
18
|
+
ANDROID_CLIENT_ID,
|
|
19
|
+
ANDROID_CLIENT_SECRET,
|
|
20
|
+
)
|
|
21
|
+
from .datatypes import AccessToken
|
|
22
|
+
from .user_agent import generate_android_useragent
|
|
18
23
|
|
|
19
24
|
__all__ = ("ApiClient", "OAuthClient")
|
|
20
25
|
|
|
26
|
+
HH_API_URL = "https://api.hh.ru/"
|
|
27
|
+
HH_OAUTH_URL = "https://hh.ru/oauth/"
|
|
21
28
|
DEFAULT_DELAY = 0.334
|
|
22
29
|
|
|
23
30
|
AllowedMethods = Literal["GET", "POST", "PUT", "DELETE"]
|
|
@@ -38,7 +45,7 @@ class BaseClient:
|
|
|
38
45
|
_previous_request_time: float = 0.0
|
|
39
46
|
|
|
40
47
|
def __post_init__(self) -> None:
|
|
41
|
-
assert self.base_url.endswith("/"), "base_url must
|
|
48
|
+
assert self.base_url.endswith("/"), "base_url must ends with /"
|
|
42
49
|
self.lock = Lock()
|
|
43
50
|
# logger.debug(f"user agent: {self.user_agent}")
|
|
44
51
|
if not self.session:
|
|
@@ -51,10 +58,9 @@ class BaseClient:
|
|
|
51
58
|
def proxies(self):
|
|
52
59
|
return self.session.proxies
|
|
53
60
|
|
|
54
|
-
def
|
|
61
|
+
def _default_headers(self) -> dict[str, str]:
|
|
55
62
|
return {
|
|
56
|
-
"user-agent": self.user_agent
|
|
57
|
-
or "Mozilla/5.0 (+https://github.com/s3rgeym/hh-applicant-tool)",
|
|
63
|
+
"user-agent": self.user_agent or generate_android_useragent(),
|
|
58
64
|
"x-hh-app-active": "true",
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -87,7 +93,7 @@ class BaseClient:
|
|
|
87
93
|
method,
|
|
88
94
|
url,
|
|
89
95
|
**payload,
|
|
90
|
-
headers=self.
|
|
96
|
+
headers=self._default_headers(),
|
|
91
97
|
allow_redirects=False,
|
|
92
98
|
)
|
|
93
99
|
try:
|
|
@@ -139,14 +145,19 @@ class BaseClient:
|
|
|
139
145
|
|
|
140
146
|
@dataclass
|
|
141
147
|
class OAuthClient(BaseClient):
|
|
142
|
-
client_id: str
|
|
143
|
-
client_secret: str
|
|
148
|
+
client_id: str | None = None
|
|
149
|
+
client_secret: str | None = None
|
|
144
150
|
_: dataclasses.KW_ONLY
|
|
145
|
-
base_url: str =
|
|
151
|
+
base_url: str = HH_OAUTH_URL
|
|
146
152
|
state: str = ""
|
|
147
153
|
scope: str = ""
|
|
148
154
|
redirect_uri: str = ""
|
|
149
155
|
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
super().__post_init__()
|
|
158
|
+
self.client_id = self.client_id or ANDROID_CLIENT_ID
|
|
159
|
+
self.client_secret = self.client_secret or ANDROID_CLIENT_SECRET
|
|
160
|
+
|
|
150
161
|
@property
|
|
151
162
|
def authorize_url(self) -> str:
|
|
152
163
|
params = dict(
|
|
@@ -197,7 +208,7 @@ class ApiClient(BaseClient):
|
|
|
197
208
|
_: dataclasses.KW_ONLY
|
|
198
209
|
client_id: str | None = None
|
|
199
210
|
client_secret: str | None = None
|
|
200
|
-
base_url: str =
|
|
211
|
+
base_url: str = HH_API_URL
|
|
201
212
|
|
|
202
213
|
@property
|
|
203
214
|
def is_access_expired(self) -> bool:
|
|
@@ -212,10 +223,10 @@ class ApiClient(BaseClient):
|
|
|
212
223
|
session=self.session,
|
|
213
224
|
)
|
|
214
225
|
|
|
215
|
-
def
|
|
226
|
+
def _default_headers(
|
|
216
227
|
self,
|
|
217
228
|
) -> dict[str, str]:
|
|
218
|
-
headers = super().
|
|
229
|
+
headers = super()._default_headers()
|
|
219
230
|
if not self.access_token:
|
|
220
231
|
return headers
|
|
221
232
|
# Это очень интересно, что access token'ы начинаются с USER, т.е. API может содержать какую-то уязвимость, связанную с этим
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
ANDROID_CLIENT_ID = (
|
|
2
|
+
"HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
|
|
3
|
+
)
|
|
4
4
|
|
|
5
5
|
ANDROID_CLIENT_SECRET = (
|
|
6
6
|
"V9M870DE342BGHFRUJ5FTCGCUA1482AN0DI8C5TFI9ULMA89H10N60NOP8I4JMVS"
|
hh_applicant_tool/api/errors.py
CHANGED
|
@@ -46,7 +46,11 @@ class ApiError(BadResponse):
|
|
|
46
46
|
|
|
47
47
|
@property
|
|
48
48
|
def message(self) -> str:
|
|
49
|
-
return
|
|
49
|
+
return (
|
|
50
|
+
self._data.get("error_description")
|
|
51
|
+
or self._data.get("description")
|
|
52
|
+
or str(self._data)
|
|
53
|
+
)
|
|
50
54
|
|
|
51
55
|
# def __getattr__(self, name: str) -> Any:
|
|
52
56
|
# try:
|
|
@@ -62,7 +66,9 @@ class ApiError(BadResponse):
|
|
|
62
66
|
return any(v.get("value") == value for v in data.get("errors", []))
|
|
63
67
|
|
|
64
68
|
@classmethod
|
|
65
|
-
def raise_for_status(
|
|
69
|
+
def raise_for_status(
|
|
70
|
+
cls: Type[ApiError], response: Response, data: dict
|
|
71
|
+
) -> None:
|
|
66
72
|
match response.status_code:
|
|
67
73
|
case status if 300 <= status <= 308:
|
|
68
74
|
raise Redirect(response, data)
|
hh_applicant_tool/main.py
CHANGED
|
@@ -17,9 +17,8 @@ from typing import Any, Iterable
|
|
|
17
17
|
import requests
|
|
18
18
|
import urllib3
|
|
19
19
|
|
|
20
|
-
from . import
|
|
21
|
-
from .api import ApiClient
|
|
22
|
-
from .constants import ANDROID_CLIENT_ID, ANDROID_CLIENT_SECRET
|
|
20
|
+
from . import utils
|
|
21
|
+
from .api import ApiClient, datatypes
|
|
23
22
|
from .storage import StorageFacade
|
|
24
23
|
from .utils.log import setup_logger
|
|
25
24
|
from .utils.mixins import MegaTool
|
|
@@ -205,13 +204,13 @@ class HHApplicantTool(MegaTool):
|
|
|
205
204
|
config = self.config
|
|
206
205
|
token = config.get("token", {})
|
|
207
206
|
api = ApiClient(
|
|
208
|
-
client_id=config.get("client_id"
|
|
209
|
-
client_secret=config.get("client_id"
|
|
207
|
+
client_id=config.get("client_id"),
|
|
208
|
+
client_secret=config.get("client_id"),
|
|
210
209
|
access_token=token.get("access_token"),
|
|
211
210
|
refresh_token=token.get("refresh_token"),
|
|
212
211
|
access_expires_at=token.get("access_expires_at"),
|
|
213
212
|
delay=args.delay,
|
|
214
|
-
user_agent=config
|
|
213
|
+
user_agent=config.get("user_agent"),
|
|
215
214
|
session=self.session,
|
|
216
215
|
)
|
|
217
216
|
return api
|
|
@@ -219,13 +218,12 @@ class HHApplicantTool(MegaTool):
|
|
|
219
218
|
def get_me(self) -> datatypes.User:
|
|
220
219
|
return self.api_client.get("/me")
|
|
221
220
|
|
|
222
|
-
def get_resumes(self) ->
|
|
223
|
-
return self.api_client.get("/resumes/mine")
|
|
221
|
+
def get_resumes(self) -> list[datatypes.Resume]:
|
|
222
|
+
return self.api_client.get("/resumes/mine")["items"]
|
|
224
223
|
|
|
225
|
-
def first_resume_id(self):
|
|
226
|
-
resumes = self.
|
|
227
|
-
|
|
228
|
-
return resumes["items"][0]["id"]
|
|
224
|
+
def first_resume_id(self) -> str:
|
|
225
|
+
resumes = self.get_resumes()
|
|
226
|
+
return resumes[0]["id"]
|
|
229
227
|
|
|
230
228
|
def get_blacklisted(self) -> list[str]:
|
|
231
229
|
rv = []
|
|
@@ -305,9 +303,10 @@ class HHApplicantTool(MegaTool):
|
|
|
305
303
|
return 2
|
|
306
304
|
finally:
|
|
307
305
|
try:
|
|
308
|
-
self.
|
|
306
|
+
self._check_system()
|
|
309
307
|
except Exception:
|
|
310
308
|
pass
|
|
309
|
+
# raise
|
|
311
310
|
|
|
312
311
|
def _parse_args(self, argv) -> None:
|
|
313
312
|
self._parser = self._create_parser()
|
|
@@ -4,14 +4,14 @@ import argparse
|
|
|
4
4
|
import logging
|
|
5
5
|
import random
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Iterator
|
|
7
|
+
from typing import TYPE_CHECKING, Iterator
|
|
8
8
|
|
|
9
|
-
from .. import datatypes
|
|
10
9
|
from ..ai.base import AIError
|
|
11
|
-
from ..api import BadResponse, Redirect
|
|
10
|
+
from ..api import BadResponse, Redirect, datatypes
|
|
11
|
+
from ..api.datatypes import PaginatedItems, SearchVacancy
|
|
12
12
|
from ..api.errors import ApiError, LimitExceeded
|
|
13
|
-
from ..datatypes import PaginatedItems, SearchVacancy
|
|
14
13
|
from ..main import BaseNamespace, BaseOperation
|
|
14
|
+
from ..storage.repositories.errors import RepositoryError
|
|
15
15
|
from ..utils import bool2str, list2str, rand_text, shorten
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
@@ -23,7 +23,7 @@ logger = logging.getLogger(__package__)
|
|
|
23
23
|
|
|
24
24
|
class Namespace(BaseNamespace):
|
|
25
25
|
resume_id: str | None
|
|
26
|
-
|
|
26
|
+
message_list_path: Path
|
|
27
27
|
ignore_employers: Path | None
|
|
28
28
|
force_message: bool
|
|
29
29
|
use_ai: bool
|
|
@@ -62,10 +62,9 @@ class Namespace(BaseNamespace):
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class Operation(BaseOperation):
|
|
65
|
-
"""Откликнуться на все подходящие вакансии.
|
|
65
|
+
"""Откликнуться на все подходящие вакансии."""
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
"""
|
|
67
|
+
__aliases__ = ("apply",)
|
|
69
68
|
|
|
70
69
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
71
70
|
parser.add_argument("--resume-id", help="Идентефикатор резюме")
|
|
@@ -76,9 +75,10 @@ class Operation(BaseOperation):
|
|
|
76
75
|
)
|
|
77
76
|
parser.add_argument(
|
|
78
77
|
"-L",
|
|
78
|
+
"--message-list-path",
|
|
79
79
|
"--message-list",
|
|
80
80
|
help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки.", # noqa: E501
|
|
81
|
-
type=
|
|
81
|
+
type=Path,
|
|
82
82
|
)
|
|
83
83
|
parser.add_argument(
|
|
84
84
|
"-f",
|
|
@@ -243,7 +243,7 @@ class Operation(BaseOperation):
|
|
|
243
243
|
self.api_client = tool.api_client
|
|
244
244
|
args: Namespace = tool.args
|
|
245
245
|
self.application_messages = self._get_application_messages(
|
|
246
|
-
args.
|
|
246
|
+
args.message_list_path
|
|
247
247
|
)
|
|
248
248
|
self.area = args.area
|
|
249
249
|
self.bottom_lat = args.bottom_lat
|
|
@@ -268,7 +268,7 @@ class Operation(BaseOperation):
|
|
|
268
268
|
self.pre_prompt = args.prompt
|
|
269
269
|
self.premium = args.premium
|
|
270
270
|
self.professional_role = args.professional_role
|
|
271
|
-
self.resume_id = args.resume_id
|
|
271
|
+
self.resume_id = args.resume_id
|
|
272
272
|
self.right_lng = args.right_lng
|
|
273
273
|
self.salary = args.salary
|
|
274
274
|
self.schedule = args.schedule
|
|
@@ -283,52 +283,96 @@ class Operation(BaseOperation):
|
|
|
283
283
|
)
|
|
284
284
|
self._apply_similar()
|
|
285
285
|
|
|
286
|
-
def _get_application_messages(
|
|
287
|
-
self, message_list: TextIO | None
|
|
288
|
-
) -> list[str]:
|
|
289
|
-
return (
|
|
290
|
-
list(filter(None, map(str.strip, message_list)))
|
|
291
|
-
if message_list
|
|
292
|
-
else [
|
|
293
|
-
"{Меня заинтересовала|Мне понравилась} ваша вакансия %(vacancy_name)s",
|
|
294
|
-
"{Прошу рассмотреть|Предлагаю рассмотреть} {мою кандидатуру|мое резюме} на вакансию %(vacancy_name)s", # noqa: E501
|
|
295
|
-
]
|
|
296
|
-
)
|
|
297
|
-
|
|
298
286
|
def _apply_similar(self) -> None:
|
|
287
|
+
resumes: list[datatypes.Resume] = self.tool.get_resumes()
|
|
288
|
+
try:
|
|
289
|
+
self.tool.storage.resumes.save_batch(resumes)
|
|
290
|
+
except RepositoryError as ex:
|
|
291
|
+
logger.exception(ex)
|
|
292
|
+
resumes = (
|
|
293
|
+
list(filter(lambda x: x["id"] == self.resume_id, resumes))
|
|
294
|
+
if self.resume_id
|
|
295
|
+
else resumes
|
|
296
|
+
)
|
|
297
|
+
# Выбираем только опубликованные
|
|
298
|
+
resumes = list(
|
|
299
|
+
filter(lambda x: x["status"]["id"] == "published", resumes)
|
|
300
|
+
)
|
|
301
|
+
if not resumes:
|
|
302
|
+
logger.warning("У вас нет опубликованных резюме")
|
|
303
|
+
return
|
|
304
|
+
|
|
299
305
|
me: datatypes.User = self.tool.get_me()
|
|
306
|
+
seen_employers = set()
|
|
300
307
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
for resume in resumes:
|
|
309
|
+
self._apply_resume(
|
|
310
|
+
resume=resume,
|
|
311
|
+
user=me,
|
|
312
|
+
seen_employers=seen_employers,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Синхронизация откликов
|
|
316
|
+
# for neg in self.tool.get_negotiations():
|
|
317
|
+
# try:
|
|
318
|
+
# self.tool.storage.negotiations.save(neg)
|
|
319
|
+
# except RepositoryError as e:
|
|
320
|
+
# logger.warning(e)
|
|
321
|
+
|
|
322
|
+
print("📝 Отклики на вакансии разосланы!")
|
|
323
|
+
|
|
324
|
+
def _apply_resume(
|
|
325
|
+
self,
|
|
326
|
+
resume: datatypes.Resume,
|
|
327
|
+
user: datatypes.User,
|
|
328
|
+
seen_employers: set[str],
|
|
329
|
+
) -> None:
|
|
330
|
+
logger.info("Начинаю рассылку откликов для резюме: %s", resume["title"])
|
|
331
|
+
|
|
332
|
+
placeholders = {
|
|
333
|
+
"first_name": user.get("first_name") or "",
|
|
334
|
+
"last_name": user.get("last_name") or "",
|
|
335
|
+
"email": user.get("email") or "",
|
|
336
|
+
"phone": user.get("phone") or "",
|
|
337
|
+
"resume_title": resume.get("title") or "",
|
|
306
338
|
}
|
|
307
339
|
|
|
308
|
-
|
|
309
|
-
for vacancy in self._get_vacancies():
|
|
340
|
+
for vacancy in self._get_similar_vacancies(resume_id=resume["id"]):
|
|
310
341
|
try:
|
|
311
342
|
employer = vacancy.get("employer", {})
|
|
312
343
|
|
|
313
|
-
|
|
344
|
+
message_placeholders = {
|
|
314
345
|
"vacancy_name": vacancy.get("name", ""),
|
|
315
346
|
"employer_name": employer.get("name", ""),
|
|
316
|
-
**
|
|
347
|
+
**placeholders,
|
|
317
348
|
}
|
|
318
349
|
|
|
319
350
|
storage = self.tool.storage
|
|
320
|
-
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
storage.vacancies.save(vacancy)
|
|
354
|
+
except RepositoryError as ex:
|
|
355
|
+
logger.debug(ex)
|
|
356
|
+
|
|
321
357
|
if employer := vacancy.get("employer"):
|
|
322
358
|
employer_id = employer.get("id")
|
|
323
359
|
if employer_id and employer_id not in seen_employers:
|
|
324
360
|
employer_profile: datatypes.Employer = (
|
|
325
361
|
self.api_client.get(f"/employers/{employer_id}")
|
|
326
362
|
)
|
|
327
|
-
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
storage.employers.save(employer_profile)
|
|
366
|
+
except RepositoryError as ex:
|
|
367
|
+
logger.exception(ex)
|
|
328
368
|
|
|
329
369
|
# По факту контакты можно получить только здесь?!
|
|
330
370
|
if vacancy.get("contacts"):
|
|
331
|
-
|
|
371
|
+
try:
|
|
372
|
+
# logger.debug(vacancy)
|
|
373
|
+
storage.vacancy_contacts.save(vacancy)
|
|
374
|
+
except RecursionError as ex:
|
|
375
|
+
logger.exception(ex)
|
|
332
376
|
|
|
333
377
|
if vacancy.get("has_test"):
|
|
334
378
|
logger.debug(
|
|
@@ -363,13 +407,20 @@ class Operation(BaseOperation):
|
|
|
363
407
|
)
|
|
364
408
|
if "got_rejection" in relations:
|
|
365
409
|
logger.debug(
|
|
366
|
-
"Вы получили
|
|
410
|
+
"Вы получили отказ от %s на резюме %s",
|
|
411
|
+
vacancy["alternate_url"],
|
|
412
|
+
resume["alternate_url"],
|
|
413
|
+
)
|
|
414
|
+
print(
|
|
415
|
+
"⛔ Пришел отказ от",
|
|
416
|
+
vacancy["alternate_url"],
|
|
417
|
+
"на резюме",
|
|
418
|
+
resume["alternate_url"],
|
|
367
419
|
)
|
|
368
|
-
print("⛔ Пришел отказ", vacancy["alternate_url"])
|
|
369
420
|
continue
|
|
370
421
|
|
|
371
422
|
params = {
|
|
372
|
-
"resume_id":
|
|
423
|
+
"resume_id": resume["id"],
|
|
373
424
|
"vacancy_id": vacancy_id,
|
|
374
425
|
"message": "",
|
|
375
426
|
}
|
|
@@ -379,13 +430,19 @@ class Operation(BaseOperation):
|
|
|
379
430
|
):
|
|
380
431
|
if self.openai_chat:
|
|
381
432
|
msg = self.pre_prompt + "\n\n"
|
|
382
|
-
msg +=
|
|
433
|
+
msg += (
|
|
434
|
+
"Название вакансии: "
|
|
435
|
+
+ message_placeholders["vacancy_name"]
|
|
436
|
+
)
|
|
437
|
+
msg += (
|
|
438
|
+
"Мое резюме:" + message_placeholders["resume_title"]
|
|
439
|
+
)
|
|
383
440
|
logger.debug("prompt: %s", msg)
|
|
384
441
|
msg = self.openai_chat.send_message(msg)
|
|
385
442
|
else:
|
|
386
443
|
msg = (
|
|
387
444
|
rand_text(random.choice(self.application_messages))
|
|
388
|
-
%
|
|
445
|
+
% message_placeholders
|
|
389
446
|
)
|
|
390
447
|
|
|
391
448
|
logger.debug(msg)
|
|
@@ -400,12 +457,18 @@ class Operation(BaseOperation):
|
|
|
400
457
|
)
|
|
401
458
|
assert res == {}
|
|
402
459
|
logger.debug(
|
|
403
|
-
"
|
|
460
|
+
"Откликнулись на %s с резюме %s",
|
|
461
|
+
vacancy["alternate_url"],
|
|
462
|
+
resume["alternate_url"],
|
|
404
463
|
)
|
|
405
464
|
print(
|
|
406
|
-
"📨 Отправили
|
|
465
|
+
"📨 Отправили отклик для резюме",
|
|
466
|
+
resume["alternate_url"],
|
|
467
|
+
"на вакансию",
|
|
407
468
|
vacancy["alternate_url"],
|
|
469
|
+
"(",
|
|
408
470
|
shorten(vacancy["name"]),
|
|
471
|
+
")",
|
|
409
472
|
)
|
|
410
473
|
except Redirect:
|
|
411
474
|
logger.warning(
|
|
@@ -414,15 +477,12 @@ class Operation(BaseOperation):
|
|
|
414
477
|
except LimitExceeded:
|
|
415
478
|
logger.info("Достигли лимита на отклики")
|
|
416
479
|
print("⚠️ Достигли лимита рассылки")
|
|
417
|
-
# self.tool.storage.settings.set_value("_")
|
|
418
480
|
break
|
|
419
481
|
except ApiError as ex:
|
|
420
482
|
logger.warning(ex)
|
|
421
483
|
except (BadResponse, AIError) as ex:
|
|
422
484
|
logger.error(ex)
|
|
423
485
|
|
|
424
|
-
print("📝 Отклики на вакансии разосланы!")
|
|
425
|
-
|
|
426
486
|
def _get_search_params(self, page: int) -> dict:
|
|
427
487
|
params = {
|
|
428
488
|
"page": page,
|
|
@@ -489,11 +549,11 @@ class Operation(BaseOperation):
|
|
|
489
549
|
|
|
490
550
|
return params
|
|
491
551
|
|
|
492
|
-
def
|
|
552
|
+
def _get_similar_vacancies(self, resume_id: str) -> Iterator[SearchVacancy]:
|
|
493
553
|
for page in range(self.total_pages):
|
|
494
554
|
params = self._get_search_params(page)
|
|
495
555
|
res: PaginatedItems[SearchVacancy] = self.api_client.get(
|
|
496
|
-
f"/resumes/{
|
|
556
|
+
f"/resumes/{resume_id}/similar_vacancies",
|
|
497
557
|
params,
|
|
498
558
|
)
|
|
499
559
|
if not res["items"]:
|
|
@@ -503,3 +563,21 @@ class Operation(BaseOperation):
|
|
|
503
563
|
|
|
504
564
|
if page >= res["pages"] - 1:
|
|
505
565
|
return
|
|
566
|
+
|
|
567
|
+
def _get_application_messages(self, path: Path | None) -> list[str]:
|
|
568
|
+
return (
|
|
569
|
+
list(
|
|
570
|
+
filter(
|
|
571
|
+
None,
|
|
572
|
+
map(
|
|
573
|
+
str.strip,
|
|
574
|
+
path.open(encoding="utf-8", errors="replace"),
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
if path
|
|
579
|
+
else [
|
|
580
|
+
"Здравствуйте, меня зовут %(first_name)s. {Меня заинтересовала|Мне понравилась} ваша вакансия «%(vacancy_name)s». Хотелось бы {пообщаться|задать вопросы} о ней.",
|
|
581
|
+
"{Прошу|Предлагаю} рассмотреть {мою кандидатуру|мое резюме «%(resume_title)s»} на вакансию «%(vacancy_name)s». С уважением, %(first_name)s.", # noqa: E501
|
|
582
|
+
]
|
|
583
|
+
)
|