hh-applicant-tool 1.4.7__py3-none-any.whl → 1.5.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/__main__.py +1 -1
- hh_applicant_tool/ai/__init__.py +1 -0
- hh_applicant_tool/ai/openai.py +30 -14
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +32 -17
- 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 +63 -38
- hh_applicant_tool/operations/apply_similar.py +136 -52
- hh_applicant_tool/operations/authorize.py +97 -28
- hh_applicant_tool/operations/call_api.py +3 -3
- hh_applicant_tool/operations/{check_negotiations.py → clear_negotiations.py} +40 -26
- hh_applicant_tool/operations/list_resumes.py +5 -7
- hh_applicant_tool/operations/query.py +5 -3
- hh_applicant_tool/operations/refresh_token.py +9 -2
- 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 +3 -3
- hh_applicant_tool/storage/__init__.py +5 -1
- hh_applicant_tool/storage/facade.py +2 -2
- hh_applicant_tool/storage/models/base.py +9 -4
- hh_applicant_tool/storage/models/contacts.py +42 -0
- hh_applicant_tool/storage/queries/schema.sql +23 -10
- 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 +12 -15
- hh_applicant_tool/utils/__init__.py +3 -3
- hh_applicant_tool/utils/config.py +1 -1
- hh_applicant_tool/utils/log.py +6 -3
- hh_applicant_tool/utils/mixins.py +28 -46
- hh_applicant_tool/utils/string.py +15 -0
- hh_applicant_tool/utils/terminal.py +115 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/METADATA +384 -162
- hh_applicant_tool-1.5.7.dist-info/RECORD +68 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/WHEEL +1 -1
- 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.5.7.dist-info}/entry_points.txt +0 -0
hh_applicant_tool/__main__.py
CHANGED
hh_applicant_tool/ai/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .openai import ChatOpenAI, OpenAIError
|
hh_applicant_tool/ai/openai.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from dataclasses import dataclass, field
|
|
3
|
-
from typing import ClassVar
|
|
2
|
+
from dataclasses import KW_ONLY, dataclass, field
|
|
4
3
|
|
|
5
4
|
import requests
|
|
6
5
|
|
|
@@ -9,43 +8,60 @@ from .base import AIError
|
|
|
9
8
|
logger = logging.getLogger(__package__)
|
|
10
9
|
|
|
11
10
|
|
|
11
|
+
DEFAULT_COMPLETION_ENDPOINT = "https://api.openai.com/v1/chat/completions"
|
|
12
|
+
|
|
13
|
+
|
|
12
14
|
class OpenAIError(AIError):
|
|
13
15
|
pass
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
17
19
|
class ChatOpenAI:
|
|
18
|
-
chat_endpoint: ClassVar[str] = "https://api.openai.com/v1/chat/completions"
|
|
19
|
-
|
|
20
20
|
token: str
|
|
21
|
-
|
|
21
|
+
_: KW_ONLY
|
|
22
22
|
system_prompt: str | None = None
|
|
23
|
+
timeout: float = 15.0
|
|
23
24
|
temperature: float = 0.7
|
|
24
25
|
max_completion_tokens: int = 1000
|
|
26
|
+
model: str | None = None
|
|
27
|
+
completion_endpoint: str = None
|
|
25
28
|
session: requests.Session = field(default_factory=requests.Session)
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
self.completion_endpoint = (
|
|
32
|
+
self.completion_endpoint or DEFAULT_COMPLETION_ENDPOINT
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _default_headers(self) -> dict[str, str]:
|
|
28
36
|
return {
|
|
29
37
|
"Authorization": f"Bearer {self.token}",
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
def send_message(self, message: str) -> str:
|
|
41
|
+
messages = []
|
|
42
|
+
|
|
43
|
+
# Добавляем системный промпт только если он не пустой и не None
|
|
44
|
+
if self.system_prompt:
|
|
45
|
+
messages.append({"role": "system", "content": self.system_prompt})
|
|
46
|
+
|
|
47
|
+
# Пользовательское сообщение всегда обязательно
|
|
48
|
+
messages.append({"role": "user", "content": message})
|
|
49
|
+
|
|
33
50
|
payload = {
|
|
34
|
-
"
|
|
35
|
-
"messages": [
|
|
36
|
-
{"role": "system", "content": self.system_prompt},
|
|
37
|
-
{"role": "user", "content": message},
|
|
38
|
-
],
|
|
51
|
+
"messages": messages,
|
|
39
52
|
"temperature": self.temperature,
|
|
40
53
|
"max_completion_tokens": self.max_completion_tokens,
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
if self.model:
|
|
57
|
+
payload["model"] = self.model
|
|
58
|
+
|
|
43
59
|
try:
|
|
44
60
|
response = self.session.post(
|
|
45
|
-
self.
|
|
61
|
+
self.completion_endpoint,
|
|
46
62
|
json=payload,
|
|
47
|
-
headers=self.
|
|
48
|
-
timeout=
|
|
63
|
+
headers=self._default_headers(),
|
|
64
|
+
timeout=self.timeout,
|
|
49
65
|
)
|
|
50
66
|
response.raise_for_status()
|
|
51
67
|
|
hh_applicant_tool/api/client.py
CHANGED
|
@@ -13,12 +13,20 @@ from urllib.parse import urlencode, urljoin
|
|
|
13
13
|
import requests
|
|
14
14
|
from requests import Session
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from hh_applicant_tool.api.user_agent import generate_android_useragent
|
|
17
|
+
|
|
17
18
|
from . import errors
|
|
19
|
+
from .client_keys import (
|
|
20
|
+
ANDROID_CLIENT_ID,
|
|
21
|
+
ANDROID_CLIENT_SECRET,
|
|
22
|
+
)
|
|
23
|
+
from .datatypes import AccessToken
|
|
18
24
|
|
|
19
25
|
__all__ = ("ApiClient", "OAuthClient")
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
HH_API_URL = "https://api.hh.ru/"
|
|
28
|
+
HH_OAUTH_URL = "https://hh.ru/oauth/"
|
|
29
|
+
DEFAULT_DELAY = 0.345
|
|
22
30
|
|
|
23
31
|
AllowedMethods = Literal["GET", "POST", "PUT", "DELETE"]
|
|
24
32
|
T = TypeVar("T")
|
|
@@ -34,27 +42,29 @@ class BaseClient:
|
|
|
34
42
|
_: dataclasses.KW_ONLY
|
|
35
43
|
user_agent: str | None = None
|
|
36
44
|
session: Session | None = None
|
|
37
|
-
delay: float =
|
|
45
|
+
delay: float | None = None
|
|
38
46
|
_previous_request_time: float = 0.0
|
|
39
47
|
|
|
40
48
|
def __post_init__(self) -> None:
|
|
41
|
-
assert self.base_url.endswith("/"), "base_url must
|
|
42
|
-
self.
|
|
49
|
+
assert self.base_url.endswith("/"), "base_url must ends with /"
|
|
50
|
+
self.delay = self.delay or DEFAULT_DELAY
|
|
51
|
+
self.user_agent = self.user_agent or generate_android_useragent()
|
|
52
|
+
|
|
43
53
|
# logger.debug(f"user agent: {self.user_agent}")
|
|
54
|
+
|
|
44
55
|
if not self.session:
|
|
45
56
|
logger.debug("create new session")
|
|
46
57
|
self.session = requests.session()
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
|
|
59
|
+
self.lock = Lock()
|
|
49
60
|
|
|
50
61
|
@property
|
|
51
62
|
def proxies(self):
|
|
52
63
|
return self.session.proxies
|
|
53
64
|
|
|
54
|
-
def
|
|
65
|
+
def _default_headers(self) -> dict[str, str]:
|
|
55
66
|
return {
|
|
56
|
-
"user-agent": self.user_agent
|
|
57
|
-
or "Mozilla/5.0 (+https://github.com/s3rgeym/hh-applicant-tool)",
|
|
67
|
+
"user-agent": self.user_agent,
|
|
58
68
|
"x-hh-app-active": "true",
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -87,7 +97,7 @@ class BaseClient:
|
|
|
87
97
|
method,
|
|
88
98
|
url,
|
|
89
99
|
**payload,
|
|
90
|
-
headers=self.
|
|
100
|
+
headers=self._default_headers(),
|
|
91
101
|
allow_redirects=False,
|
|
92
102
|
)
|
|
93
103
|
try:
|
|
@@ -139,14 +149,19 @@ class BaseClient:
|
|
|
139
149
|
|
|
140
150
|
@dataclass
|
|
141
151
|
class OAuthClient(BaseClient):
|
|
142
|
-
client_id: str
|
|
143
|
-
client_secret: str
|
|
152
|
+
client_id: str | None = None
|
|
153
|
+
client_secret: str | None = None
|
|
144
154
|
_: dataclasses.KW_ONLY
|
|
145
|
-
base_url: str =
|
|
155
|
+
base_url: str = HH_OAUTH_URL
|
|
146
156
|
state: str = ""
|
|
147
157
|
scope: str = ""
|
|
148
158
|
redirect_uri: str = ""
|
|
149
159
|
|
|
160
|
+
def __post_init__(self) -> None:
|
|
161
|
+
super().__post_init__()
|
|
162
|
+
self.client_id = self.client_id or ANDROID_CLIENT_ID
|
|
163
|
+
self.client_secret = self.client_secret or ANDROID_CLIENT_SECRET
|
|
164
|
+
|
|
150
165
|
@property
|
|
151
166
|
def authorize_url(self) -> str:
|
|
152
167
|
params = dict(
|
|
@@ -197,7 +212,7 @@ class ApiClient(BaseClient):
|
|
|
197
212
|
_: dataclasses.KW_ONLY
|
|
198
213
|
client_id: str | None = None
|
|
199
214
|
client_secret: str | None = None
|
|
200
|
-
base_url: str =
|
|
215
|
+
base_url: str = HH_API_URL
|
|
201
216
|
|
|
202
217
|
@property
|
|
203
218
|
def is_access_expired(self) -> bool:
|
|
@@ -212,10 +227,10 @@ class ApiClient(BaseClient):
|
|
|
212
227
|
session=self.session,
|
|
213
228
|
)
|
|
214
229
|
|
|
215
|
-
def
|
|
230
|
+
def _default_headers(
|
|
216
231
|
self,
|
|
217
232
|
) -> dict[str, str]:
|
|
218
|
-
headers = super().
|
|
233
|
+
headers = super()._default_headers()
|
|
219
234
|
if not self.access_token:
|
|
220
235
|
return headers
|
|
221
236
|
# Это очень интересно, что 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,7 @@ 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 ai, api, utils
|
|
23
21
|
from .storage import StorageFacade
|
|
24
22
|
from .utils.log import setup_logger
|
|
25
23
|
from .utils.mixins import MegaTool
|
|
@@ -30,7 +28,6 @@ DEFAULT_CONFIG_DIR = utils.get_config_path() / (__package__ or "").replace(
|
|
|
30
28
|
DEFAULT_CONFIG_FILENAME = "config.json"
|
|
31
29
|
DEFAULT_LOG_FILENAME = "log.txt"
|
|
32
30
|
DEFAULT_DATABASE_FILENAME = "data"
|
|
33
|
-
DEFAULT_PROFILE_ID = "."
|
|
34
31
|
|
|
35
32
|
logger = logging.getLogger(__package__)
|
|
36
33
|
|
|
@@ -55,7 +52,6 @@ class BaseNamespace(argparse.Namespace):
|
|
|
55
52
|
delay: float
|
|
56
53
|
user_agent: str
|
|
57
54
|
proxy_url: str
|
|
58
|
-
disable_telemetry: bool
|
|
59
55
|
|
|
60
56
|
|
|
61
57
|
class HHApplicantTool(MegaTool):
|
|
@@ -90,19 +86,18 @@ class HHApplicantTool(MegaTool):
|
|
|
90
86
|
"--config",
|
|
91
87
|
help="Путь до директории с конфигом",
|
|
92
88
|
type=Path,
|
|
93
|
-
default=
|
|
89
|
+
default=None,
|
|
94
90
|
)
|
|
95
91
|
parser.add_argument(
|
|
96
92
|
"--profile-id",
|
|
97
93
|
"--profile",
|
|
98
|
-
help="Используемый профиль — подкаталог в --config-dir",
|
|
99
|
-
default=DEFAULT_PROFILE_ID,
|
|
94
|
+
help="Используемый профиль — подкаталог в --config-dir. Так же можно передать через переменную окружения HH_PROFILE_ID.",
|
|
100
95
|
)
|
|
101
96
|
parser.add_argument(
|
|
102
97
|
"-d",
|
|
98
|
+
"--api-delay",
|
|
103
99
|
"--delay",
|
|
104
100
|
type=float,
|
|
105
|
-
default=0.654,
|
|
106
101
|
help="Задержка между запросами к API HH по умолчанию",
|
|
107
102
|
)
|
|
108
103
|
parser.add_argument(
|
|
@@ -169,14 +164,20 @@ class HHApplicantTool(MegaTool):
|
|
|
169
164
|
session.verify = False
|
|
170
165
|
|
|
171
166
|
if proxies := self._get_proxies():
|
|
172
|
-
logger.info("Use proxies: %r", proxies)
|
|
167
|
+
logger.info("Use proxies for requests: %r", proxies)
|
|
173
168
|
session.proxies = proxies
|
|
174
169
|
|
|
175
170
|
return session
|
|
176
171
|
|
|
177
|
-
@
|
|
172
|
+
@cached_property
|
|
178
173
|
def config_path(self) -> Path:
|
|
179
|
-
return (
|
|
174
|
+
return (
|
|
175
|
+
(
|
|
176
|
+
self.args.config_dir
|
|
177
|
+
or Path(getenv("CONFIG_DIR", DEFAULT_CONFIG_DIR))
|
|
178
|
+
)
|
|
179
|
+
/ (self.args.profile_id or getenv("HH_PROFILE_ID", "."))
|
|
180
|
+
).resolve()
|
|
180
181
|
|
|
181
182
|
@cached_property
|
|
182
183
|
def config(self) -> utils.Config:
|
|
@@ -200,37 +201,35 @@ class HHApplicantTool(MegaTool):
|
|
|
200
201
|
return StorageFacade(self.db)
|
|
201
202
|
|
|
202
203
|
@cached_property
|
|
203
|
-
def api_client(self) -> ApiClient:
|
|
204
|
+
def api_client(self) -> api.client.ApiClient:
|
|
204
205
|
args = self.args
|
|
205
206
|
config = self.config
|
|
206
207
|
token = config.get("token", {})
|
|
207
|
-
api
|
|
208
|
-
client_id=config.get("client_id"
|
|
209
|
-
client_secret=config.get("client_id"
|
|
208
|
+
return api.client.ApiClient(
|
|
209
|
+
client_id=config.get("client_id"),
|
|
210
|
+
client_secret=config.get("client_id"),
|
|
210
211
|
access_token=token.get("access_token"),
|
|
211
212
|
refresh_token=token.get("refresh_token"),
|
|
212
213
|
access_expires_at=token.get("access_expires_at"),
|
|
213
|
-
delay=args.
|
|
214
|
-
user_agent=
|
|
214
|
+
delay=args.api_delay or config.get("api_delay"),
|
|
215
|
+
user_agent=args.user_agent or config.get("user_agent"),
|
|
215
216
|
session=self.session,
|
|
216
217
|
)
|
|
217
|
-
return api
|
|
218
218
|
|
|
219
|
-
def get_me(self) -> datatypes.User:
|
|
219
|
+
def get_me(self) -> api.datatypes.User:
|
|
220
220
|
return self.api_client.get("/me")
|
|
221
221
|
|
|
222
|
-
def get_resumes(self) ->
|
|
223
|
-
return self.api_client.get("/resumes/mine")
|
|
222
|
+
def get_resumes(self) -> list[api.datatypes.Resume]:
|
|
223
|
+
return self.api_client.get("/resumes/mine")["items"]
|
|
224
224
|
|
|
225
|
-
def first_resume_id(self):
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return resumes["items"][0]["id"]
|
|
225
|
+
def first_resume_id(self) -> str:
|
|
226
|
+
resume = self.get_resumes()[0]
|
|
227
|
+
return resume["id"]
|
|
229
228
|
|
|
230
229
|
def get_blacklisted(self) -> list[str]:
|
|
231
230
|
rv = []
|
|
232
231
|
for page in count():
|
|
233
|
-
r: datatypes.PaginatedItems[datatypes.EmployerShort] = (
|
|
232
|
+
r: api.datatypes.PaginatedItems[api.datatypes.EmployerShort] = (
|
|
234
233
|
self.api_client.get("/employers/blacklisted", page=page)
|
|
235
234
|
)
|
|
236
235
|
rv += [item["id"] for item in r["items"]]
|
|
@@ -240,7 +239,7 @@ class HHApplicantTool(MegaTool):
|
|
|
240
239
|
|
|
241
240
|
def get_negotiations(
|
|
242
241
|
self, status: str = "active"
|
|
243
|
-
) -> Iterable[datatypes.Negotiation]:
|
|
242
|
+
) -> Iterable[api.datatypes.Negotiation]:
|
|
244
243
|
for page in count():
|
|
245
244
|
r: dict[str, Any] = self.api_client.get(
|
|
246
245
|
"/negotiations",
|
|
@@ -259,6 +258,8 @@ class HHApplicantTool(MegaTool):
|
|
|
259
258
|
if page + 1 >= r.get("pages", 0):
|
|
260
259
|
break
|
|
261
260
|
|
|
261
|
+
# TODO: добавить еще методов или те удалить?
|
|
262
|
+
|
|
262
263
|
def save_token(self) -> bool:
|
|
263
264
|
if self.api_client.access_token != self.config.get("token", {}).get(
|
|
264
265
|
"access_token"
|
|
@@ -267,6 +268,20 @@ class HHApplicantTool(MegaTool):
|
|
|
267
268
|
return True
|
|
268
269
|
return False
|
|
269
270
|
|
|
271
|
+
def get_openai_chat(self, system_prompt: str) -> ai.ChatOpenAI:
|
|
272
|
+
c = self.config.get("openai", {})
|
|
273
|
+
if not (token := c.get("token")):
|
|
274
|
+
raise ValueError("Токен для OpenAI не задан")
|
|
275
|
+
return ai.ChatOpenAI(
|
|
276
|
+
token=token,
|
|
277
|
+
model=c.get("model"),
|
|
278
|
+
temperature=c.get("temperature", 0.7),
|
|
279
|
+
max_completion_tokens=c.get("max_completion_tokens", 1000),
|
|
280
|
+
system_prompt=system_prompt,
|
|
281
|
+
completion_endpoint=c.get("completion_endpoint"),
|
|
282
|
+
session=self.session,
|
|
283
|
+
)
|
|
284
|
+
|
|
270
285
|
def run(self) -> None | int:
|
|
271
286
|
verbosity_level = max(
|
|
272
287
|
logging.DEBUG,
|
|
@@ -275,19 +290,25 @@ class HHApplicantTool(MegaTool):
|
|
|
275
290
|
|
|
276
291
|
setup_logger(logger, verbosity_level, self.log_file)
|
|
277
292
|
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
logger.debug("Путь до профиля: %s", self.config_path)
|
|
294
|
+
|
|
295
|
+
utils.setup_terminal()
|
|
280
296
|
|
|
281
297
|
try:
|
|
282
298
|
if self.args.run:
|
|
283
299
|
try:
|
|
284
|
-
|
|
285
|
-
if self.save_token():
|
|
286
|
-
logger.info("Токен был обновлен.")
|
|
287
|
-
return res
|
|
300
|
+
return self.args.run(self)
|
|
288
301
|
except KeyboardInterrupt:
|
|
289
302
|
logger.warning("Выполнение прервано пользователем!")
|
|
290
|
-
|
|
303
|
+
except api.errors.CaptchaRequired as ex:
|
|
304
|
+
logger.error(f"Требуется ввод капчи: {ex.captcha_url}")
|
|
305
|
+
except api.errors.InternalServerError:
|
|
306
|
+
logger.error(
|
|
307
|
+
"Сервер HH.RU не смог обработать запрос из-за высокой"
|
|
308
|
+
" нагрузки или по иной причине"
|
|
309
|
+
)
|
|
310
|
+
except api.errors.Forbidden:
|
|
311
|
+
logger.error("Требуется авторизация")
|
|
291
312
|
except sqlite3.Error as ex:
|
|
292
313
|
logger.exception(ex)
|
|
293
314
|
|
|
@@ -297,17 +318,21 @@ class HHApplicantTool(MegaTool):
|
|
|
297
318
|
f"Возможно база данных повреждена, попробуйте выполнить команду:\n\n" # noqa: E501
|
|
298
319
|
f" {script_name} migrate-db"
|
|
299
320
|
)
|
|
300
|
-
return 1
|
|
301
321
|
except Exception as e:
|
|
302
322
|
logger.exception(e)
|
|
303
|
-
|
|
323
|
+
finally:
|
|
324
|
+
# Токен мог автоматически обновиться
|
|
325
|
+
if self.save_token():
|
|
326
|
+
logger.info("Токен был сохранен после обновления.")
|
|
327
|
+
return 1
|
|
304
328
|
self._parser.print_help(file=sys.stderr)
|
|
305
329
|
return 2
|
|
306
330
|
finally:
|
|
307
331
|
try:
|
|
308
|
-
self.
|
|
332
|
+
self._check_system()
|
|
309
333
|
except Exception:
|
|
310
334
|
pass
|
|
335
|
+
# raise
|
|
311
336
|
|
|
312
337
|
def _parse_args(self, argv) -> None:
|
|
313
338
|
self._parser = self._create_parser()
|