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
@@ -1,137 +1,330 @@
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
 
8
- from ..api import ApiClient # noqa: E402
9
- from ..main import BaseOperation, Namespace # noqa: E402
11
+ try:
12
+ from playwright.async_api import async_playwright
13
+ except ImportError:
14
+ pass
15
+
16
+ from ..main import BaseOperation
17
+ from ..utils.terminal import print_kitty_image
18
+
19
+ if TYPE_CHECKING:
20
+ from ..main import HHApplicantTool
21
+
10
22
 
11
23
  HH_ANDROID_SCHEME = "hhandroid"
12
24
 
13
- logger = logging.getLogger(__package__)
25
+ logger = logging.getLogger(__name__)
26
+ _executor = ThreadPoolExecutor()
14
27
 
15
- QT_IMPORTED = False
16
28
 
17
- try:
18
- from PyQt6.QtCore import QUrl
19
- from PyQt6.QtWidgets import QApplication, QMainWindow
20
- from PyQt6.QtWebEngineCore import QWebEngineUrlScheme
21
- from PyQt6.QtWebEngineCore import QWebEngineUrlSchemeHandler
22
- from PyQt6.QtWebEngineWidgets import QWebEngineView
23
- from PyQt6.QtWebEngineCore import QWebEngineSettings
24
-
25
- QT_IMPORTED = True
26
- except ImportError as ex:
27
- logger.debug(ex)
28
- # Заглушки чтобы на сервере не нужно было ставить сотни мегабайт qt-говна
29
-
30
- class QUrl:
31
- pass
32
-
33
- class QApplication:
34
- pass
35
-
36
- class QMainWindow:
37
- pass
38
-
39
- class QWebEngineUrlSchemeHandler:
40
- pass
41
-
42
- class QWebEngineView:
43
- pass
44
-
45
-
46
-
47
- class HHAndroidUrlSchemeHandler(QWebEngineUrlSchemeHandler):
48
- def __init__(self, parent: "WebViewWindow") -> None:
49
- super().__init__()
50
- self.parent = parent
51
-
52
- def requestStarted(self, info: Any) -> None:
53
- url = info.requestUrl().toString()
54
- if url.startswith(f"{HH_ANDROID_SCHEME}://"):
55
- self.parent.handle_redirect_uri(url)
56
-
57
- def register_hhandroid_scheme() -> None:
58
- scheme = QWebEngineUrlScheme(HH_ANDROID_SCHEME.encode())
59
- scheme.setSyntax(QWebEngineUrlScheme.Syntax.Path)
60
- scheme.setFlags(
61
- QWebEngineUrlScheme.Flag.SecureScheme |
62
- QWebEngineUrlScheme.Flag.LocalScheme |
63
- QWebEngineUrlScheme.Flag.LocalAccessAllowed |
64
- QWebEngineUrlScheme.Flag.CorsEnabled
65
- )
66
- QWebEngineUrlScheme.registerScheme(scheme)
67
-
68
-
69
- class WebViewWindow(QMainWindow):
70
- def __init__(self, api_client: ApiClient) -> None:
71
- super().__init__()
72
- self.api_client = api_client
73
-
74
- self.web_view = QWebEngineView()
75
- self.web_view.settings().setUnknownUrlSchemePolicy(
76
- QWebEngineSettings.UnknownUrlSchemePolicy.AllowAllUnknownUrlSchemes
77
- )
78
-
79
- self.setCentralWidget(self.web_view)
80
- self.setWindowTitle("Авторизация на HH.RU")
81
- self.hhandroid_handler = HHAndroidUrlSchemeHandler(self)
82
-
83
- profile = self.web_view.page().profile()
84
- profile.installUrlSchemeHandler(HH_ANDROID_SCHEME.encode(), self.hhandroid_handler)
85
-
86
- self.web_view.page().acceptNavigationRequest = self._filter_http_requests
87
-
88
- self.resize(480, 800)
89
- oauth_url = api_client.oauth_client.authorize_url
90
- logger.debug(f"{oauth_url = }")
91
- self.web_view.setUrl(QUrl(oauth_url))
92
-
93
- def _filter_http_requests(self, url: QUrl, _type, is_main_frame):
94
- """Блокирует любые переходы по протоколу HTTP"""
95
- if url.scheme().lower() == "http":
96
- logger.warning(f"🚫 Заблокирован небезопасный запрос: {url.toString()}")
97
- return False
98
- return True
99
-
100
- def handle_redirect_uri(self, redirect_uri: str) -> None:
101
- logger.debug(f"handle redirect uri: {redirect_uri}")
102
- sp = urlsplit(redirect_uri)
103
- code = parse_qs(sp.query).get("code", [None])[0]
104
- if code:
105
- token = self.api_client.oauth_client.authenticate(code)
106
- self.api_client.handle_access_token(token)
107
- print("🔓 Авторизация прошла успешно!")
108
- self.close()
29
+ async def ainput(prompt: str) -> str:
30
+ loop = asyncio.get_running_loop()
31
+ return await loop.run_in_executor(_executor, input, prompt)
109
32
 
110
33
 
111
34
  class Operation(BaseOperation):
112
- """Авторизоваться на сайте"""
35
+ """Авторизация через Playwright"""
36
+
37
+ __aliases__: list = ["auth", "authen", "authenticate"]
38
+
39
+ # Селекторы
40
+ SELECT_LOGIN_INPUT = 'input[data-qa="login-input-username"]'
41
+ SELECT_EXPAND_PASSWORD = 'button[data-qa="expand-login-by_password"]'
42
+ SELECT_PASSWORD_INPUT = 'input[data-qa="login-input-password"]'
43
+ SELECT_CODE_CONTAINER = 'div[data-qa="account-login-code-input"]'
44
+ SELECT_PIN_CODE_INPUT = 'input[data-qa="magritte-pincode-input-field"]'
45
+ SELECT_CAPTCHA_IMAGE = 'img[data-qa="account-captcha-picture"]'
46
+ SELECT_CAPTCHA_INPUT = 'input[data-qa="account-captcha-input"]'
47
+
48
+ def __init__(self, *args, **kwargs):
49
+ super().__init__(*args, **kwargs)
50
+ self._args = None
51
+
52
+ @property
53
+ def is_headless(self) -> bool:
54
+ """Свойство, определяющее режим работы браузера"""
55
+ return not self._args.no_headless and self.is_automated
56
+
57
+ @property
58
+ def is_automated(self) -> bool:
59
+ return not self._args.manual
60
+
61
+ @property
62
+ def selector_timeout(self) -> int | None:
63
+ """Вспомогательное свойство для таймаутов: None если headless, иначе 500мс"""
64
+ return None if self.is_headless else 5000
113
65
 
114
66
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
115
- pass
67
+ parser.add_argument(
68
+ "username",
69
+ nargs="?",
70
+ help="Email или телефон",
71
+ )
72
+ parser.add_argument(
73
+ "--password",
74
+ "-p",
75
+ help="Пароль для входа (если не указать, то вход будет по одноразовому коду)",
76
+ )
77
+ parser.add_argument(
78
+ "--no-headless",
79
+ "-n",
80
+ action="store_true",
81
+ help="Показать окно браузера для отладки (отключает headless режим).",
82
+ )
83
+ parser.add_argument(
84
+ "-m",
85
+ "--manual",
86
+ action="store_true",
87
+ help="Ручной режим ввода кредов, редирект будет перехвачен.",
88
+ )
89
+ parser.add_argument(
90
+ "-k",
91
+ "--use-kitty",
92
+ "--kitty",
93
+ action="store_true",
94
+ help="Использовать kitty protocol для вывода изображения в терминал. Гуглите поддерживает ли ваш терминал его",
95
+ )
116
96
 
117
- def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
118
- if not QT_IMPORTED:
119
- print_err(
120
- "❗Ошибка: PyQt6 не был импортирован, возможно, вы забыли его установить, либо же это ошибка самой библиотеки."
121
- )
122
- sys.exit(1)
97
+ def run(self, tool: HHApplicantTool) -> None:
98
+ self._args = tool.args
99
+ try:
100
+ asyncio.run(self._main(tool))
101
+ except (KeyboardInterrupt, asyncio.TimeoutError):
102
+ # _executor.shutdown(wait=False, cancel_futures=True)
103
+ logger.warning("Что-то пошло не так")
104
+ # os._exit(1)
105
+ return 1
106
+
107
+ async def _main(self, tool: HHApplicantTool) -> None:
108
+ args = tool.args
109
+ api_client = tool.api_client
110
+ storage = tool.storage
111
+
112
+ if self.is_automated:
113
+ username = (
114
+ args.username
115
+ or storage.settings.get_value("auth.username")
116
+ or (await ainput("👤 Введите email или телефон: "))
117
+ ).strip()
118
+
119
+ if not username:
120
+ raise RuntimeError("Empty username")
121
+
122
+ logger.debug(f"authenticate with: {username}")
123
123
 
124
124
  proxies = api_client.proxies
125
- if proxy_url := proxies.get("https"):
126
- import os
127
-
128
- qtwebengine_chromium_flags = f"--proxy-server={proxy_url}"
129
- logger.debug(f"set {qtwebengine_chromium_flags = }")
130
- os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = qtwebengine_chromium_flags
131
-
132
- register_hhandroid_scheme()
133
- app = QApplication(sys.argv)
134
- window = WebViewWindow(api_client=api_client)
135
- window.show()
136
-
137
- app.exec()
125
+ proxy_url = proxies.get("https")
126
+
127
+ chromium_args: list[str] = []
128
+ if proxy_url:
129
+ chromium_args.append(f"--proxy-server={proxy_url}")
130
+ logger.debug(f"Используется прокси: {proxy_url}")
131
+
132
+ if self.is_headless:
133
+ logger.debug("Headless режим активен")
134
+
135
+ async with async_playwright() as pw:
136
+ logger.debug("Запуск браузера...")
137
+
138
+ browser = await pw.chromium.launch(
139
+ headless=self.is_headless,
140
+ args=chromium_args,
141
+ )
142
+
143
+ try:
144
+ # https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
145
+ android_device = pw.devices["Galaxy A55"]
146
+ context = await browser.new_context(**android_device)
147
+ page = await context.new_page()
148
+
149
+ # async def route_handler(route):
150
+ # req = route.request
151
+ # url = req.url.lower()
152
+
153
+ # # Блокировка сканирования локальных портов
154
+ # if any(d in url for d in ["localhost", "127.0.0.1", "::1"]):
155
+ # logger.debug(f"🛑 Блокировка запроса на локальный порт: {url}")
156
+ # return await route.abort()
157
+
158
+ # # Оптимизация трафика в headless
159
+ # if is_headless and req.resource_type in [
160
+ # "image",
161
+ # "stylesheet",
162
+ # "font",
163
+ # "media",
164
+ # ]:
165
+ # return await route.abort()
166
+
167
+ # await route.continue_()
168
+
169
+ # почему-то добавление этого обработчика вешает все
170
+ # await page.route("**/*", route_handler)
171
+
172
+ code_future: asyncio.Future[str | None] = asyncio.Future()
173
+
174
+ def handle_request(request):
175
+ url = request.url
176
+ if url.startswith(f"{HH_ANDROID_SCHEME}://"):
177
+ logger.info(f"Перехвачен OAuth redirect: {url}")
178
+ if not code_future.done():
179
+ sp = urlsplit(url)
180
+ code = parse_qs(sp.query).get("code", [None])[0]
181
+ code_future.set_result(code)
182
+
183
+ page.on("request", handle_request)
184
+
185
+ logger.debug(
186
+ f"Переход на страницу OAuth: {api_client.oauth_client.authorize_url}"
187
+ )
188
+ await page.goto(
189
+ api_client.oauth_client.authorize_url,
190
+ timeout=30000,
191
+ wait_until="load",
192
+ )
193
+
194
+ if self.is_automated:
195
+ logger.debug(
196
+ f"Ожидание поля логина {self.SELECT_LOGIN_INPUT}"
197
+ )
198
+ await page.wait_for_selector(
199
+ self.SELECT_LOGIN_INPUT, timeout=self.selector_timeout
200
+ )
201
+ await page.fill(self.SELECT_LOGIN_INPUT, username)
202
+ logger.debug("Логин введен")
203
+
204
+ if args.password:
205
+ await self._direct_login(page, args.password)
206
+ else:
207
+ await self._onetime_code_login(page)
208
+
209
+ logger.debug("Ожидание OAuth-кода...")
210
+
211
+ auth_code = await asyncio.wait_for(
212
+ code_future, timeout=[None, 30.0][self.is_automated]
213
+ )
214
+
215
+ page.remove_listener("request", handle_request)
216
+
217
+ logger.debug("Код получен, пробуем получить токен...")
218
+ token = await asyncio.to_thread(
219
+ api_client.oauth_client.authenticate,
220
+ auth_code,
221
+ )
222
+ api_client.handle_access_token(token)
223
+
224
+ print("🔓 Авторизация прошла успешно!")
225
+
226
+ # Сохраняем логин и пароль
227
+ if self.is_automated:
228
+ storage.settings.set_value("auth.username", username)
229
+ if args.password:
230
+ storage.settings.set_value(
231
+ "auth.password", args.password
232
+ )
233
+
234
+ storage.settings.set_value("auth.last_login", datetime.now())
235
+
236
+ # storage.settings.set_value(
237
+ # "auth.access_token", token["access_token"]
238
+ # )
239
+ # storage.settings.set_value(
240
+ # "auth.refresh_token", token["refresh_token"]
241
+ # )
242
+ # storage.settings.set_value(
243
+ # "auth.refresh_token", token["expires_in"]
244
+ # )
245
+
246
+ finally:
247
+ logger.debug("Закрытие браузера")
248
+ await browser.close()
249
+
250
+ async def _direct_login(self, page, password: str) -> None:
251
+ logger.info("Вход по паролю...")
252
+
253
+ logger.debug(
254
+ f"Клик по кнопке развертывания пароля: {self.SELECT_EXPAND_PASSWORD}"
255
+ )
256
+ await page.click(self.SELECT_EXPAND_PASSWORD)
257
+
258
+ await self._handle_captcha(page)
259
+
260
+ logger.debug(f"Ожидание поля пароля: {self.SELECT_PASSWORD_INPUT}")
261
+ await page.wait_for_selector(
262
+ self.SELECT_PASSWORD_INPUT, timeout=self.selector_timeout
263
+ )
264
+ await page.fill(self.SELECT_PASSWORD_INPUT, password)
265
+ await page.press(self.SELECT_PASSWORD_INPUT, "Enter")
266
+ logger.debug("Форма с паролем отправлена")
267
+
268
+ async def _onetime_code_login(self, page) -> None:
269
+ logger.info("Вход по одноразовому коду...")
270
+
271
+ await page.press(self.SELECT_LOGIN_INPUT, "Enter")
272
+
273
+ await self._handle_captcha(page)
274
+
275
+ logger.debug(
276
+ f"Ожидание контейнера ввода кода: {self.SELECT_CODE_CONTAINER}"
277
+ )
278
+
279
+ await page.wait_for_selector(
280
+ self.SELECT_CODE_CONTAINER, timeout=self.selector_timeout
281
+ )
282
+
283
+ print("📨 Код был отправлен. Проверьте почту или SMS.")
284
+ code = (await ainput("📩 Введите полученный код: ")).strip()
285
+
286
+ if not code:
287
+ raise RuntimeError("Код подтверждения не может быть пустым.")
288
+
289
+ logger.debug(f"Ввод кода в {self.SELECT_PIN_CODE_INPUT}")
290
+ await page.fill(self.SELECT_PIN_CODE_INPUT, code)
291
+ await page.press(self.SELECT_PIN_CODE_INPUT, "Enter")
292
+ logger.debug("Форма с кодом отправлена")
293
+
294
+ async def _handle_captcha(self, page):
295
+ try:
296
+ captcha_element = await page.wait_for_selector(
297
+ self.SELECT_CAPTCHA_IMAGE,
298
+ timeout=self.selector_timeout,
299
+ state="visible",
300
+ )
301
+ except Exception:
302
+ logger.debug("Капчи нет, продолжаем как обычно.")
303
+ return
304
+
305
+ if not self._args.use_kitty:
306
+ raise RuntimeError(
307
+ "Используйте флаг --use-kitty/-k для вывода капчи в терминал."
308
+ "Работает не во всех терминалах!",
309
+ )
310
+
311
+ logger.info("Обнаружена капча!")
312
+
313
+ # box = await captcha_element.bounding_box()
314
+
315
+ # width = int(box["width"])
316
+ # height = int(box["height"])
317
+
318
+ img_bytes = await captcha_element.screenshot()
319
+
320
+ print(
321
+ "Если вы не видите картинку ниже, то ваш терминал не поддерживает"
322
+ " kitty protocol."
323
+ )
324
+ print()
325
+ print_kitty_image(img_bytes)
326
+
327
+ captcha_text = (await ainput("Введите текст с картинки: ")).strip()
328
+
329
+ await page.fill(self.SELECT_CAPTCHA_INPUT, captcha_text)
330
+ await page.press(self.SELECT_CAPTCHA_INPUT, "Enter")
@@ -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, tool: HHApplicantTool) -> None:
43
+ args = tool.args
44
+ api_client = 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,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)