hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hh_applicant_tool/__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 +25 -35
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +65 -68
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
- hh_applicant_tool/api/datatypes.py +293 -0
- hh_applicant_tool/api/errors.py +57 -7
- hh_applicant_tool/api/user_agent.py +17 -0
- hh_applicant_tool/main.py +234 -113
- hh_applicant_tool/operations/apply_similar.py +353 -371
- hh_applicant_tool/operations/authorize.py +313 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/clear_negotiations.py +90 -82
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +23 -11
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +122 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +201 -180
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +23 -11
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +8 -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/contacts.py +28 -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 +132 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +230 -0
- hh_applicant_tool/storage/repositories/contacts.py +14 -0
- hh_applicant_tool/storage/repositories/employers.py +14 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +13 -0
- hh_applicant_tool/storage/repositories/resumes.py +9 -0
- hh_applicant_tool/storage/repositories/settings.py +35 -0
- hh_applicant_tool/storage/repositories/vacancies.py +9 -0
- hh_applicant_tool/storage/utils.py +40 -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/date.py +19 -0
- hh_applicant_tool/utils/json.py +61 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/log.py +147 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +221 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +32 -0
- hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
- hh_applicant_tool-1.4.12.dist-info/RECORD +68 -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/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.12.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.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,71 +1,61 @@
|
|
|
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
|
-
def
|
|
27
|
+
def _default_headers(self) -> dict[str, str]:
|
|
30
28
|
return {
|
|
31
29
|
"Authorization": f"Bearer {self.token}",
|
|
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:
|
|
53
44
|
response = self.session.post(
|
|
54
45
|
self.chat_endpoint,
|
|
55
46
|
json=payload,
|
|
56
|
-
headers=self.
|
|
57
|
-
|
|
58
|
-
timeout=30
|
|
47
|
+
headers=self._default_headers(),
|
|
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,31 @@ 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 ..types import AccessToken
|
|
17
16
|
from . import errors
|
|
17
|
+
from .client_keys import (
|
|
18
|
+
ANDROID_CLIENT_ID,
|
|
19
|
+
ANDROID_CLIENT_SECRET,
|
|
20
|
+
)
|
|
21
|
+
from .datatypes import AccessToken
|
|
22
|
+
from .user_agent import generate_android_useragent
|
|
18
23
|
|
|
19
24
|
__all__ = ("ApiClient", "OAuthClient")
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
HH_API_URL = "https://api.hh.ru/"
|
|
27
|
+
HH_OAUTH_URL = "https://hh.ru/oauth/"
|
|
28
|
+
DEFAULT_DELAY = 0.334
|
|
29
|
+
|
|
30
|
+
AllowedMethods = Literal["GET", "POST", "PUT", "DELETE"]
|
|
31
|
+
T = TypeVar("T")
|
|
22
32
|
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
logger = logging.getLogger(__package__)
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
# Thread-safe
|
|
@@ -30,39 +40,40 @@ class BaseClient:
|
|
|
30
40
|
base_url: str
|
|
31
41
|
_: dataclasses.KW_ONLY
|
|
32
42
|
user_agent: str | None = None
|
|
33
|
-
proxies: dict | None = None
|
|
34
43
|
session: Session | None = None
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
delay: float = DEFAULT_DELAY
|
|
45
|
+
_previous_request_time: float = 0.0
|
|
37
46
|
|
|
38
47
|
def __post_init__(self) -> None:
|
|
48
|
+
assert self.base_url.endswith("/"), "base_url must ends with /"
|
|
39
49
|
self.lock = Lock()
|
|
50
|
+
# logger.debug(f"user agent: {self.user_agent}")
|
|
40
51
|
if not self.session:
|
|
52
|
+
logger.debug("create new session")
|
|
41
53
|
self.session = requests.session()
|
|
42
|
-
if self.proxies:
|
|
43
|
-
|
|
54
|
+
# if self.proxies:
|
|
55
|
+
# logger.debug(f"client proxies: {self.proxies}")
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
@property
|
|
58
|
+
def proxies(self):
|
|
59
|
+
return self.session.proxies
|
|
60
|
+
|
|
61
|
+
def _default_headers(self) -> dict[str, str]:
|
|
46
62
|
return {
|
|
47
|
-
"user-agent": self.user_agent or
|
|
63
|
+
"user-agent": self.user_agent or generate_android_useragent(),
|
|
48
64
|
"x-hh-app-active": "true",
|
|
49
65
|
}
|
|
50
|
-
|
|
51
|
-
def additional_headers(
|
|
52
|
-
self,
|
|
53
|
-
) -> dict[str, str]:
|
|
54
|
-
return {}
|
|
55
66
|
|
|
56
67
|
def request(
|
|
57
68
|
self,
|
|
58
|
-
method:
|
|
69
|
+
method: AllowedMethods,
|
|
59
70
|
endpoint: str,
|
|
60
71
|
params: dict[str, Any] | None = None,
|
|
61
72
|
delay: float | None = None,
|
|
62
73
|
**kwargs: Any,
|
|
63
|
-
) ->
|
|
74
|
+
) -> T:
|
|
64
75
|
# Не знаю насколько это "правильно"
|
|
65
|
-
assert method in
|
|
76
|
+
assert method in AllowedMethods.__args__
|
|
66
77
|
params = dict(params or {})
|
|
67
78
|
params.update(kwargs)
|
|
68
79
|
url = self.resolve_url(endpoint)
|
|
@@ -71,20 +82,18 @@ class BaseClient:
|
|
|
71
82
|
if (
|
|
72
83
|
delay := (self.delay if delay is None else delay)
|
|
73
84
|
- time.monotonic()
|
|
74
|
-
+ self.
|
|
85
|
+
+ self._previous_request_time
|
|
75
86
|
) > 0:
|
|
76
87
|
logger.debug("wait %fs before request", delay)
|
|
77
88
|
time.sleep(delay)
|
|
78
89
|
has_body = method in ["POST", "PUT"]
|
|
79
90
|
payload = {"data" if has_body else "params": params}
|
|
80
|
-
headers =
|
|
81
|
-
logger.debug(f"request info: {method = }, {url = }, {headers = }, params = {repr(params)[:255]}")
|
|
91
|
+
# logger.debug(f"request info: {method = }, {url = }, {headers = }, params = {repr(params)[:255]}")
|
|
82
92
|
response = self.session.request(
|
|
83
93
|
method,
|
|
84
94
|
url,
|
|
85
95
|
**payload,
|
|
86
|
-
headers=
|
|
87
|
-
proxies=self.proxies,
|
|
96
|
+
headers=self._default_headers(),
|
|
88
97
|
allow_redirects=False,
|
|
89
98
|
)
|
|
90
99
|
try:
|
|
@@ -102,67 +111,53 @@ class BaseClient:
|
|
|
102
111
|
) from ex
|
|
103
112
|
finally:
|
|
104
113
|
log_url = url
|
|
105
|
-
if not has_body
|
|
106
|
-
|
|
114
|
+
if params and not has_body:
|
|
115
|
+
encoded_params = urlencode(params)
|
|
116
|
+
log_url += ("?", "&")["?" in url] + encoded_params
|
|
107
117
|
logger.debug(
|
|
108
|
-
"%
|
|
118
|
+
"%d %s %.1000s",
|
|
119
|
+
response.status_code,
|
|
109
120
|
method,
|
|
110
121
|
log_url,
|
|
111
|
-
response.status_code,
|
|
112
122
|
)
|
|
113
|
-
self.
|
|
114
|
-
|
|
123
|
+
self._previous_request_time = time.monotonic()
|
|
124
|
+
errors.ApiError.raise_for_status(response, rv)
|
|
115
125
|
assert 300 > response.status_code >= 200, (
|
|
116
126
|
f"Unexpected status code for {method} {url}: {response.status_code}"
|
|
117
127
|
)
|
|
118
128
|
return rv
|
|
119
129
|
|
|
120
|
-
def get(self, *args, **kwargs):
|
|
130
|
+
def get(self, *args, **kwargs) -> T:
|
|
121
131
|
return self.request("GET", *args, **kwargs)
|
|
122
132
|
|
|
123
|
-
def post(self, *args, **kwargs):
|
|
133
|
+
def post(self, *args, **kwargs) -> T:
|
|
124
134
|
return self.request("POST", *args, **kwargs)
|
|
125
135
|
|
|
126
|
-
def put(self, *args, **kwargs):
|
|
136
|
+
def put(self, *args, **kwargs) -> T:
|
|
127
137
|
return self.request("PUT", *args, **kwargs)
|
|
128
138
|
|
|
129
|
-
def delete(self, *args, **kwargs):
|
|
139
|
+
def delete(self, *args, **kwargs) -> T:
|
|
130
140
|
return self.request("DELETE", *args, **kwargs)
|
|
131
141
|
|
|
132
142
|
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)
|
|
143
|
+
return urljoin(self.base_url, url.lstrip("/"))
|
|
154
144
|
|
|
155
145
|
|
|
156
146
|
@dataclass
|
|
157
147
|
class OAuthClient(BaseClient):
|
|
158
|
-
client_id: str
|
|
159
|
-
client_secret: str
|
|
148
|
+
client_id: str | None = None
|
|
149
|
+
client_secret: str | None = None
|
|
160
150
|
_: dataclasses.KW_ONLY
|
|
161
|
-
base_url: str =
|
|
151
|
+
base_url: str = HH_OAUTH_URL
|
|
162
152
|
state: str = ""
|
|
163
153
|
scope: str = ""
|
|
164
154
|
redirect_uri: str = ""
|
|
165
155
|
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
super().__post_init__()
|
|
158
|
+
self.client_id = self.client_id or ANDROID_CLIENT_ID
|
|
159
|
+
self.client_secret = self.client_secret or ANDROID_CLIENT_SECRET
|
|
160
|
+
|
|
166
161
|
@property
|
|
167
162
|
def authorize_url(self) -> str:
|
|
168
163
|
params = dict(
|
|
@@ -213,7 +208,7 @@ class ApiClient(BaseClient):
|
|
|
213
208
|
_: dataclasses.KW_ONLY
|
|
214
209
|
client_id: str | None = None
|
|
215
210
|
client_secret: str | None = None
|
|
216
|
-
base_url: str =
|
|
211
|
+
base_url: str = HH_API_URL
|
|
217
212
|
|
|
218
213
|
@property
|
|
219
214
|
def is_access_expired(self) -> bool:
|
|
@@ -225,30 +220,32 @@ class ApiClient(BaseClient):
|
|
|
225
220
|
client_id=self.client_id,
|
|
226
221
|
client_secret=self.client_secret,
|
|
227
222
|
user_agent=self.user_agent,
|
|
228
|
-
proxies=dict(self.proxies or {}),
|
|
229
223
|
session=self.session,
|
|
230
224
|
)
|
|
231
225
|
|
|
232
|
-
def
|
|
226
|
+
def _default_headers(
|
|
233
227
|
self,
|
|
234
228
|
) -> dict[str, str]:
|
|
229
|
+
headers = super()._default_headers()
|
|
235
230
|
if not self.access_token:
|
|
236
|
-
return
|
|
231
|
+
return headers
|
|
237
232
|
# Это очень интересно, что access token'ы начинаются с USER, т.е. API может содержать какую-то уязвимость, связанную с этим
|
|
238
233
|
assert self.access_token.startswith("USER")
|
|
239
|
-
return {"authorization": f"Bearer {self.access_token}"}
|
|
234
|
+
return headers | {"authorization": f"Bearer {self.access_token}"}
|
|
240
235
|
|
|
241
236
|
# Реализовано автоматическое обновление токена
|
|
242
237
|
def request(
|
|
243
238
|
self,
|
|
244
|
-
method:
|
|
239
|
+
method: AllowedMethods,
|
|
245
240
|
endpoint: str,
|
|
246
241
|
params: dict[str, Any] | None = None,
|
|
247
242
|
delay: float | None = None,
|
|
248
243
|
**kwargs: Any,
|
|
249
|
-
) ->
|
|
244
|
+
) -> T:
|
|
250
245
|
def do_request():
|
|
251
|
-
return BaseClient.request(
|
|
246
|
+
return BaseClient.request(
|
|
247
|
+
self, method, endpoint, params, delay, **kwargs
|
|
248
|
+
)
|
|
252
249
|
|
|
253
250
|
try:
|
|
254
251
|
return do_request()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
ANDROID_CLIENT_ID = (
|
|
2
|
+
"HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
|
|
3
|
+
)
|
|
4
4
|
|
|
5
5
|
ANDROID_CLIENT_SECRET = (
|
|
6
6
|
"V9M870DE342BGHFRUJ5FTCGCUA1482AN0DI8C5TFI9ULMA89H10N60NOP8I4JMVS"
|
|
@@ -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"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Generic, List, Literal, Optional, TypedDict, TypeVar
|
|
4
|
+
|
|
5
|
+
NegotiationStateId = Literal[
|
|
6
|
+
"discard", # отказ
|
|
7
|
+
"interview", # собес
|
|
8
|
+
"response", # отклик
|
|
9
|
+
"invitation", # приглашение
|
|
10
|
+
"hired", # выход на работу
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AccessToken(TypedDict):
|
|
15
|
+
access_token: str
|
|
16
|
+
refresh_token: str
|
|
17
|
+
expires_in: int
|
|
18
|
+
token_type: Literal["bearer"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Item = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PaginatedItems(TypedDict, Generic[Item]):
|
|
25
|
+
items: list[Item]
|
|
26
|
+
found: int
|
|
27
|
+
page: int
|
|
28
|
+
pages: int
|
|
29
|
+
per_page: int
|
|
30
|
+
# Это не все поля
|
|
31
|
+
clusters: Optional[Any]
|
|
32
|
+
arguments: Optional[Any]
|
|
33
|
+
fixes: Optional[Any]
|
|
34
|
+
suggests: Optional[Any]
|
|
35
|
+
# Это выглядит как глюк. Я нейронке скармливал выхлоп апи, а она писала эти
|
|
36
|
+
# типы
|
|
37
|
+
alternate_url: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IdName(TypedDict):
|
|
41
|
+
id: str
|
|
42
|
+
name: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Snippet(TypedDict):
|
|
46
|
+
requirement: Optional[str]
|
|
47
|
+
responsibility: Optional[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ManagerActivity(TypedDict):
|
|
51
|
+
last_activity_at: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Salary = TypedDict(
|
|
55
|
+
"Salary",
|
|
56
|
+
{
|
|
57
|
+
"from": Optional[int],
|
|
58
|
+
"to": Optional[int],
|
|
59
|
+
"currency": str,
|
|
60
|
+
"gross": bool,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
SalaryRange = TypedDict(
|
|
65
|
+
"SalaryRange",
|
|
66
|
+
{
|
|
67
|
+
"from": Optional[int],
|
|
68
|
+
"to": Optional[int],
|
|
69
|
+
"currency": str,
|
|
70
|
+
"gross": bool,
|
|
71
|
+
"mode": IdName,
|
|
72
|
+
"frequency": IdName,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
LogoUrls = TypedDict(
|
|
78
|
+
"LogoUrls",
|
|
79
|
+
{
|
|
80
|
+
"original": str,
|
|
81
|
+
"90": str,
|
|
82
|
+
"240": str,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class EmployerShort(TypedDict):
|
|
88
|
+
id: str
|
|
89
|
+
name: str
|
|
90
|
+
url: str
|
|
91
|
+
alternate_url: str
|
|
92
|
+
logo_urls: Optional[LogoUrls]
|
|
93
|
+
vacancies_url: str
|
|
94
|
+
accredited_it_employer: bool
|
|
95
|
+
trusted: bool
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SearchEmployer(EmployerShort):
|
|
99
|
+
country_id: Optional[int]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class NegotiationEmployer(EmployerShort):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class VacancyShort(TypedDict):
|
|
107
|
+
id: str
|
|
108
|
+
premium: bool
|
|
109
|
+
name: str
|
|
110
|
+
department: Optional[dict]
|
|
111
|
+
has_test: bool
|
|
112
|
+
# HH API fields
|
|
113
|
+
response_letter_required: bool
|
|
114
|
+
area: IdName
|
|
115
|
+
salary: Optional[Salary]
|
|
116
|
+
salary_range: Optional[SalaryRange]
|
|
117
|
+
type: IdName
|
|
118
|
+
address: Optional[dict]
|
|
119
|
+
response_url: Optional[str]
|
|
120
|
+
sort_point_distance: Optional[float]
|
|
121
|
+
published_at: str
|
|
122
|
+
created_at: str
|
|
123
|
+
archived: bool
|
|
124
|
+
apply_alternate_url: str
|
|
125
|
+
show_contacts: bool
|
|
126
|
+
benefits: List[Any]
|
|
127
|
+
insider_interview: Optional[dict]
|
|
128
|
+
url: str
|
|
129
|
+
alternate_url: str
|
|
130
|
+
professional_roles: List[IdName]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class NegotiationVacancy(VacancyShort):
|
|
134
|
+
employer: NegotiationEmployer
|
|
135
|
+
show_logo_in_search: Optional[bool]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SearchVacancy(VacancyShort):
|
|
139
|
+
employer: SearchEmployer
|
|
140
|
+
relations: List[Any]
|
|
141
|
+
experimental_modes: List[str]
|
|
142
|
+
manager_activity: Optional[ManagerActivity]
|
|
143
|
+
snippet: Snippet
|
|
144
|
+
contacts: Optional[dict]
|
|
145
|
+
schedule: IdName
|
|
146
|
+
working_days: List[Any]
|
|
147
|
+
working_time_intervals: List[Any]
|
|
148
|
+
working_time_modes: List[Any]
|
|
149
|
+
accept_temporary: bool
|
|
150
|
+
fly_in_fly_out_duration: List[Any]
|
|
151
|
+
work_format: List[IdName]
|
|
152
|
+
working_hours: List[IdName]
|
|
153
|
+
work_schedule_by_days: List[IdName]
|
|
154
|
+
accept_labor_contract: bool
|
|
155
|
+
civil_law_contracts: List[Any]
|
|
156
|
+
night_shifts: bool
|
|
157
|
+
accept_incomplete_resumes: bool
|
|
158
|
+
experience: IdName
|
|
159
|
+
employment: IdName
|
|
160
|
+
employment_form: IdName
|
|
161
|
+
internship: bool
|
|
162
|
+
adv_response_url: Optional[str]
|
|
163
|
+
is_adv_vacancy: bool
|
|
164
|
+
adv_context: Optional[dict]
|
|
165
|
+
allow_chat_with_manager: bool
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Phone(TypedDict):
|
|
169
|
+
country: str
|
|
170
|
+
city: str
|
|
171
|
+
number: str
|
|
172
|
+
formatted: str
|
|
173
|
+
comment: Optional[str]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ContactData(TypedDict):
|
|
177
|
+
name: Optional[str]
|
|
178
|
+
email: Optional[str]
|
|
179
|
+
phones: List[Phone]
|
|
180
|
+
call_tracking_enabled: bool
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ResumeShort(TypedDict):
|
|
184
|
+
id: str
|
|
185
|
+
title: str
|
|
186
|
+
url: str
|
|
187
|
+
alternate_url: str
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ResumeCounters(TypedDict):
|
|
191
|
+
total_views: int
|
|
192
|
+
new_views: int
|
|
193
|
+
invitations: int
|
|
194
|
+
new_invitations: int
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Resume(ResumeShort):
|
|
198
|
+
status: IdName
|
|
199
|
+
created_at: str
|
|
200
|
+
updated_at: str
|
|
201
|
+
can_publish_or_update: bool
|
|
202
|
+
counters: ResumeCounters
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class UserCounters(TypedDict):
|
|
206
|
+
resumes_count: int
|
|
207
|
+
new_resume_views: int
|
|
208
|
+
unread_negotiations: int
|
|
209
|
+
# ... and more
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class User(TypedDict):
|
|
213
|
+
id: int
|
|
214
|
+
first_name: str
|
|
215
|
+
last_name: str
|
|
216
|
+
middle_name: Optional[str]
|
|
217
|
+
email: Optional[str]
|
|
218
|
+
phone: Optional[str]
|
|
219
|
+
is_applicant: bool
|
|
220
|
+
is_employer: bool
|
|
221
|
+
is_admin: bool
|
|
222
|
+
is_anonymous: bool
|
|
223
|
+
is_application: bool
|
|
224
|
+
counters: UserCounters
|
|
225
|
+
# ... and more
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Message(TypedDict):
|
|
229
|
+
id: str
|
|
230
|
+
text: str
|
|
231
|
+
author: dict # Could be more specific, e.g. Participant(TypedDict)
|
|
232
|
+
created_at: str
|
|
233
|
+
viewed_by_opponent: bool
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class Counters(TypedDict):
|
|
237
|
+
messages: int
|
|
238
|
+
unread_messages: int
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class ChatStates(TypedDict):
|
|
242
|
+
# response_reminder_state: {"allowed": bool}
|
|
243
|
+
response_reminder_state: dict[str, bool]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class NegotiaionState(IdName):
|
|
247
|
+
id: NegotiationStateId
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Negotiation(TypedDict):
|
|
251
|
+
id: str
|
|
252
|
+
state: IdName
|
|
253
|
+
created_at: str
|
|
254
|
+
updated_at: str
|
|
255
|
+
resume: ResumeShort
|
|
256
|
+
viewed_by_opponent: bool
|
|
257
|
+
has_updates: bool
|
|
258
|
+
messages_url: str
|
|
259
|
+
url: str
|
|
260
|
+
counters: Counters
|
|
261
|
+
chat_states: ChatStates
|
|
262
|
+
source: str
|
|
263
|
+
chat_id: int
|
|
264
|
+
messaging_status: str
|
|
265
|
+
decline_allowed: bool
|
|
266
|
+
read: bool
|
|
267
|
+
has_new_messages: bool
|
|
268
|
+
applicant_question_state: bool
|
|
269
|
+
hidden: bool
|
|
270
|
+
vacancy: NegotiationVacancy
|
|
271
|
+
tags: List[Any]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class EmployerApplicantServices(TypedDict):
|
|
275
|
+
target_employer: dict[str, int]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class Employer(EmployerShort):
|
|
279
|
+
has_divisions: bool
|
|
280
|
+
type: str
|
|
281
|
+
description: Optional[str]
|
|
282
|
+
site_url: str
|
|
283
|
+
relations: List[Any]
|
|
284
|
+
area: IdName
|
|
285
|
+
country_code: str
|
|
286
|
+
industries: List[Any]
|
|
287
|
+
is_identified_by_esia: bool
|
|
288
|
+
badges: List[Any]
|
|
289
|
+
branded_description: Optional[str]
|
|
290
|
+
branding: Optional[dict]
|
|
291
|
+
insider_interviews: List[Any]
|
|
292
|
+
open_vacancies: int
|
|
293
|
+
applicant_services: EmployerApplicantServices
|