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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
# class DateAwareJSONEncoder(json.JSONEncoder):
|
|
6
|
+
# def default(self, o):
|
|
7
|
+
# if isinstance(o, dt.datetime):
|
|
8
|
+
# return o.isoformat()
|
|
9
|
+
|
|
10
|
+
# return super().default(o)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Костыль чтобы в key-value хранить даты
|
|
14
|
+
class DateAwareJSONEncoder(json.JSONEncoder):
|
|
15
|
+
def default(self, o):
|
|
16
|
+
if isinstance(o, dt.datetime):
|
|
17
|
+
return int(o.timestamp())
|
|
18
|
+
|
|
19
|
+
return super().default(o)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# def date_parser_hook(dct):
|
|
23
|
+
# for k, v in dct.items():
|
|
24
|
+
# if isinstance(v, str):
|
|
25
|
+
# try:
|
|
26
|
+
# dct[k] = dt.datetime.fromisoformat(v)
|
|
27
|
+
# except (ValueError, TypeError):
|
|
28
|
+
# pass
|
|
29
|
+
# return dct
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# class DateAwareJSONDecoder(json.JSONDecoder):
|
|
33
|
+
# def __init__(self, *args, **kwargs):
|
|
34
|
+
# super().__init__(*args, object_hook=date_parser_hook, **kwargs)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dumps(obj, *args: Any, **kwargs: Any) -> str:
|
|
38
|
+
kwargs.setdefault("cls", DateAwareJSONEncoder)
|
|
39
|
+
kwargs.setdefault("ensure_ascii", False)
|
|
40
|
+
return json.dumps(obj, *args, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def dump(fp, obj, *args: Any, **kwargs: Any) -> None:
|
|
44
|
+
kwargs.setdefault("cls", DateAwareJSONEncoder)
|
|
45
|
+
kwargs.setdefault("ensure_ascii", False)
|
|
46
|
+
json.dump(fp, obj, *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def loads(s, *args: Any, **kwargs: Any) -> Any:
|
|
50
|
+
# kwargs.setdefault("object_hook", date_parser_hook)
|
|
51
|
+
return json.loads(s, *args, **kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load(fp, *args: Any, **kwargs: Any) -> Any:
|
|
55
|
+
# kwargs.setdefault("object_hook", date_parser_hook)
|
|
56
|
+
return json.load(fp, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
d = {"created_at": dt.datetime.now()}
|
|
61
|
+
print(loads(dumps(d)))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Unused
|
|
2
1
|
"""Парсер JSON с комментариями"""
|
|
3
2
|
|
|
4
|
-
import
|
|
3
|
+
import ast
|
|
5
4
|
import enum
|
|
5
|
+
import re
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
import ast
|
|
8
7
|
from typing import Any, Iterator
|
|
8
|
+
|
|
9
9
|
# from collections import OrderedDict
|
|
10
10
|
|
|
11
11
|
|
|
@@ -42,7 +42,8 @@ def tokenize(s: str) -> Iterator[Token]:
|
|
|
42
42
|
class JSONCParser:
|
|
43
43
|
def parse(self, s: str) -> Any:
|
|
44
44
|
self.token_it = filter(
|
|
45
|
-
lambda t: t.token_type
|
|
45
|
+
lambda t: t.token_type
|
|
46
|
+
not in [TokenType.COMMENT, TokenType.WHITESPACE],
|
|
46
47
|
tokenize(s),
|
|
47
48
|
)
|
|
48
49
|
self.token: Token
|
|
@@ -90,7 +91,9 @@ class JSONCParser:
|
|
|
90
91
|
num = self.token.value
|
|
91
92
|
return float(num) if "." in num else int(num)
|
|
92
93
|
elif self.match(TokenType.KEYWORD):
|
|
93
|
-
return {"null": None, "true": True, "false": False}[
|
|
94
|
+
return {"null": None, "true": True, "false": False}[
|
|
95
|
+
self.token.value
|
|
96
|
+
]
|
|
94
97
|
else:
|
|
95
98
|
raise SyntaxError(f"Unexpected token: {self.token.token_type.name}")
|
|
96
99
|
|
|
@@ -103,7 +106,10 @@ class JSONCParser:
|
|
|
103
106
|
# print(f"{self.token =}, {self.next_token =}")
|
|
104
107
|
|
|
105
108
|
def match(self, token_type: TokenType) -> bool:
|
|
106
|
-
if
|
|
109
|
+
if (
|
|
110
|
+
self.next_token is not None
|
|
111
|
+
and self.next_token.token_type == token_type
|
|
112
|
+
):
|
|
107
113
|
self.advance()
|
|
108
114
|
return True
|
|
109
115
|
return False
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from collections import deque
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import auto
|
|
7
|
+
from logging.handlers import RotatingFileHandler
|
|
8
|
+
from os import PathLike
|
|
9
|
+
from typing import Callable, TextIO
|
|
10
|
+
|
|
11
|
+
# 10MB
|
|
12
|
+
MAX_LOG_SIZE = 10 << 20
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Color(enum.Enum):
|
|
16
|
+
BLACK = 30
|
|
17
|
+
RED = auto()
|
|
18
|
+
GREEN = auto()
|
|
19
|
+
YELLOW = auto()
|
|
20
|
+
BLUE = auto()
|
|
21
|
+
PURPLE = auto()
|
|
22
|
+
CYAN = auto()
|
|
23
|
+
WHITE = auto()
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return str(self.value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ColorHandler(logging.StreamHandler):
|
|
30
|
+
_color_map = {
|
|
31
|
+
"CRITICAL": Color.RED,
|
|
32
|
+
"ERROR": Color.RED,
|
|
33
|
+
"WARNING": Color.RED,
|
|
34
|
+
"INFO": Color.GREEN,
|
|
35
|
+
"DEBUG": Color.BLUE,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
39
|
+
# Подавляем вывод подробного сообщения об ошибке
|
|
40
|
+
orig_exc_info = record.exc_info
|
|
41
|
+
|
|
42
|
+
# Детали ошибки показываем только при отладке
|
|
43
|
+
if self.level > logging.DEBUG:
|
|
44
|
+
record.exc_info = None
|
|
45
|
+
|
|
46
|
+
message = super().format(record)
|
|
47
|
+
# Обязательно нужно восстановить оригинальное значение или в файловом
|
|
48
|
+
# логе не будет деталей ошибки
|
|
49
|
+
record.exc_info = orig_exc_info
|
|
50
|
+
# isatty = getattr(self.stream, "isatty", None)
|
|
51
|
+
# if isatty and isatty():
|
|
52
|
+
color_code = self._color_map[record.levelname]
|
|
53
|
+
return f"\033[{color_code}m{message}\033[0m"
|
|
54
|
+
# return message
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RedactingFilter(logging.Filter):
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
patterns: list[str],
|
|
61
|
+
# По умолчанию количество звездочек равно оригинальной строке
|
|
62
|
+
placeholder: str | Callable = lambda m: "*" * len(m.group(0)),
|
|
63
|
+
):
|
|
64
|
+
super().__init__()
|
|
65
|
+
self.pattern = (
|
|
66
|
+
re.compile(f"({'|'.join(patterns)})") if patterns else None
|
|
67
|
+
)
|
|
68
|
+
self.placeholder = placeholder
|
|
69
|
+
|
|
70
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
71
|
+
if self.pattern:
|
|
72
|
+
msg = record.getMessage()
|
|
73
|
+
msg = self.pattern.sub(self.placeholder, msg)
|
|
74
|
+
record.msg, record.args = msg, ()
|
|
75
|
+
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def setup_logger(
|
|
80
|
+
logger: logging.Logger,
|
|
81
|
+
verbosity_level: int,
|
|
82
|
+
log_file: PathLike,
|
|
83
|
+
) -> None:
|
|
84
|
+
# В лог-файл пишем все!
|
|
85
|
+
logger.setLevel(logging.DEBUG)
|
|
86
|
+
color_handler = ColorHandler()
|
|
87
|
+
# [C] Critical Error Occurred
|
|
88
|
+
color_handler.setFormatter(
|
|
89
|
+
logging.Formatter("[%(levelname).1s] %(message)s")
|
|
90
|
+
)
|
|
91
|
+
color_handler.setLevel(verbosity_level)
|
|
92
|
+
|
|
93
|
+
# Логи
|
|
94
|
+
file_handler = RotatingFileHandler(
|
|
95
|
+
log_file,
|
|
96
|
+
maxBytes=MAX_LOG_SIZE,
|
|
97
|
+
# backupCount=1,
|
|
98
|
+
encoding="utf-8",
|
|
99
|
+
)
|
|
100
|
+
file_handler.setFormatter(
|
|
101
|
+
logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
102
|
+
)
|
|
103
|
+
file_handler.setLevel(logging.DEBUG)
|
|
104
|
+
|
|
105
|
+
redactor = RedactingFilter(
|
|
106
|
+
[
|
|
107
|
+
r"\b[A-Z0-9]{64,}\b",
|
|
108
|
+
r"\b[a-fA-F0-9]{32,}\b", # request_id, resume_id
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
file_handler.addFilter(redactor)
|
|
113
|
+
|
|
114
|
+
for h in [color_handler, file_handler]:
|
|
115
|
+
logger.addHandler(h)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
TS_RE = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def collect_traceback_logs(
|
|
122
|
+
fp: TextIO,
|
|
123
|
+
after_dt: datetime,
|
|
124
|
+
maxlen: int = 1000,
|
|
125
|
+
) -> str:
|
|
126
|
+
error_lines = deque(maxlen=maxlen)
|
|
127
|
+
prev_line = ""
|
|
128
|
+
log_dt = None
|
|
129
|
+
collecting_traceback = False
|
|
130
|
+
for line in fp:
|
|
131
|
+
if ts_match := TS_RE.match(line):
|
|
132
|
+
log_dt = datetime.strptime(ts_match.group(0), "%Y-%m-%d %H:%M:%S")
|
|
133
|
+
collecting_traceback = False
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
line.startswith("Traceback (most recent call last):")
|
|
137
|
+
and log_dt
|
|
138
|
+
and log_dt >= after_dt
|
|
139
|
+
):
|
|
140
|
+
error_lines.append(prev_line)
|
|
141
|
+
collecting_traceback = True
|
|
142
|
+
|
|
143
|
+
if collecting_traceback:
|
|
144
|
+
error_lines.append(line)
|
|
145
|
+
|
|
146
|
+
prev_line = line
|
|
147
|
+
return "".join(error_lines)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import socket
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
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
|
+
# персональных данных нет
|
|
47
|
+
vacancy_contacts = [
|
|
48
|
+
c.to_dict()
|
|
49
|
+
for c in self.storage.vacancy_contacts.find(
|
|
50
|
+
updated_at__ge=last_report
|
|
51
|
+
)
|
|
52
|
+
][-10000:]
|
|
53
|
+
|
|
54
|
+
for c in vacancy_contacts:
|
|
55
|
+
c.pop("id", 0)
|
|
56
|
+
|
|
57
|
+
employers = [
|
|
58
|
+
{
|
|
59
|
+
k: v
|
|
60
|
+
for k, v in emp.to_dict().items()
|
|
61
|
+
if k
|
|
62
|
+
in [
|
|
63
|
+
"id",
|
|
64
|
+
"type",
|
|
65
|
+
"alternate_url",
|
|
66
|
+
"area_id",
|
|
67
|
+
"area_name",
|
|
68
|
+
"name",
|
|
69
|
+
"site_url",
|
|
70
|
+
"created_at",
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
for emp in self.storage.employers.find(updated_at__ge=last_report)
|
|
74
|
+
][-10000:]
|
|
75
|
+
|
|
76
|
+
vacancies = [
|
|
77
|
+
{
|
|
78
|
+
k: v
|
|
79
|
+
for k, v in vac.to_dict().items()
|
|
80
|
+
if k
|
|
81
|
+
in [
|
|
82
|
+
"id",
|
|
83
|
+
"alternate_url",
|
|
84
|
+
"area_id",
|
|
85
|
+
"area_name",
|
|
86
|
+
"salary_from",
|
|
87
|
+
"salary_to",
|
|
88
|
+
"currency",
|
|
89
|
+
"name",
|
|
90
|
+
"professional_roles",
|
|
91
|
+
"experience",
|
|
92
|
+
"remote",
|
|
93
|
+
"created_at",
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
for vac in self.storage.vacancies.find(updated_at__ge=last_report)
|
|
97
|
+
][-10000:]
|
|
98
|
+
|
|
99
|
+
# log.info("num vacncies: %d", len(vacancies))
|
|
100
|
+
|
|
101
|
+
system_info = {
|
|
102
|
+
"os": platform.system(),
|
|
103
|
+
"os_release": platform.release(),
|
|
104
|
+
"hostname": socket.gethostname(),
|
|
105
|
+
"python_version": platform.python_version(),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return dict(
|
|
109
|
+
error_logs=error_logs,
|
|
110
|
+
vacancy_contacts=vacancy_contacts,
|
|
111
|
+
employers=employers,
|
|
112
|
+
vacancies=vacancies,
|
|
113
|
+
package_version=get_package_version(),
|
|
114
|
+
system_info=system_info,
|
|
115
|
+
report_created=datetime.now(timezone.utc),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def __send_report(self: HHApplicantTool, data: bytes) -> int:
|
|
119
|
+
try:
|
|
120
|
+
r = self.session.post(
|
|
121
|
+
# "http://localhost:8000/report",
|
|
122
|
+
"https://hh-applicant-tool.mooo.com:54157/report",
|
|
123
|
+
data=data,
|
|
124
|
+
timeout=15.0,
|
|
125
|
+
)
|
|
126
|
+
r.raise_for_status()
|
|
127
|
+
return r.status_code == 200
|
|
128
|
+
except RequestException:
|
|
129
|
+
# log.error("Network error: %s", e)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _process_reporting(self):
|
|
133
|
+
# Получаем timestamp последнего репорта
|
|
134
|
+
last_report = datetime.fromtimestamp(
|
|
135
|
+
self.storage.settings.get_value("_last_report", 0)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if datetime.now() >= last_report + timedelta(hours=72):
|
|
139
|
+
try:
|
|
140
|
+
report_dict = self.__build_report(last_report)
|
|
141
|
+
has_data = any(
|
|
142
|
+
[
|
|
143
|
+
report_dict.get("error_logs"),
|
|
144
|
+
report_dict.get("employers"),
|
|
145
|
+
report_dict.get("vacancy_contacts"),
|
|
146
|
+
report_dict.get("vacancies"),
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
if has_data:
|
|
150
|
+
data = binpack.serialize(report_dict)
|
|
151
|
+
log.debug("Report body size: %d", len(data))
|
|
152
|
+
# print(binpack.deserialize(data))
|
|
153
|
+
if self.__send_report(data):
|
|
154
|
+
log.debug("Report was sent")
|
|
155
|
+
else:
|
|
156
|
+
log.debug("Report failed")
|
|
157
|
+
else:
|
|
158
|
+
log.debug("Nothing to report")
|
|
159
|
+
finally:
|
|
160
|
+
# Сохраняем время последней попытки/удачного репорта
|
|
161
|
+
self.storage.settings.set_value("_last_report", datetime.now())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class VersionChecker:
|
|
165
|
+
def __get_latest_version(self: HHApplicantTool) -> Literal[False] | str:
|
|
166
|
+
try:
|
|
167
|
+
response = self.session.get(
|
|
168
|
+
"https://pypi.org/pypi/hh-applicant-tool/json", timeout=15
|
|
169
|
+
)
|
|
170
|
+
ver = response.json().get("info", {}).get("version")
|
|
171
|
+
# log.debug(ver)
|
|
172
|
+
return ver
|
|
173
|
+
except requests.RequestException:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def _check_version(self: HHApplicantTool) -> bool:
|
|
177
|
+
if datetime.now().timestamp() >= self.storage.settings.get_value(
|
|
178
|
+
"_next_version_check", 0
|
|
179
|
+
):
|
|
180
|
+
if v := self.__get_latest_version():
|
|
181
|
+
self.storage.settings.set_value("_latest_version", v)
|
|
182
|
+
self.storage.settings.set_value(
|
|
183
|
+
"_next_version_check", datetime.now() + timedelta(hours=1)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
latest_ver := self.storage.settings.get_value("_latest_version")
|
|
188
|
+
) and (cur_ver := get_package_version()):
|
|
189
|
+
if parse_version(latest_ver) > parse_version(cur_ver):
|
|
190
|
+
log.warning(
|
|
191
|
+
"ТЕКУЩАЯ ВЕРСИЯ %s УСТАРЕЛА. РЕКОМЕНДУЕТСЯ ОБНОВИТЬ ЕЁ ДО ВЕРСИИ %s.",
|
|
192
|
+
cur_ver,
|
|
193
|
+
latest_ver,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ChatOpenAISupport:
|
|
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):
|
|
214
|
+
def _check_system(self: HHApplicantTool):
|
|
215
|
+
if not self.storage.settings.get_value("disable_version_check", False):
|
|
216
|
+
self._check_version()
|
|
217
|
+
|
|
218
|
+
if self.storage.settings.get_value("send_error_reports", True):
|
|
219
|
+
self._process_reporting()
|
|
220
|
+
else:
|
|
221
|
+
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,32 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import ctypes
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def setup_terminal() -> None:
|
|
8
|
+
if platform.system() != "Windows":
|
|
9
|
+
return
|
|
10
|
+
try:
|
|
11
|
+
kernel32 = ctypes.windll.kernel32
|
|
12
|
+
# -11 = STD_OUTPUT_HANDLE
|
|
13
|
+
handle = kernel32.GetStdHandle(-11)
|
|
14
|
+
mode = ctypes.c_uint()
|
|
15
|
+
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
16
|
+
# 0x0004 = ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
17
|
+
kernel32.SetConsoleMode(handle, mode.value | 0x0004)
|
|
18
|
+
except Exception:
|
|
19
|
+
# Если что-то пошло не так (старая Windows или нет прав),
|
|
20
|
+
# просто продолжаем работу без цветов
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_kitty_image(data: bytes) -> None:
|
|
25
|
+
# Кодируем весь файл целиком (он уже сжат в PNG)
|
|
26
|
+
b64data = base64.b64encode(data).decode("ascii")
|
|
27
|
+
|
|
28
|
+
# f=100 говорит терминалу: "это PNG, разберись сам с размерами"
|
|
29
|
+
# Нам больше не нужно указывать s=... и v=...
|
|
30
|
+
sys.stdout.write(f"\033_Ga=T,f=100;{b64data}\033\\")
|
|
31
|
+
sys.stdout.flush()
|
|
32
|
+
print()
|