hh-applicant-tool 0.5.8__tar.gz → 0.5.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hh-applicant-tool might be problematic. Click here for more details.
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/PKG-INFO +3 -3
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/README.md +2 -2
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/api/client.py +25 -13
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/main.py +17 -5
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/mixins.py +1 -1
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/apply_similar.py +16 -14
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/authorize.py +3 -5
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/call_api.py +6 -6
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/clear_negotiations.py +6 -6
- hh_applicant_tool-0.5.9/hh_applicant_tool/operations/config.py +51 -0
- hh_applicant_tool-0.5.9/hh_applicant_tool/operations/delete_telemetry.py +30 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/get_employer_contacts.py +7 -13
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/list_resumes.py +2 -2
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/refresh_token.py +4 -6
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/reply_employers.py +6 -6
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/update_resumes.py +3 -3
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/whoami.py +3 -3
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/telemetry_client.py +14 -2
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/pyproject.toml +4 -2
- hh_applicant_tool-0.5.8/hh_applicant_tool/operations/config.py +0 -36
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/__init__.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/__main__.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/ai/__init__.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/ai/blackbox.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/api/__init__.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/api/errors.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/color_log.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/constants.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/jsonc.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/__init__.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/types.py +0 -0
- {hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.9
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
|
|
40
40
|
> Утилита для генерации сопроводительного письма может использовать AI
|
|
41
41
|
|
|
42
|
-
Утилита для успешных волчат и старых волков с опытом, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме (бесплатный аналог услуги на HH). Утилита
|
|
42
|
+
Утилита для успешных волчат и старых волков с опытом, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме (бесплатный аналог услуги на HH). Утилита сохраняет контактные данные, всех кто вам писал, и позволяет вам по ним проводить поиск (название компании, сайт и тп). Это удобно, так как контакт сохранится даже, если вышлют отказ в дальнейшем. У утилиты есть группа: [HH Resume Automate](https://t.me/hh_resume_automate).
|
|
43
43
|
|
|
44
44
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
45
45
|
asdf/pyenv/conda и что-то еще. В школотронской Manjaro и даже в последних Ubuntu
|
|
@@ -257,7 +257,7 @@ https://hh.ru/employer/1918903
|
|
|
257
257
|
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
258
258
|
| **refresh-token** | Обновляет access_token. |
|
|
259
259
|
| **config** | Редактировать конфигурационный файл. |
|
|
260
|
-
| **get-employer-contacts** | Получить список контактов
|
|
260
|
+
| **get-employer-contacts** | Получить список контактов работадателей. |
|
|
261
261
|
|
|
262
262
|
### Формат текста сообщений
|
|
263
263
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
> Утилита для генерации сопроводительного письма может использовать AI
|
|
22
22
|
|
|
23
|
-
Утилита для успешных волчат и старых волков с опытом, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме (бесплатный аналог услуги на HH). Утилита
|
|
23
|
+
Утилита для успешных волчат и старых волков с опытом, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме (бесплатный аналог услуги на HH). Утилита сохраняет контактные данные, всех кто вам писал, и позволяет вам по ним проводить поиск (название компании, сайт и тп). Это удобно, так как контакт сохранится даже, если вышлют отказ в дальнейшем. У утилиты есть группа: [HH Resume Automate](https://t.me/hh_resume_automate).
|
|
24
24
|
|
|
25
25
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
26
26
|
asdf/pyenv/conda и что-то еще. В школотронской Manjaro и даже в последних Ubuntu
|
|
@@ -238,7 +238,7 @@ https://hh.ru/employer/1918903
|
|
|
238
238
|
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
239
239
|
| **refresh-token** | Обновляет access_token. |
|
|
240
240
|
| **config** | Редактировать конфигурационный файл. |
|
|
241
|
-
| **get-employer-contacts** | Получить список контактов
|
|
241
|
+
| **get-employer-contacts** | Получить список контактов работадателей. |
|
|
242
242
|
|
|
243
243
|
### Формат текста сообщений
|
|
244
244
|
|
|
@@ -124,10 +124,17 @@ class BaseClient:
|
|
|
124
124
|
assert 300 > response.status_code >= 200
|
|
125
125
|
return rv
|
|
126
126
|
|
|
127
|
-
get
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
def get(self, *args, **kwargs):
|
|
128
|
+
return self.request("GET", *args, **kwargs)
|
|
129
|
+
|
|
130
|
+
def post(self, *args, **kwargs):
|
|
131
|
+
return self.request("POST", *args, **kwargs)
|
|
132
|
+
|
|
133
|
+
def put(self, *args, **kwargs):
|
|
134
|
+
return self.request("PUT", *args, **kwargs)
|
|
135
|
+
|
|
136
|
+
def delete(self, *args, **kwargs):
|
|
137
|
+
return self.request("DELETE", *args, **kwargs)
|
|
131
138
|
|
|
132
139
|
def resolve_url(self, url: str) -> str:
|
|
133
140
|
return url if "://" in url else f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
|
|
@@ -194,7 +201,7 @@ class OAuthClient(BaseClient):
|
|
|
194
201
|
}
|
|
195
202
|
return self.request_access_token("/token", params)
|
|
196
203
|
|
|
197
|
-
def
|
|
204
|
+
def refresh_access_token(self, refresh_token: str) -> AccessToken:
|
|
198
205
|
# refresh_token можно использовать только один раз и только по истечению срока действия access_token.
|
|
199
206
|
return self.request_access_token(
|
|
200
207
|
"/token", grant_type="refresh_token", refresh_token=refresh_token
|
|
@@ -243,25 +250,30 @@ class ApiClient(BaseClient):
|
|
|
243
250
|
**kwargs: Any,
|
|
244
251
|
) -> dict:
|
|
245
252
|
def do_request():
|
|
246
|
-
return
|
|
253
|
+
return BaseClient.request(self, method, endpoint, params, delay, **kwargs)
|
|
247
254
|
|
|
248
255
|
try:
|
|
249
256
|
return do_request()
|
|
250
257
|
# TODO: добавить класс для ошибок типа AccessTokenExpired
|
|
251
|
-
except errors.
|
|
252
|
-
if not self.is_access_expired:
|
|
258
|
+
except errors.Forbidden as ex:
|
|
259
|
+
if not self.is_access_expired or not self.refresh_token:
|
|
253
260
|
raise ex
|
|
254
261
|
logger.info("try refresh access_token")
|
|
255
262
|
# Пробуем обновить токен
|
|
256
|
-
|
|
257
|
-
self.handle_access_token(token)
|
|
263
|
+
self.refresh_access_token()
|
|
258
264
|
# И повторно отправляем запрос
|
|
259
265
|
return do_request()
|
|
260
266
|
|
|
261
267
|
def handle_access_token(self, token: AccessToken) -> None:
|
|
262
|
-
for
|
|
263
|
-
if
|
|
264
|
-
setattr(self,
|
|
268
|
+
for field in ["access_token", "refresh_token", "access_expires_at"]:
|
|
269
|
+
if field in token and hasattr(self, field):
|
|
270
|
+
setattr(self, field, token[field])
|
|
271
|
+
|
|
272
|
+
def refresh_access_token(self) -> None:
|
|
273
|
+
if not self.refresh_token:
|
|
274
|
+
raise ValueError("Refresh token required.")
|
|
275
|
+
token = self.oauth_client.refresh_access_token(self.refresh_token)
|
|
276
|
+
self.handle_access_token(token)
|
|
265
277
|
|
|
266
278
|
def get_access_token(self) -> AccessToken:
|
|
267
279
|
return {
|
|
@@ -12,6 +12,8 @@ from typing import Literal, Sequence
|
|
|
12
12
|
from .api import ApiClient
|
|
13
13
|
from .color_log import ColorHandler
|
|
14
14
|
from .utils import Config, get_config_path
|
|
15
|
+
from .telemetry_client import TelemetryClient
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
DEFAULT_CONFIG_PATH = (
|
|
17
19
|
get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
|
|
@@ -46,11 +48,12 @@ def get_proxies(args: Namespace) -> dict[Literal["http", "https"], str | None]:
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
def
|
|
51
|
+
def get_api_client(args: Namespace) -> ApiClient:
|
|
50
52
|
token = args.config.get("token", {})
|
|
51
53
|
api = ApiClient(
|
|
52
54
|
access_token=token.get("access_token"),
|
|
53
55
|
refresh_token=token.get("refresh_token"),
|
|
56
|
+
access_expires_at=token.get("access_expires_at"),
|
|
54
57
|
delay=args.delay,
|
|
55
58
|
user_agent=args.config["user_agent"],
|
|
56
59
|
proxies=get_proxies(args),
|
|
@@ -134,10 +137,19 @@ class HHApplicantTool:
|
|
|
134
137
|
logger.addHandler(handler)
|
|
135
138
|
if args.run:
|
|
136
139
|
try:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
args.config.save(
|
|
140
|
+
if not args.config["telemetry_client_id"]:
|
|
141
|
+
import uuid
|
|
142
|
+
|
|
143
|
+
args.config.save(telemetry_client_id=str(uuid.uuid4()))
|
|
144
|
+
api_client = get_api_client(args)
|
|
145
|
+
telemetry_client = TelemetryClient(
|
|
146
|
+
telemetry_client_id=args.config["telemetry_client_id"],
|
|
147
|
+
proxies=api_client.proxies.copy(),
|
|
148
|
+
)
|
|
149
|
+
# 0 or None = success
|
|
150
|
+
res = args.run(args, api_client, telemetry_client)
|
|
151
|
+
if (token := api_client.get_access_token()) != args.config["token"]:
|
|
152
|
+
args.config.save(token=token)
|
|
141
153
|
return res
|
|
142
154
|
except Exception as e:
|
|
143
155
|
logger.exception(e)
|
|
@@ -5,7 +5,7 @@ from .types import ApiListResponse
|
|
|
5
5
|
class GetResumeIdMixin:
|
|
6
6
|
def _get_resume_id(self) -> str:
|
|
7
7
|
try:
|
|
8
|
-
resumes: ApiListResponse = self.
|
|
8
|
+
resumes: ApiListResponse = self.api_client.get("/resumes/mine")
|
|
9
9
|
return resumes["items"][0]["id"]
|
|
10
10
|
except (ApiError, KeyError, IndexError) as ex:
|
|
11
11
|
raise Exception("Не могу получить идентификатор резюме") from ex
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/apply_similar.py
RENAMED
|
@@ -6,13 +6,11 @@ from collections import defaultdict
|
|
|
6
6
|
from datetime import datetime, timedelta, timezone
|
|
7
7
|
from typing import TextIO
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
|
|
9
|
+
from ..api.errors import LimitExceeded
|
|
11
10
|
from ..ai.blackbox import BlackboxChat, BlackboxError
|
|
12
11
|
from ..api import ApiError, ApiClient
|
|
13
12
|
from ..main import BaseOperation
|
|
14
13
|
from ..main import Namespace as BaseNamespace
|
|
15
|
-
from ..main import get_api
|
|
16
14
|
from ..mixins import GetResumeIdMixin
|
|
17
15
|
from ..telemetry_client import TelemetryClient, TelemetryError
|
|
18
16
|
from ..types import ApiListResponse, VacancyItem
|
|
@@ -109,7 +107,9 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
109
107
|
action=argparse.BooleanOptionalAction,
|
|
110
108
|
)
|
|
111
109
|
|
|
112
|
-
def run(
|
|
110
|
+
def run(
|
|
111
|
+
self, args: Namespace, api_client: ApiClient, telemetry_client: TelemetryClient
|
|
112
|
+
) -> None:
|
|
113
113
|
self.enable_telemetry = True
|
|
114
114
|
if args.disable_telemetry:
|
|
115
115
|
# print(
|
|
@@ -126,7 +126,8 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
126
126
|
# logger.info("Спасибо за то что оставили телеметрию включенной!")
|
|
127
127
|
self.enable_telemetry = False
|
|
128
128
|
|
|
129
|
-
self.
|
|
129
|
+
self.api_client = api_client
|
|
130
|
+
self.telemetry_client = telemetry_client
|
|
130
131
|
self.resume_id = args.resume_id or self._get_resume_id()
|
|
131
132
|
self.application_messages = self._get_application_messages(args.message_list)
|
|
132
133
|
self.chat = None
|
|
@@ -135,7 +136,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
135
136
|
self.chat = BlackboxChat(
|
|
136
137
|
session_id=config["session_id"],
|
|
137
138
|
chat_payload=config["chat_payload"],
|
|
138
|
-
proxies=self.
|
|
139
|
+
proxies=self.api_client.proxies or {},
|
|
139
140
|
)
|
|
140
141
|
|
|
141
142
|
self.pre_prompt = args.pre_prompt
|
|
@@ -160,7 +161,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
160
161
|
return application_messages
|
|
161
162
|
|
|
162
163
|
def _apply_similar(self) -> None:
|
|
163
|
-
telemetry_client =
|
|
164
|
+
telemetry_client = self.telemetry_client
|
|
164
165
|
telemetry_data = defaultdict(dict)
|
|
165
166
|
|
|
166
167
|
vacancies = self._get_vacancies()
|
|
@@ -190,7 +191,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
190
191
|
# Остальное неинтересно
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
me = self.
|
|
194
|
+
me = self.api_client.get("/me")
|
|
194
195
|
|
|
195
196
|
basic_message_placeholders = {
|
|
196
197
|
"first_name": me.get("first_name", ""),
|
|
@@ -244,7 +245,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
244
245
|
> datetime.now(tz=timezone.utc)
|
|
245
246
|
)
|
|
246
247
|
):
|
|
247
|
-
employer = self.
|
|
248
|
+
employer = self.api_client.get(f"/employers/{employer_id}")
|
|
248
249
|
|
|
249
250
|
employer_data = {
|
|
250
251
|
"name": employer.get("name"),
|
|
@@ -269,9 +270,10 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
269
270
|
response["topic_url"],
|
|
270
271
|
)
|
|
271
272
|
else:
|
|
272
|
-
print(
|
|
273
|
-
|
|
274
|
-
)
|
|
273
|
+
# print(
|
|
274
|
+
# "Создание темы для обсуждения работодателя добавлено в очередь..."
|
|
275
|
+
# )
|
|
276
|
+
...
|
|
275
277
|
complained_employers.add(employer_id)
|
|
276
278
|
except TelemetryError as ex:
|
|
277
279
|
logger.error(ex)
|
|
@@ -331,7 +333,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
331
333
|
)
|
|
332
334
|
time.sleep(interval)
|
|
333
335
|
|
|
334
|
-
res = self.
|
|
336
|
+
res = self.api_client.post("/negotiations", params)
|
|
335
337
|
assert res == {}
|
|
336
338
|
print(
|
|
337
339
|
"📨 Отправили отклик",
|
|
@@ -375,7 +377,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
375
377
|
}
|
|
376
378
|
if self.search:
|
|
377
379
|
params["text"] = self.search
|
|
378
|
-
res: ApiListResponse = self.
|
|
380
|
+
res: ApiListResponse = self.api_client.get(
|
|
379
381
|
f"/resumes/{self.resume_id}/similar_vacancies", params
|
|
380
382
|
)
|
|
381
383
|
rv.extend(res["items"])
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/authorize.py
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import logging
|
|
3
|
-
import time
|
|
4
3
|
from urllib.parse import parse_qs, urlsplit
|
|
5
4
|
import sys
|
|
6
5
|
from typing import Any
|
|
@@ -35,9 +34,8 @@ except ImportError:
|
|
|
35
34
|
pass
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
from ..api import ApiClient
|
|
39
|
-
from ..main import BaseOperation, Namespace
|
|
40
|
-
from ..utils import Config
|
|
37
|
+
from ..api import ApiClient # noqa: E402
|
|
38
|
+
from ..main import BaseOperation, Namespace # noqa: E402
|
|
41
39
|
|
|
42
40
|
logger = logging.getLogger(__package__)
|
|
43
41
|
|
|
@@ -86,7 +84,7 @@ class Operation(BaseOperation):
|
|
|
86
84
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
87
85
|
pass
|
|
88
86
|
|
|
89
|
-
def run(self, api_client: ApiClient,
|
|
87
|
+
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
90
88
|
if not QT_IMPORTED:
|
|
91
89
|
print_err(
|
|
92
90
|
"❗Критиническая Ошибка: PyQt6 не был импортирован, возможно, вы долбоеб и забыли его установить, либо же криворукие разрабы этой либы опять все сломали..."
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/call_api.py
RENAMED
|
@@ -22,22 +22,22 @@ class Operation(BaseOperation):
|
|
|
22
22
|
"""Вызвать произвольный метод API <https://github.com/hhru/api>."""
|
|
23
23
|
|
|
24
24
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
25
|
-
parser.add_argument("endpoint")
|
|
25
|
+
parser.add_argument("endpoint", help="Путь до эндпоинта API")
|
|
26
26
|
parser.add_argument(
|
|
27
27
|
"param",
|
|
28
28
|
nargs="*",
|
|
29
|
-
help="PARAM=VALUE",
|
|
29
|
+
help="Параметры указываются в виде PARAM=VALUE",
|
|
30
30
|
default=[],
|
|
31
31
|
)
|
|
32
32
|
parser.add_argument(
|
|
33
33
|
"-m", "--method", "--meth", default="GET", help="HTTP Метод"
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
def run(self,
|
|
36
|
+
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
37
37
|
params = dict(x.split("=", 1) for x in args.param)
|
|
38
38
|
try:
|
|
39
|
-
result =
|
|
40
|
-
print(json.dumps(result, ensure_ascii=
|
|
39
|
+
result = api_client.request(args.method, args.endpoint, params=params)
|
|
40
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
41
41
|
except ApiError as ex:
|
|
42
|
-
json.dump(ex.data, sys.stderr, ensure_ascii=
|
|
42
|
+
json.dump(ex.data, sys.stderr, ensure_ascii=False)
|
|
43
43
|
return 1
|
|
@@ -43,12 +43,12 @@ class Operation(BaseOperation):
|
|
|
43
43
|
action=argparse.BooleanOptionalAction,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
def _get_active_negotiations(self,
|
|
46
|
+
def _get_active_negotiations(self, api_client: ApiClient) -> list[dict]:
|
|
47
47
|
rv = []
|
|
48
48
|
page = 0
|
|
49
49
|
per_page = 100
|
|
50
50
|
while True:
|
|
51
|
-
r: ApiListResponse =
|
|
51
|
+
r: ApiListResponse = api_client.get(
|
|
52
52
|
"/negotiations", page=page, per_page=per_page, status="active"
|
|
53
53
|
)
|
|
54
54
|
rv.extend(r["items"])
|
|
@@ -57,8 +57,8 @@ class Operation(BaseOperation):
|
|
|
57
57
|
break
|
|
58
58
|
return rv
|
|
59
59
|
|
|
60
|
-
def run(self,
|
|
61
|
-
negotiations = self._get_active_negotiations(
|
|
60
|
+
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
61
|
+
negotiations = self._get_active_negotiations(api_client)
|
|
62
62
|
print("Всего активных:", len(negotiations))
|
|
63
63
|
for item in negotiations:
|
|
64
64
|
state = item["state"]
|
|
@@ -78,7 +78,7 @@ class Operation(BaseOperation):
|
|
|
78
78
|
)
|
|
79
79
|
):
|
|
80
80
|
decline_allowed = item.get("decline_allowed") or False
|
|
81
|
-
r =
|
|
81
|
+
r = api_client.delete(
|
|
82
82
|
f"/negotiations/active/{item['id']}",
|
|
83
83
|
with_decline_message=decline_allowed,
|
|
84
84
|
)
|
|
@@ -95,7 +95,7 @@ class Operation(BaseOperation):
|
|
|
95
95
|
if is_discard and args.blacklist_discard:
|
|
96
96
|
employer = vacancy["employer"]
|
|
97
97
|
try:
|
|
98
|
-
r =
|
|
98
|
+
r = api_client.put(f"/employers/blacklisted/{employer['id']}")
|
|
99
99
|
assert not r
|
|
100
100
|
print(
|
|
101
101
|
"🚫 Заблокировали",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..main import BaseOperation
|
|
8
|
+
from ..main import Namespace as BaseNamespace
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__package__)
|
|
11
|
+
|
|
12
|
+
EDITOR = os.getenv("EDITOR", "nano")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Namespace(BaseNamespace):
|
|
16
|
+
show_path: bool
|
|
17
|
+
key: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_value(data: dict[str, Any], path: str) -> Any:
|
|
21
|
+
for key in path.split("."):
|
|
22
|
+
if key not in data:
|
|
23
|
+
return None
|
|
24
|
+
data = data[key]
|
|
25
|
+
return data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Operation(BaseOperation):
|
|
29
|
+
"""Операции с конфигурационным файлом"""
|
|
30
|
+
|
|
31
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-p",
|
|
34
|
+
"--show-path",
|
|
35
|
+
"--path",
|
|
36
|
+
type=bool,
|
|
37
|
+
default=False,
|
|
38
|
+
action=argparse.BooleanOptionalAction,
|
|
39
|
+
help="Вывести полный путь к конфигу",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument("-k", "--key", help="Вывести отдельное значение из конфига")
|
|
42
|
+
|
|
43
|
+
def run(self, args: Namespace, *_) -> None:
|
|
44
|
+
if args.key:
|
|
45
|
+
print(get_value(args.config, args.key))
|
|
46
|
+
return
|
|
47
|
+
config_path = str(args.config._config_path)
|
|
48
|
+
if args.show_path:
|
|
49
|
+
print(config_path)
|
|
50
|
+
else:
|
|
51
|
+
subprocess.call([EDITOR, config_path])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Этот модуль можно использовать как образец для других
|
|
2
|
+
import argparse
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from ..telemetry_client import TelemetryClient, TelemetryError
|
|
6
|
+
|
|
7
|
+
from ..main import BaseOperation
|
|
8
|
+
from ..main import Namespace as BaseNamespace
|
|
9
|
+
from ..utils import print_err
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__package__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Namespace(BaseNamespace):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Operation(BaseOperation):
|
|
19
|
+
"""Удалить всю телеметрию, сохраненную на сервере."""
|
|
20
|
+
|
|
21
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def run(self, a, b, telemetry_client: TelemetryClient) -> None:
|
|
25
|
+
try:
|
|
26
|
+
telemetry_client.send_telemetry("/delete")
|
|
27
|
+
print("✅ Вся телеметрия, сохраненная на сервере, была успешно удалена!")
|
|
28
|
+
except TelemetryError as ex:
|
|
29
|
+
print_err("❗ Ошибка:", ex)
|
|
30
|
+
return 1
|
|
@@ -4,7 +4,6 @@ from os import getenv
|
|
|
4
4
|
|
|
5
5
|
from ..main import BaseOperation
|
|
6
6
|
from ..main import Namespace as BaseNamespace
|
|
7
|
-
from ..main import get_proxies
|
|
8
7
|
from ..telemetry_client import TelemetryClient
|
|
9
8
|
|
|
10
9
|
logger = logging.getLogger(__package__)
|
|
@@ -48,23 +47,18 @@ class Operation(BaseOperation):
|
|
|
48
47
|
help="Номер страницы в выдаче",
|
|
49
48
|
)
|
|
50
49
|
|
|
51
|
-
def run(self, _,
|
|
52
|
-
|
|
53
|
-
client = TelemetryClient(proxies=proxies)
|
|
54
|
-
auth = (
|
|
55
|
-
(args.username, args.password) if args.username and args.password else None
|
|
56
|
-
)
|
|
57
|
-
# Аутентификация пользователя
|
|
58
|
-
results = client.get_telemetry(
|
|
50
|
+
def run(self, args: Namespace, _, telemetry_client: TelemetryClient) -> None:
|
|
51
|
+
results = telemetry_client.get_telemetry(
|
|
59
52
|
"/contact/persons",
|
|
60
53
|
{"search": args.search, "per_page": 10, "page": args.page},
|
|
61
|
-
auth=auth,
|
|
62
54
|
)
|
|
63
55
|
if "contact_persons" not in results:
|
|
64
56
|
print("❌", results)
|
|
65
57
|
return 1
|
|
66
58
|
|
|
67
|
-
print(
|
|
59
|
+
print(
|
|
60
|
+
"Тут отображаются только данные, собранные с вашего telemetry_client_id. Вы так же можете их удалить с помощью команды delete-telemetry."
|
|
61
|
+
)
|
|
68
62
|
print()
|
|
69
63
|
|
|
70
64
|
self._print_contacts(results)
|
|
@@ -73,7 +67,7 @@ class Operation(BaseOperation):
|
|
|
73
67
|
"""Вывод всех контактов в древовидной структуре."""
|
|
74
68
|
page = data["page"]
|
|
75
69
|
pages = (data["total"] // data["per_page"]) + 1
|
|
76
|
-
print(f"
|
|
70
|
+
print(f"Страница {page}/{pages}:")
|
|
77
71
|
contacts = data.get("contact_persons", [])
|
|
78
72
|
for idx, contact in enumerate(contacts):
|
|
79
73
|
is_last_contact = idx == len(contacts) - 1
|
|
@@ -83,7 +77,7 @@ class Operation(BaseOperation):
|
|
|
83
77
|
def _print_contact(self, contact: dict, is_last_contact: bool) -> None:
|
|
84
78
|
"""Вывод информации о конкретном контакте."""
|
|
85
79
|
prefix = "└──" if is_last_contact else "├──"
|
|
86
|
-
print(f" {prefix} 🧑
|
|
80
|
+
print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
|
|
87
81
|
prefix2 = " " if is_last_contact else " │ "
|
|
88
82
|
print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
|
|
89
83
|
employer = contact.get("employer") or {}
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/list_resumes.py
RENAMED
|
@@ -23,8 +23,8 @@ class Operation(BaseOperation):
|
|
|
23
23
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
|
-
def run(self,
|
|
27
|
-
resumes: ApiListResponse =
|
|
26
|
+
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
27
|
+
resumes: ApiListResponse = api_client.get("/resumes/mine")
|
|
28
28
|
t = PrettyTable(field_names=["ID", "Название", "Статус"], align="l", valign="t")
|
|
29
29
|
t.add_rows(
|
|
30
30
|
[
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/refresh_token.py
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Этот модуль можно использовать как образец для других
|
|
2
2
|
import argparse
|
|
3
3
|
import logging
|
|
4
|
-
|
|
5
|
-
from ..api import ApiError, ApiClient
|
|
4
|
+
from typing import Any
|
|
5
|
+
from ..api import ApiError, ApiClient
|
|
6
6
|
from ..main import BaseOperation
|
|
7
7
|
from ..main import Namespace as BaseNamespace
|
|
8
8
|
from ..utils import print_err
|
|
@@ -20,11 +20,9 @@ class Operation(BaseOperation):
|
|
|
20
20
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
|
-
def run(self,
|
|
23
|
+
def run(self, _, api_client: ApiClient, *args: Any) -> None:
|
|
24
24
|
try:
|
|
25
|
-
|
|
26
|
-
token = oauth.refresh_access(api.refresh_token)
|
|
27
|
-
api.handle_access_token(token)
|
|
25
|
+
api_client.refresh_access_token()
|
|
28
26
|
print("✅ Токен обновлен!")
|
|
29
27
|
except ApiError as ex:
|
|
30
28
|
print_err("❗ Ошибка:", ex)
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/reply_employers.py
RENAMED
|
@@ -66,8 +66,8 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
66
66
|
action=argparse.BooleanOptionalAction,
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
def run(self,
|
|
70
|
-
self.
|
|
69
|
+
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
70
|
+
self.api_client = api_client
|
|
71
71
|
self.resume_id = self._get_resume_id()
|
|
72
72
|
self.reply_min_interval, self.reply_max_interval = args.reply_interval
|
|
73
73
|
self.reply_message = args.reply_message or args.config["reply_message"]
|
|
@@ -79,7 +79,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
79
79
|
self._reply_chats()
|
|
80
80
|
|
|
81
81
|
def _reply_chats(self) -> None:
|
|
82
|
-
me = self.me = self.
|
|
82
|
+
me = self.me = self.api_client.get("/me")
|
|
83
83
|
|
|
84
84
|
basic_message_placeholders = {
|
|
85
85
|
"first_name": me.get("first_name", ""),
|
|
@@ -123,7 +123,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
123
123
|
last_message: dict | None = None
|
|
124
124
|
message_history: list[str] = []
|
|
125
125
|
while True:
|
|
126
|
-
messages_res = self.
|
|
126
|
+
messages_res = self.api_client.get(
|
|
127
127
|
f"/negotiations/{nid}/messages", page=page
|
|
128
128
|
)
|
|
129
129
|
|
|
@@ -193,7 +193,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
193
193
|
self.reply_max_interval,
|
|
194
194
|
)
|
|
195
195
|
)
|
|
196
|
-
self.
|
|
196
|
+
self.api_client.post(
|
|
197
197
|
f"/negotiations/{nid}/messages",
|
|
198
198
|
message=message,
|
|
199
199
|
)
|
|
@@ -209,7 +209,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
|
|
|
209
209
|
def _get_negotiations(self) -> list[dict]:
|
|
210
210
|
rv = []
|
|
211
211
|
for page in range(self.max_pages):
|
|
212
|
-
res = self.
|
|
212
|
+
res = self.api_client.get("/negotiations", page=page, status="active")
|
|
213
213
|
rv.extend(res["items"])
|
|
214
214
|
if page >= res["pages"] - 1:
|
|
215
215
|
break
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/update_resumes.py
RENAMED
|
@@ -21,11 +21,11 @@ class Operation(BaseOperation):
|
|
|
21
21
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
|
-
def run(self,
|
|
25
|
-
resumes: ApiListResponse =
|
|
24
|
+
def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
|
|
25
|
+
resumes: ApiListResponse = api_client.get("/resumes/mine")
|
|
26
26
|
for resume in resumes["items"]:
|
|
27
27
|
try:
|
|
28
|
-
res =
|
|
28
|
+
res = api_client.post(f"/resumes/{resume['id']}/publish")
|
|
29
29
|
assert res == {}
|
|
30
30
|
print("✅ Обновлено", truncate_string(resume["title"]))
|
|
31
31
|
except ApiError as ex:
|
|
@@ -20,6 +20,6 @@ class Operation(BaseOperation):
|
|
|
20
20
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
|
-
def run(self,
|
|
24
|
-
result =
|
|
25
|
-
print(json.dumps(result, ensure_ascii=
|
|
23
|
+
def run(self, args: Namespace, api_client: ApiClient, _) -> None:
|
|
24
|
+
result = api_client.get("/me")
|
|
25
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import logging
|
|
3
5
|
import os
|
|
@@ -6,8 +8,8 @@ import warnings
|
|
|
6
8
|
from functools import partialmethod
|
|
7
9
|
from typing import Any, Dict, Optional
|
|
8
10
|
from urllib.parse import urljoin
|
|
9
|
-
|
|
10
11
|
import requests
|
|
12
|
+
from .utils import Config
|
|
11
13
|
|
|
12
14
|
# Сертификат на сервере давно истек, но его обновлять мне лень...
|
|
13
15
|
warnings.filterwarnings("ignore", message="Unverified HTTPS request")
|
|
@@ -29,6 +31,7 @@ class TelemetryClient:
|
|
|
29
31
|
|
|
30
32
|
def __init__(
|
|
31
33
|
self,
|
|
34
|
+
telemetry_client_id: str,
|
|
32
35
|
server_address: Optional[str] = None,
|
|
33
36
|
*,
|
|
34
37
|
session: Optional[requests.Session] = None,
|
|
@@ -36,6 +39,7 @@ class TelemetryClient:
|
|
|
36
39
|
proxies: dict | None = None,
|
|
37
40
|
delay: Optional[float] = None,
|
|
38
41
|
) -> None:
|
|
42
|
+
self.send_telemetry_id = telemetry_client_id
|
|
39
43
|
self.server_address = os.getenv(
|
|
40
44
|
"TELEMETRY_SERVER", server_address or self.server_address
|
|
41
45
|
)
|
|
@@ -68,7 +72,10 @@ class TelemetryClient:
|
|
|
68
72
|
response = self.session.request(
|
|
69
73
|
method,
|
|
70
74
|
url,
|
|
71
|
-
headers={
|
|
75
|
+
headers={
|
|
76
|
+
"User-Agent": self.user_agent,
|
|
77
|
+
"X-Telemetry-Client-ID": self.send_telemetry_id,
|
|
78
|
+
},
|
|
72
79
|
proxies=self.proxies,
|
|
73
80
|
params=data if not has_body else None,
|
|
74
81
|
json=data if has_body else None,
|
|
@@ -92,3 +99,8 @@ class TelemetryClient:
|
|
|
92
99
|
|
|
93
100
|
get_telemetry = partialmethod(request, "GET")
|
|
94
101
|
send_telemetry = partialmethod(request, "POST")
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def create_from_config(cls, config: Config) -> "TelemetryClient":
|
|
105
|
+
assert "telemetry_client_id" in config
|
|
106
|
+
return cls(config["telemetry_client_id"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "hh-applicant-tool"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.9"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Senior YAML Developer <yamldeveloper@proton.me>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -17,7 +17,6 @@ pyqt6-webengine = { version = "6.7.0", optional = true }
|
|
|
17
17
|
qt = ["pyqt6", "pyqt6-webengine"]
|
|
18
18
|
|
|
19
19
|
[tool.poetry.group.dev.dependencies]
|
|
20
|
-
black = "^23.1.0"
|
|
21
20
|
isort = "^5.12.0"
|
|
22
21
|
pylint = "^2.16.4"
|
|
23
22
|
pytest = "^7.2.2"
|
|
@@ -36,3 +35,6 @@ hh-applicant-tool = "hh_applicant_tool.main:main"
|
|
|
36
35
|
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md
|
|
37
36
|
# Ошибки показывать он не бросит, но заебывать перестанет
|
|
38
37
|
typeCheckingMode = "off"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
select = ["E", "F", "I", "W"]
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
|
|
6
|
-
from ..main import BaseOperation
|
|
7
|
-
from ..main import Namespace as BaseNamespace
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__package__)
|
|
10
|
-
|
|
11
|
-
EDITOR = os.getenv("EDITOR", "nano")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Namespace(BaseNamespace):
|
|
15
|
-
print: bool
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Operation(BaseOperation):
|
|
19
|
-
"""Редактировать конфигурационный файл или показать путь до него"""
|
|
20
|
-
|
|
21
|
-
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
|
-
parser.add_argument(
|
|
23
|
-
"-p",
|
|
24
|
-
"--print",
|
|
25
|
-
type=bool,
|
|
26
|
-
default=False,
|
|
27
|
-
action=argparse.BooleanOptionalAction,
|
|
28
|
-
help="Напечатать путь и выйти",
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
def run(self, _, args: Namespace) -> None:
|
|
32
|
-
config_path = str(args.config._config_path)
|
|
33
|
-
if args.print:
|
|
34
|
-
print(config_path)
|
|
35
|
-
else:
|
|
36
|
-
subprocess.call([EDITOR, config_path])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.5.8 → hh_applicant_tool-0.5.9}/hh_applicant_tool/operations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|