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,5 +1,7 @@
1
- # from copy import deepcopy
2
- from typing import Any
1
+ from __future__ import annotations
2
+
3
+ from functools import cached_property
4
+ from typing import Any, Type
3
5
 
4
6
  from requests import Request, Response
5
7
  from requests.adapters import CaseInsensitiveDict
@@ -24,11 +26,11 @@ class BadResponse(Exception):
24
26
  class ApiError(BadResponse):
25
27
  def __init__(self, response: Response, data: dict[str, Any]) -> None:
26
28
  self._response = response
27
- self._raw = data
29
+ self._data = data
28
30
 
29
31
  @property
30
32
  def data(self) -> dict:
31
- return self._raw
33
+ return self._data
32
34
 
33
35
  @property
34
36
  def request(self) -> Request:
@@ -42,6 +44,14 @@ class ApiError(BadResponse):
42
44
  def response_headers(self) -> CaseInsensitiveDict:
43
45
  return self._response.headers
44
46
 
47
+ @property
48
+ def message(self) -> str:
49
+ return (
50
+ self._data.get("error_description")
51
+ or self._data.get("description")
52
+ or str(self._data)
53
+ )
54
+
45
55
  # def __getattr__(self, name: str) -> Any:
46
56
  # try:
47
57
  # return self._raw[name]
@@ -49,11 +59,35 @@ class ApiError(BadResponse):
49
59
  # raise AttributeError(name) from ex
50
60
 
51
61
  def __str__(self) -> str:
52
- return str(self._raw)
62
+ return self.message
53
63
 
54
64
  @staticmethod
55
- def is_limit_exceeded(data) -> bool:
56
- return any(x["value"] == "limit_exceeded" for x in data.get("errors", []))
65
+ def has_error_value(value: str, data: dict) -> bool:
66
+ return any(v.get("value") == value for v in data.get("errors", []))
67
+
68
+ @classmethod
69
+ def raise_for_status(
70
+ cls: Type[ApiError], response: Response, data: dict
71
+ ) -> None:
72
+ match response.status_code:
73
+ case status if 300 <= status <= 308:
74
+ raise Redirect(response, data)
75
+ case 400:
76
+ if cls.has_error_value("limit_exceeded", data):
77
+ raise LimitExceeded(response, data)
78
+ raise BadRequest(response, data)
79
+ case 403:
80
+ if cls.has_error_value("captcha_required", data):
81
+ raise CaptchaRequired(response, data)
82
+ raise Forbidden(response, data)
83
+ case 404:
84
+ raise ResourceNotFound(response, data)
85
+ case status if 500 > status >= 400:
86
+ raise ClientError(response, data)
87
+ case 502:
88
+ raise BadGateway(response, data)
89
+ case status if status >= 500:
90
+ raise InternalServerError(response, data)
57
91
 
58
92
 
59
93
  class Redirect(ApiError):
@@ -76,6 +110,22 @@ class Forbidden(ClientError):
76
110
  pass
77
111
 
78
112
 
113
+ class CaptchaRequired(ClientError):
114
+ @cached_property
115
+ def captcha_url(self) -> str:
116
+ return next(
117
+ filter(
118
+ lambda v: v["value"] == "captcha_required",
119
+ self._data["errors"],
120
+ ),
121
+ {},
122
+ ).get("captcha_url")
123
+
124
+ @property
125
+ def message(self) -> str:
126
+ return f"Captcha required: {self.captcha_url}"
127
+
128
+
79
129
  class ResourceNotFound(ClientError):
80
130
  pass
81
131
 
@@ -0,0 +1,17 @@
1
+ import random
2
+ import uuid
3
+
4
+
5
+ def generate_android_useragent() -> str:
6
+ """Generates Android App User-Agent"""
7
+ devices = (
8
+ "23053RN02A, 23053RN02Y, 23053RN02I, 23053RN02L, 23077RABDC".split(", ")
9
+ )
10
+ device = random.choice(devices)
11
+ minor = random.randint(100, 150)
12
+ patch = random.randint(10000, 15000)
13
+ android = random.randint(11, 15)
14
+ return (
15
+ f"ru.hh.android/7.{minor}.{patch}, Device: {device}, "
16
+ f"Android OS: {android} (UUID: {uuid.uuid4()})"
17
+ )
hh_applicant_tool/main.py CHANGED
@@ -2,22 +2,34 @@ 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
 
12
- from .api import ApiClient
13
- from .color_log import ColorHandler
14
- 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
17
+ import requests
18
+ import urllib3
17
19
 
18
- DEFAULT_CONFIG_PATH = (
19
- get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
20
+ from . import utils
21
+ from .api import ApiClient, datatypes
22
+ from .storage import StorageFacade
23
+ from .utils.log import setup_logger
24
+ from .utils.mixins import MegaTool
25
+
26
+ DEFAULT_CONFIG_DIR = utils.get_config_path() / (__package__ or "").replace(
27
+ "_", "-"
20
28
  )
29
+ DEFAULT_CONFIG_FILENAME = "config.json"
30
+ DEFAULT_LOG_FILENAME = "log.txt"
31
+ DEFAULT_DATABASE_FILENAME = "data"
32
+ DEFAULT_PROFILE_ID = "."
21
33
 
22
34
  logger = logging.getLogger(__package__)
23
35
 
@@ -25,15 +37,19 @@ logger = logging.getLogger(__package__)
25
37
  class BaseOperation:
26
38
  def setup_parser(self, parser: argparse.ArgumentParser) -> None: ...
27
39
 
28
- def run(self, args: argparse.Namespace, api_client: ApiClient, telemetry_client: TelemetryClient) -> None | int:
40
+ def run(
41
+ self,
42
+ tool: HHApplicantTool,
43
+ ) -> None | int:
29
44
  raise NotImplementedError()
30
45
 
31
46
 
32
47
  OPERATIONS = "operations"
33
48
 
34
49
 
35
- class Namespace(argparse.Namespace):
36
- config: Config
50
+ class BaseNamespace(argparse.Namespace):
51
+ profile_id: str
52
+ config_dir: Path
37
53
  verbosity: int
38
54
  delay: float
39
55
  user_agent: str
@@ -41,44 +57,7 @@ class Namespace(argparse.Namespace):
41
57
  disable_telemetry: bool
42
58
 
43
59
 
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:
60
+ class HHApplicantTool(MegaTool):
82
61
  """Утилита для автоматизации действий соискателя на сайте hh.ru.
83
62
 
84
63
  Исходники и предложения: <https://github.com/s3rgeym/hh-applicant-tool>
@@ -92,63 +71,58 @@ class HHApplicantTool:
92
71
  ):
93
72
  pass
94
73
 
95
- def create_parser(self) -> argparse.ArgumentParser:
74
+ def _create_parser(self) -> argparse.ArgumentParser:
96
75
  parser = argparse.ArgumentParser(
97
76
  description=self.__doc__,
98
77
  formatter_class=self.ArgumentFormatter,
99
78
  )
100
- parser.add_argument(
101
- "-c",
102
- "--config",
103
- help="Путь до файла конфигурации",
104
- type=Config,
105
- default=Config(DEFAULT_CONFIG_PATH),
106
- )
107
79
  parser.add_argument(
108
80
  "-v",
109
81
  "--verbosity",
110
- help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе",
82
+ help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе", # noqa: E501
111
83
  action="count",
112
84
  default=0,
113
85
  )
86
+ parser.add_argument(
87
+ "-c",
88
+ "--config-dir",
89
+ "--config",
90
+ help="Путь до директории с конфигом",
91
+ type=Path,
92
+ default=DEFAULT_CONFIG_DIR,
93
+ )
94
+ parser.add_argument(
95
+ "--profile-id",
96
+ "--profile",
97
+ help="Используемый профиль — подкаталог в --config-dir",
98
+ default=DEFAULT_PROFILE_ID,
99
+ )
114
100
  parser.add_argument(
115
101
  "-d",
116
102
  "--delay",
117
103
  type=float,
118
- default=0.334,
119
- help="Задержка между запросами к API HH",
104
+ default=0.654,
105
+ help="Задержка между запросами к API HH по умолчанию",
120
106
  )
121
- parser.add_argument("--user-agent", help="User-Agent для каждого запроса")
122
107
  parser.add_argument(
123
- "--proxy-url", help="Прокси, используемый для запросов к API"
108
+ "--user-agent",
109
+ help="User-Agent для каждого запроса",
124
110
  )
125
111
  parser.add_argument(
126
- "--disable-telemetry",
127
- default=False,
128
- action=argparse.BooleanOptionalAction,
129
- help="Отключить телеметрию",
112
+ "--proxy-url",
113
+ help="Прокси, используемый для запросов и авторизации",
130
114
  )
131
115
  subparsers = parser.add_subparsers(help="commands")
132
116
  package_dir = Path(__file__).resolve().parent / OPERATIONS
133
117
  for _, module_name, _ in iter_modules([str(package_dir)]):
118
+ if module_name.startswith("_"):
119
+ continue
134
120
  mod = import_module(f"{__package__}.{OPERATIONS}.{module_name}")
135
121
  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
-
122
+ kebab_name = module_name.replace("_", "-")
148
123
  op_parser = subparsers.add_parser(
149
124
  kebab_name,
150
- # Добавляем остальные варианты в псевдонимы
151
- aliases=[camel_case_name, flat_name],
125
+ aliases=getattr(op, "__aliases__", []),
152
126
  description=op.__doc__,
153
127
  formatter_class=self.ArgumentFormatter,
154
128
  )
@@ -157,40 +131,187 @@ class HHApplicantTool:
157
131
  parser.set_defaults(run=None)
158
132
  return parser
159
133
 
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:
134
+ def __init__(self, argv: Sequence[str] | None):
135
+ self._parse_args(argv)
136
+
137
+ # Создаем путь до конфига
138
+ self.config_path.mkdir(
139
+ parents=True,
140
+ exist_ok=True,
141
+ )
142
+
143
+ def _get_proxies(self) -> dict[str, str]:
144
+ proxy_url = self.args.proxy_url or self.config.get("proxy_url")
145
+
146
+ if proxy_url:
147
+ return {
148
+ "http": proxy_url,
149
+ "https": proxy_url,
150
+ }
151
+
152
+ proxies = {}
153
+ http_env = getenv("HTTP_PROXY") or getenv("http_proxy")
154
+ https_env = getenv("HTTPS_PROXY") or getenv("https_proxy") or http_env
155
+
156
+ if http_env:
157
+ proxies["http"] = http_env
158
+ if https_env:
159
+ proxies["https"] = https_env
160
+
161
+ return proxies
162
+
163
+ @cached_property
164
+ def session(self) -> requests.Session:
165
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
166
+
167
+ session = requests.session()
168
+ session.verify = False
169
+
170
+ if proxies := self._get_proxies():
171
+ logger.info("Use proxies: %r", proxies)
172
+ session.proxies = proxies
173
+
174
+ return session
175
+
176
+ @property
177
+ def config_path(self) -> Path:
178
+ return (self.args.config_dir / self.args.profile_id).resolve()
179
+
180
+ @cached_property
181
+ def config(self) -> utils.Config:
182
+ return utils.Config(self.config_path / DEFAULT_CONFIG_FILENAME)
183
+
184
+ @cached_property
185
+ def log_file(self) -> Path:
186
+ return self.config_path / DEFAULT_LOG_FILENAME
187
+
188
+ @cached_property
189
+ def db_path(self) -> Path:
190
+ return self.config_path / DEFAULT_DATABASE_FILENAME
191
+
192
+ @cached_property
193
+ def db(self) -> sqlite3.Connection:
194
+ conn = sqlite3.connect(self.db_path)
195
+ return conn
196
+
197
+ @cached_property
198
+ def storage(self) -> StorageFacade:
199
+ return StorageFacade(self.db)
200
+
201
+ @cached_property
202
+ def api_client(self) -> ApiClient:
203
+ args = self.args
204
+ config = self.config
205
+ token = config.get("token", {})
206
+ api = ApiClient(
207
+ client_id=config.get("client_id"),
208
+ client_secret=config.get("client_id"),
209
+ access_token=token.get("access_token"),
210
+ refresh_token=token.get("refresh_token"),
211
+ access_expires_at=token.get("access_expires_at"),
212
+ delay=args.delay,
213
+ user_agent=config.get("user_agent"),
214
+ session=self.session,
215
+ )
216
+ return api
217
+
218
+ def get_me(self) -> datatypes.User:
219
+ return self.api_client.get("/me")
220
+
221
+ def get_resumes(self) -> list[datatypes.Resume]:
222
+ return self.api_client.get("/resumes/mine")["items"]
223
+
224
+ def first_resume_id(self) -> str:
225
+ resumes = self.get_resumes()
226
+ return resumes[0]["id"]
227
+
228
+ def get_blacklisted(self) -> list[str]:
229
+ rv = []
230
+ for page in count():
231
+ r: datatypes.PaginatedItems[datatypes.EmployerShort] = (
232
+ self.api_client.get("/employers/blacklisted", page=page)
233
+ )
234
+ rv += [item["id"] for item in r["items"]]
235
+ if page + 1 >= r["pages"]:
236
+ break
237
+ return rv
238
+
239
+ def get_negotiations(
240
+ self, status: str = "active"
241
+ ) -> Iterable[datatypes.Negotiation]:
242
+ for page in count():
243
+ r: dict[str, Any] = self.api_client.get(
244
+ "/negotiations",
245
+ page=page,
246
+ per_page=100,
247
+ status=status,
248
+ )
249
+
250
+ items = r.get("items", [])
251
+
252
+ if not items:
253
+ break
254
+
255
+ yield from items
256
+
257
+ if page + 1 >= r.get("pages", 0):
258
+ break
259
+
260
+ def save_token(self) -> bool:
261
+ if self.api_client.access_token != self.config.get("token", {}).get(
262
+ "access_token"
263
+ ):
264
+ self.config.save(token=self.api_client.get_access_token())
265
+ return True
266
+ return False
267
+
268
+ def run(self) -> None | int:
269
+ verbosity_level = max(
270
+ logging.DEBUG,
271
+ logging.WARNING - self.args.verbosity * 10,
272
+ )
273
+
274
+ setup_logger(logger, verbosity_level, self.log_file)
275
+
276
+ if sys.platform == "win32":
277
+ utils.setup_terminal()
278
+
279
+ try:
280
+ if self.args.run:
281
+ try:
282
+ res = self.args.run(self)
283
+ if self.save_token():
284
+ logger.info("Токен был обновлен.")
285
+ return res
286
+ except KeyboardInterrupt:
287
+ logger.warning("Выполнение прервано пользователем!")
288
+ return 1
289
+ except sqlite3.Error as ex:
290
+ logger.exception(ex)
291
+
292
+ script_name = sys.argv[0].split(os.sep)[-1]
293
+
294
+ logger.warning(
295
+ f"Возможно база данных повреждена, попробуйте выполнить команду:\n\n" # noqa: E501
296
+ f" {script_name} migrate-db"
297
+ )
298
+ return 1
299
+ except Exception as e:
300
+ logger.exception(e)
301
+ return 1
302
+ self._parser.print_help(file=sys.stderr)
303
+ return 2
304
+ finally:
170
305
  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
306
+ self._check_system()
307
+ except Exception:
308
+ pass
309
+ # raise
310
+
311
+ def _parse_args(self, argv) -> None:
312
+ self._parser = self._create_parser()
313
+ self.args = self._parser.parse_args(argv, namespace=BaseNamespace())
193
314
 
194
315
 
195
316
  def main(argv: Sequence[str] | None = None) -> None | int:
196
- return HHApplicantTool().run(argv)
317
+ return HHApplicantTool(argv).run()