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
@@ -1,98 +1,273 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
4
+ import asyncio
2
5
  import logging
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from datetime import datetime
8
+ from typing import TYPE_CHECKING
3
9
  from urllib.parse import parse_qs, urlsplit
4
- import sys
5
- from typing import Any
6
- from ..utils import print_err
7
10
 
11
+ try:
12
+ from playwright.async_api import async_playwright
13
+ except ImportError:
14
+ pass
8
15
 
9
- QT_IMPORTED = False
16
+ from ..main import BaseOperation
10
17
 
11
- try:
12
- from PyQt6.QtCore import QUrl
13
- from PyQt6.QtWidgets import QApplication, QMainWindow
14
- from PyQt6.QtWebEngineCore import QWebEngineUrlSchemeHandler
15
- from PyQt6.QtWebEngineWidgets import QWebEngineView
18
+ if TYPE_CHECKING:
19
+ from ..main import HHApplicantTool
16
20
 
17
- QT_IMPORTED = True
18
- except ImportError:
19
- # Заглушки чтобы на сервере не нужно было ставить сотни мегабайт qt-говна
20
21
 
21
- class QUrl:
22
- pass
22
+ HH_ANDROID_SCHEME = "hhandroid"
23
23
 
24
- class QApplication:
25
- pass
24
+ logger = logging.getLogger(__name__)
25
+ _executor = ThreadPoolExecutor()
26
26
 
27
- class QMainWindow:
28
- pass
29
27
 
30
- class QWebEngineUrlSchemeHandler:
31
- pass
28
+ async def ainput(prompt: str) -> str:
29
+ loop = asyncio.get_running_loop()
30
+ return await loop.run_in_executor(_executor, input, prompt)
32
31
 
33
- class QWebEngineView:
34
- pass
35
32
 
33
+ class Operation(BaseOperation):
34
+ """Авторизация через Playwright"""
36
35
 
37
- from ..api import ApiClient # noqa: E402
38
- from ..main import BaseOperation, Namespace # noqa: E402
36
+ __aliases__: list = ["auth", "authen", "authenticate"]
39
37
 
40
- logger = logging.getLogger(__package__)
38
+ # Селекторы
39
+ SEL_LOGIN_INPUT = 'input[data-qa="login-input-username"]'
40
+ SEL_EXPAND_PASSWORD_BTN = 'button[data-qa="expand-login-by_password"]'
41
+ SEL_PASSWORD_INPUT = 'input[data-qa="login-input-password"]'
42
+ SEL_CODE_CONTAINER = 'div[data-qa="account-login-code-input"]'
43
+ SEL_PIN_CODE_INPUT = 'input[data-qa="magritte-pincode-input-field"]'
41
44
 
45
+ def __init__(self, *args, **kwargs):
46
+ super().__init__(*args, **kwargs)
47
+ self._args = None
42
48
 
43
- class HHAndroidUrlSchemeHandler(QWebEngineUrlSchemeHandler):
44
- def __init__(self, parent: "WebViewWindow") -> None:
45
- super().__init__()
46
- self.parent = parent
49
+ @property
50
+ def is_headless(self) -> bool:
51
+ """Свойство, определяющее режим работы браузера"""
52
+ return not self._args.no_headless and self.is_automated
47
53
 
48
- def requestStarted(self, info: Any) -> None:
49
- url = info.requestUrl().toString()
50
- if url.startswith("hhandroid://"):
51
- self.parent.handle_redirect_uri(url)
54
+ @property
55
+ def is_automated(self) -> bool:
56
+ return not self._args.manual
52
57
 
58
+ @property
59
+ def selector_timeout(self) -> int | None:
60
+ """Вспомогательное свойство для таймаутов: None если headless, иначе 500мс"""
61
+ return None if self.is_headless else 5000
53
62
 
54
- class WebViewWindow(QMainWindow):
55
- def __init__(self, api_client: ApiClient) -> None:
56
- super().__init__()
57
- self.api_client = api_client
58
- # Настройка WebEngineView
59
- self.web_view = QWebEngineView()
60
- self.setCentralWidget(self.web_view)
61
- self.setWindowTitle("Авторизация на HH.RU")
62
- self.hhandroid_handler = HHAndroidUrlSchemeHandler(self)
63
- # Установка перехватчика запросов и обработчика кастомной схемы
64
- profile = self.web_view.page().profile()
65
- profile.installUrlSchemeHandler(b"hhandroid", self.hhandroid_handler)
66
- # Настройки окна для мобильного вида
67
- self.resize(480, 800)
68
- self.web_view.setUrl(QUrl(api_client.oauth_client.authorize_url))
63
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
64
+ parser.add_argument(
65
+ "username",
66
+ nargs="?",
67
+ help="Email или телефон",
68
+ )
69
+ parser.add_argument(
70
+ "--password",
71
+ "-p",
72
+ help="Пароль для входа (если не указать, то вход будет по одноразовому коду)",
73
+ )
74
+ parser.add_argument(
75
+ "--no-headless",
76
+ action="store_true",
77
+ help="Показать окно браузера для отладки (отключает headless режим).",
78
+ )
79
+ parser.add_argument(
80
+ "--manual",
81
+ action="store_true",
82
+ help="Ручной режим ввода кредов, редирект будет перехвачен.",
83
+ )
69
84
 
70
- def handle_redirect_uri(self, redirect_uri: str) -> None:
71
- logger.debug(f"handle redirect uri: {redirect_uri}")
72
- sp = urlsplit(redirect_uri)
73
- code = parse_qs(sp.query).get("code", [None])[0]
74
- if code:
75
- token = self.api_client.oauth_client.authenticate(code)
76
- self.api_client.handle_access_token(token)
77
- print("🔓 Авторизация прошла успешно!")
78
- self.close()
85
+ def run(self, tool: HHApplicantTool) -> None:
86
+ self._args = tool.args
87
+ try:
88
+ asyncio.run(self._main(tool))
89
+ except (KeyboardInterrupt, asyncio.TimeoutError):
90
+ logger.warning("Что-то пошло не так")
91
+ # os._exit(1)
92
+ return 1
79
93
 
94
+ async def _main(self, tool: HHApplicantTool) -> None:
95
+ args = tool.args
96
+ api_client = tool.api_client
97
+ storage = tool.storage
80
98
 
81
- class Operation(BaseOperation):
82
- """Авторизоваться на сайте"""
99
+ if self.is_automated:
100
+ username = (
101
+ args.username
102
+ or storage.settings.get_value("auth.username")
103
+ or (await ainput("👤 Введите email или телефон: "))
104
+ ).strip()
83
105
 
84
- def setup_parser(self, parser: argparse.ArgumentParser) -> None:
85
- pass
106
+ if not username:
107
+ raise RuntimeError("Empty username")
108
+
109
+ logger.debug(f"authenticate with: {username}")
86
110
 
87
- def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
88
- if not QT_IMPORTED:
89
- print_err(
90
- "❗Критиническая Ошибка: PyQt6 не был импортирован, возможно, вы долбоеб и забыли его установить, либо же криворукие разрабы этой либы опять все сломали..."
111
+ proxies = api_client.proxies
112
+ proxy_url = proxies.get("https")
113
+
114
+ chromium_args: list[str] = []
115
+ if proxy_url:
116
+ chromium_args.append(f"--proxy-server={proxy_url}")
117
+ logger.debug(f"Используется прокси: {proxy_url}")
118
+
119
+ if self.is_headless:
120
+ logger.debug("Headless режим активен")
121
+
122
+ async with async_playwright() as pw:
123
+ logger.debug("Запуск браузера...")
124
+
125
+ browser = await pw.chromium.launch(
126
+ headless=self.is_headless,
127
+ args=chromium_args,
91
128
  )
92
- sys.exit(1)
93
129
 
94
- app = QApplication(sys.argv)
95
- window = WebViewWindow(api_client=api_client)
96
- window.show()
130
+ try:
131
+ # https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
132
+ android_device = pw.devices["Galaxy A55"]
133
+ context = await browser.new_context(**android_device)
134
+ page = await context.new_page()
135
+
136
+ # async def route_handler(route):
137
+ # req = route.request
138
+ # url = req.url.lower()
139
+
140
+ # # Блокировка сканирования локальных портов
141
+ # if any(d in url for d in ["localhost", "127.0.0.1", "::1"]):
142
+ # logger.debug(f"🛑 Блокировка запроса на локальный порт: {url}")
143
+ # return await route.abort()
144
+
145
+ # # Оптимизация трафика в headless
146
+ # if is_headless and req.resource_type in [
147
+ # "image",
148
+ # "stylesheet",
149
+ # "font",
150
+ # "media",
151
+ # ]:
152
+ # return await route.abort()
153
+
154
+ # await route.continue_()
155
+
156
+ # почему-то добавление этого обработчика вешает все
157
+ # await page.route("**/*", route_handler)
158
+
159
+ code_future: asyncio.Future[str | None] = asyncio.Future()
160
+
161
+ def handle_request(request):
162
+ url = request.url
163
+ if url.startswith(f"{HH_ANDROID_SCHEME}://"):
164
+ logger.info(f"Перехвачен OAuth redirect: {url}")
165
+ if not code_future.done():
166
+ sp = urlsplit(url)
167
+ code = parse_qs(sp.query).get("code", [None])[0]
168
+ code_future.set_result(code)
169
+
170
+ page.on("request", handle_request)
171
+
172
+ logger.debug(
173
+ f"Переход на страницу OAuth: {api_client.oauth_client.authorize_url}"
174
+ )
175
+ await page.goto(
176
+ api_client.oauth_client.authorize_url,
177
+ timeout=30000,
178
+ wait_until="load",
179
+ )
180
+
181
+ if self.is_automated:
182
+ # Шаг 1: Логин
183
+ logger.debug(f"Ожидание поля логина {self.SEL_LOGIN_INPUT}")
184
+ await page.wait_for_selector(
185
+ self.SEL_LOGIN_INPUT, timeout=self.selector_timeout
186
+ )
187
+ await page.fill(self.SEL_LOGIN_INPUT, username)
188
+ logger.debug("Логин введен")
189
+
190
+ # Шаг 2: Выбор метода входа
191
+ if args.password:
192
+ await self._direct_login(page, args.password)
193
+ else:
194
+ await self._onetime_code_login(page)
195
+
196
+ # Шаг 3: Ожидание OAuth кода
197
+ logger.debug("Ожидание появления OAuth кода в трафике...")
198
+
199
+ auth_code = await asyncio.wait_for(
200
+ code_future, timeout=[None, 30.0][self.is_automated]
201
+ )
202
+
203
+ page.remove_listener("request", handle_request)
204
+
205
+ logger.debug("Код получен, пробуем получить токен...")
206
+ token = await asyncio.to_thread(
207
+ api_client.oauth_client.authenticate,
208
+ auth_code,
209
+ )
210
+ api_client.handle_access_token(token)
211
+
212
+ print("🔓 Авторизация прошла успешно!")
213
+
214
+ # Сохраняем логин и пароль
215
+ if self.is_automated:
216
+ storage.settings.set_value("auth.username", username)
217
+ if args.password:
218
+ storage.settings.set_value(
219
+ "auth.password", args.password
220
+ )
221
+
222
+ storage.settings.set_value("auth.last_login", datetime.now())
223
+
224
+ # storage.settings.set_value(
225
+ # "auth.access_token", token["access_token"]
226
+ # )
227
+ # storage.settings.set_value(
228
+ # "auth.refresh_token", token["refresh_token"]
229
+ # )
230
+ # storage.settings.set_value(
231
+ # "auth.refresh_token", token["expires_in"]
232
+ # )
233
+
234
+ finally:
235
+ logger.debug("Закрытие браузера")
236
+ await browser.close()
237
+
238
+ async def _direct_login(self, page, password: str) -> None:
239
+ logger.info("Вход по паролю...")
240
+ logger.debug(
241
+ f"Клик по кнопке развертывания пароля: {self.SEL_EXPAND_PASSWORD_BTN}"
242
+ )
243
+ await page.click(self.SEL_EXPAND_PASSWORD_BTN)
244
+
245
+ logger.debug(f"Ожидание поля пароля: {self.SEL_PASSWORD_INPUT}")
246
+ await page.wait_for_selector(
247
+ self.SEL_PASSWORD_INPUT, timeout=self.selector_timeout
248
+ )
249
+ await page.fill(self.SEL_PASSWORD_INPUT, password)
250
+ await page.press(self.SEL_PASSWORD_INPUT, "Enter")
251
+ logger.debug("Форма с паролем отправлена")
252
+
253
+ async def _onetime_code_login(self, page) -> None:
254
+ logger.info("Вход по одноразовому коду...")
255
+ await page.press(self.SEL_LOGIN_INPUT, "Enter")
256
+
257
+ logger.debug(
258
+ f"Ожидание контейнера ввода кода: {self.SEL_CODE_CONTAINER}"
259
+ )
260
+ await page.wait_for_selector(
261
+ self.SEL_CODE_CONTAINER, timeout=self.selector_timeout
262
+ )
263
+
264
+ print("📨 Код был отправлен. Проверьте почту или SMS.")
265
+ code = (await ainput("📩 Введите полученный код: ")).strip()
266
+
267
+ if not code:
268
+ raise RuntimeError("Код подтверждения не может быть пустым")
97
269
 
98
- app.exec()
270
+ logger.debug(f"Ввод кода в {self.SEL_PIN_CODE_INPUT}")
271
+ await page.fill(self.SEL_PIN_CODE_INPUT, code)
272
+ await page.press(self.SEL_PIN_CODE_INPUT, "Enter")
273
+ logger.debug("Форма с кодом отправлена")
@@ -1,12 +1,17 @@
1
- # Этот модуль можно использовать как образец для других
1
+ from __future__ import annotations
2
+
2
3
  import argparse
3
4
  import json
4
5
  import logging
5
6
  import sys
7
+ from typing import TYPE_CHECKING
8
+
9
+ from ..api import ApiError
10
+ from ..main import BaseNamespace, BaseOperation
11
+
12
+ if TYPE_CHECKING:
13
+ from ..main import HHApplicantTool
6
14
 
7
- from ..api import ApiClient, ApiError
8
- from ..main import BaseOperation
9
- from ..main import Namespace as BaseNamespace
10
15
 
11
16
  logger = logging.getLogger(__package__)
12
17
 
@@ -15,12 +20,13 @@ class Namespace(BaseNamespace):
15
20
  method: str
16
21
  endpoint: str
17
22
  params: list[str]
18
- pretty_print: bool
19
23
 
20
24
 
21
25
  class Operation(BaseOperation):
22
26
  """Вызвать произвольный метод API <https://github.com/hhru/api>."""
23
27
 
28
+ __aliases__ = ("api",)
29
+
24
30
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
25
31
  parser.add_argument("endpoint", help="Путь до эндпоинта API")
26
32
  parser.add_argument(
@@ -30,13 +36,17 @@ class Operation(BaseOperation):
30
36
  default=[],
31
37
  )
32
38
  parser.add_argument(
33
- "-m", "--method", "--meth", default="GET", help="HTTP Метод"
39
+ "-m", "--method", "--meth", "-X", default="GET", help="HTTP Метод"
34
40
  )
35
41
 
36
- def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
42
+ def run(self, applicant_tool: HHApplicantTool) -> None:
43
+ args = applicant_tool.args
44
+ api_client = applicant_tool.api_client
37
45
  params = dict(x.split("=", 1) for x in args.param)
38
46
  try:
39
- result = api_client.request(args.method, args.endpoint, params=params)
47
+ result = api_client.request(
48
+ args.method, args.endpoint, params=params
49
+ )
40
50
  print(json.dumps(result, ensure_ascii=False))
41
51
  except ApiError as ex:
42
52
  json.dump(ex.data, sys.stderr, ensure_ascii=False)
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..api.errors import ApiError
8
+ from ..datatypes import NegotiationStateId
9
+ from ..main import BaseNamespace, BaseOperation
10
+
11
+ if TYPE_CHECKING:
12
+ from ..main import HHApplicantTool
13
+
14
+ logger = logging.getLogger(__package__)
15
+
16
+
17
+ class Namespace(BaseNamespace):
18
+ cleanup: bool
19
+ blacklist_discard: bool
20
+ dry_run: bool
21
+
22
+
23
+ class Operation(BaseOperation):
24
+ """Проверяет и синхронизирует отклики с локальной базой и опционально удаляет отказы."""
25
+
26
+ __aliases__ = ["sync-negotiations"]
27
+
28
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
29
+ parser.add_argument(
30
+ "--cleanup",
31
+ "--clean",
32
+ action=argparse.BooleanOptionalAction,
33
+ help="Удалить отклики с отказами",
34
+ )
35
+ parser.add_argument(
36
+ "-b",
37
+ "--blacklist-discard",
38
+ "--blacklist",
39
+ action=argparse.BooleanOptionalAction,
40
+ help="Блокировать работодателя за отказ",
41
+ )
42
+ parser.add_argument(
43
+ "-n",
44
+ "--dry-run",
45
+ action=argparse.BooleanOptionalAction,
46
+ help="Тестовый запуск без реального удаления",
47
+ )
48
+
49
+ def run(self, tool: HHApplicantTool) -> None:
50
+ self.tool = tool
51
+ self.args = tool.args
52
+ self._sync()
53
+
54
+ def _sync(self) -> None:
55
+ storage = self.tool.storage
56
+ for negotiation in self.tool.get_negotiations():
57
+ vacancy = negotiation["vacancy"]
58
+ employer = vacancy.get("employer", {})
59
+ employer_id = employer.get("id")
60
+
61
+ # Если работодателя блокируют, то он превращается в null
62
+ # ХХ позволяет скрывать компанию, когда id нет, а вместо имени "Крупная российская компания"
63
+ # sqlite3.IntegrityError: NOT NULL constraint failed: negotiations.employer_id
64
+ if employer_id:
65
+ storage.negotiations.save(negotiation)
66
+
67
+ state_id: NegotiationStateId = negotiation["state"]["id"]
68
+ if not self.args.cleanup:
69
+ continue
70
+ if state_id != "discard":
71
+ continue
72
+ try:
73
+ if not self.args.dry_run:
74
+ self.tool.api_client.delete(
75
+ f"/negotiations/active/{negotiation['id']}",
76
+ with_decline_message=True,
77
+ )
78
+
79
+ print(
80
+ "🗑️ Отменили отклик на вакансию:",
81
+ vacancy["name"],
82
+ vacancy["alternate_url"],
83
+ )
84
+
85
+ if (
86
+ employer_id
87
+ and employer_id not in self.args.blacklist_discard
88
+ ):
89
+ if not self.args.dry_run:
90
+ self.tool.api_client.put(
91
+ f"/employers/blacklisted/{employer_id}"
92
+ )
93
+
94
+ print(
95
+ "🚫 Работодатель заблокирован:",
96
+ employer["name"],
97
+ employer["alternate_url"],
98
+ )
99
+ except ApiError as err:
100
+ logger.error(err)
101
+
102
+ print("✅ Синхронизация завершена.")
@@ -0,0 +1,30 @@
1
+ # Этот модуль можно использовать как образец для других
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ..main import BaseNamespace, BaseOperation
9
+
10
+ if TYPE_CHECKING:
11
+ from ..main import HHApplicantTool
12
+
13
+
14
+ logger = logging.getLogger(__package__)
15
+
16
+
17
+ class Namespace(BaseNamespace):
18
+ pass
19
+
20
+
21
+ class Operation(BaseOperation):
22
+ """Проверить прокси"""
23
+
24
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
25
+ pass
26
+
27
+ def run(self, applicant_tool: HHApplicantTool) -> None:
28
+ session = applicant_tool.session
29
+ assert session.proxies, "Прокси не заданы"
30
+ print(session.get("https://icanhazip.com").text)