hh-applicant-tool 0.6.12__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 +24 -30
  5. hh_applicant_tool/api/client.py +82 -98
  6. hh_applicant_tool/api/errors.py +57 -8
  7. hh_applicant_tool/constants.py +0 -3
  8. hh_applicant_tool/datatypes.py +291 -0
  9. hh_applicant_tool/main.py +236 -82
  10. hh_applicant_tool/operations/apply_similar.py +268 -348
  11. hh_applicant_tool/operations/authorize.py +245 -70
  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 -18
  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 -35
  65. hh_applicant_tool/mixins.py +0 -13
  66. hh_applicant_tool/operations/clear_negotiations.py +0 -113
  67. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  68. hh_applicant_tool/operations/get_employer_contacts.py +0 -293
  69. hh_applicant_tool/telemetry_client.py +0 -106
  70. hh_applicant_tool/types.py +0 -45
  71. hh_applicant_tool/utils.py +0 -104
  72. hh_applicant_tool-0.6.12.dist-info/METADATA +0 -349
  73. hh_applicant_tool-0.6.12.dist-info/RECORD +0 -33
  74. {hh_applicant_tool-0.6.12.dist-info → hh_applicant_tool-1.4.7.dist-info}/WHEEL +0 -0
  75. {hh_applicant_tool-0.6.12.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,21 +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
- from .telemetry_client import TelemetryClient
15
- from .utils import Config, get_config_path
22
+ from .constants import ANDROID_CLIENT_ID, ANDROID_CLIENT_SECRET
23
+ from .storage import StorageFacade
24
+ from .utils.log import setup_logger
25
+ from .utils.mixins import MegaTool
16
26
 
17
- DEFAULT_CONFIG_PATH = (
18
- get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
27
+ DEFAULT_CONFIG_DIR = utils.get_config_path() / (__package__ or "").replace(
28
+ "_", "-"
19
29
  )
30
+ DEFAULT_CONFIG_FILENAME = "config.json"
31
+ DEFAULT_LOG_FILENAME = "log.txt"
32
+ DEFAULT_DATABASE_FILENAME = "data"
33
+ DEFAULT_PROFILE_ID = "."
20
34
 
21
35
  logger = logging.getLogger(__package__)
22
36
 
@@ -24,15 +38,19 @@ logger = logging.getLogger(__package__)
24
38
  class BaseOperation:
25
39
  def setup_parser(self, parser: argparse.ArgumentParser) -> None: ...
26
40
 
27
- def run(self, args: argparse.Namespace) -> None | int:
41
+ def run(
42
+ self,
43
+ tool: HHApplicantTool,
44
+ ) -> None | int:
28
45
  raise NotImplementedError()
29
46
 
30
47
 
31
48
  OPERATIONS = "operations"
32
49
 
33
50
 
34
- class Namespace(argparse.Namespace):
35
- config: Config
51
+ class BaseNamespace(argparse.Namespace):
52
+ profile_id: str
53
+ config_dir: Path
36
54
  verbosity: int
37
55
  delay: float
38
56
  user_agent: str
@@ -40,32 +58,12 @@ class Namespace(argparse.Namespace):
40
58
  disable_telemetry: bool
41
59
 
42
60
 
43
- def get_proxies(args: Namespace) -> dict[Literal["http", "https"], str | None]:
44
- return {
45
- "http": args.config["proxy_url"] or getenv("HTTP_PROXY"),
46
- "https": args.config["proxy_url"] or getenv("HTTPS_PROXY"),
47
- }
48
-
49
-
50
- def get_api_client(args: Namespace) -> ApiClient:
51
- token = args.config.get("token", {})
52
- api = ApiClient(
53
- access_token=token.get("access_token"),
54
- refresh_token=token.get("refresh_token"),
55
- access_expires_at=token.get("access_expires_at"),
56
- delay=args.delay,
57
- user_agent=args.config["user_agent"],
58
- proxies=get_proxies(args),
59
- )
60
- return api
61
-
62
-
63
- class HHApplicantTool:
61
+ class HHApplicantTool(MegaTool):
64
62
  """Утилита для автоматизации действий соискателя на сайте hh.ru.
65
63
 
66
64
  Исходники и предложения: <https://github.com/s3rgeym/hh-applicant-tool>
67
65
 
68
- Группа поддержки: <https://t.me/otzyvy_headhunter>
66
+ Группа поддержки: <https://t.me/hh_applicant_tool>
69
67
  """
70
68
 
71
69
  class ArgumentFormatter(
@@ -74,49 +72,58 @@ class HHApplicantTool:
74
72
  ):
75
73
  pass
76
74
 
77
- def create_parser(self) -> argparse.ArgumentParser:
75
+ def _create_parser(self) -> argparse.ArgumentParser:
78
76
  parser = argparse.ArgumentParser(
79
77
  description=self.__doc__,
80
78
  formatter_class=self.ArgumentFormatter,
81
79
  )
82
- parser.add_argument(
83
- "-c",
84
- "--config",
85
- help="Путь до файла конфигурации",
86
- type=Config,
87
- default=Config(DEFAULT_CONFIG_PATH),
88
- )
89
80
  parser.add_argument(
90
81
  "-v",
91
82
  "--verbosity",
92
- help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе",
83
+ help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе", # noqa: E501
93
84
  action="count",
94
85
  default=0,
95
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
+ )
96
101
  parser.add_argument(
97
102
  "-d",
98
103
  "--delay",
99
104
  type=float,
100
- default=0.334,
101
- help="Задержка между запросами к API HH",
105
+ default=0.654,
106
+ help="Задержка между запросами к API HH по умолчанию",
102
107
  )
103
- parser.add_argument("--user-agent", help="User-Agent для каждого запроса")
104
108
  parser.add_argument(
105
- "--proxy-url", help="Прокси, используемый для запросов к API"
109
+ "--user-agent",
110
+ help="User-Agent для каждого запроса",
106
111
  )
107
112
  parser.add_argument(
108
- "--disable-telemetry",
109
- default=False,
110
- action=argparse.BooleanOptionalAction,
111
- help="Отключить телеметрию",
113
+ "--proxy-url",
114
+ help="Прокси, используемый для запросов и авторизации",
112
115
  )
113
116
  subparsers = parser.add_subparsers(help="commands")
114
117
  package_dir = Path(__file__).resolve().parent / OPERATIONS
115
118
  for _, module_name, _ in iter_modules([str(package_dir)]):
119
+ if module_name.startswith("_"):
120
+ continue
116
121
  mod = import_module(f"{__package__}.{OPERATIONS}.{module_name}")
117
122
  op: BaseOperation = mod.Operation()
123
+ kebab_name = module_name.replace("_", "-")
118
124
  op_parser = subparsers.add_parser(
119
- module_name.replace("_", "-"),
125
+ kebab_name,
126
+ aliases=getattr(op, "__aliases__", []),
120
127
  description=op.__doc__,
121
128
  formatter_class=self.ArgumentFormatter,
122
129
  )
@@ -125,40 +132,187 @@ class HHApplicantTool:
125
132
  parser.set_defaults(run=None)
126
133
  return parser
127
134
 
128
- def run(self, argv: Sequence[str] | None) -> None | int:
129
- parser = self.create_parser()
130
- args = parser.parse_args(argv, namespace=Namespace())
131
- log_level = max(logging.DEBUG, logging.WARNING - args.verbosity * 10)
132
- logger.setLevel(log_level)
133
- handler = ColorHandler()
134
- # [C] Critical Error Occurred
135
- handler.setFormatter(logging.Formatter("[%(levelname).1s] %(message)s"))
136
- logger.addHandler(handler)
137
- 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:
138
307
  try:
139
- if not args.config["telemetry_client_id"]:
140
- import uuid
141
-
142
- args.config.save(telemetry_client_id=str(uuid.uuid4()))
143
- api_client = get_api_client(args)
144
- telemetry_client = TelemetryClient(
145
- telemetry_client_id=args.config["telemetry_client_id"],
146
- proxies=api_client.proxies.copy(),
147
- )
148
- # 0 or None = success
149
- res = args.run(args, api_client, telemetry_client)
150
- if (token := api_client.get_access_token()) != args.config["token"]:
151
- args.config.save(token=token)
152
- return res
153
- except KeyboardInterrupt:
154
- logger.warning("Interrupted by user")
155
- return 1
156
- except Exception as e:
157
- logger.exception(e)
158
- return 1
159
- parser.print_help(file=sys.stderr)
160
- 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())
161
315
 
162
316
 
163
317
  def main(argv: Sequence[str] | None = None) -> None | int:
164
- return HHApplicantTool().run(argv)
318
+ return HHApplicantTool(argv).run()