hh-applicant-tool 1.4.12__py3-none-any.whl → 1.5.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/__main__.py +1 -1
- hh_applicant_tool/ai/__init__.py +1 -0
- hh_applicant_tool/ai/openai.py +28 -12
- hh_applicant_tool/api/client.py +11 -7
- hh_applicant_tool/main.py +57 -31
- hh_applicant_tool/operations/apply_similar.py +12 -6
- hh_applicant_tool/operations/authorize.py +24 -12
- hh_applicant_tool/operations/clear_negotiations.py +0 -1
- hh_applicant_tool/operations/query.py +2 -2
- hh_applicant_tool/operations/refresh_token.py +9 -2
- hh_applicant_tool/operations/whoami.py +2 -2
- hh_applicant_tool/storage/models/base.py +5 -0
- hh_applicant_tool/storage/models/contacts.py +15 -1
- hh_applicant_tool/storage/queries/schema.sql +1 -1
- hh_applicant_tool/storage/utils.py +7 -1
- hh_applicant_tool/utils/log.py +2 -2
- hh_applicant_tool/utils/mixins.py +9 -28
- hh_applicant_tool/utils/string.py +15 -0
- hh_applicant_tool/utils/terminal.py +102 -0
- {hh_applicant_tool-1.4.12.dist-info → hh_applicant_tool-1.5.7.dist-info}/METADATA +247 -82
- {hh_applicant_tool-1.4.12.dist-info → hh_applicant_tool-1.5.7.dist-info}/RECORD +23 -23
- {hh_applicant_tool-1.4.12.dist-info → hh_applicant_tool-1.5.7.dist-info}/WHEEL +1 -1
- {hh_applicant_tool-1.4.12.dist-info → hh_applicant_tool-1.5.7.dist-info}/entry_points.txt +0 -0
|
@@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Literal
|
|
|
11
11
|
import requests
|
|
12
12
|
from requests.exceptions import RequestException
|
|
13
13
|
|
|
14
|
-
from ..ai.openai import ChatOpenAI
|
|
15
14
|
from . import binpack
|
|
16
15
|
from .log import collect_traceback_logs
|
|
17
16
|
|
|
@@ -38,9 +37,7 @@ class ErrorReporter:
|
|
|
38
37
|
error_logs = ""
|
|
39
38
|
if self.log_file.exists():
|
|
40
39
|
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
|
-
)
|
|
40
|
+
error_logs = collect_traceback_logs(fp, last_report)
|
|
44
41
|
|
|
45
42
|
# Эти данные нужны для воспроизведения ошибок. Среди них ваших
|
|
46
43
|
# персональных данных нет
|
|
@@ -49,7 +46,7 @@ class ErrorReporter:
|
|
|
49
46
|
for c in self.storage.vacancy_contacts.find(
|
|
50
47
|
updated_at__ge=last_report
|
|
51
48
|
)
|
|
52
|
-
]
|
|
49
|
+
]
|
|
53
50
|
|
|
54
51
|
for c in vacancy_contacts:
|
|
55
52
|
c.pop("id", 0)
|
|
@@ -71,7 +68,7 @@ class ErrorReporter:
|
|
|
71
68
|
]
|
|
72
69
|
}
|
|
73
70
|
for emp in self.storage.employers.find(updated_at__ge=last_report)
|
|
74
|
-
]
|
|
71
|
+
]
|
|
75
72
|
|
|
76
73
|
vacancies = [
|
|
77
74
|
{
|
|
@@ -94,7 +91,7 @@ class ErrorReporter:
|
|
|
94
91
|
]
|
|
95
92
|
}
|
|
96
93
|
for vac in self.storage.vacancies.find(updated_at__ge=last_report)
|
|
97
|
-
]
|
|
94
|
+
]
|
|
98
95
|
|
|
99
96
|
# log.info("num vacncies: %d", len(vacancies))
|
|
100
97
|
|
|
@@ -106,10 +103,10 @@ class ErrorReporter:
|
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
return dict(
|
|
109
|
-
error_logs=error_logs,
|
|
110
|
-
vacancy_contacts=vacancy_contacts,
|
|
111
|
-
employers=employers,
|
|
112
|
-
vacancies=vacancies,
|
|
106
|
+
error_logs=error_logs[-100000:],
|
|
107
|
+
vacancy_contacts=vacancy_contacts[-10000:],
|
|
108
|
+
employers=employers[-10000:],
|
|
109
|
+
vacancies=vacancies[-10000:],
|
|
113
110
|
package_version=get_package_version(),
|
|
114
111
|
system_info=system_info,
|
|
115
112
|
report_created=datetime.now(timezone.utc),
|
|
@@ -194,23 +191,7 @@ class VersionChecker:
|
|
|
194
191
|
)
|
|
195
192
|
|
|
196
193
|
|
|
197
|
-
class
|
|
198
|
-
def get_openai_chat(
|
|
199
|
-
self: HHApplicantTool,
|
|
200
|
-
system_prompt: str,
|
|
201
|
-
) -> ChatOpenAI:
|
|
202
|
-
c = self.config.get("openai", {})
|
|
203
|
-
if not (token := c.get("token")):
|
|
204
|
-
raise ValueError("Токен для OpenAI не задан")
|
|
205
|
-
return ChatOpenAI(
|
|
206
|
-
token=token,
|
|
207
|
-
model=c.get("model", "gpt-5.1"),
|
|
208
|
-
system_prompt=system_prompt,
|
|
209
|
-
session=self.session,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
class MegaTool(ErrorReporter, VersionChecker, ChatOpenAISupport):
|
|
194
|
+
class MegaTool(ErrorReporter, VersionChecker):
|
|
214
195
|
def _check_system(self: HHApplicantTool):
|
|
215
196
|
if not self.storage.settings.get_value("disable_version_check", False):
|
|
216
197
|
self._check_version()
|
|
@@ -4,9 +4,11 @@ import random
|
|
|
4
4
|
import re
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
def shorten(s: str, limit: int = 75, ellipsis: str = "…") -> str:
|
|
8
9
|
return s[:limit] + bool(s[limit:]) * ellipsis
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
def rand_text(s: str) -> str:
|
|
11
13
|
while (
|
|
12
14
|
temp := re.sub(
|
|
@@ -20,8 +22,21 @@ def rand_text(s: str) -> str:
|
|
|
20
22
|
s = temp
|
|
21
23
|
return s
|
|
22
24
|
|
|
25
|
+
|
|
23
26
|
def bool2str(v: bool) -> str:
|
|
24
27
|
return str(v).lower()
|
|
25
28
|
|
|
29
|
+
|
|
26
30
|
def list2str(items: list[Any] | None) -> str:
|
|
27
31
|
return ",".join(f"{v}" for v in items) if items else ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unescape_string(text: str) -> str:
|
|
35
|
+
if not text:
|
|
36
|
+
return ""
|
|
37
|
+
return (
|
|
38
|
+
text.replace(r"\n", "\n")
|
|
39
|
+
.replace(r"\r", "\r")
|
|
40
|
+
.replace(r"\t", "\t")
|
|
41
|
+
.replace(r"\\", "\\")
|
|
42
|
+
)
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import ctypes
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
3
7
|
import platform
|
|
4
8
|
import sys
|
|
5
9
|
|
|
10
|
+
try:
|
|
11
|
+
from PIL import Image
|
|
12
|
+
except ImportError:
|
|
13
|
+
|
|
14
|
+
class Image:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ESC = "\x1b"
|
|
19
|
+
|
|
6
20
|
|
|
7
21
|
def setup_terminal() -> None:
|
|
8
22
|
if platform.system() != "Windows":
|
|
@@ -30,3 +44,91 @@ def print_kitty_image(data: bytes) -> None:
|
|
|
30
44
|
sys.stdout.write(f"\033_Ga=T,f=100;{b64data}\033\\")
|
|
31
45
|
sys.stdout.flush()
|
|
32
46
|
print()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_sixel_mage(image_bytes: bytes) -> None:
|
|
50
|
+
img = Image.open(io.BytesIO(image_bytes))
|
|
51
|
+
|
|
52
|
+
# Рекомендуется оставить ограничение размера,
|
|
53
|
+
# иначе Zellij может "лагать" на огромных картинках
|
|
54
|
+
# max_size = (800, 600)
|
|
55
|
+
# img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
|
56
|
+
img = img.convert("RGB")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
img = img.quantize(colors=256, method=Image.Quantize.MAXCOVERAGE)
|
|
60
|
+
except Exception:
|
|
61
|
+
img = img.quantize(colors=256)
|
|
62
|
+
|
|
63
|
+
palette = img.getpalette()[: 256 * 3]
|
|
64
|
+
width, height = img.size
|
|
65
|
+
pixels = img.load()
|
|
66
|
+
|
|
67
|
+
is_multiplexer = "ZELLIJ" in os.environ or "TMUX" in os.environ
|
|
68
|
+
|
|
69
|
+
# Собираем всё в список строк, чтобы минимизировать количество вызовов write
|
|
70
|
+
out = []
|
|
71
|
+
|
|
72
|
+
# 1. Начало (Обертка для Zellij)
|
|
73
|
+
if is_multiplexer:
|
|
74
|
+
out.append(f"{ESC}P+p")
|
|
75
|
+
|
|
76
|
+
# 2. Sixel заголовок + Растр
|
|
77
|
+
out.append(f'{ESC}Pq"1;1;{width};{height}')
|
|
78
|
+
|
|
79
|
+
# 3. Палитра
|
|
80
|
+
for i in range(256):
|
|
81
|
+
r, g, b = palette[i * 3 : i * 3 + 3]
|
|
82
|
+
out.append(f"#{i};2;{r * 100 // 255};{g * 100 // 255};{b * 100 // 255}")
|
|
83
|
+
|
|
84
|
+
# 4. Отрисовка
|
|
85
|
+
for y in range(0, height, 6):
|
|
86
|
+
h_chunk = min(6, height - y)
|
|
87
|
+
|
|
88
|
+
# Считаем уникальные цвета в полосе (быстрее, чем перебирать всю палитру)
|
|
89
|
+
colors_in_band = set()
|
|
90
|
+
for dy in range(h_chunk):
|
|
91
|
+
for x in range(width):
|
|
92
|
+
colors_in_band.add(pixels[x, y + dy])
|
|
93
|
+
|
|
94
|
+
for color in colors_in_band:
|
|
95
|
+
out.append(f"#{color}")
|
|
96
|
+
last_char = ""
|
|
97
|
+
count = 0
|
|
98
|
+
|
|
99
|
+
for x in range(width):
|
|
100
|
+
bits = 0
|
|
101
|
+
for dy in range(h_chunk):
|
|
102
|
+
if pixels[x, y + dy] == color:
|
|
103
|
+
bits |= 1 << dy
|
|
104
|
+
|
|
105
|
+
char = chr(63 + bits)
|
|
106
|
+
if char == last_char:
|
|
107
|
+
count += 1
|
|
108
|
+
else:
|
|
109
|
+
if count > 0:
|
|
110
|
+
out.append(
|
|
111
|
+
f"!{count}{last_char}"
|
|
112
|
+
if count > 3
|
|
113
|
+
else last_char * count
|
|
114
|
+
)
|
|
115
|
+
last_char, count = char, 1
|
|
116
|
+
|
|
117
|
+
if count > 0:
|
|
118
|
+
out.append(
|
|
119
|
+
f"!{count}{last_char}" if count > 3 else last_char * count
|
|
120
|
+
)
|
|
121
|
+
out.append("$")
|
|
122
|
+
out.append("-")
|
|
123
|
+
|
|
124
|
+
# 5. Конец Sixel
|
|
125
|
+
out.append(f"{ESC}\\")
|
|
126
|
+
|
|
127
|
+
# 6. Конец обертки мультиплексора
|
|
128
|
+
if is_multiplexer:
|
|
129
|
+
out.append(f"{ESC}\\")
|
|
130
|
+
|
|
131
|
+
# ВЫВОД: Одной строкой БЕЗ лишних переносов в конце
|
|
132
|
+
sys.stdout.write("".join(out))
|
|
133
|
+
sys.stdout.flush()
|
|
134
|
+
print()
|