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,28 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
4
|
import logging
|
|
3
5
|
import random
|
|
4
|
-
import time
|
|
5
|
-
from collections import defaultdict
|
|
6
|
-
from datetime import datetime, timedelta, timezone
|
|
7
6
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
9
|
-
|
|
10
|
-
from ..
|
|
11
|
-
from ..ai.
|
|
12
|
-
from ..api import
|
|
13
|
-
from ..api.errors import LimitExceeded
|
|
14
|
-
from ..
|
|
15
|
-
from ..main import
|
|
16
|
-
from ..
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from ..
|
|
20
|
-
|
|
21
|
-
parse_interval,
|
|
22
|
-
parse_invalid_datetime,
|
|
23
|
-
random_text,
|
|
24
|
-
truncate_string,
|
|
25
|
-
)
|
|
7
|
+
from typing import TYPE_CHECKING, Iterator, TextIO
|
|
8
|
+
|
|
9
|
+
from .. import datatypes
|
|
10
|
+
from ..ai.base import AIError
|
|
11
|
+
from ..api import BadResponse, Redirect
|
|
12
|
+
from ..api.errors import ApiError, LimitExceeded
|
|
13
|
+
from ..datatypes import PaginatedItems, SearchVacancy
|
|
14
|
+
from ..main import BaseNamespace, BaseOperation
|
|
15
|
+
from ..utils import bool2str, list2str, rand_text, shorten
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..main import HHApplicantTool
|
|
19
|
+
|
|
26
20
|
|
|
27
21
|
logger = logging.getLogger(__package__)
|
|
28
22
|
|
|
@@ -33,9 +27,8 @@ class Namespace(BaseNamespace):
|
|
|
33
27
|
ignore_employers: Path | None
|
|
34
28
|
force_message: bool
|
|
35
29
|
use_ai: bool
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
page_interval: tuple[float, float]
|
|
30
|
+
first_prompt: str
|
|
31
|
+
prompt: str
|
|
39
32
|
order_by: str
|
|
40
33
|
search: str
|
|
41
34
|
schedule: str
|
|
@@ -64,17 +57,11 @@ class Namespace(BaseNamespace):
|
|
|
64
57
|
sort_point_lng: float | None
|
|
65
58
|
no_magic: bool
|
|
66
59
|
premium: bool
|
|
60
|
+
per_page: int
|
|
61
|
+
total_pages: int
|
|
67
62
|
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
return str(v).lower()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _join_list(items: list[Any] | None) -> str:
|
|
74
|
-
return ",".join(f"{v}" for v in items) if items else ""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class Operation(BaseOperation, GetResumeIdMixin):
|
|
64
|
+
class Operation(BaseOperation):
|
|
78
65
|
"""Откликнуться на все подходящие вакансии.
|
|
79
66
|
|
|
80
67
|
Описание фильтров для поиска вакансий: <https://api.hh.ru/openapi/redoc#tag/Poisk-vakansij-dlya-soiskatelya/operation/get-vacancies-similar-to-resume>
|
|
@@ -82,52 +69,67 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
82
69
|
|
|
83
70
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
84
71
|
parser.add_argument("--resume-id", help="Идентефикатор резюме")
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--search",
|
|
74
|
+
help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500'", # noqa: E501
|
|
75
|
+
type=str,
|
|
76
|
+
)
|
|
85
77
|
parser.add_argument(
|
|
86
78
|
"-L",
|
|
87
79
|
"--message-list",
|
|
88
|
-
help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки.",
|
|
80
|
+
help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки.", # noqa: E501
|
|
89
81
|
type=argparse.FileType("r", encoding="utf-8", errors="replace"),
|
|
90
82
|
)
|
|
91
|
-
parser.add_argument(
|
|
92
|
-
"--ignore-employers",
|
|
93
|
-
help="Путь к файлу со списком ID игнорируемых работодателей (по одному ID на строку)",
|
|
94
|
-
type=Path,
|
|
95
|
-
default=None,
|
|
96
|
-
)
|
|
97
83
|
parser.add_argument(
|
|
98
84
|
"-f",
|
|
99
85
|
"--force-message",
|
|
100
86
|
"--force",
|
|
101
87
|
help="Всегда отправлять сообщение при отклике",
|
|
102
|
-
default=False,
|
|
103
88
|
action=argparse.BooleanOptionalAction,
|
|
104
89
|
)
|
|
105
90
|
parser.add_argument(
|
|
106
91
|
"--use-ai",
|
|
107
92
|
"--ai",
|
|
108
93
|
help="Использовать AI для генерации сообщений",
|
|
109
|
-
default=False,
|
|
110
94
|
action=argparse.BooleanOptionalAction,
|
|
111
95
|
)
|
|
112
96
|
parser.add_argument(
|
|
113
|
-
"--
|
|
97
|
+
"--first-prompt",
|
|
98
|
+
help="Начальный помпт чата для генерации сопроводительного письма",
|
|
99
|
+
default="Напиши сопроводительное письмо для отклика на эту вакансию. Не используй placeholder'ы, твой ответ будет отправлен без обработки.", # noqa: E501
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
114
102
|
"--prompt",
|
|
115
|
-
help="
|
|
116
|
-
default="Сгенерируй сопроводительное письмо не более 5-7 предложений от моего имени для вакансии",
|
|
103
|
+
help="Промпт для генерации сопроводительного письма",
|
|
104
|
+
default="Сгенерируй сопроводительное письмо не более 5-7 предложений от моего имени для вакансии", # noqa: E501
|
|
117
105
|
)
|
|
118
106
|
parser.add_argument(
|
|
119
|
-
"--
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
"--total-pages",
|
|
108
|
+
"--pages",
|
|
109
|
+
help="Количество обрабатываемых страниц поиска", # noqa: E501
|
|
110
|
+
default=20,
|
|
111
|
+
type=int,
|
|
123
112
|
)
|
|
124
113
|
parser.add_argument(
|
|
125
|
-
"--page
|
|
126
|
-
help="
|
|
127
|
-
default=
|
|
128
|
-
type=
|
|
114
|
+
"--per-page",
|
|
115
|
+
help="Сколько должно быть результатов на странице", # noqa: E501
|
|
116
|
+
default=100,
|
|
117
|
+
type=int,
|
|
129
118
|
)
|
|
130
119
|
parser.add_argument(
|
|
120
|
+
"--dry-run",
|
|
121
|
+
help="Не отправлять отклики, а только выводить информацию",
|
|
122
|
+
action=argparse.BooleanOptionalAction,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Дальше идут параметры в точности соответствующие параметрам запроса
|
|
126
|
+
# при поиске подходящих вакансий
|
|
127
|
+
search_params_group = parser.add_argument_group(
|
|
128
|
+
"Параметры поиска вакансий",
|
|
129
|
+
"Эти параметры напрямую соответствуют фильтрам поиска HeadHunter API",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
search_params_group.add_argument(
|
|
131
133
|
"--order-by",
|
|
132
134
|
help="Сортировка вакансий",
|
|
133
135
|
choices=[
|
|
@@ -137,246 +139,196 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
137
139
|
"relevance",
|
|
138
140
|
"distance",
|
|
139
141
|
],
|
|
140
|
-
default="relevance",
|
|
142
|
+
# default="relevance",
|
|
141
143
|
)
|
|
142
|
-
|
|
143
|
-
"--
|
|
144
|
-
help="
|
|
144
|
+
search_params_group.add_argument(
|
|
145
|
+
"--experience",
|
|
146
|
+
help="Уровень опыта работы (noExperience, between1And3, between3And6, moreThan6)",
|
|
145
147
|
type=str,
|
|
146
148
|
default=None,
|
|
147
149
|
)
|
|
148
|
-
|
|
149
|
-
parser.add_argument(
|
|
150
|
+
search_params_group.add_argument(
|
|
150
151
|
"--schedule",
|
|
151
|
-
help="Тип
|
|
152
|
+
help="Тип графика (fullDay, shift, flexible, remote, flyInFlyOut)",
|
|
152
153
|
type=str,
|
|
153
|
-
default=None,
|
|
154
154
|
)
|
|
155
|
-
|
|
156
|
-
"--
|
|
157
|
-
|
|
155
|
+
search_params_group.add_argument(
|
|
156
|
+
"--employment", nargs="+", help="Тип занятости"
|
|
157
|
+
)
|
|
158
|
+
search_params_group.add_argument(
|
|
159
|
+
"--area", nargs="+", help="Регион (area id)"
|
|
160
|
+
)
|
|
161
|
+
search_params_group.add_argument(
|
|
162
|
+
"--metro", nargs="+", help="Станции метро (metro id)"
|
|
163
|
+
)
|
|
164
|
+
search_params_group.add_argument(
|
|
165
|
+
"--professional-role", nargs="+", help="Проф. роль (id)"
|
|
166
|
+
)
|
|
167
|
+
search_params_group.add_argument(
|
|
168
|
+
"--industry", nargs="+", help="Индустрия (industry id)"
|
|
169
|
+
)
|
|
170
|
+
search_params_group.add_argument(
|
|
171
|
+
"--employer-id", nargs="+", help="ID работодателей"
|
|
172
|
+
)
|
|
173
|
+
search_params_group.add_argument(
|
|
174
|
+
"--excluded-employer-id", nargs="+", help="Исключить работодателей"
|
|
175
|
+
)
|
|
176
|
+
search_params_group.add_argument(
|
|
177
|
+
"--currency", help="Код валюты (RUR, USD, EUR)"
|
|
178
|
+
)
|
|
179
|
+
search_params_group.add_argument(
|
|
180
|
+
"--salary", type=int, help="Минимальная зарплата"
|
|
181
|
+
)
|
|
182
|
+
search_params_group.add_argument(
|
|
183
|
+
"--only-with-salary",
|
|
158
184
|
default=False,
|
|
159
185
|
action=argparse.BooleanOptionalAction,
|
|
160
186
|
)
|
|
161
|
-
|
|
162
|
-
"--
|
|
163
|
-
help="Уровень опыта работы в вакансии. Возможные значения: noExperience, between1And3, between3And6, moreThan6",
|
|
164
|
-
type=str,
|
|
165
|
-
default=None,
|
|
187
|
+
search_params_group.add_argument(
|
|
188
|
+
"--label", nargs="+", help="Метки вакансий (label)"
|
|
166
189
|
)
|
|
167
|
-
|
|
168
|
-
"--
|
|
190
|
+
search_params_group.add_argument(
|
|
191
|
+
"--period", type=int, help="Искать вакансии за N дней"
|
|
169
192
|
)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
parser.add_argument("--professional-role", nargs="+", help="Проф. роль (id)")
|
|
173
|
-
parser.add_argument("--industry", nargs="+", help="Индустрия (industry id)")
|
|
174
|
-
parser.add_argument("--employer-id", nargs="+", help="ID работодателей")
|
|
175
|
-
parser.add_argument(
|
|
176
|
-
"--excluded-employer-id", nargs="+", help="Исключить работодателей"
|
|
193
|
+
search_params_group.add_argument(
|
|
194
|
+
"--date-from", help="Дата публикации с (YYYY-MM-DD)"
|
|
177
195
|
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
search_params_group.add_argument(
|
|
197
|
+
"--date-to", help="Дата публикации по (YYYY-MM-DD)"
|
|
198
|
+
)
|
|
199
|
+
search_params_group.add_argument(
|
|
200
|
+
"--top-lat", type=float, help="Гео: верхняя широта"
|
|
201
|
+
)
|
|
202
|
+
search_params_group.add_argument(
|
|
203
|
+
"--bottom-lat", type=float, help="Гео: нижняя широта"
|
|
204
|
+
)
|
|
205
|
+
search_params_group.add_argument(
|
|
206
|
+
"--left-lng", type=float, help="Гео: левая долгота"
|
|
207
|
+
)
|
|
208
|
+
search_params_group.add_argument(
|
|
209
|
+
"--right-lng", type=float, help="Гео: правая долгота"
|
|
210
|
+
)
|
|
211
|
+
search_params_group.add_argument(
|
|
192
212
|
"--sort-point-lat",
|
|
193
213
|
type=float,
|
|
194
214
|
help="Координата lat для сортировки по расстоянию",
|
|
195
215
|
)
|
|
196
|
-
|
|
216
|
+
search_params_group.add_argument(
|
|
197
217
|
"--sort-point-lng",
|
|
198
218
|
type=float,
|
|
199
219
|
help="Координата lng для сортировки по расстоянию",
|
|
200
220
|
)
|
|
201
|
-
|
|
221
|
+
search_params_group.add_argument(
|
|
202
222
|
"--no-magic",
|
|
203
223
|
action="store_true",
|
|
204
224
|
help="Отключить авторазбор текста запроса",
|
|
205
225
|
)
|
|
206
|
-
|
|
226
|
+
search_params_group.add_argument(
|
|
207
227
|
"--premium",
|
|
208
228
|
default=False,
|
|
209
229
|
action=argparse.BooleanOptionalAction,
|
|
210
230
|
help="Только премиум вакансии",
|
|
211
231
|
)
|
|
212
|
-
|
|
213
|
-
"--search-field",
|
|
232
|
+
search_params_group.add_argument(
|
|
233
|
+
"--search-field",
|
|
234
|
+
nargs="+",
|
|
235
|
+
help="Поля поиска (name, company_name и т.п.)",
|
|
214
236
|
)
|
|
215
|
-
parser.add_argument(
|
|
216
|
-
"--clusters",
|
|
217
|
-
action=argparse.BooleanOptionalAction,
|
|
218
|
-
help="Включить кластеры (по умолчанию None)",
|
|
219
|
-
)
|
|
220
|
-
# parser.add_argument("--describe-arguments", action=argparse.BooleanOptionalAction, help="Вернуть описание параметров запроса")
|
|
221
237
|
|
|
222
238
|
def run(
|
|
223
|
-
self,
|
|
239
|
+
self,
|
|
240
|
+
tool: HHApplicantTool,
|
|
224
241
|
) -> None:
|
|
225
|
-
self.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# input("Вы действительно хотите отключить телеметрию (д/Н)? ")
|
|
232
|
-
# .lower()
|
|
233
|
-
# .startswith(("д", "y"))
|
|
234
|
-
# ):
|
|
235
|
-
# self.enable_telemetry = False
|
|
236
|
-
# logger.info("Телеметрия отключена.")
|
|
237
|
-
# else:
|
|
238
|
-
# logger.info("Спасибо за то что оставили телеметрию включенной!")
|
|
239
|
-
self.enable_telemetry = False
|
|
240
|
-
|
|
241
|
-
self.api_client = api_client
|
|
242
|
-
self.telemetry_client = telemetry_client
|
|
243
|
-
self.resume_id = args.resume_id or self._get_resume_id()
|
|
244
|
-
self.application_messages = self._get_application_messages(args.message_list)
|
|
245
|
-
self.ignored_employers = self._get_ignored_employers(args.ignore_employers)
|
|
246
|
-
self.chat = None
|
|
247
|
-
|
|
248
|
-
if config := args.config.get("blackbox"):
|
|
249
|
-
self.chat = BlackboxChat(
|
|
250
|
-
session_id=config["session_id"],
|
|
251
|
-
chat_payload=config["chat_payload"],
|
|
252
|
-
proxies=self.api_client.proxies or {},
|
|
253
|
-
)
|
|
254
|
-
elif config := args.config.get("openai"):
|
|
255
|
-
model = "gpt-5.1"
|
|
256
|
-
system_prompt = "Напиши сопроводительное письмо для отклика на эту вакансию. Не используй placeholder'ы, твой ответ будет отправлен без обработки."
|
|
257
|
-
if "model" in config.keys():
|
|
258
|
-
model = config["model"]
|
|
259
|
-
if "system_prompt" in config.keys():
|
|
260
|
-
system_prompt = config["system_prompt"]
|
|
261
|
-
self.chat = OpenAIChat(
|
|
262
|
-
token=config["token"],
|
|
263
|
-
model=model,
|
|
264
|
-
system_prompt=system_prompt,
|
|
265
|
-
proxies=self.api_client.proxies or {},
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
self.pre_prompt = args.pre_prompt
|
|
269
|
-
|
|
270
|
-
self.apply_min_interval, self.apply_max_interval = args.apply_interval
|
|
271
|
-
self.page_min_interval, self.page_max_interval = args.page_interval
|
|
272
|
-
|
|
273
|
-
self.force_message = args.force_message
|
|
274
|
-
self.order_by = args.order_by
|
|
275
|
-
self.search = args.search
|
|
276
|
-
self.schedule = args.schedule
|
|
277
|
-
self.dry_run = args.dry_run
|
|
278
|
-
self.experience = args.experience
|
|
279
|
-
self.search_field = args.search_field
|
|
280
|
-
self.employment = args.employment
|
|
242
|
+
self.tool = tool
|
|
243
|
+
self.api_client = tool.api_client
|
|
244
|
+
args: Namespace = tool.args
|
|
245
|
+
self.application_messages = self._get_application_messages(
|
|
246
|
+
args.message_list
|
|
247
|
+
)
|
|
281
248
|
self.area = args.area
|
|
282
|
-
self.
|
|
283
|
-
self.professional_role = args.professional_role
|
|
284
|
-
self.industry = args.industry
|
|
285
|
-
self.employer_id = args.employer_id
|
|
286
|
-
self.excluded_employer_id = args.excluded_employer_id
|
|
249
|
+
self.bottom_lat = args.bottom_lat
|
|
287
250
|
self.currency = args.currency
|
|
288
|
-
self.salary = args.salary
|
|
289
|
-
self.only_with_salary = args.only_with_salary
|
|
290
|
-
self.label = args.label
|
|
291
|
-
self.period = args.period
|
|
292
251
|
self.date_from = args.date_from
|
|
293
252
|
self.date_to = args.date_to
|
|
294
|
-
self.
|
|
295
|
-
self.
|
|
253
|
+
self.dry_run = args.dry_run
|
|
254
|
+
self.employer_id = args.employer_id
|
|
255
|
+
self.employment = args.employment
|
|
256
|
+
self.excluded_employer_id = args.excluded_employer_id
|
|
257
|
+
self.experience = args.experience
|
|
258
|
+
self.force_message = args.force_message
|
|
259
|
+
self.industry = args.industry
|
|
260
|
+
self.label = args.label
|
|
296
261
|
self.left_lng = args.left_lng
|
|
262
|
+
self.metro = args.metro
|
|
263
|
+
self.no_magic = args.no_magic
|
|
264
|
+
self.only_with_salary = args.only_with_salary
|
|
265
|
+
self.order_by = args.order_by
|
|
266
|
+
self.per_page = args.per_page
|
|
267
|
+
self.period = args.period
|
|
268
|
+
self.pre_prompt = args.prompt
|
|
269
|
+
self.premium = args.premium
|
|
270
|
+
self.professional_role = args.professional_role
|
|
271
|
+
self.resume_id = args.resume_id or tool.first_resume_id()
|
|
297
272
|
self.right_lng = args.right_lng
|
|
273
|
+
self.salary = args.salary
|
|
274
|
+
self.schedule = args.schedule
|
|
275
|
+
self.search = args.search
|
|
276
|
+
self.search_field = args.search_field
|
|
298
277
|
self.sort_point_lat = args.sort_point_lat
|
|
299
278
|
self.sort_point_lng = args.sort_point_lng
|
|
300
|
-
self.
|
|
301
|
-
|
|
302
|
-
self.
|
|
303
|
-
|
|
279
|
+
self.top_lat = args.top_lat
|
|
280
|
+
self.total_pages = args.total_pages
|
|
281
|
+
self.openai_chat = (
|
|
282
|
+
tool.get_openai_chat(args.first_prompt) if args.use_ai else None
|
|
283
|
+
)
|
|
304
284
|
self._apply_similar()
|
|
305
285
|
|
|
306
|
-
def _get_application_messages(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 [
|
|
311
293
|
"{Меня заинтересовала|Мне понравилась} ваша вакансия %(vacancy_name)s",
|
|
312
|
-
"{Прошу рассмотреть|Предлагаю рассмотреть} {мою кандидатуру|мое резюме} на вакансию %(vacancy_name)s",
|
|
294
|
+
"{Прошу рассмотреть|Предлагаю рассмотреть} {мою кандидатуру|мое резюме} на вакансию %(vacancy_name)s", # noqa: E501
|
|
313
295
|
]
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def _get_ignored_employers(self, file_path: Path | None) -> set[str]:
|
|
317
|
-
ignored = set()
|
|
318
|
-
if file_path is not None:
|
|
319
|
-
with file_path.open("r", encoding="utf-8") as f:
|
|
320
|
-
for line in f:
|
|
321
|
-
if clean_id := line.strip():
|
|
322
|
-
ignored.add(int(clean_id))
|
|
323
|
-
logger.info("Загружено %d ID игнорируемых работодателей", len(ignored))
|
|
324
|
-
return ignored
|
|
296
|
+
)
|
|
325
297
|
|
|
326
298
|
def _apply_similar(self) -> None:
|
|
327
|
-
|
|
328
|
-
telemetry_data = defaultdict(dict)
|
|
329
|
-
|
|
330
|
-
vacancies = self._get_vacancies()
|
|
331
|
-
|
|
332
|
-
if self.enable_telemetry:
|
|
333
|
-
for vacancy in vacancies:
|
|
334
|
-
vacancy_id = vacancy["id"]
|
|
335
|
-
telemetry_data["vacancies"][vacancy_id] = {
|
|
336
|
-
"name": vacancy.get("name"),
|
|
337
|
-
"type": vacancy.get("type", {}).get("id"), # open/closed
|
|
338
|
-
"area": vacancy.get("area", {}).get("name"), # город
|
|
339
|
-
"salary": vacancy.get("salary"), # from, to, currency, gross
|
|
340
|
-
"direct_url": vacancy.get("alternate_url"), # ссылка на вакансию
|
|
341
|
-
"created_at": fix_datetime(
|
|
342
|
-
vacancy.get("created_at")
|
|
343
|
-
), # будем вычислять говно-вакансии, которые по полгода висят
|
|
344
|
-
"published_at": fix_datetime(vacancy.get("published_at")),
|
|
345
|
-
"contacts": vacancy.get(
|
|
346
|
-
"contacts"
|
|
347
|
-
), # пиздорванки там телеграм для связи указывают
|
|
348
|
-
# HH с точки зрения перфикциониста — кусок говна, где кривые
|
|
349
|
-
# форматы даты, у вакансий может не быть работодателя...
|
|
350
|
-
"employer_id": int(vacancy["employer"]["id"])
|
|
351
|
-
if "employer" in vacancy and "id" in vacancy["employer"]
|
|
352
|
-
else None,
|
|
353
|
-
# "relations": vacancy.get("relations", []),
|
|
354
|
-
# Остальное неинтересно
|
|
355
|
-
}
|
|
299
|
+
me: datatypes.User = self.tool.get_me()
|
|
356
300
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
basic_message_placeholders = {
|
|
301
|
+
basic_placeholders = {
|
|
360
302
|
"first_name": me.get("first_name", ""),
|
|
361
303
|
"last_name": me.get("last_name", ""),
|
|
362
304
|
"email": me.get("email", ""),
|
|
363
305
|
"phone": me.get("phone", ""),
|
|
364
306
|
}
|
|
365
307
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for vacancy in vacancies:
|
|
308
|
+
seen_employers = set()
|
|
309
|
+
for vacancy in self._get_vacancies():
|
|
369
310
|
try:
|
|
370
|
-
|
|
311
|
+
employer = vacancy.get("employer", {})
|
|
312
|
+
|
|
313
|
+
placeholders = {
|
|
371
314
|
"vacancy_name": vacancy.get("name", ""),
|
|
372
|
-
"employer_name":
|
|
373
|
-
**
|
|
315
|
+
"employer_name": employer.get("name", ""),
|
|
316
|
+
**basic_placeholders,
|
|
374
317
|
}
|
|
375
318
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
319
|
+
storage = self.tool.storage
|
|
320
|
+
storage.vacancies.save(vacancy)
|
|
321
|
+
if employer := vacancy.get("employer"):
|
|
322
|
+
employer_id = employer.get("id")
|
|
323
|
+
if employer_id and employer_id not in seen_employers:
|
|
324
|
+
employer_profile: datatypes.Employer = (
|
|
325
|
+
self.api_client.get(f"/employers/{employer_id}")
|
|
326
|
+
)
|
|
327
|
+
storage.employers.save(employer_profile)
|
|
328
|
+
|
|
329
|
+
# По факту контакты можно получить только здесь?!
|
|
330
|
+
if vacancy.get("contacts"):
|
|
331
|
+
storage.employer_contacts.save(vacancy)
|
|
380
332
|
|
|
381
333
|
if vacancy.get("has_test"):
|
|
382
334
|
logger.debug(
|
|
@@ -386,137 +338,95 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
386
338
|
continue
|
|
387
339
|
|
|
388
340
|
if vacancy.get("archived"):
|
|
389
|
-
logger.
|
|
341
|
+
logger.debug(
|
|
390
342
|
"Пропускаем вакансию в архиве: %s",
|
|
391
343
|
vacancy["alternate_url"],
|
|
392
344
|
)
|
|
393
345
|
continue
|
|
394
346
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
and employer_id
|
|
401
|
-
and employer_id not in telemetry_data["employers"]
|
|
402
|
-
and employer_id not in self.ignored_employers
|
|
403
|
-
and (
|
|
404
|
-
not relations
|
|
405
|
-
or parse_invalid_datetime(vacancy["created_at"])
|
|
406
|
-
+ timedelta(days=7)
|
|
407
|
-
> datetime.now(tz=timezone.utc)
|
|
347
|
+
if redirect_url := vacancy.get("response_url"):
|
|
348
|
+
logger.debug(
|
|
349
|
+
"Пропускаем вакансию %s с перенаправлением: %s",
|
|
350
|
+
vacancy["alternate_url"],
|
|
351
|
+
redirect_url,
|
|
408
352
|
)
|
|
409
|
-
|
|
410
|
-
employer = self.api_client.get(f"/employers/{employer_id}")
|
|
411
|
-
|
|
412
|
-
employer_data = {
|
|
413
|
-
"name": employer.get("name"),
|
|
414
|
-
"type": employer.get("type"),
|
|
415
|
-
"description": employer.get("description"),
|
|
416
|
-
"site_url": employer.get("site_url"),
|
|
417
|
-
"area": employer.get("area", {}).get("name"), # город
|
|
418
|
-
}
|
|
419
|
-
if "got_rejection" in relations:
|
|
420
|
-
print(
|
|
421
|
-
"🚨 Вы получили отказ от https://hh.ru/employer/%s"
|
|
422
|
-
% employer_id
|
|
423
|
-
)
|
|
424
|
-
|
|
425
|
-
self.ignored_employers.add(employer_id)
|
|
353
|
+
continue
|
|
426
354
|
|
|
427
|
-
|
|
428
|
-
telemetry_data["employers"][employer_id] = employer_data
|
|
355
|
+
vacancy_id = vacancy["id"]
|
|
429
356
|
|
|
430
|
-
|
|
431
|
-
logger.debug(
|
|
432
|
-
"Останавливаем рассылку откликов, так как достигли лимита, попробуйте через сутки."
|
|
433
|
-
)
|
|
434
|
-
break
|
|
357
|
+
relations = vacancy.get("relations", [])
|
|
435
358
|
|
|
436
359
|
if relations:
|
|
437
360
|
logger.debug(
|
|
438
361
|
"Пропускаем вакансию с откликом: %s",
|
|
439
362
|
vacancy["alternate_url"],
|
|
440
363
|
)
|
|
364
|
+
if "got_rejection" in relations:
|
|
365
|
+
logger.debug(
|
|
366
|
+
"Вы получили отказ: %s", vacancy["alternate_url"]
|
|
367
|
+
)
|
|
368
|
+
print("⛔ Пришел отказ", vacancy["alternate_url"])
|
|
441
369
|
continue
|
|
442
370
|
|
|
443
371
|
params = {
|
|
444
372
|
"resume_id": self.resume_id,
|
|
445
|
-
"vacancy_id":
|
|
373
|
+
"vacancy_id": vacancy_id,
|
|
446
374
|
"message": "",
|
|
447
375
|
}
|
|
448
376
|
|
|
449
|
-
if self.force_message or vacancy.get(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
logger.error(ex)
|
|
458
|
-
continue
|
|
377
|
+
if self.force_message or vacancy.get(
|
|
378
|
+
"response_letter_required"
|
|
379
|
+
):
|
|
380
|
+
if self.openai_chat:
|
|
381
|
+
msg = self.pre_prompt + "\n\n"
|
|
382
|
+
msg += placeholders["vacancy_name"]
|
|
383
|
+
logger.debug("prompt: %s", msg)
|
|
384
|
+
msg = self.openai_chat.send_message(msg)
|
|
459
385
|
else:
|
|
460
386
|
msg = (
|
|
461
|
-
|
|
462
|
-
%
|
|
387
|
+
rand_text(random.choice(self.application_messages))
|
|
388
|
+
% placeholders
|
|
463
389
|
)
|
|
464
390
|
|
|
465
391
|
logger.debug(msg)
|
|
466
392
|
params["message"] = msg
|
|
467
393
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
394
|
+
try:
|
|
395
|
+
if not self.dry_run:
|
|
396
|
+
res = self.api_client.post(
|
|
397
|
+
"/negotiations",
|
|
398
|
+
params,
|
|
399
|
+
delay=random.uniform(1, 3),
|
|
400
|
+
)
|
|
401
|
+
assert res == {}
|
|
402
|
+
logger.debug(
|
|
403
|
+
"Отправили отклик: %s", vacancy["alternate_url"]
|
|
404
|
+
)
|
|
405
|
+
print(
|
|
406
|
+
"📨 Отправили отклик:",
|
|
471
407
|
vacancy["alternate_url"],
|
|
472
|
-
|
|
408
|
+
shorten(vacancy["name"]),
|
|
409
|
+
)
|
|
410
|
+
except Redirect:
|
|
411
|
+
logger.warning(
|
|
412
|
+
f"Игнорирую перенаправление на форму: {vacancy['alternate_url']}" # noqa: E501
|
|
473
413
|
)
|
|
474
|
-
continue
|
|
475
|
-
|
|
476
|
-
# Задержка перед отправкой отклика
|
|
477
|
-
interval = random.uniform(
|
|
478
|
-
self.apply_min_interval, self.apply_max_interval
|
|
479
|
-
)
|
|
480
|
-
time.sleep(interval)
|
|
481
|
-
|
|
482
|
-
res = self.api_client.post("/negotiations", params)
|
|
483
|
-
assert res == {}
|
|
484
|
-
print(
|
|
485
|
-
"📨 Отправили отклик",
|
|
486
|
-
vacancy["alternate_url"],
|
|
487
|
-
"(",
|
|
488
|
-
truncate_string(vacancy["name"]),
|
|
489
|
-
")",
|
|
490
|
-
)
|
|
491
414
|
except LimitExceeded:
|
|
415
|
+
logger.info("Достигли лимита на отклики")
|
|
492
416
|
print("⚠️ Достигли лимита рассылки")
|
|
493
|
-
|
|
494
|
-
|
|
417
|
+
# self.tool.storage.settings.set_value("_")
|
|
418
|
+
break
|
|
419
|
+
except ApiError as ex:
|
|
420
|
+
logger.warning(ex)
|
|
421
|
+
except (BadResponse, AIError) as ex:
|
|
495
422
|
logger.error(ex)
|
|
496
423
|
|
|
497
424
|
print("📝 Отклики на вакансии разосланы!")
|
|
498
425
|
|
|
499
|
-
|
|
500
|
-
if self.dry_run:
|
|
501
|
-
# С --dry-run можно посмотреть что отправляется
|
|
502
|
-
logger.info(
|
|
503
|
-
"Dry Run: Данные телеметрии для отправки на сервер: %r",
|
|
504
|
-
telemetry_data,
|
|
505
|
-
)
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
try:
|
|
509
|
-
response = telemetry_client.send_telemetry(
|
|
510
|
-
"/collect", dict(telemetry_data)
|
|
511
|
-
)
|
|
512
|
-
logger.debug(response)
|
|
513
|
-
except TelemetryError as ex:
|
|
514
|
-
logger.error(ex)
|
|
515
|
-
|
|
516
|
-
def _get_search_params(self, page: int, per_page: int) -> dict:
|
|
426
|
+
def _get_search_params(self, page: int) -> dict:
|
|
517
427
|
params = {
|
|
518
428
|
"page": page,
|
|
519
|
-
"per_page": per_page,
|
|
429
|
+
"per_page": self.per_page,
|
|
520
430
|
"order_by": self.order_by,
|
|
521
431
|
}
|
|
522
432
|
|
|
@@ -549,53 +459,47 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
549
459
|
if self.sort_point_lng:
|
|
550
460
|
params["sort_point_lng"] = self.sort_point_lng
|
|
551
461
|
if self.search_field:
|
|
552
|
-
params["search_field"] =
|
|
462
|
+
params["search_field"] = list2str(self.search_field)
|
|
553
463
|
if self.employment:
|
|
554
|
-
params["employment"] =
|
|
464
|
+
params["employment"] = list2str(self.employment)
|
|
555
465
|
if self.area:
|
|
556
|
-
params["area"] =
|
|
466
|
+
params["area"] = list2str(self.area)
|
|
557
467
|
if self.metro:
|
|
558
|
-
params["metro"] =
|
|
468
|
+
params["metro"] = list2str(self.metro)
|
|
559
469
|
if self.professional_role:
|
|
560
|
-
params["professional_role"] =
|
|
470
|
+
params["professional_role"] = list2str(self.professional_role)
|
|
561
471
|
if self.industry:
|
|
562
|
-
params["industry"] =
|
|
472
|
+
params["industry"] = list2str(self.industry)
|
|
563
473
|
if self.employer_id:
|
|
564
|
-
params["employer_id"] =
|
|
474
|
+
params["employer_id"] = list2str(self.employer_id)
|
|
565
475
|
if self.excluded_employer_id:
|
|
566
|
-
params["excluded_employer_id"] =
|
|
476
|
+
params["excluded_employer_id"] = list2str(self.excluded_employer_id)
|
|
567
477
|
if self.label:
|
|
568
|
-
params["label"] =
|
|
569
|
-
if self.only_with_salary
|
|
570
|
-
params["only_with_salary"] =
|
|
571
|
-
if self.clusters
|
|
572
|
-
|
|
573
|
-
if self.no_magic
|
|
574
|
-
params["no_magic"] =
|
|
575
|
-
if self.premium
|
|
576
|
-
params["premium"] =
|
|
478
|
+
params["label"] = list2str(self.label)
|
|
479
|
+
if self.only_with_salary:
|
|
480
|
+
params["only_with_salary"] = bool2str(self.only_with_salary)
|
|
481
|
+
# if self.clusters:
|
|
482
|
+
# params["clusters"] = bool2str(self.clusters)
|
|
483
|
+
if self.no_magic:
|
|
484
|
+
params["no_magic"] = bool2str(self.no_magic)
|
|
485
|
+
if self.premium:
|
|
486
|
+
params["premium"] = bool2str(self.premium)
|
|
577
487
|
# if self.responses_count_enabled is not None:
|
|
578
|
-
# params["responses_count_enabled"] =
|
|
488
|
+
# params["responses_count_enabled"] = bool2str(self.responses_count_enabled)
|
|
579
489
|
|
|
580
490
|
return params
|
|
581
491
|
|
|
582
|
-
def _get_vacancies(self
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
f"/resumes/{self.resume_id}/similar_vacancies", params
|
|
492
|
+
def _get_vacancies(self) -> Iterator[SearchVacancy]:
|
|
493
|
+
for page in range(self.total_pages):
|
|
494
|
+
params = self._get_search_params(page)
|
|
495
|
+
res: PaginatedItems[SearchVacancy] = self.api_client.get(
|
|
496
|
+
f"/resumes/{self.resume_id}/similar_vacancies",
|
|
497
|
+
params,
|
|
589
498
|
)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
break
|
|
499
|
+
if not res["items"]:
|
|
500
|
+
return
|
|
593
501
|
|
|
594
|
-
|
|
595
|
-
if page > 0:
|
|
596
|
-
interval = random.uniform(
|
|
597
|
-
self.page_min_interval, self.page_max_interval
|
|
598
|
-
)
|
|
599
|
-
time.sleep(interval)
|
|
502
|
+
yield from res["items"]
|
|
600
503
|
|
|
601
|
-
|
|
504
|
+
if page >= res["pages"] - 1:
|
|
505
|
+
return
|