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.
- hh_applicant_tool/__init__.py +1 -0
- hh_applicant_tool/__main__.py +1 -1
- hh_applicant_tool/ai/base.py +2 -0
- hh_applicant_tool/ai/openai.py +25 -35
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +65 -68
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
- hh_applicant_tool/api/datatypes.py +293 -0
- hh_applicant_tool/api/errors.py +57 -7
- hh_applicant_tool/api/user_agent.py +17 -0
- hh_applicant_tool/main.py +234 -113
- hh_applicant_tool/operations/apply_similar.py +353 -371
- hh_applicant_tool/operations/authorize.py +313 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/clear_negotiations.py +90 -82
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +23 -11
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +122 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +201 -180
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +23 -11
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +8 -0
- hh_applicant_tool/storage/facade.py +24 -0
- hh_applicant_tool/storage/models/__init__.py +0 -0
- hh_applicant_tool/storage/models/base.py +169 -0
- hh_applicant_tool/storage/models/contacts.py +28 -0
- hh_applicant_tool/storage/models/employer.py +12 -0
- hh_applicant_tool/storage/models/negotiation.py +16 -0
- hh_applicant_tool/storage/models/resume.py +19 -0
- hh_applicant_tool/storage/models/setting.py +6 -0
- hh_applicant_tool/storage/models/vacancy.py +36 -0
- hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
- hh_applicant_tool/storage/queries/schema.sql +132 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +230 -0
- hh_applicant_tool/storage/repositories/contacts.py +14 -0
- hh_applicant_tool/storage/repositories/employers.py +14 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +13 -0
- hh_applicant_tool/storage/repositories/resumes.py +9 -0
- hh_applicant_tool/storage/repositories/settings.py +35 -0
- hh_applicant_tool/storage/repositories/vacancies.py +9 -0
- hh_applicant_tool/storage/utils.py +40 -0
- hh_applicant_tool/utils/__init__.py +31 -0
- hh_applicant_tool/utils/attrdict.py +6 -0
- hh_applicant_tool/utils/binpack.py +167 -0
- hh_applicant_tool/utils/config.py +55 -0
- hh_applicant_tool/utils/date.py +19 -0
- hh_applicant_tool/utils/json.py +61 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/log.py +147 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +221 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +32 -0
- hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
- hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
- hh_applicant_tool/ai/blackbox.py +0 -55
- hh_applicant_tool/color_log.py +0 -47
- hh_applicant_tool/mixins.py +0 -13
- hh_applicant_tool/operations/delete_telemetry.py +0 -30
- hh_applicant_tool/operations/get_employer_contacts.py +0 -348
- hh_applicant_tool/telemetry_client.py +0 -106
- hh_applicant_tool/types.py +0 -45
- hh_applicant_tool/utils.py +0 -119
- hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
- hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
hh_applicant_tool/api/errors.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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.
|
|
29
|
+
self._data = data
|
|
28
30
|
|
|
29
31
|
@property
|
|
30
32
|
def data(self) -> dict:
|
|
31
|
-
return self.
|
|
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
|
|
62
|
+
return self.message
|
|
53
63
|
|
|
54
64
|
@staticmethod
|
|
55
|
-
def
|
|
56
|
-
return any(
|
|
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
|
|
15
|
+
from typing import Any, Iterable
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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(
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"--
|
|
108
|
+
"--user-agent",
|
|
109
|
+
help="User-Agent для каждого запроса",
|
|
124
110
|
)
|
|
125
111
|
parser.add_argument(
|
|
126
|
-
"--
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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(
|
|
317
|
+
return HHApplicantTool(argv).run()
|