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
hh_applicant_tool/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import HHApplicantTool
|
hh_applicant_tool/__main__.py
CHANGED
hh_applicant_tool/ai/openai.py
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import ClassVar
|
|
2
4
|
|
|
3
5
|
import requests
|
|
4
6
|
|
|
7
|
+
from .base import AIError
|
|
8
|
+
|
|
5
9
|
logger = logging.getLogger(__package__)
|
|
6
10
|
|
|
7
11
|
|
|
8
|
-
class OpenAIError(
|
|
12
|
+
class OpenAIError(AIError):
|
|
9
13
|
pass
|
|
10
14
|
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
@dataclass
|
|
17
|
+
class ChatOpenAI:
|
|
18
|
+
chat_endpoint: ClassVar[str] = "https://api.openai.com/v1/chat/completions"
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
session: requests.Session | None = None
|
|
22
|
-
):
|
|
23
|
-
self.token = token
|
|
24
|
-
self.model = model
|
|
25
|
-
self.system_prompt = system_prompt
|
|
26
|
-
self.proxies = proxies
|
|
27
|
-
self.session = session or requests.session()
|
|
20
|
+
token: str
|
|
21
|
+
model: str
|
|
22
|
+
system_prompt: str | None = None
|
|
23
|
+
temperature: float = 0.7
|
|
24
|
+
max_completion_tokens: int = 1000
|
|
25
|
+
session: requests.Session = field(default_factory=requests.Session)
|
|
28
26
|
|
|
29
27
|
def default_headers(self) -> dict[str, str]:
|
|
30
28
|
return {
|
|
@@ -32,21 +30,14 @@ class OpenAIChat:
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
def send_message(self, message: str) -> str:
|
|
35
|
-
|
|
36
33
|
payload = {
|
|
37
34
|
"model": self.model,
|
|
38
35
|
"messages": [
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
"content": self.system_prompt
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"role": "user",
|
|
45
|
-
"content": message
|
|
46
|
-
}
|
|
36
|
+
{"role": "system", "content": self.system_prompt},
|
|
37
|
+
{"role": "user", "content": message},
|
|
47
38
|
],
|
|
48
|
-
"temperature":
|
|
49
|
-
"max_completion_tokens":
|
|
39
|
+
"temperature": self.temperature,
|
|
40
|
+
"max_completion_tokens": self.max_completion_tokens,
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
try:
|
|
@@ -54,18 +45,17 @@ class OpenAIChat:
|
|
|
54
45
|
self.chat_endpoint,
|
|
55
46
|
json=payload,
|
|
56
47
|
headers=self.default_headers(),
|
|
57
|
-
|
|
58
|
-
timeout=30
|
|
48
|
+
timeout=30,
|
|
59
49
|
)
|
|
60
50
|
response.raise_for_status()
|
|
61
51
|
|
|
62
52
|
data = response.json()
|
|
63
|
-
if
|
|
64
|
-
raise OpenAIError(data[
|
|
65
|
-
|
|
53
|
+
if "error" in data:
|
|
54
|
+
raise OpenAIError(data["error"]["message"])
|
|
55
|
+
|
|
66
56
|
assistant_message = data["choices"][0]["message"]["content"]
|
|
67
57
|
|
|
68
58
|
return assistant_message
|
|
69
59
|
|
|
70
60
|
except requests.exceptions.RequestException as ex:
|
|
71
|
-
raise OpenAIError(
|
|
61
|
+
raise OpenAIError(f"Network error: {ex}") from ex
|
hh_applicant_tool/api/client.py
CHANGED
|
@@ -7,21 +7,24 @@ import time
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from functools import cached_property
|
|
9
9
|
from threading import Lock
|
|
10
|
-
from typing import Any, Literal
|
|
11
|
-
from urllib.parse import urlencode
|
|
10
|
+
from typing import Any, Literal, TypeVar
|
|
11
|
+
from urllib.parse import urlencode, urljoin
|
|
12
12
|
|
|
13
13
|
import requests
|
|
14
|
-
from requests import
|
|
14
|
+
from requests import Session
|
|
15
15
|
|
|
16
|
-
from ..
|
|
16
|
+
from ..datatypes import AccessToken
|
|
17
17
|
from . import errors
|
|
18
18
|
|
|
19
19
|
__all__ = ("ApiClient", "OAuthClient")
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
DEFAULT_DELAY = 0.334
|
|
22
|
+
|
|
23
|
+
AllowedMethods = Literal["GET", "POST", "PUT", "DELETE"]
|
|
24
|
+
T = TypeVar("T")
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
logger = logging.getLogger(__package__)
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
# Thread-safe
|
|
@@ -30,39 +33,41 @@ class BaseClient:
|
|
|
30
33
|
base_url: str
|
|
31
34
|
_: dataclasses.KW_ONLY
|
|
32
35
|
user_agent: str | None = None
|
|
33
|
-
proxies: dict | None = None
|
|
34
36
|
session: Session | None = None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
delay: float = DEFAULT_DELAY
|
|
38
|
+
_previous_request_time: float = 0.0
|
|
37
39
|
|
|
38
40
|
def __post_init__(self) -> None:
|
|
41
|
+
assert self.base_url.endswith("/"), "base_url must end with /"
|
|
39
42
|
self.lock = Lock()
|
|
43
|
+
# logger.debug(f"user agent: {self.user_agent}")
|
|
40
44
|
if not self.session:
|
|
45
|
+
logger.debug("create new session")
|
|
41
46
|
self.session = requests.session()
|
|
42
|
-
if self.proxies:
|
|
43
|
-
|
|
47
|
+
# if self.proxies:
|
|
48
|
+
# logger.debug(f"client proxies: {self.proxies}")
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def proxies(self):
|
|
52
|
+
return self.session.proxies
|
|
44
53
|
|
|
45
54
|
def default_headers(self) -> dict[str, str]:
|
|
46
55
|
return {
|
|
47
|
-
"user-agent": self.user_agent
|
|
56
|
+
"user-agent": self.user_agent
|
|
57
|
+
or "Mozilla/5.0 (+https://github.com/s3rgeym/hh-applicant-tool)",
|
|
48
58
|
"x-hh-app-active": "true",
|
|
49
59
|
}
|
|
50
|
-
|
|
51
|
-
def additional_headers(
|
|
52
|
-
self,
|
|
53
|
-
) -> dict[str, str]:
|
|
54
|
-
return {}
|
|
55
60
|
|
|
56
61
|
def request(
|
|
57
62
|
self,
|
|
58
|
-
method:
|
|
63
|
+
method: AllowedMethods,
|
|
59
64
|
endpoint: str,
|
|
60
65
|
params: dict[str, Any] | None = None,
|
|
61
66
|
delay: float | None = None,
|
|
62
67
|
**kwargs: Any,
|
|
63
|
-
) ->
|
|
68
|
+
) -> T:
|
|
64
69
|
# Не знаю насколько это "правильно"
|
|
65
|
-
assert method in
|
|
70
|
+
assert method in AllowedMethods.__args__
|
|
66
71
|
params = dict(params or {})
|
|
67
72
|
params.update(kwargs)
|
|
68
73
|
url = self.resolve_url(endpoint)
|
|
@@ -71,20 +76,18 @@ class BaseClient:
|
|
|
71
76
|
if (
|
|
72
77
|
delay := (self.delay if delay is None else delay)
|
|
73
78
|
- time.monotonic()
|
|
74
|
-
+ self.
|
|
79
|
+
+ self._previous_request_time
|
|
75
80
|
) > 0:
|
|
76
81
|
logger.debug("wait %fs before request", delay)
|
|
77
82
|
time.sleep(delay)
|
|
78
83
|
has_body = method in ["POST", "PUT"]
|
|
79
84
|
payload = {"data" if has_body else "params": params}
|
|
80
|
-
headers =
|
|
81
|
-
logger.debug(f"request info: {method = }, {url = }, {headers = }, params = {repr(params)[:255]}")
|
|
85
|
+
# logger.debug(f"request info: {method = }, {url = }, {headers = }, params = {repr(params)[:255]}")
|
|
82
86
|
response = self.session.request(
|
|
83
87
|
method,
|
|
84
88
|
url,
|
|
85
89
|
**payload,
|
|
86
|
-
headers=
|
|
87
|
-
proxies=self.proxies,
|
|
90
|
+
headers=self.default_headers(),
|
|
88
91
|
allow_redirects=False,
|
|
89
92
|
)
|
|
90
93
|
try:
|
|
@@ -102,55 +105,36 @@ class BaseClient:
|
|
|
102
105
|
) from ex
|
|
103
106
|
finally:
|
|
104
107
|
log_url = url
|
|
105
|
-
if not has_body
|
|
106
|
-
|
|
108
|
+
if params and not has_body:
|
|
109
|
+
encoded_params = urlencode(params)
|
|
110
|
+
log_url += ("?", "&")["?" in url] + encoded_params
|
|
107
111
|
logger.debug(
|
|
108
|
-
"%
|
|
112
|
+
"%d %s %.1000s",
|
|
113
|
+
response.status_code,
|
|
109
114
|
method,
|
|
110
115
|
log_url,
|
|
111
|
-
response.status_code,
|
|
112
116
|
)
|
|
113
|
-
self.
|
|
114
|
-
|
|
117
|
+
self._previous_request_time = time.monotonic()
|
|
118
|
+
errors.ApiError.raise_for_status(response, rv)
|
|
115
119
|
assert 300 > response.status_code >= 200, (
|
|
116
120
|
f"Unexpected status code for {method} {url}: {response.status_code}"
|
|
117
121
|
)
|
|
118
122
|
return rv
|
|
119
123
|
|
|
120
|
-
def get(self, *args, **kwargs):
|
|
124
|
+
def get(self, *args, **kwargs) -> T:
|
|
121
125
|
return self.request("GET", *args, **kwargs)
|
|
122
126
|
|
|
123
|
-
def post(self, *args, **kwargs):
|
|
127
|
+
def post(self, *args, **kwargs) -> T:
|
|
124
128
|
return self.request("POST", *args, **kwargs)
|
|
125
129
|
|
|
126
|
-
def put(self, *args, **kwargs):
|
|
130
|
+
def put(self, *args, **kwargs) -> T:
|
|
127
131
|
return self.request("PUT", *args, **kwargs)
|
|
128
132
|
|
|
129
|
-
def delete(self, *args, **kwargs):
|
|
133
|
+
def delete(self, *args, **kwargs) -> T:
|
|
130
134
|
return self.request("DELETE", *args, **kwargs)
|
|
131
135
|
|
|
132
136
|
def resolve_url(self, url: str) -> str:
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
@staticmethod
|
|
136
|
-
def raise_for_status(response: Response, data: dict) -> None:
|
|
137
|
-
match response.status_code:
|
|
138
|
-
case 301 | 302:
|
|
139
|
-
raise errors.Redirect(response, data)
|
|
140
|
-
case 400:
|
|
141
|
-
if errors.ApiError.is_limit_exceeded(data):
|
|
142
|
-
raise errors.LimitExceeded(response=response, data=data)
|
|
143
|
-
raise errors.BadRequest(response, data)
|
|
144
|
-
case 403:
|
|
145
|
-
raise errors.Forbidden(response, data)
|
|
146
|
-
case 404:
|
|
147
|
-
raise errors.ResourceNotFound(response, data)
|
|
148
|
-
case status if 500 > status >= 400:
|
|
149
|
-
raise errors.ClientError(response, data)
|
|
150
|
-
case 502:
|
|
151
|
-
raise errors.BadGateway(response, data)
|
|
152
|
-
case status if status >= 500:
|
|
153
|
-
raise errors.InternalServerError(response, data)
|
|
137
|
+
return urljoin(self.base_url, url.lstrip("/"))
|
|
154
138
|
|
|
155
139
|
|
|
156
140
|
@dataclass
|
|
@@ -158,7 +142,7 @@ class OAuthClient(BaseClient):
|
|
|
158
142
|
client_id: str
|
|
159
143
|
client_secret: str
|
|
160
144
|
_: dataclasses.KW_ONLY
|
|
161
|
-
base_url: str = "https://hh.ru/oauth"
|
|
145
|
+
base_url: str = "https://hh.ru/oauth/"
|
|
162
146
|
state: str = ""
|
|
163
147
|
scope: str = ""
|
|
164
148
|
redirect_uri: str = ""
|
|
@@ -225,30 +209,32 @@ class ApiClient(BaseClient):
|
|
|
225
209
|
client_id=self.client_id,
|
|
226
210
|
client_secret=self.client_secret,
|
|
227
211
|
user_agent=self.user_agent,
|
|
228
|
-
proxies=dict(self.proxies or {}),
|
|
229
212
|
session=self.session,
|
|
230
213
|
)
|
|
231
214
|
|
|
232
|
-
def
|
|
215
|
+
def default_headers(
|
|
233
216
|
self,
|
|
234
217
|
) -> dict[str, str]:
|
|
218
|
+
headers = super().default_headers()
|
|
235
219
|
if not self.access_token:
|
|
236
|
-
return
|
|
220
|
+
return headers
|
|
237
221
|
# Это очень интересно, что access token'ы начинаются с USER, т.е. API может содержать какую-то уязвимость, связанную с этим
|
|
238
222
|
assert self.access_token.startswith("USER")
|
|
239
|
-
return {"authorization": f"Bearer {self.access_token}"}
|
|
223
|
+
return headers | {"authorization": f"Bearer {self.access_token}"}
|
|
240
224
|
|
|
241
225
|
# Реализовано автоматическое обновление токена
|
|
242
226
|
def request(
|
|
243
227
|
self,
|
|
244
|
-
method:
|
|
228
|
+
method: AllowedMethods,
|
|
245
229
|
endpoint: str,
|
|
246
230
|
params: dict[str, Any] | None = None,
|
|
247
231
|
delay: float | None = None,
|
|
248
232
|
**kwargs: Any,
|
|
249
|
-
) ->
|
|
233
|
+
) -> T:
|
|
250
234
|
def do_request():
|
|
251
|
-
return BaseClient.request(
|
|
235
|
+
return BaseClient.request(
|
|
236
|
+
self, method, endpoint, params, delay, **kwargs
|
|
237
|
+
)
|
|
252
238
|
|
|
253
239
|
try:
|
|
254
240
|
return do_request()
|
hh_applicant_tool/api/errors.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import Any, Type
|
|
3
5
|
|
|
4
6
|
from requests import Request, Response
|
|
5
7
|
from requests.adapters import CaseInsensitiveDict
|
|
@@ -24,11 +26,11 @@ class BadResponse(Exception):
|
|
|
24
26
|
class ApiError(BadResponse):
|
|
25
27
|
def __init__(self, response: Response, data: dict[str, Any]) -> None:
|
|
26
28
|
self._response = response
|
|
27
|
-
self.
|
|
29
|
+
self._data = data
|
|
28
30
|
|
|
29
31
|
@property
|
|
30
32
|
def data(self) -> dict:
|
|
31
|
-
return self.
|
|
33
|
+
return self._data
|
|
32
34
|
|
|
33
35
|
@property
|
|
34
36
|
def request(self) -> Request:
|
|
@@ -42,6 +44,10 @@ class ApiError(BadResponse):
|
|
|
42
44
|
def response_headers(self) -> CaseInsensitiveDict:
|
|
43
45
|
return self._response.headers
|
|
44
46
|
|
|
47
|
+
@property
|
|
48
|
+
def message(self) -> str:
|
|
49
|
+
return self._data.get("description") or str(self._data)
|
|
50
|
+
|
|
45
51
|
# def __getattr__(self, name: str) -> Any:
|
|
46
52
|
# try:
|
|
47
53
|
# return self._raw[name]
|
|
@@ -49,11 +55,33 @@ class ApiError(BadResponse):
|
|
|
49
55
|
# raise AttributeError(name) from ex
|
|
50
56
|
|
|
51
57
|
def __str__(self) -> str:
|
|
52
|
-
return
|
|
58
|
+
return self.message
|
|
53
59
|
|
|
54
60
|
@staticmethod
|
|
55
|
-
def
|
|
56
|
-
return any(
|
|
61
|
+
def has_error_value(value: str, data: dict) -> bool:
|
|
62
|
+
return any(v.get("value") == value for v in data.get("errors", []))
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def raise_for_status(cls: Type[ApiError], response: Response, data: dict) -> None:
|
|
66
|
+
match response.status_code:
|
|
67
|
+
case status if 300 <= status <= 308:
|
|
68
|
+
raise Redirect(response, data)
|
|
69
|
+
case 400:
|
|
70
|
+
if cls.has_error_value("limit_exceeded", data):
|
|
71
|
+
raise LimitExceeded(response, data)
|
|
72
|
+
raise BadRequest(response, data)
|
|
73
|
+
case 403:
|
|
74
|
+
if cls.has_error_value("captcha_required", data):
|
|
75
|
+
raise CaptchaRequired(response, data)
|
|
76
|
+
raise Forbidden(response, data)
|
|
77
|
+
case 404:
|
|
78
|
+
raise ResourceNotFound(response, data)
|
|
79
|
+
case status if 500 > status >= 400:
|
|
80
|
+
raise ClientError(response, data)
|
|
81
|
+
case 502:
|
|
82
|
+
raise BadGateway(response, data)
|
|
83
|
+
case status if status >= 500:
|
|
84
|
+
raise InternalServerError(response, data)
|
|
57
85
|
|
|
58
86
|
|
|
59
87
|
class Redirect(ApiError):
|
|
@@ -76,6 +104,22 @@ class Forbidden(ClientError):
|
|
|
76
104
|
pass
|
|
77
105
|
|
|
78
106
|
|
|
107
|
+
class CaptchaRequired(ClientError):
|
|
108
|
+
@cached_property
|
|
109
|
+
def captcha_url(self) -> str:
|
|
110
|
+
return next(
|
|
111
|
+
filter(
|
|
112
|
+
lambda v: v["value"] == "captcha_required",
|
|
113
|
+
self._data["errors"],
|
|
114
|
+
),
|
|
115
|
+
{},
|
|
116
|
+
).get("captcha_url")
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def message(self) -> str:
|
|
120
|
+
return f"Captcha required: {self.captcha_url}"
|
|
121
|
+
|
|
122
|
+
|
|
79
123
|
class ResourceNotFound(ClientError):
|
|
80
124
|
pass
|
|
81
125
|
|
hh_applicant_tool/constants.py
CHANGED
|
@@ -9,6 +9,3 @@ ANDROID_CLIENT_SECRET = (
|
|
|
9
9
|
# Используется для прямой авторизации. Этот способ мной не используется, так как
|
|
10
10
|
# для отображения капчи все равно нужен webview.
|
|
11
11
|
# K811HJNKQA8V1UN53I6PN1J1CMAD2L1M3LU6LPAU849BCT031KDSSM485FDPJ6UF
|
|
12
|
-
|
|
13
|
-
# Кривой формат, который используют эти долбоебы
|
|
14
|
-
INVALID_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|