hh-applicant-tool 0.7.10__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.
- 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 +23 -33
- hh_applicant_tool/api/client.py +50 -64
- hh_applicant_tool/api/errors.py +51 -7
- hh_applicant_tool/constants.py +0 -3
- hh_applicant_tool/datatypes.py +291 -0
- hh_applicant_tool/main.py +233 -111
- hh_applicant_tool/operations/apply_similar.py +266 -362
- hh_applicant_tool/operations/authorize.py +256 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_negotiations.py +102 -0
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +24 -10
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +120 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +148 -167
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +21 -10
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +4 -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/contact.py +16 -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 +119 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +176 -0
- hh_applicant_tool/storage/repositories/contacts.py +19 -0
- hh_applicant_tool/storage/repositories/employers.py +13 -0
- hh_applicant_tool/storage/repositories/negotiations.py +12 -0
- hh_applicant_tool/storage/repositories/resumes.py +14 -0
- hh_applicant_tool/storage/repositories/settings.py +34 -0
- hh_applicant_tool/storage/repositories/vacancies.py +8 -0
- hh_applicant_tool/storage/utils.py +49 -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/dateutil.py +19 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/jsonutil.py +61 -0
- hh_applicant_tool/utils/log.py +144 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +220 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +19 -0
- hh_applicant_tool/utils/user_agent.py +17 -0
- hh_applicant_tool-1.4.7.dist-info/METADATA +628 -0
- hh_applicant_tool-1.4.7.dist-info/RECORD +67 -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/clear_negotiations.py +0 -109
- 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.7.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import socket
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from functools import cache
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
from logging import getLogger
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from requests.exceptions import RequestException
|
|
13
|
+
|
|
14
|
+
from ..ai.openai import ChatOpenAI
|
|
15
|
+
from . import binpack
|
|
16
|
+
from .log import collect_traceback_logs
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..main import HHApplicantTool
|
|
20
|
+
|
|
21
|
+
log = getLogger(__package__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_version(v: str) -> tuple[int, int, int]:
|
|
25
|
+
return tuple(map(int, v.split(".")))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cache
|
|
29
|
+
def get_package_version() -> str | None:
|
|
30
|
+
return version("hh-applicant-tool")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ErrorReporter:
|
|
34
|
+
def build_report(
|
|
35
|
+
self: HHApplicantTool,
|
|
36
|
+
last_report: datetime,
|
|
37
|
+
) -> dict:
|
|
38
|
+
error_logs = ""
|
|
39
|
+
if self.log_file.exists():
|
|
40
|
+
with self.log_file.open(encoding="utf-8", errors="ignore") as fp:
|
|
41
|
+
error_logs = collect_traceback_logs(
|
|
42
|
+
fp, last_report, maxlen=10000
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Эти данные нужны для воспроизведения ошибок. Среди них ваших нет
|
|
46
|
+
contacts = [
|
|
47
|
+
c.to_dict()
|
|
48
|
+
for c in self.storage.employer_contacts.find(
|
|
49
|
+
updated_at__ge=last_report
|
|
50
|
+
)
|
|
51
|
+
][-10000:]
|
|
52
|
+
|
|
53
|
+
for c in contacts:
|
|
54
|
+
c.pop("id", 0)
|
|
55
|
+
|
|
56
|
+
employers = [
|
|
57
|
+
{
|
|
58
|
+
k: v
|
|
59
|
+
for k, v in emp.to_dict().items()
|
|
60
|
+
if k
|
|
61
|
+
in [
|
|
62
|
+
"id",
|
|
63
|
+
"type",
|
|
64
|
+
"alternate_url",
|
|
65
|
+
"area_id",
|
|
66
|
+
"area_name",
|
|
67
|
+
"name",
|
|
68
|
+
"site_url",
|
|
69
|
+
"created_at",
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
for emp in self.storage.employers.find(updated_at__ge=last_report)
|
|
73
|
+
][-10000:]
|
|
74
|
+
|
|
75
|
+
vacancies = [
|
|
76
|
+
{
|
|
77
|
+
k: v
|
|
78
|
+
for k, v in vac.to_dict().items()
|
|
79
|
+
if k
|
|
80
|
+
in [
|
|
81
|
+
"id",
|
|
82
|
+
"alternate_url",
|
|
83
|
+
"area_id",
|
|
84
|
+
"area_name",
|
|
85
|
+
"salary_from",
|
|
86
|
+
"salary_to",
|
|
87
|
+
"currency",
|
|
88
|
+
"name",
|
|
89
|
+
"professional_roles",
|
|
90
|
+
"experience",
|
|
91
|
+
"remote",
|
|
92
|
+
"created_at",
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
for vac in self.storage.vacancies.find(updated_at__ge=last_report)
|
|
96
|
+
][-10000:]
|
|
97
|
+
|
|
98
|
+
# log.info("num vacncies: %d", len(vacancies))
|
|
99
|
+
|
|
100
|
+
system_info = {
|
|
101
|
+
"os": platform.system(),
|
|
102
|
+
"os_release": platform.release(),
|
|
103
|
+
"hostname": socket.gethostname(),
|
|
104
|
+
"python_version": platform.python_version(),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return dict(
|
|
108
|
+
error_logs=error_logs,
|
|
109
|
+
contacts=contacts,
|
|
110
|
+
employers=employers,
|
|
111
|
+
vacancies=vacancies,
|
|
112
|
+
package_version=get_package_version(),
|
|
113
|
+
system_info=system_info,
|
|
114
|
+
report_created=datetime.now(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def send_report(self: HHApplicantTool, data: bytes) -> int:
|
|
118
|
+
try:
|
|
119
|
+
r = self.session.post(
|
|
120
|
+
# "http://localhost:8000/report",
|
|
121
|
+
"https://hh-applicant-tool.mooo.com:54157/report",
|
|
122
|
+
data=data,
|
|
123
|
+
timeout=15.0,
|
|
124
|
+
)
|
|
125
|
+
r.raise_for_status()
|
|
126
|
+
return r.status_code == 200
|
|
127
|
+
except RequestException:
|
|
128
|
+
# log.error("Network error: %s", e)
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def process_reporting(self):
|
|
132
|
+
# Получаем timestamp последнего репорта
|
|
133
|
+
last_report = datetime.fromtimestamp(
|
|
134
|
+
self.storage.settings.get_value("_last_report", 0)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if datetime.now() >= last_report + timedelta(hours=72):
|
|
138
|
+
try:
|
|
139
|
+
report_dict = self.build_report(last_report)
|
|
140
|
+
has_data = any(
|
|
141
|
+
[
|
|
142
|
+
report_dict.get("error_logs"),
|
|
143
|
+
report_dict.get("employers"),
|
|
144
|
+
report_dict.get("contacts"),
|
|
145
|
+
report_dict.get("vacancies"),
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
if has_data:
|
|
149
|
+
data = binpack.serialize(report_dict)
|
|
150
|
+
log.debug("Report body size: %d", len(data))
|
|
151
|
+
# print(binpack.deserialize(data))
|
|
152
|
+
if self.send_report(data):
|
|
153
|
+
log.debug("Report was sent")
|
|
154
|
+
else:
|
|
155
|
+
log.debug("Report failed")
|
|
156
|
+
else:
|
|
157
|
+
log.debug("Nothing to report")
|
|
158
|
+
finally:
|
|
159
|
+
# Сохраняем время последней попытки/удачного репорта
|
|
160
|
+
self.storage.settings.set_value("_last_report", datetime.now())
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class VersionChecker:
|
|
164
|
+
def get_latest_version(self: HHApplicantTool) -> Literal[False] | str:
|
|
165
|
+
try:
|
|
166
|
+
response = self.session.get(
|
|
167
|
+
"https://pypi.org/pypi/hh-applicant-tool/json", timeout=15
|
|
168
|
+
)
|
|
169
|
+
ver = response.json().get("info", {}).get("version")
|
|
170
|
+
# log.debug(ver)
|
|
171
|
+
return ver
|
|
172
|
+
except requests.RequestException:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def check_version(self: HHApplicantTool) -> bool:
|
|
176
|
+
if datetime.now().timestamp() >= self.storage.settings.get_value(
|
|
177
|
+
"_next_version_check", 0
|
|
178
|
+
):
|
|
179
|
+
if v := self.get_latest_version():
|
|
180
|
+
self.storage.settings.set_value("_latest_version", v)
|
|
181
|
+
self.storage.settings.set_value(
|
|
182
|
+
"_next_version_check", datetime.now() + timedelta(hours=1)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
latest_ver := self.storage.settings.get_value("_latest_version")
|
|
187
|
+
) and (cur_ver := get_package_version()):
|
|
188
|
+
if parse_version(latest_ver) > parse_version(cur_ver):
|
|
189
|
+
log.warning(
|
|
190
|
+
"ТЕКУЩАЯ ВЕРСИЯ %s УСТАРЕЛА. РЕКОМЕНДУЕТСЯ ОБНОВИТЬ ЕЁ ДО ВЕРСИИ %s.",
|
|
191
|
+
cur_ver,
|
|
192
|
+
latest_ver,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ChatOpenAISupport:
|
|
197
|
+
def get_openai_chat(
|
|
198
|
+
self: HHApplicantTool,
|
|
199
|
+
system_prompt: str,
|
|
200
|
+
) -> ChatOpenAI:
|
|
201
|
+
c = self.config.get("openai", {})
|
|
202
|
+
if not (token := c.get("token")):
|
|
203
|
+
raise ValueError("Токен для OpenAI не задан")
|
|
204
|
+
return ChatOpenAI(
|
|
205
|
+
token=token,
|
|
206
|
+
model=c.get("model", "gpt-5.1"),
|
|
207
|
+
system_prompt=system_prompt,
|
|
208
|
+
session=self.session,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class MegaTool(ErrorReporter, VersionChecker, ChatOpenAISupport):
|
|
213
|
+
def check_system(self: HHApplicantTool):
|
|
214
|
+
if not self.storage.settings.get_value("disable_version_check", False):
|
|
215
|
+
self.check_version()
|
|
216
|
+
|
|
217
|
+
if self.storage.settings.get_value("send_error_reports", True):
|
|
218
|
+
self.process_reporting()
|
|
219
|
+
else:
|
|
220
|
+
log.warning("ОТКЛЮЧЕНА ОТПРАВКА СООБЩЕНИЙ ОБ ОШИБКАХ!")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
def shorten(s: str, limit: int = 75, ellipsis: str = "…") -> str:
|
|
8
|
+
return s[:limit] + bool(s[limit:]) * ellipsis
|
|
9
|
+
|
|
10
|
+
def rand_text(s: str) -> str:
|
|
11
|
+
while (
|
|
12
|
+
temp := re.sub(
|
|
13
|
+
r"{([^{}]+)}",
|
|
14
|
+
lambda m: random.choice(
|
|
15
|
+
m.group(1).split("|"),
|
|
16
|
+
),
|
|
17
|
+
s,
|
|
18
|
+
)
|
|
19
|
+
) != s:
|
|
20
|
+
s = temp
|
|
21
|
+
return s
|
|
22
|
+
|
|
23
|
+
def bool2str(v: bool) -> str:
|
|
24
|
+
return str(v).lower()
|
|
25
|
+
|
|
26
|
+
def list2str(items: list[Any] | None) -> str:
|
|
27
|
+
return ",".join(f"{v}" for v in items) if items else ""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import platform
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_terminal() -> None:
|
|
6
|
+
if platform.system() != "Windows":
|
|
7
|
+
return
|
|
8
|
+
try:
|
|
9
|
+
kernel32 = ctypes.windll.kernel32
|
|
10
|
+
# -11 = STD_OUTPUT_HANDLE
|
|
11
|
+
handle = kernel32.GetStdHandle(-11)
|
|
12
|
+
mode = ctypes.c_uint()
|
|
13
|
+
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
14
|
+
# 0x0004 = ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
15
|
+
kernel32.SetConsoleMode(handle, mode.value | 0x0004)
|
|
16
|
+
except Exception:
|
|
17
|
+
# Если что-то пошло не так (старая Windows или нет прав),
|
|
18
|
+
# просто продолжаем работу без цветов
|
|
19
|
+
pass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def hh_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
|
+
)
|