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.
Files changed (76) hide show
  1. hh_applicant_tool/__init__.py +1 -0
  2. hh_applicant_tool/__main__.py +1 -1
  3. hh_applicant_tool/ai/base.py +2 -0
  4. hh_applicant_tool/ai/openai.py +25 -35
  5. hh_applicant_tool/api/__init__.py +4 -2
  6. hh_applicant_tool/api/client.py +65 -68
  7. hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
  8. hh_applicant_tool/api/datatypes.py +293 -0
  9. hh_applicant_tool/api/errors.py +57 -7
  10. hh_applicant_tool/api/user_agent.py +17 -0
  11. hh_applicant_tool/main.py +234 -113
  12. hh_applicant_tool/operations/apply_similar.py +353 -371
  13. hh_applicant_tool/operations/authorize.py +313 -120
  14. hh_applicant_tool/operations/call_api.py +18 -8
  15. hh_applicant_tool/operations/check_proxy.py +30 -0
  16. hh_applicant_tool/operations/clear_negotiations.py +90 -82
  17. hh_applicant_tool/operations/config.py +119 -16
  18. hh_applicant_tool/operations/install.py +34 -0
  19. hh_applicant_tool/operations/list_resumes.py +23 -11
  20. hh_applicant_tool/operations/log.py +77 -0
  21. hh_applicant_tool/operations/migrate_db.py +65 -0
  22. hh_applicant_tool/operations/query.py +122 -0
  23. hh_applicant_tool/operations/refresh_token.py +14 -13
  24. hh_applicant_tool/operations/reply_employers.py +201 -180
  25. hh_applicant_tool/operations/settings.py +95 -0
  26. hh_applicant_tool/operations/uninstall.py +26 -0
  27. hh_applicant_tool/operations/update_resumes.py +23 -11
  28. hh_applicant_tool/operations/whoami.py +40 -7
  29. hh_applicant_tool/storage/__init__.py +8 -0
  30. hh_applicant_tool/storage/facade.py +24 -0
  31. hh_applicant_tool/storage/models/__init__.py +0 -0
  32. hh_applicant_tool/storage/models/base.py +169 -0
  33. hh_applicant_tool/storage/models/contacts.py +28 -0
  34. hh_applicant_tool/storage/models/employer.py +12 -0
  35. hh_applicant_tool/storage/models/negotiation.py +16 -0
  36. hh_applicant_tool/storage/models/resume.py +19 -0
  37. hh_applicant_tool/storage/models/setting.py +6 -0
  38. hh_applicant_tool/storage/models/vacancy.py +36 -0
  39. hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
  40. hh_applicant_tool/storage/queries/schema.sql +132 -0
  41. hh_applicant_tool/storage/repositories/__init__.py +0 -0
  42. hh_applicant_tool/storage/repositories/base.py +230 -0
  43. hh_applicant_tool/storage/repositories/contacts.py +14 -0
  44. hh_applicant_tool/storage/repositories/employers.py +14 -0
  45. hh_applicant_tool/storage/repositories/errors.py +19 -0
  46. hh_applicant_tool/storage/repositories/negotiations.py +13 -0
  47. hh_applicant_tool/storage/repositories/resumes.py +9 -0
  48. hh_applicant_tool/storage/repositories/settings.py +35 -0
  49. hh_applicant_tool/storage/repositories/vacancies.py +9 -0
  50. hh_applicant_tool/storage/utils.py +40 -0
  51. hh_applicant_tool/utils/__init__.py +31 -0
  52. hh_applicant_tool/utils/attrdict.py +6 -0
  53. hh_applicant_tool/utils/binpack.py +167 -0
  54. hh_applicant_tool/utils/config.py +55 -0
  55. hh_applicant_tool/utils/date.py +19 -0
  56. hh_applicant_tool/utils/json.py +61 -0
  57. hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
  58. hh_applicant_tool/utils/log.py +147 -0
  59. hh_applicant_tool/utils/misc.py +12 -0
  60. hh_applicant_tool/utils/mixins.py +221 -0
  61. hh_applicant_tool/utils/string.py +27 -0
  62. hh_applicant_tool/utils/terminal.py +32 -0
  63. hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
  64. hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
  65. hh_applicant_tool/ai/blackbox.py +0 -55
  66. hh_applicant_tool/color_log.py +0 -47
  67. hh_applicant_tool/mixins.py +0 -13
  68. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  69. hh_applicant_tool/operations/get_employer_contacts.py +0 -348
  70. hh_applicant_tool/telemetry_client.py +0 -106
  71. hh_applicant_tool/types.py +0 -45
  72. hh_applicant_tool/utils.py +0 -119
  73. hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
  74. hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
  75. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
  76. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1 @@
1
+ from .main import HHApplicantTool
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
 
3
- from .main import main
3
+ from . import main
4
4
 
5
5
  if __name__ == "__main__":
6
6
  sys.exit(main())
@@ -0,0 +1,2 @@
1
+ class AIError(Exception):
2
+ pass
@@ -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(Exception):
12
+ class OpenAIError(AIError):
9
13
  pass
10
14
 
11
15
 
12
- class OpenAIChat:
13
- chat_endpoint: str = "https://api.openai.com/v1/chat/completions"
16
+ @dataclass
17
+ class ChatOpenAI:
18
+ chat_endpoint: ClassVar[str] = "https://api.openai.com/v1/chat/completions"
14
19
 
15
- def __init__(
16
- self,
17
- token: str,
18
- model: str,
19
- system_prompt: str,
20
- proxies: dict[str, str] | None = None,
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 default_headers(self) -> dict[str, str]:
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
- "role": "system",
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": 0.7,
49
- "max_completion_tokens": 1000
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.default_headers(),
57
- proxies=self.proxies,
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 'error' in data:
64
- raise OpenAIError(data['error']['message'])
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(str(ex)) from ex
61
+ raise OpenAIError(f"Network error: {ex}") from ex
@@ -1,3 +1,5 @@
1
1
  """See <https://github.com/hhru/api>"""
2
- from .client import *
3
- from .errors import *
2
+
3
+ from .client import * # noqa: F403
4
+ from .datatypes import * # noqa: F403
5
+ from .errors import * # noqa: F403
@@ -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 Response, Session
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
- logger = logging.getLogger(__package__)
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
- ALLOWED_METHODS = Literal["GET", "POST", "PUT", "DELETE"]
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
- previous_request_time: float = 0.0
36
- delay: float = 0.334
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
- logger.debug(f"client proxies: {self.proxies}")
54
+ # if self.proxies:
55
+ # logger.debug(f"client proxies: {self.proxies}")
44
56
 
45
- def default_headers(self) -> dict[str, str]:
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 "Mozilla/5.0",
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: ALLOWED_METHODS,
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
- ) -> dict:
74
+ ) -> T:
64
75
  # Не знаю насколько это "правильно"
65
- assert method in ALLOWED_METHODS.__args__
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.previous_request_time
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 = self.default_headers() | self.additional_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=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 and params:
106
- log_url += "?" + urlencode(params)
114
+ if params and not has_body:
115
+ encoded_params = urlencode(params)
116
+ log_url += ("?", "&")["?" in url] + encoded_params
107
117
  logger.debug(
108
- "%s %s: %d",
118
+ "%d %s %.1000s",
119
+ response.status_code,
109
120
  method,
110
121
  log_url,
111
- response.status_code,
112
122
  )
113
- self.previous_request_time = time.monotonic()
114
- self.raise_for_status(response, rv)
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 url if "://" in url else f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
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 = "https://hh.ru/oauth"
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 = "https://api.hh.ru/"
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 additional_headers(
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: ALLOWED_METHODS,
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
- ) -> dict:
244
+ ) -> T:
250
245
  def do_request():
251
- return BaseClient.request(self, method, endpoint, params, delay, **kwargs)
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
- # USER_AGENT_TEMPLATE = "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Mobile Safari/537.36"
2
-
3
- ANDROID_CLIENT_ID = "HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
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