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.
Files changed (75) 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 +23 -33
  5. hh_applicant_tool/api/client.py +50 -64
  6. hh_applicant_tool/api/errors.py +51 -7
  7. hh_applicant_tool/constants.py +0 -3
  8. hh_applicant_tool/datatypes.py +291 -0
  9. hh_applicant_tool/main.py +233 -111
  10. hh_applicant_tool/operations/apply_similar.py +266 -362
  11. hh_applicant_tool/operations/authorize.py +256 -120
  12. hh_applicant_tool/operations/call_api.py +18 -8
  13. hh_applicant_tool/operations/check_negotiations.py +102 -0
  14. hh_applicant_tool/operations/check_proxy.py +30 -0
  15. hh_applicant_tool/operations/config.py +119 -16
  16. hh_applicant_tool/operations/install.py +34 -0
  17. hh_applicant_tool/operations/list_resumes.py +24 -10
  18. hh_applicant_tool/operations/log.py +77 -0
  19. hh_applicant_tool/operations/migrate_db.py +65 -0
  20. hh_applicant_tool/operations/query.py +120 -0
  21. hh_applicant_tool/operations/refresh_token.py +14 -13
  22. hh_applicant_tool/operations/reply_employers.py +148 -167
  23. hh_applicant_tool/operations/settings.py +95 -0
  24. hh_applicant_tool/operations/uninstall.py +26 -0
  25. hh_applicant_tool/operations/update_resumes.py +21 -10
  26. hh_applicant_tool/operations/whoami.py +40 -7
  27. hh_applicant_tool/storage/__init__.py +4 -0
  28. hh_applicant_tool/storage/facade.py +24 -0
  29. hh_applicant_tool/storage/models/__init__.py +0 -0
  30. hh_applicant_tool/storage/models/base.py +169 -0
  31. hh_applicant_tool/storage/models/contact.py +16 -0
  32. hh_applicant_tool/storage/models/employer.py +12 -0
  33. hh_applicant_tool/storage/models/negotiation.py +16 -0
  34. hh_applicant_tool/storage/models/resume.py +19 -0
  35. hh_applicant_tool/storage/models/setting.py +6 -0
  36. hh_applicant_tool/storage/models/vacancy.py +36 -0
  37. hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
  38. hh_applicant_tool/storage/queries/schema.sql +119 -0
  39. hh_applicant_tool/storage/repositories/__init__.py +0 -0
  40. hh_applicant_tool/storage/repositories/base.py +176 -0
  41. hh_applicant_tool/storage/repositories/contacts.py +19 -0
  42. hh_applicant_tool/storage/repositories/employers.py +13 -0
  43. hh_applicant_tool/storage/repositories/negotiations.py +12 -0
  44. hh_applicant_tool/storage/repositories/resumes.py +14 -0
  45. hh_applicant_tool/storage/repositories/settings.py +34 -0
  46. hh_applicant_tool/storage/repositories/vacancies.py +8 -0
  47. hh_applicant_tool/storage/utils.py +49 -0
  48. hh_applicant_tool/utils/__init__.py +31 -0
  49. hh_applicant_tool/utils/attrdict.py +6 -0
  50. hh_applicant_tool/utils/binpack.py +167 -0
  51. hh_applicant_tool/utils/config.py +55 -0
  52. hh_applicant_tool/utils/dateutil.py +19 -0
  53. hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
  54. hh_applicant_tool/utils/jsonutil.py +61 -0
  55. hh_applicant_tool/utils/log.py +144 -0
  56. hh_applicant_tool/utils/misc.py +12 -0
  57. hh_applicant_tool/utils/mixins.py +220 -0
  58. hh_applicant_tool/utils/string.py +27 -0
  59. hh_applicant_tool/utils/terminal.py +19 -0
  60. hh_applicant_tool/utils/user_agent.py +17 -0
  61. hh_applicant_tool-1.4.7.dist-info/METADATA +628 -0
  62. hh_applicant_tool-1.4.7.dist-info/RECORD +67 -0
  63. hh_applicant_tool/ai/blackbox.py +0 -55
  64. hh_applicant_tool/color_log.py +0 -47
  65. hh_applicant_tool/mixins.py +0 -13
  66. hh_applicant_tool/operations/clear_negotiations.py +0 -109
  67. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  68. hh_applicant_tool/operations/get_employer_contacts.py +0 -348
  69. hh_applicant_tool/telemetry_client.py +0 -106
  70. hh_applicant_tool/types.py +0 -45
  71. hh_applicant_tool/utils.py +0 -119
  72. hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
  73. hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
  74. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/WHEEL +0 -0
  75. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,291 @@
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
+ alternate_url: str
36
+
37
+
38
+ class IdName(TypedDict):
39
+ id: str
40
+ name: str
41
+
42
+
43
+ class Snippet(TypedDict):
44
+ requirement: Optional[str]
45
+ responsibility: Optional[str]
46
+
47
+
48
+ class ManagerActivity(TypedDict):
49
+ last_activity_at: str
50
+
51
+
52
+ Salary = TypedDict(
53
+ "Salary",
54
+ {
55
+ "from": Optional[int],
56
+ "to": Optional[int],
57
+ "currency": str,
58
+ "gross": bool,
59
+ },
60
+ )
61
+
62
+ SalaryRange = TypedDict(
63
+ "SalaryRange",
64
+ {
65
+ "from": Optional[int],
66
+ "to": Optional[int],
67
+ "currency": str,
68
+ "gross": bool,
69
+ "mode": IdName,
70
+ "frequency": IdName,
71
+ },
72
+ )
73
+
74
+
75
+ LogoUrls = TypedDict(
76
+ "LogoUrls",
77
+ {
78
+ "original": str,
79
+ "90": str,
80
+ "240": str,
81
+ },
82
+ )
83
+
84
+
85
+ class EmployerShort(TypedDict):
86
+ id: str
87
+ name: str
88
+ url: str
89
+ alternate_url: str
90
+ logo_urls: Optional[LogoUrls]
91
+ vacancies_url: str
92
+ accredited_it_employer: bool
93
+ trusted: bool
94
+
95
+
96
+ class SearchEmployer(EmployerShort):
97
+ country_id: Optional[int]
98
+
99
+
100
+ class NegotiationEmployer(EmployerShort):
101
+ pass
102
+
103
+
104
+ class VacancyShort(TypedDict):
105
+ id: str
106
+ premium: bool
107
+ name: str
108
+ department: Optional[dict]
109
+ has_test: bool
110
+ # HH API fields
111
+ response_letter_required: bool
112
+ area: IdName
113
+ salary: Optional[Salary]
114
+ salary_range: Optional[SalaryRange]
115
+ type: IdName
116
+ address: Optional[dict]
117
+ response_url: Optional[str]
118
+ sort_point_distance: Optional[float]
119
+ published_at: str
120
+ created_at: str
121
+ archived: bool
122
+ apply_alternate_url: str
123
+ show_contacts: bool
124
+ benefits: List[Any]
125
+ insider_interview: Optional[dict]
126
+ url: str
127
+ alternate_url: str
128
+ professional_roles: List[IdName]
129
+
130
+
131
+ class NegotiationVacancy(VacancyShort):
132
+ employer: NegotiationEmployer
133
+ show_logo_in_search: Optional[bool]
134
+
135
+
136
+ class SearchVacancy(VacancyShort):
137
+ employer: SearchEmployer
138
+ relations: List[Any]
139
+ experimental_modes: List[str]
140
+ manager_activity: Optional[ManagerActivity]
141
+ snippet: Snippet
142
+ contacts: Optional[dict]
143
+ schedule: IdName
144
+ working_days: List[Any]
145
+ working_time_intervals: List[Any]
146
+ working_time_modes: List[Any]
147
+ accept_temporary: bool
148
+ fly_in_fly_out_duration: List[Any]
149
+ work_format: List[IdName]
150
+ working_hours: List[IdName]
151
+ work_schedule_by_days: List[IdName]
152
+ accept_labor_contract: bool
153
+ civil_law_contracts: List[Any]
154
+ night_shifts: bool
155
+ accept_incomplete_resumes: bool
156
+ experience: IdName
157
+ employment: IdName
158
+ employment_form: IdName
159
+ internship: bool
160
+ adv_response_url: Optional[str]
161
+ is_adv_vacancy: bool
162
+ adv_context: Optional[dict]
163
+ allow_chat_with_manager: bool
164
+
165
+
166
+ class Phone(TypedDict):
167
+ country: str
168
+ city: str
169
+ number: str
170
+ formatted: str
171
+ comment: Optional[str]
172
+
173
+
174
+ class ContactData(TypedDict):
175
+ name: Optional[str]
176
+ email: Optional[str]
177
+ phones: List[Phone]
178
+ call_tracking_enabled: bool
179
+
180
+
181
+ class ResumeShort(TypedDict):
182
+ id: str
183
+ title: str
184
+ url: str
185
+ alternate_url: str
186
+
187
+
188
+ class ResumeCounters(TypedDict):
189
+ total_views: int
190
+ new_views: int
191
+ invitations: int
192
+ new_invitations: int
193
+
194
+
195
+ class Resume(ResumeShort):
196
+ status: IdName
197
+ created_at: str
198
+ updated_at: str
199
+ can_publish_or_update: bool
200
+ counters: ResumeCounters
201
+
202
+
203
+ class UserCounters(TypedDict):
204
+ resumes_count: int
205
+ new_resume_views: int
206
+ unread_negotiations: int
207
+ # ... and more
208
+
209
+
210
+ class User(TypedDict):
211
+ id: int
212
+ first_name: str
213
+ last_name: str
214
+ middle_name: Optional[str]
215
+ email: Optional[str]
216
+ phone: Optional[str]
217
+ is_applicant: bool
218
+ is_employer: bool
219
+ is_admin: bool
220
+ is_anonymous: bool
221
+ is_application: bool
222
+ counters: UserCounters
223
+ # ... and more
224
+
225
+
226
+ class Message(TypedDict):
227
+ id: str
228
+ text: str
229
+ author: dict # Could be more specific, e.g. Participant(TypedDict)
230
+ created_at: str
231
+ viewed_by_opponent: bool
232
+
233
+
234
+ class Counters(TypedDict):
235
+ messages: int
236
+ unread_messages: int
237
+
238
+
239
+ class ChatStates(TypedDict):
240
+ # response_reminder_state: {"allowed": bool}
241
+ response_reminder_state: dict[str, bool]
242
+
243
+
244
+ class NegotiaionState(IdName):
245
+ id: NegotiationStateId
246
+
247
+
248
+ class Negotiation(TypedDict):
249
+ id: str
250
+ state: IdName
251
+ created_at: str
252
+ updated_at: str
253
+ resume: ResumeShort
254
+ viewed_by_opponent: bool
255
+ has_updates: bool
256
+ messages_url: str
257
+ url: str
258
+ counters: Counters
259
+ chat_states: ChatStates
260
+ source: str
261
+ chat_id: int
262
+ messaging_status: str
263
+ decline_allowed: bool
264
+ read: bool
265
+ has_new_messages: bool
266
+ applicant_question_state: bool
267
+ hidden: bool
268
+ vacancy: NegotiationVacancy
269
+ tags: List[Any]
270
+
271
+
272
+ class EmployerApplicantServices(TypedDict):
273
+ target_employer: dict[str, int]
274
+
275
+
276
+ class Employer(EmployerShort):
277
+ has_divisions: bool
278
+ type: str
279
+ description: Optional[str]
280
+ site_url: str
281
+ relations: List[Any]
282
+ area: IdName
283
+ country_code: str
284
+ industries: List[Any]
285
+ is_identified_by_esia: bool
286
+ badges: List[Any]
287
+ branded_description: Optional[str]
288
+ branding: Optional[dict]
289
+ insider_interviews: List[Any]
290
+ open_vacancies: int
291
+ applicant_services: EmployerApplicantServices
hh_applicant_tool/main.py CHANGED
@@ -2,22 +2,35 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import logging
5
+ import os
6
+ import sqlite3
5
7
  import sys
8
+ from collections.abc import Sequence
9
+ from functools import cached_property
6
10
  from importlib import import_module
11
+ from itertools import count
7
12
  from os import getenv
8
13
  from pathlib import Path
9
14
  from pkgutil import iter_modules
10
- from typing import Literal, Sequence
15
+ from typing import Any, Iterable
11
16
 
17
+ import requests
18
+ import urllib3
19
+
20
+ from . import datatypes, utils
12
21
  from .api import ApiClient
13
- from .color_log import ColorHandler
14
22
  from .constants import ANDROID_CLIENT_ID, ANDROID_CLIENT_SECRET
15
- from .telemetry_client import TelemetryClient
16
- from .utils import Config, android_user_agent, get_config_path
23
+ from .storage import StorageFacade
24
+ from .utils.log import setup_logger
25
+ from .utils.mixins import MegaTool
17
26
 
18
- DEFAULT_CONFIG_PATH = (
19
- get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
27
+ DEFAULT_CONFIG_DIR = utils.get_config_path() / (__package__ or "").replace(
28
+ "_", "-"
20
29
  )
30
+ DEFAULT_CONFIG_FILENAME = "config.json"
31
+ DEFAULT_LOG_FILENAME = "log.txt"
32
+ DEFAULT_DATABASE_FILENAME = "data"
33
+ DEFAULT_PROFILE_ID = "."
21
34
 
22
35
  logger = logging.getLogger(__package__)
23
36
 
@@ -25,15 +38,19 @@ logger = logging.getLogger(__package__)
25
38
  class BaseOperation:
26
39
  def setup_parser(self, parser: argparse.ArgumentParser) -> None: ...
27
40
 
28
- def run(self, args: argparse.Namespace, api_client: ApiClient, telemetry_client: TelemetryClient) -> None | int:
41
+ def run(
42
+ self,
43
+ tool: HHApplicantTool,
44
+ ) -> None | int:
29
45
  raise NotImplementedError()
30
46
 
31
47
 
32
48
  OPERATIONS = "operations"
33
49
 
34
50
 
35
- class Namespace(argparse.Namespace):
36
- config: Config
51
+ class BaseNamespace(argparse.Namespace):
52
+ profile_id: str
53
+ config_dir: Path
37
54
  verbosity: int
38
55
  delay: float
39
56
  user_agent: str
@@ -41,44 +58,7 @@ class Namespace(argparse.Namespace):
41
58
  disable_telemetry: bool
42
59
 
43
60
 
44
- def get_proxies(args: Namespace) -> dict[str, str]:
45
- proxy_url = args.proxy_url or args.config.get("proxy_url")
46
-
47
- if proxy_url:
48
- return {
49
- "http": proxy_url,
50
- "https": proxy_url,
51
- }
52
-
53
- proxies = {}
54
- http_env = getenv("HTTP_PROXY") or getenv("http_proxy")
55
- https_env = getenv("HTTPS_PROXY") or getenv("https_proxy") or http_env
56
-
57
- if http_env:
58
- proxies["http"] = http_env
59
- if https_env:
60
- proxies["https"] = https_env
61
-
62
- return proxies
63
-
64
-
65
- def get_api_client(args: Namespace) -> ApiClient:
66
- config = args.config
67
- token = config.get("token", {})
68
- api = ApiClient(
69
- client_id=config.get("client_id", ANDROID_CLIENT_ID),
70
- client_secret=config.get("client_id", ANDROID_CLIENT_SECRET),
71
- access_token=token.get("access_token"),
72
- refresh_token=token.get("refresh_token"),
73
- access_expires_at=token.get("access_expires_at"),
74
- delay=args.delay,
75
- user_agent=config["user_agent"] or android_user_agent(),
76
- proxies=get_proxies(args),
77
- )
78
- return api
79
-
80
-
81
- class HHApplicantTool:
61
+ class HHApplicantTool(MegaTool):
82
62
  """Утилита для автоматизации действий соискателя на сайте hh.ru.
83
63
 
84
64
  Исходники и предложения: <https://github.com/s3rgeym/hh-applicant-tool>
@@ -92,63 +72,58 @@ class HHApplicantTool:
92
72
  ):
93
73
  pass
94
74
 
95
- def create_parser(self) -> argparse.ArgumentParser:
75
+ def _create_parser(self) -> argparse.ArgumentParser:
96
76
  parser = argparse.ArgumentParser(
97
77
  description=self.__doc__,
98
78
  formatter_class=self.ArgumentFormatter,
99
79
  )
100
- parser.add_argument(
101
- "-c",
102
- "--config",
103
- help="Путь до файла конфигурации",
104
- type=Config,
105
- default=Config(DEFAULT_CONFIG_PATH),
106
- )
107
80
  parser.add_argument(
108
81
  "-v",
109
82
  "--verbosity",
110
- help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе",
83
+ help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе", # noqa: E501
111
84
  action="count",
112
85
  default=0,
113
86
  )
87
+ parser.add_argument(
88
+ "-c",
89
+ "--config-dir",
90
+ "--config",
91
+ help="Путь до директории с конфигом",
92
+ type=Path,
93
+ default=DEFAULT_CONFIG_DIR,
94
+ )
95
+ parser.add_argument(
96
+ "--profile-id",
97
+ "--profile",
98
+ help="Используемый профиль — подкаталог в --config-dir",
99
+ default=DEFAULT_PROFILE_ID,
100
+ )
114
101
  parser.add_argument(
115
102
  "-d",
116
103
  "--delay",
117
104
  type=float,
118
- default=0.334,
119
- help="Задержка между запросами к API HH",
105
+ default=0.654,
106
+ help="Задержка между запросами к API HH по умолчанию",
120
107
  )
121
- parser.add_argument("--user-agent", help="User-Agent для каждого запроса")
122
108
  parser.add_argument(
123
- "--proxy-url", help="Прокси, используемый для запросов к API"
109
+ "--user-agent",
110
+ help="User-Agent для каждого запроса",
124
111
  )
125
112
  parser.add_argument(
126
- "--disable-telemetry",
127
- default=False,
128
- action=argparse.BooleanOptionalAction,
129
- help="Отключить телеметрию",
113
+ "--proxy-url",
114
+ help="Прокси, используемый для запросов и авторизации",
130
115
  )
131
116
  subparsers = parser.add_subparsers(help="commands")
132
117
  package_dir = Path(__file__).resolve().parent / OPERATIONS
133
118
  for _, module_name, _ in iter_modules([str(package_dir)]):
119
+ if module_name.startswith("_"):
120
+ continue
134
121
  mod = import_module(f"{__package__}.{OPERATIONS}.{module_name}")
135
122
  op: BaseOperation = mod.Operation()
136
- # 1. Разбиваем имя модуля на части
137
- words = module_name.split("_")
138
-
139
- # 2. Формируем варианты имен
140
- kebab_name = "-".join(words) # call-api
141
-
142
- # camelCase: первое слово маленькими, остальные с большой
143
- camel_case_name = words[0] + "".join(word.title() for word in words[1:])
144
-
145
- # flatcase: всё слитно и в нижнем регистре
146
- flat_name = "".join(words) # callapi
147
-
123
+ kebab_name = module_name.replace("_", "-")
148
124
  op_parser = subparsers.add_parser(
149
125
  kebab_name,
150
- # Добавляем остальные варианты в псевдонимы
151
- aliases=[camel_case_name, flat_name],
126
+ aliases=getattr(op, "__aliases__", []),
152
127
  description=op.__doc__,
153
128
  formatter_class=self.ArgumentFormatter,
154
129
  )
@@ -157,40 +132,187 @@ class HHApplicantTool:
157
132
  parser.set_defaults(run=None)
158
133
  return parser
159
134
 
160
- def run(self, argv: Sequence[str] | None) -> None | int:
161
- parser = self.create_parser()
162
- args = parser.parse_args(argv, namespace=Namespace())
163
- log_level = max(logging.DEBUG, logging.WARNING - args.verbosity * 10)
164
- logger.setLevel(log_level)
165
- handler = ColorHandler()
166
- # [C] Critical Error Occurred
167
- handler.setFormatter(logging.Formatter("[%(levelname).1s] %(message)s"))
168
- logger.addHandler(handler)
169
- if args.run:
135
+ def __init__(self, argv: Sequence[str] | None):
136
+ self._parse_args(argv)
137
+
138
+ # Создаем путь до конфига
139
+ self.config_path.mkdir(
140
+ parents=True,
141
+ exist_ok=True,
142
+ )
143
+
144
+ def _get_proxies(self) -> dict[str, str]:
145
+ proxy_url = self.args.proxy_url or self.config.get("proxy_url")
146
+
147
+ if proxy_url:
148
+ return {
149
+ "http": proxy_url,
150
+ "https": proxy_url,
151
+ }
152
+
153
+ proxies = {}
154
+ http_env = getenv("HTTP_PROXY") or getenv("http_proxy")
155
+ https_env = getenv("HTTPS_PROXY") or getenv("https_proxy") or http_env
156
+
157
+ if http_env:
158
+ proxies["http"] = http_env
159
+ if https_env:
160
+ proxies["https"] = https_env
161
+
162
+ return proxies
163
+
164
+ @cached_property
165
+ def session(self) -> requests.Session:
166
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
167
+
168
+ session = requests.session()
169
+ session.verify = False
170
+
171
+ if proxies := self._get_proxies():
172
+ logger.info("Use proxies: %r", proxies)
173
+ session.proxies = proxies
174
+
175
+ return session
176
+
177
+ @property
178
+ def config_path(self) -> Path:
179
+ return (self.args.config_dir / self.args.profile_id).resolve()
180
+
181
+ @cached_property
182
+ def config(self) -> utils.Config:
183
+ return utils.Config(self.config_path / DEFAULT_CONFIG_FILENAME)
184
+
185
+ @cached_property
186
+ def log_file(self) -> Path:
187
+ return self.config_path / DEFAULT_LOG_FILENAME
188
+
189
+ @cached_property
190
+ def db_path(self) -> Path:
191
+ return self.config_path / DEFAULT_DATABASE_FILENAME
192
+
193
+ @cached_property
194
+ def db(self) -> sqlite3.Connection:
195
+ conn = sqlite3.connect(self.db_path)
196
+ return conn
197
+
198
+ @cached_property
199
+ def storage(self) -> StorageFacade:
200
+ return StorageFacade(self.db)
201
+
202
+ @cached_property
203
+ def api_client(self) -> ApiClient:
204
+ args = self.args
205
+ config = self.config
206
+ token = config.get("token", {})
207
+ api = ApiClient(
208
+ client_id=config.get("client_id", ANDROID_CLIENT_ID),
209
+ client_secret=config.get("client_id", ANDROID_CLIENT_SECRET),
210
+ access_token=token.get("access_token"),
211
+ refresh_token=token.get("refresh_token"),
212
+ access_expires_at=token.get("access_expires_at"),
213
+ delay=args.delay,
214
+ user_agent=config["user_agent"] or utils.hh_android_useragent(),
215
+ session=self.session,
216
+ )
217
+ return api
218
+
219
+ def get_me(self) -> datatypes.User:
220
+ return self.api_client.get("/me")
221
+
222
+ def get_resumes(self) -> datatypes.PaginatedItems[datatypes.Resume]:
223
+ return self.api_client.get("/resumes/mine")
224
+
225
+ def first_resume_id(self):
226
+ resumes = self.api_client.get("/resumes/mine")
227
+ assert len(resumes["items"]), "Empty resume list"
228
+ return resumes["items"][0]["id"]
229
+
230
+ def get_blacklisted(self) -> list[str]:
231
+ rv = []
232
+ for page in count():
233
+ r: datatypes.PaginatedItems[datatypes.EmployerShort] = (
234
+ self.api_client.get("/employers/blacklisted", page=page)
235
+ )
236
+ rv += [item["id"] for item in r["items"]]
237
+ if page + 1 >= r["pages"]:
238
+ break
239
+ return rv
240
+
241
+ def get_negotiations(
242
+ self, status: str = "active"
243
+ ) -> Iterable[datatypes.Negotiation]:
244
+ for page in count():
245
+ r: dict[str, Any] = self.api_client.get(
246
+ "/negotiations",
247
+ page=page,
248
+ per_page=100,
249
+ status=status,
250
+ )
251
+
252
+ items = r.get("items", [])
253
+
254
+ if not items:
255
+ break
256
+
257
+ yield from items
258
+
259
+ if page + 1 >= r.get("pages", 0):
260
+ break
261
+
262
+ def save_token(self) -> bool:
263
+ if self.api_client.access_token != self.config.get("token", {}).get(
264
+ "access_token"
265
+ ):
266
+ self.config.save(token=self.api_client.get_access_token())
267
+ return True
268
+ return False
269
+
270
+ def run(self) -> None | int:
271
+ verbosity_level = max(
272
+ logging.DEBUG,
273
+ logging.WARNING - self.args.verbosity * 10,
274
+ )
275
+
276
+ setup_logger(logger, verbosity_level, self.log_file)
277
+
278
+ if sys.platform == "win32":
279
+ utils.setup_terminal()
280
+
281
+ try:
282
+ if self.args.run:
283
+ try:
284
+ res = self.args.run(self)
285
+ if self.save_token():
286
+ logger.info("Токен был обновлен.")
287
+ return res
288
+ except KeyboardInterrupt:
289
+ logger.warning("Выполнение прервано пользователем!")
290
+ return 1
291
+ except sqlite3.Error as ex:
292
+ logger.exception(ex)
293
+
294
+ script_name = sys.argv[0].split(os.sep)[-1]
295
+
296
+ logger.warning(
297
+ f"Возможно база данных повреждена, попробуйте выполнить команду:\n\n" # noqa: E501
298
+ f" {script_name} migrate-db"
299
+ )
300
+ return 1
301
+ except Exception as e:
302
+ logger.exception(e)
303
+ return 1
304
+ self._parser.print_help(file=sys.stderr)
305
+ return 2
306
+ finally:
170
307
  try:
171
- if not args.config["telemetry_client_id"]:
172
- import uuid
173
-
174
- args.config.save(telemetry_client_id=str(uuid.uuid4()))
175
- api_client = get_api_client(args)
176
- telemetry_client = TelemetryClient(
177
- telemetry_client_id=args.config["telemetry_client_id"],
178
- proxies=api_client.proxies.copy(),
179
- )
180
- # 0 or None = success
181
- res = args.run(args, api_client, telemetry_client)
182
- if (token := api_client.get_access_token()) != args.config["token"]:
183
- args.config.save(token=token)
184
- return res
185
- except KeyboardInterrupt:
186
- logger.warning("Interrupted by user")
187
- return 1
188
- except Exception as e:
189
- logger.exception(e, exc_info=log_level <= logging.DEBUG)
190
- return 1
191
- parser.print_help(file=sys.stderr)
192
- return 2
308
+ self.check_system()
309
+ except Exception:
310
+ pass
311
+
312
+ def _parse_args(self, argv) -> None:
313
+ self._parser = self._create_parser()
314
+ self.args = self._parser.parse_args(argv, namespace=BaseNamespace())
193
315
 
194
316
 
195
317
  def main(argv: Sequence[str] | None = None) -> None | int:
196
- return HHApplicantTool().run(argv)
318
+ return HHApplicantTool(argv).run()