hh-applicant-tool 1.4.7__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 +30 -14
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +32 -17
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -3
- hh_applicant_tool/{datatypes.py → api/datatypes.py} +2 -0
- hh_applicant_tool/api/errors.py +8 -2
- hh_applicant_tool/{utils → api}/user_agent.py +1 -1
- hh_applicant_tool/main.py +63 -38
- hh_applicant_tool/operations/apply_similar.py +136 -52
- hh_applicant_tool/operations/authorize.py +97 -28
- hh_applicant_tool/operations/call_api.py +3 -3
- hh_applicant_tool/operations/{check_negotiations.py → clear_negotiations.py} +40 -26
- hh_applicant_tool/operations/list_resumes.py +5 -7
- hh_applicant_tool/operations/query.py +5 -3
- hh_applicant_tool/operations/refresh_token.py +9 -2
- hh_applicant_tool/operations/reply_employers.py +80 -40
- hh_applicant_tool/operations/settings.py +2 -2
- hh_applicant_tool/operations/update_resumes.py +5 -4
- hh_applicant_tool/operations/whoami.py +3 -3
- hh_applicant_tool/storage/__init__.py +5 -1
- hh_applicant_tool/storage/facade.py +2 -2
- hh_applicant_tool/storage/models/base.py +9 -4
- hh_applicant_tool/storage/models/contacts.py +42 -0
- hh_applicant_tool/storage/queries/schema.sql +23 -10
- hh_applicant_tool/storage/repositories/base.py +69 -15
- hh_applicant_tool/storage/repositories/contacts.py +5 -10
- hh_applicant_tool/storage/repositories/employers.py +1 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +1 -0
- hh_applicant_tool/storage/repositories/resumes.py +2 -7
- hh_applicant_tool/storage/repositories/settings.py +1 -0
- hh_applicant_tool/storage/repositories/vacancies.py +1 -0
- hh_applicant_tool/storage/utils.py +12 -15
- hh_applicant_tool/utils/__init__.py +3 -3
- hh_applicant_tool/utils/config.py +1 -1
- hh_applicant_tool/utils/log.py +6 -3
- hh_applicant_tool/utils/mixins.py +28 -46
- hh_applicant_tool/utils/string.py +15 -0
- hh_applicant_tool/utils/terminal.py +115 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/METADATA +384 -162
- hh_applicant_tool-1.5.7.dist-info/RECORD +68 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/WHEEL +1 -1
- hh_applicant_tool/storage/models/contact.py +0 -16
- hh_applicant_tool-1.4.7.dist-info/RECORD +0 -67
- /hh_applicant_tool/utils/{dateutil.py → date.py} +0 -0
- /hh_applicant_tool/utils/{jsonutil.py → json.py} +0 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/entry_points.txt +0 -0
|
@@ -4,11 +4,10 @@ import logging
|
|
|
4
4
|
import sqlite3
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from functools import cached_property
|
|
8
7
|
from typing import Any, ClassVar, Iterator, Mapping, Self, Type
|
|
9
8
|
|
|
10
9
|
from ..models.base import BaseModel
|
|
11
|
-
from
|
|
10
|
+
from .errors import wrap_db_errors
|
|
12
11
|
|
|
13
12
|
DEFAULT_PRIMARY_KEY = "id"
|
|
14
13
|
|
|
@@ -19,18 +18,23 @@ logger = logging.getLogger(__package__)
|
|
|
19
18
|
class BaseRepository:
|
|
20
19
|
model: ClassVar[Type[BaseModel] | None] = None
|
|
21
20
|
pkey: ClassVar[str] = DEFAULT_PRIMARY_KEY
|
|
21
|
+
conflict_columns: ClassVar[tuple[str, ...] | None] = None
|
|
22
|
+
update_excludes: ClassVar[tuple[str, ...]] = ("created_at", "updated_at")
|
|
23
|
+
__table__: ClassVar[str | None] = None
|
|
22
24
|
|
|
23
25
|
conn: sqlite3.Connection
|
|
24
26
|
auto_commit: bool = True
|
|
25
27
|
|
|
26
|
-
@
|
|
28
|
+
@property
|
|
27
29
|
def table_name(self) -> str:
|
|
28
|
-
return
|
|
30
|
+
return self.__table__ or self.model.__name__
|
|
29
31
|
|
|
32
|
+
@wrap_db_errors
|
|
30
33
|
def commit(self):
|
|
31
34
|
if self.conn.in_transaction:
|
|
32
35
|
self.conn.commit()
|
|
33
36
|
|
|
37
|
+
@wrap_db_errors
|
|
34
38
|
def rollback(self):
|
|
35
39
|
if self.conn.in_transaction:
|
|
36
40
|
self.conn.rollback()
|
|
@@ -53,6 +57,7 @@ class BaseRepository:
|
|
|
53
57
|
data = {col[0]: value for col, value in zip(cursor.description, row)} # noqa: B905
|
|
54
58
|
return self.model.from_db(data)
|
|
55
59
|
|
|
60
|
+
@wrap_db_errors
|
|
56
61
|
def find(self, **kwargs: Any) -> Iterator[BaseModel]:
|
|
57
62
|
# logger.debug(kwargs)
|
|
58
63
|
operators = {
|
|
@@ -94,25 +99,37 @@ class BaseRepository:
|
|
|
94
99
|
if conditions:
|
|
95
100
|
sql += f" WHERE {' AND '.join(conditions)}"
|
|
96
101
|
sql += " ORDER BY rowid DESC;"
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
try:
|
|
103
|
+
cur = self.conn.execute(sql, sql_params)
|
|
104
|
+
except sqlite3.Error:
|
|
105
|
+
logger.warning("SQL ERROR: %s", sql)
|
|
106
|
+
raise
|
|
107
|
+
|
|
99
108
|
yield from (self._row_to_model(cur, row) for row in cur.fetchall())
|
|
100
109
|
|
|
110
|
+
@wrap_db_errors
|
|
101
111
|
def get(self, pk: Any) -> BaseModel | None:
|
|
102
112
|
return next(self.find(**{f"{self.pkey}": pk}), None)
|
|
103
113
|
|
|
114
|
+
@wrap_db_errors
|
|
104
115
|
def count_total(self) -> int:
|
|
105
116
|
cur = self.conn.execute(f"SELECT count(*) FROM {self.table_name};")
|
|
106
117
|
return cur.fetchone()[0]
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
@wrap_db_errors
|
|
120
|
+
def delete(self, obj_or_pkey: Any, /, commit: bool | None = None) -> None:
|
|
109
121
|
sql = f"DELETE FROM {self.table_name} WHERE {self.pkey} = ?"
|
|
110
|
-
pk_value =
|
|
122
|
+
pk_value = (
|
|
123
|
+
getattr(obj_or_pkey, self.pkey)
|
|
124
|
+
if isinstance(obj_or_pkey, BaseModel)
|
|
125
|
+
else obj_or_pkey
|
|
126
|
+
)
|
|
111
127
|
self.conn.execute(sql, (pk_value,))
|
|
112
128
|
self.maybe_commit(commit=commit)
|
|
113
129
|
|
|
114
130
|
remove = delete
|
|
115
131
|
|
|
132
|
+
@wrap_db_errors
|
|
116
133
|
def clear(self, commit: bool | None = None):
|
|
117
134
|
self.conn.execute(f"DELETE FROM {self.table_name};")
|
|
118
135
|
self.maybe_commit(commit)
|
|
@@ -121,14 +138,21 @@ class BaseRepository:
|
|
|
121
138
|
|
|
122
139
|
def _insert(
|
|
123
140
|
self,
|
|
124
|
-
data: Mapping[str, Any],
|
|
141
|
+
data: Mapping[str, Any] | list[Mapping[str, Any]],
|
|
125
142
|
/,
|
|
143
|
+
batch: bool = False,
|
|
126
144
|
upsert: bool = True,
|
|
127
145
|
conflict_columns: Sequence[str] | None = None,
|
|
128
|
-
update_excludes: Sequence[str] =
|
|
146
|
+
update_excludes: Sequence[str] | None = None,
|
|
129
147
|
commit: bool | None = None,
|
|
130
148
|
):
|
|
131
|
-
|
|
149
|
+
conflict_columns = conflict_columns or self.conflict_columns
|
|
150
|
+
update_excludes = update_excludes or self.update_excludes
|
|
151
|
+
|
|
152
|
+
if batch and not data:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
columns = list(dict(data[0] if batch else data).keys())
|
|
132
156
|
sql = (
|
|
133
157
|
f"INSERT INTO {self.table_name} ({', '.join(columns)})"
|
|
134
158
|
f" VALUES (:{', :'.join(columns)})"
|
|
@@ -151,7 +175,10 @@ class BaseRepository:
|
|
|
151
175
|
# 2. Primary key (никогда не меняем)
|
|
152
176
|
# 3. Технические поля (created_at и т.д.)
|
|
153
177
|
update_set = (
|
|
154
|
-
cols_set
|
|
178
|
+
cols_set
|
|
179
|
+
- conflict_set
|
|
180
|
+
- {self.pkey}
|
|
181
|
+
- set(update_excludes or [])
|
|
155
182
|
)
|
|
156
183
|
|
|
157
184
|
if update_set:
|
|
@@ -163,14 +190,41 @@ class BaseRepository:
|
|
|
163
190
|
sql += " DO NOTHING"
|
|
164
191
|
|
|
165
192
|
sql += ";"
|
|
166
|
-
logger.debug("%.2000s", sql)
|
|
167
|
-
|
|
193
|
+
# logger.debug("%.2000s", sql)
|
|
194
|
+
try:
|
|
195
|
+
if batch:
|
|
196
|
+
self.conn.executemany(sql, data)
|
|
197
|
+
else:
|
|
198
|
+
self.conn.execute(sql, data)
|
|
199
|
+
except sqlite3.Error:
|
|
200
|
+
logger.warning("SQL ERROR: %s", sql)
|
|
201
|
+
|
|
202
|
+
raise
|
|
168
203
|
self.maybe_commit(commit)
|
|
169
204
|
|
|
205
|
+
@wrap_db_errors
|
|
170
206
|
def save(
|
|
171
|
-
self,
|
|
207
|
+
self,
|
|
208
|
+
obj: BaseModel | Mapping[str, Any],
|
|
209
|
+
/,
|
|
210
|
+
**kwargs: Any,
|
|
172
211
|
) -> None:
|
|
173
212
|
if isinstance(obj, Mapping):
|
|
174
213
|
obj = self.model.from_api(obj)
|
|
175
214
|
data = obj.to_db()
|
|
176
215
|
self._insert(data, **kwargs)
|
|
216
|
+
|
|
217
|
+
@wrap_db_errors
|
|
218
|
+
def save_batch(
|
|
219
|
+
self,
|
|
220
|
+
items: list[BaseModel | Mapping[str, Any]],
|
|
221
|
+
/,
|
|
222
|
+
**kwargs: Any,
|
|
223
|
+
) -> None:
|
|
224
|
+
if not items:
|
|
225
|
+
return
|
|
226
|
+
data = [
|
|
227
|
+
(self.model.from_api(i) if isinstance(i, Mapping) else i).to_db()
|
|
228
|
+
for i in items
|
|
229
|
+
]
|
|
230
|
+
self._insert(data, batch=True, **kwargs)
|
|
@@ -2,18 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
|
|
5
|
-
from ..models.
|
|
5
|
+
from ..models.contacts import VacancyContactsModel
|
|
6
6
|
from .base import BaseRepository
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__package__)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# logger.debug(contact)
|
|
16
|
-
super().save(
|
|
17
|
-
contact,
|
|
18
|
-
conflict_columns=["employer_id", "email"],
|
|
19
|
-
)
|
|
11
|
+
class VacancyContactsRepository(BaseRepository):
|
|
12
|
+
__table__ = "vacancy_contacts"
|
|
13
|
+
model = VacancyContactsModel
|
|
14
|
+
conflict_columns = ("vacancy_id", "email")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RepositoryError(sqlite3.Error):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def wrap_db_errors(func):
|
|
10
|
+
@wraps(func)
|
|
11
|
+
def wrapper(*args, **kwargs):
|
|
12
|
+
try:
|
|
13
|
+
return func(*args, **kwargs)
|
|
14
|
+
except sqlite3.Error as e:
|
|
15
|
+
raise RepositoryError(
|
|
16
|
+
f"Database error in {func.__name__}: {e}"
|
|
17
|
+
) from e
|
|
18
|
+
|
|
19
|
+
return wrapper
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import sqlite3
|
|
4
|
-
|
|
5
3
|
from ..models.resume import ResumeModel
|
|
6
4
|
from .base import BaseRepository
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class ResumesRepository(BaseRepository):
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def __init__(self, conn: sqlite3.Connection):
|
|
13
|
-
super().__init__(conn)
|
|
14
|
-
self.model = ResumeModel
|
|
8
|
+
__table__ = "resumes"
|
|
9
|
+
model = ResumeModel
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import re
|
|
5
4
|
import sqlite3
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
@@ -14,10 +13,16 @@ logger: logging.Logger = logging.getLogger(__package__)
|
|
|
14
13
|
|
|
15
14
|
def init_db(conn: sqlite3.Connection) -> None:
|
|
16
15
|
"""Создает схему БД"""
|
|
16
|
+
changes_before = conn.total_changes
|
|
17
|
+
|
|
17
18
|
conn.executescript(
|
|
18
19
|
(QUERIES_PATH / "schema.sql").read_text(encoding="utf-8")
|
|
19
20
|
)
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
if conn.total_changes > changes_before:
|
|
23
|
+
logger.info("Применена схема бд")
|
|
24
|
+
# else:
|
|
25
|
+
# logger.debug("База данных не изменилась.")
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def list_migrations() -> list[str]:
|
|
@@ -34,16 +39,8 @@ def apply_migration(conn: sqlite3.Connection, name: str) -> None:
|
|
|
34
39
|
)
|
|
35
40
|
|
|
36
41
|
|
|
37
|
-
def model2table(o: type) -> str:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# y -> ies (если перед y согласная: vacancy -> vacancies)
|
|
43
|
-
if name.endswith("y") and not name.endswith(("ay", "ey", "iy", "oy", "uy")):
|
|
44
|
-
return name[:-1] + "ies"
|
|
45
|
-
# s, x, z, ch, sh -> +es (bus -> buses, match -> matches)
|
|
46
|
-
if name.endswith(("s", "x", "z", "ch", "sh")):
|
|
47
|
-
return name + "es"
|
|
48
|
-
# Обычный случай
|
|
49
|
-
return name + "s"
|
|
42
|
+
# def model2table(o: type) -> str:
|
|
43
|
+
# name: str = o.__name__
|
|
44
|
+
# if name.endswith("Model"):
|
|
45
|
+
# name = name[:-5]
|
|
46
|
+
# return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from ..api.user_agent import generate_android_useragent
|
|
3
4
|
from .attrdict import AttrDict
|
|
4
5
|
from .config import Config, get_config_path
|
|
5
|
-
from .
|
|
6
|
+
from .date import (
|
|
6
7
|
DATETIME_FORMAT,
|
|
7
8
|
parse_api_datetime,
|
|
8
9
|
try_parse_datetime,
|
|
@@ -10,7 +11,6 @@ from .dateutil import (
|
|
|
10
11
|
from .misc import calc_hash, print_err
|
|
11
12
|
from .string import bool2str, list2str, rand_text, shorten
|
|
12
13
|
from .terminal import setup_terminal
|
|
13
|
-
from .user_agent import hh_android_useragent
|
|
14
14
|
|
|
15
15
|
# Add all public symbols to __all__ for consistent import behavior
|
|
16
16
|
__all__ = [
|
|
@@ -25,7 +25,7 @@ __all__ = [
|
|
|
25
25
|
"bool2str",
|
|
26
26
|
"list2str",
|
|
27
27
|
"calc_hash",
|
|
28
|
-
"
|
|
28
|
+
"generate_android_useragent",
|
|
29
29
|
"setup_terminal",
|
|
30
30
|
"print_err",
|
|
31
31
|
]
|
hh_applicant_tool/utils/log.py
CHANGED
|
@@ -38,9 +38,11 @@ class ColorHandler(logging.StreamHandler):
|
|
|
38
38
|
def format(self, record: logging.LogRecord) -> str:
|
|
39
39
|
# Подавляем вывод подробного сообщения об ошибке
|
|
40
40
|
orig_exc_info = record.exc_info
|
|
41
|
+
|
|
41
42
|
# Детали ошибки показываем только при отладке
|
|
42
43
|
if self.level > logging.DEBUG:
|
|
43
44
|
record.exc_info = None
|
|
45
|
+
|
|
44
46
|
message = super().format(record)
|
|
45
47
|
# Обязательно нужно восстановить оригинальное значение или в файловом
|
|
46
48
|
# логе не будет деталей ошибки
|
|
@@ -107,8 +109,9 @@ def setup_logger(
|
|
|
107
109
|
]
|
|
108
110
|
)
|
|
109
111
|
|
|
112
|
+
file_handler.addFilter(redactor)
|
|
113
|
+
|
|
110
114
|
for h in [color_handler, file_handler]:
|
|
111
|
-
h.addFilter(redactor)
|
|
112
115
|
logger.addHandler(h)
|
|
113
116
|
|
|
114
117
|
|
|
@@ -118,9 +121,9 @@ TS_RE = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}")
|
|
|
118
121
|
def collect_traceback_logs(
|
|
119
122
|
fp: TextIO,
|
|
120
123
|
after_dt: datetime,
|
|
121
|
-
|
|
124
|
+
maxlines: int = 1000,
|
|
122
125
|
) -> str:
|
|
123
|
-
error_lines = deque(maxlen=
|
|
126
|
+
error_lines = deque(maxlen=maxlines)
|
|
124
127
|
prev_line = ""
|
|
125
128
|
log_dt = None
|
|
126
129
|
collecting_traceback = False
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import platform
|
|
4
4
|
import socket
|
|
5
|
-
from datetime import datetime, timedelta
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
6
|
from functools import cache
|
|
7
7
|
from importlib.metadata import version
|
|
8
8
|
from logging import getLogger
|
|
@@ -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
|
|
|
@@ -31,26 +30,25 @@ def get_package_version() -> str | None:
|
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
class ErrorReporter:
|
|
34
|
-
def
|
|
33
|
+
def __build_report(
|
|
35
34
|
self: HHApplicantTool,
|
|
36
35
|
last_report: datetime,
|
|
37
36
|
) -> dict:
|
|
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
|
-
# Эти данные нужны для воспроизведения ошибок. Среди них ваших
|
|
46
|
-
|
|
42
|
+
# Эти данные нужны для воспроизведения ошибок. Среди них ваших
|
|
43
|
+
# персональных данных нет
|
|
44
|
+
vacancy_contacts = [
|
|
47
45
|
c.to_dict()
|
|
48
|
-
for c in self.storage.
|
|
46
|
+
for c in self.storage.vacancy_contacts.find(
|
|
49
47
|
updated_at__ge=last_report
|
|
50
48
|
)
|
|
51
|
-
]
|
|
49
|
+
]
|
|
52
50
|
|
|
53
|
-
for c in
|
|
51
|
+
for c in vacancy_contacts:
|
|
54
52
|
c.pop("id", 0)
|
|
55
53
|
|
|
56
54
|
employers = [
|
|
@@ -70,7 +68,7 @@ class ErrorReporter:
|
|
|
70
68
|
]
|
|
71
69
|
}
|
|
72
70
|
for emp in self.storage.employers.find(updated_at__ge=last_report)
|
|
73
|
-
]
|
|
71
|
+
]
|
|
74
72
|
|
|
75
73
|
vacancies = [
|
|
76
74
|
{
|
|
@@ -93,7 +91,7 @@ class ErrorReporter:
|
|
|
93
91
|
]
|
|
94
92
|
}
|
|
95
93
|
for vac in self.storage.vacancies.find(updated_at__ge=last_report)
|
|
96
|
-
]
|
|
94
|
+
]
|
|
97
95
|
|
|
98
96
|
# log.info("num vacncies: %d", len(vacancies))
|
|
99
97
|
|
|
@@ -105,16 +103,16 @@ class ErrorReporter:
|
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
return dict(
|
|
108
|
-
error_logs=error_logs,
|
|
109
|
-
|
|
110
|
-
employers=employers,
|
|
111
|
-
vacancies=vacancies,
|
|
106
|
+
error_logs=error_logs[-100000:],
|
|
107
|
+
vacancy_contacts=vacancy_contacts[-10000:],
|
|
108
|
+
employers=employers[-10000:],
|
|
109
|
+
vacancies=vacancies[-10000:],
|
|
112
110
|
package_version=get_package_version(),
|
|
113
111
|
system_info=system_info,
|
|
114
|
-
report_created=datetime.now(),
|
|
112
|
+
report_created=datetime.now(timezone.utc),
|
|
115
113
|
)
|
|
116
114
|
|
|
117
|
-
def
|
|
115
|
+
def __send_report(self: HHApplicantTool, data: bytes) -> int:
|
|
118
116
|
try:
|
|
119
117
|
r = self.session.post(
|
|
120
118
|
# "http://localhost:8000/report",
|
|
@@ -128,7 +126,7 @@ class ErrorReporter:
|
|
|
128
126
|
# log.error("Network error: %s", e)
|
|
129
127
|
return False
|
|
130
128
|
|
|
131
|
-
def
|
|
129
|
+
def _process_reporting(self):
|
|
132
130
|
# Получаем timestamp последнего репорта
|
|
133
131
|
last_report = datetime.fromtimestamp(
|
|
134
132
|
self.storage.settings.get_value("_last_report", 0)
|
|
@@ -136,12 +134,12 @@ class ErrorReporter:
|
|
|
136
134
|
|
|
137
135
|
if datetime.now() >= last_report + timedelta(hours=72):
|
|
138
136
|
try:
|
|
139
|
-
report_dict = self.
|
|
137
|
+
report_dict = self.__build_report(last_report)
|
|
140
138
|
has_data = any(
|
|
141
139
|
[
|
|
142
140
|
report_dict.get("error_logs"),
|
|
143
141
|
report_dict.get("employers"),
|
|
144
|
-
report_dict.get("
|
|
142
|
+
report_dict.get("vacancy_contacts"),
|
|
145
143
|
report_dict.get("vacancies"),
|
|
146
144
|
]
|
|
147
145
|
)
|
|
@@ -149,7 +147,7 @@ class ErrorReporter:
|
|
|
149
147
|
data = binpack.serialize(report_dict)
|
|
150
148
|
log.debug("Report body size: %d", len(data))
|
|
151
149
|
# print(binpack.deserialize(data))
|
|
152
|
-
if self.
|
|
150
|
+
if self.__send_report(data):
|
|
153
151
|
log.debug("Report was sent")
|
|
154
152
|
else:
|
|
155
153
|
log.debug("Report failed")
|
|
@@ -161,7 +159,7 @@ class ErrorReporter:
|
|
|
161
159
|
|
|
162
160
|
|
|
163
161
|
class VersionChecker:
|
|
164
|
-
def
|
|
162
|
+
def __get_latest_version(self: HHApplicantTool) -> Literal[False] | str:
|
|
165
163
|
try:
|
|
166
164
|
response = self.session.get(
|
|
167
165
|
"https://pypi.org/pypi/hh-applicant-tool/json", timeout=15
|
|
@@ -172,11 +170,11 @@ class VersionChecker:
|
|
|
172
170
|
except requests.RequestException:
|
|
173
171
|
return False
|
|
174
172
|
|
|
175
|
-
def
|
|
173
|
+
def _check_version(self: HHApplicantTool) -> bool:
|
|
176
174
|
if datetime.now().timestamp() >= self.storage.settings.get_value(
|
|
177
175
|
"_next_version_check", 0
|
|
178
176
|
):
|
|
179
|
-
if v := self.
|
|
177
|
+
if v := self.__get_latest_version():
|
|
180
178
|
self.storage.settings.set_value("_latest_version", v)
|
|
181
179
|
self.storage.settings.set_value(
|
|
182
180
|
"_next_version_check", datetime.now() + timedelta(hours=1)
|
|
@@ -193,28 +191,12 @@ class VersionChecker:
|
|
|
193
191
|
)
|
|
194
192
|
|
|
195
193
|
|
|
196
|
-
class
|
|
197
|
-
def
|
|
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):
|
|
194
|
+
class MegaTool(ErrorReporter, VersionChecker):
|
|
195
|
+
def _check_system(self: HHApplicantTool):
|
|
214
196
|
if not self.storage.settings.get_value("disable_version_check", False):
|
|
215
|
-
self.
|
|
197
|
+
self._check_version()
|
|
216
198
|
|
|
217
199
|
if self.storage.settings.get_value("send_error_reports", True):
|
|
218
|
-
self.
|
|
200
|
+
self._process_reporting()
|
|
219
201
|
else:
|
|
220
202
|
log.warning("ОТКЛЮЧЕНА ОТПРАВКА СООБЩЕНИЙ ОБ ОШИБКАХ!")
|
|
@@ -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
|
+
)
|