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.
Files changed (49) hide show
  1. hh_applicant_tool/__main__.py +1 -1
  2. hh_applicant_tool/ai/__init__.py +1 -0
  3. hh_applicant_tool/ai/openai.py +30 -14
  4. hh_applicant_tool/api/__init__.py +4 -2
  5. hh_applicant_tool/api/client.py +32 -17
  6. hh_applicant_tool/{constants.py → api/client_keys.py} +3 -3
  7. hh_applicant_tool/{datatypes.py → api/datatypes.py} +2 -0
  8. hh_applicant_tool/api/errors.py +8 -2
  9. hh_applicant_tool/{utils → api}/user_agent.py +1 -1
  10. hh_applicant_tool/main.py +63 -38
  11. hh_applicant_tool/operations/apply_similar.py +136 -52
  12. hh_applicant_tool/operations/authorize.py +97 -28
  13. hh_applicant_tool/operations/call_api.py +3 -3
  14. hh_applicant_tool/operations/{check_negotiations.py → clear_negotiations.py} +40 -26
  15. hh_applicant_tool/operations/list_resumes.py +5 -7
  16. hh_applicant_tool/operations/query.py +5 -3
  17. hh_applicant_tool/operations/refresh_token.py +9 -2
  18. hh_applicant_tool/operations/reply_employers.py +80 -40
  19. hh_applicant_tool/operations/settings.py +2 -2
  20. hh_applicant_tool/operations/update_resumes.py +5 -4
  21. hh_applicant_tool/operations/whoami.py +3 -3
  22. hh_applicant_tool/storage/__init__.py +5 -1
  23. hh_applicant_tool/storage/facade.py +2 -2
  24. hh_applicant_tool/storage/models/base.py +9 -4
  25. hh_applicant_tool/storage/models/contacts.py +42 -0
  26. hh_applicant_tool/storage/queries/schema.sql +23 -10
  27. hh_applicant_tool/storage/repositories/base.py +69 -15
  28. hh_applicant_tool/storage/repositories/contacts.py +5 -10
  29. hh_applicant_tool/storage/repositories/employers.py +1 -0
  30. hh_applicant_tool/storage/repositories/errors.py +19 -0
  31. hh_applicant_tool/storage/repositories/negotiations.py +1 -0
  32. hh_applicant_tool/storage/repositories/resumes.py +2 -7
  33. hh_applicant_tool/storage/repositories/settings.py +1 -0
  34. hh_applicant_tool/storage/repositories/vacancies.py +1 -0
  35. hh_applicant_tool/storage/utils.py +12 -15
  36. hh_applicant_tool/utils/__init__.py +3 -3
  37. hh_applicant_tool/utils/config.py +1 -1
  38. hh_applicant_tool/utils/log.py +6 -3
  39. hh_applicant_tool/utils/mixins.py +28 -46
  40. hh_applicant_tool/utils/string.py +15 -0
  41. hh_applicant_tool/utils/terminal.py +115 -0
  42. {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/METADATA +384 -162
  43. hh_applicant_tool-1.5.7.dist-info/RECORD +68 -0
  44. {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/WHEEL +1 -1
  45. hh_applicant_tool/storage/models/contact.py +0 -16
  46. hh_applicant_tool-1.4.7.dist-info/RECORD +0 -67
  47. /hh_applicant_tool/utils/{dateutil.py → date.py} +0 -0
  48. /hh_applicant_tool/utils/{jsonutil.py → json.py} +0 -0
  49. {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 ..utils import model2table
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
- @cached_property
28
+ @property
27
29
  def table_name(self) -> str:
28
- return model2table(self.model)
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
- logger.debug("%.2000s", sql)
98
- cur = self.conn.execute(sql, sql_params)
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
- def delete(self, o: BaseModel, /, commit: bool | None = None) -> None:
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 = getattr(o, self.pkey)
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] = ("created_at", "updated_at"),
146
+ update_excludes: Sequence[str] | None = None,
129
147
  commit: bool | None = None,
130
148
  ):
131
- columns = list(data.keys())
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 - conflict_set - {self.pkey} - set(update_excludes)
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
- self.conn.execute(sql, data)
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, obj: BaseModel | Mapping[str, Any], /, **kwargs: Any
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.contact import EmployerContactModel
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 EmployerContactsRepository(BaseRepository):
12
- model = EmployerContactModel
13
-
14
- def save(self, contact: EmployerContactModel) -> None:
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")
@@ -7,6 +7,7 @@ from .base import BaseRepository
7
7
 
8
8
 
9
9
  class EmployersRepository(BaseRepository):
10
+ __table__ = "employers"
10
11
  model = EmployerModel
11
12
 
12
13
  def find(self, **kwargs) -> Iterator[EmployerModel]:
@@ -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
@@ -9,4 +9,5 @@ logger = getLogger(__package__)
9
9
 
10
10
 
11
11
  class NegotiationRepository(BaseRepository):
12
+ __table__ = "negotiations"
12
13
  model = NegotiationModel
@@ -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
@@ -7,6 +7,7 @@ Default = TypeVar("Default")
7
7
 
8
8
 
9
9
  class SettingsRepository(BaseRepository):
10
+ __table__ = "settings"
10
11
  pkey: str = "key"
11
12
  model = SettingModel
12
13
 
@@ -5,4 +5,5 @@ from .base import BaseRepository
5
5
 
6
6
 
7
7
  class VacanciesRepository(BaseRepository):
8
+ __table__ = "vacancies"
8
9
  model = VacancyModel
@@ -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
- logger.debug("Database unitialized")
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
- name: str = o.__name__
39
- if name.endswith("Model"):
40
- name = name[:-5]
41
- name = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
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 .dateutil import (
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
- "hh_android_useragent",
28
+ "generate_android_useragent",
29
29
  "setup_terminal",
30
30
  "print_err",
31
31
  ]
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from threading import Lock
8
8
  from typing import Any
9
9
 
10
- from . import jsonutil as json
10
+ from . import json
11
11
 
12
12
 
13
13
  @cache
@@ -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
- maxlen: int = 1000,
124
+ maxlines: int = 1000,
122
125
  ) -> str:
123
- error_lines = deque(maxlen=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 build_report(
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
- contacts = [
42
+ # Эти данные нужны для воспроизведения ошибок. Среди них ваших
43
+ # персональных данных нет
44
+ vacancy_contacts = [
47
45
  c.to_dict()
48
- for c in self.storage.employer_contacts.find(
46
+ for c in self.storage.vacancy_contacts.find(
49
47
  updated_at__ge=last_report
50
48
  )
51
- ][-10000:]
49
+ ]
52
50
 
53
- for c in contacts:
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
- ][-10000:]
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
- ][-10000:]
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
- contacts=contacts,
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 send_report(self: HHApplicantTool, data: bytes) -> int:
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 process_reporting(self):
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.build_report(last_report)
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("contacts"),
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.send_report(data):
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 get_latest_version(self: HHApplicantTool) -> Literal[False] | str:
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 check_version(self: HHApplicantTool) -> bool:
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.get_latest_version():
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 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):
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.check_version()
197
+ self._check_version()
216
198
 
217
199
  if self.storage.settings.get_value("send_error_reports", True):
218
- self.process_reporting()
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
+ )