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.
Files changed (76) hide show
  1. hh_applicant_tool/__init__.py +1 -0
  2. hh_applicant_tool/__main__.py +1 -1
  3. hh_applicant_tool/ai/base.py +2 -0
  4. hh_applicant_tool/ai/openai.py +25 -35
  5. hh_applicant_tool/api/__init__.py +4 -2
  6. hh_applicant_tool/api/client.py +65 -68
  7. hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
  8. hh_applicant_tool/api/datatypes.py +293 -0
  9. hh_applicant_tool/api/errors.py +57 -7
  10. hh_applicant_tool/api/user_agent.py +17 -0
  11. hh_applicant_tool/main.py +234 -113
  12. hh_applicant_tool/operations/apply_similar.py +353 -371
  13. hh_applicant_tool/operations/authorize.py +313 -120
  14. hh_applicant_tool/operations/call_api.py +18 -8
  15. hh_applicant_tool/operations/check_proxy.py +30 -0
  16. hh_applicant_tool/operations/clear_negotiations.py +90 -82
  17. hh_applicant_tool/operations/config.py +119 -16
  18. hh_applicant_tool/operations/install.py +34 -0
  19. hh_applicant_tool/operations/list_resumes.py +23 -11
  20. hh_applicant_tool/operations/log.py +77 -0
  21. hh_applicant_tool/operations/migrate_db.py +65 -0
  22. hh_applicant_tool/operations/query.py +122 -0
  23. hh_applicant_tool/operations/refresh_token.py +14 -13
  24. hh_applicant_tool/operations/reply_employers.py +201 -180
  25. hh_applicant_tool/operations/settings.py +95 -0
  26. hh_applicant_tool/operations/uninstall.py +26 -0
  27. hh_applicant_tool/operations/update_resumes.py +23 -11
  28. hh_applicant_tool/operations/whoami.py +40 -7
  29. hh_applicant_tool/storage/__init__.py +8 -0
  30. hh_applicant_tool/storage/facade.py +24 -0
  31. hh_applicant_tool/storage/models/__init__.py +0 -0
  32. hh_applicant_tool/storage/models/base.py +169 -0
  33. hh_applicant_tool/storage/models/contacts.py +28 -0
  34. hh_applicant_tool/storage/models/employer.py +12 -0
  35. hh_applicant_tool/storage/models/negotiation.py +16 -0
  36. hh_applicant_tool/storage/models/resume.py +19 -0
  37. hh_applicant_tool/storage/models/setting.py +6 -0
  38. hh_applicant_tool/storage/models/vacancy.py +36 -0
  39. hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
  40. hh_applicant_tool/storage/queries/schema.sql +132 -0
  41. hh_applicant_tool/storage/repositories/__init__.py +0 -0
  42. hh_applicant_tool/storage/repositories/base.py +230 -0
  43. hh_applicant_tool/storage/repositories/contacts.py +14 -0
  44. hh_applicant_tool/storage/repositories/employers.py +14 -0
  45. hh_applicant_tool/storage/repositories/errors.py +19 -0
  46. hh_applicant_tool/storage/repositories/negotiations.py +13 -0
  47. hh_applicant_tool/storage/repositories/resumes.py +9 -0
  48. hh_applicant_tool/storage/repositories/settings.py +35 -0
  49. hh_applicant_tool/storage/repositories/vacancies.py +9 -0
  50. hh_applicant_tool/storage/utils.py +40 -0
  51. hh_applicant_tool/utils/__init__.py +31 -0
  52. hh_applicant_tool/utils/attrdict.py +6 -0
  53. hh_applicant_tool/utils/binpack.py +167 -0
  54. hh_applicant_tool/utils/config.py +55 -0
  55. hh_applicant_tool/utils/date.py +19 -0
  56. hh_applicant_tool/utils/json.py +61 -0
  57. hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
  58. hh_applicant_tool/utils/log.py +147 -0
  59. hh_applicant_tool/utils/misc.py +12 -0
  60. hh_applicant_tool/utils/mixins.py +221 -0
  61. hh_applicant_tool/utils/string.py +27 -0
  62. hh_applicant_tool/utils/terminal.py +32 -0
  63. hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
  64. hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
  65. hh_applicant_tool/ai/blackbox.py +0 -55
  66. hh_applicant_tool/color_log.py +0 -47
  67. hh_applicant_tool/mixins.py +0 -13
  68. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  69. hh_applicant_tool/operations/get_employer_contacts.py +0 -348
  70. hh_applicant_tool/telemetry_client.py +0 -106
  71. hh_applicant_tool/types.py +0 -45
  72. hh_applicant_tool/utils.py +0 -119
  73. hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
  74. hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
  75. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
  76. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
@@ -1,19 +1,20 @@
1
1
  from __future__ import annotations
2
- import os
2
+
3
3
  import argparse
4
4
  import logging
5
5
  import random
6
- import time
7
- from typing import Tuple
8
-
9
- from ..api import ApiError, ApiClient
10
- from ..main import BaseOperation
11
- from ..main import Namespace as BaseNamespace
12
- from ..mixins import GetResumeIdMixin
13
- from ..utils import parse_interval, random_text
14
- from ..telemetry_client import TelemetryClient, TelemetryError
15
- import re
16
- from itertools import count
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING
8
+
9
+ from ..ai.base import AIError
10
+ from ..api import ApiError, datatypes
11
+ from ..main import BaseNamespace, BaseOperation
12
+ from ..utils.date import parse_api_datetime
13
+ from ..utils.string import rand_text
14
+
15
+ if TYPE_CHECKING:
16
+ from ..main import HHApplicantTool
17
+
17
18
 
18
19
  try:
19
20
  import readline
@@ -25,43 +26,40 @@ except ImportError:
25
26
  pass
26
27
 
27
28
 
28
- GOOGLE_DOCS_RE = re.compile(
29
- r"\b(?:https?:\/\/)?(?:docs|forms|sheets|slides|drive)\.google\.com\/(?:document|spreadsheets|presentation|forms|file)\/(?:d|u)\/[a-zA-Z0-9_\-]+(?:\/[a-zA-Z0-9_\-]+)?\/?(?:[?#].*)?\b|\b(?:https?:\/\/)?(?:goo\.gl|forms\.gle)\/[a-zA-Z0-9]+\b",
30
- re.I,
31
- )
32
-
33
29
  logger = logging.getLogger(__package__)
34
30
 
35
31
 
36
32
  class Namespace(BaseNamespace):
37
33
  reply_message: str
38
- reply_interval: Tuple[float, float]
39
34
  max_pages: int
40
35
  only_invitations: bool
41
36
  dry_run: bool
37
+ use_ai: bool
38
+ first_prompt: str
39
+ prompt: str
40
+ period: int
42
41
 
43
42
 
44
- class Operation(BaseOperation, GetResumeIdMixin):
43
+ class Operation(BaseOperation):
45
44
  """Ответ всем работодателям."""
46
45
 
46
+ __aliases__ = ["reply-empls", "reply-chats", "reall"]
47
+
47
48
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
48
- # parser.add_argument(
49
- # "reply_message",
50
- # help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ. Если не передать, то его нужно будет вводить интерактивно.",
51
- # )
52
- parser.add_argument("--resume-id", help="Идентификатор резюме")
53
49
  parser.add_argument(
54
- "-i",
55
- "--reply-interval",
56
- help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
57
- default="5-10",
58
- type=parse_interval,
50
+ "--resume-id",
51
+ help="Идентификатор резюме. Если не указан, то просматриваем чаты для всех резюме",
59
52
  )
60
53
  parser.add_argument(
61
54
  "-m",
62
55
  "--reply-message",
63
56
  "--reply",
64
- help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали ответ. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.",
57
+ help="Отправить сообщение во все чаты. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.", # noqa: E501
58
+ )
59
+ parser.add_argument(
60
+ "--period",
61
+ type=int,
62
+ help="Игнорировать отклики, которые не обновлялись больше N дней",
65
63
  )
66
64
  parser.add_argument(
67
65
  "-p",
@@ -77,7 +75,6 @@ class Operation(BaseOperation, GetResumeIdMixin):
77
75
  default=False,
78
76
  action=argparse.BooleanOptionalAction,
79
77
  )
80
-
81
78
  parser.add_argument(
82
79
  "--dry-run",
83
80
  "--dry",
@@ -85,240 +82,264 @@ class Operation(BaseOperation, GetResumeIdMixin):
85
82
  default=False,
86
83
  action=argparse.BooleanOptionalAction,
87
84
  )
85
+ parser.add_argument(
86
+ "--use-ai",
87
+ "--ai",
88
+ help="Использовать AI для автоматической генерации ответов",
89
+ action=argparse.BooleanOptionalAction,
90
+ )
91
+ parser.add_argument(
92
+ "--first-prompt",
93
+ help="Начальный промпт чата для AI",
94
+ default="Ты — соискатель на HeadHunter. Отвечай вежливо и кратко.",
95
+ )
96
+ parser.add_argument(
97
+ "--prompt",
98
+ help="Промпт для генерации сообщения",
99
+ default="Напиши короткий ответ работодателю на основе истории переписки.",
100
+ )
88
101
 
89
- def run(
90
- self, args: Namespace, api_client: ApiClient, telemetry_client: TelemetryClient
91
- ) -> None:
92
- self.api_client = api_client
93
- self.telemetry_client = telemetry_client
94
- self.enable_telemetry = not args.disable_telemetry
95
- self.resume_id = self._get_resume_id()
96
- self.reply_min_interval, self.reply_max_interval = args.reply_interval
97
- self.reply_message = args.reply_message or args.config["reply_message"]
98
- # assert self.reply_message, "`reply_message` должен быть передан чеерез аргументы или настройки"
102
+ def run(self, tool: HHApplicantTool) -> None:
103
+ args: Namespace = tool.args
104
+ self.tool = tool
105
+ self.api_client = tool.api_client
106
+ self.resume_id = tool.first_resume_id()
107
+ self.reply_message = args.reply_message or tool.config.get(
108
+ "reply_message"
109
+ )
99
110
  self.max_pages = args.max_pages
100
111
  self.dry_run = args.dry_run
101
112
  self.only_invitations = args.only_invitations
113
+
114
+ self.pre_prompt = args.prompt
115
+ self.openai_chat = (
116
+ tool.get_openai_chat(args.first_prompt) if args.use_ai else None
117
+ )
118
+ self.period = args.period
119
+
102
120
  logger.debug(f"{self.reply_message = }")
103
- self._reply_chats()
104
-
105
- def _get_blacklisted(self) -> list[str]:
106
- rv = []
107
- # В этом методе API страницы с 0 начинаются
108
- for page in count(0):
109
- r = self.api_client.get("/employers/blacklisted", page=page)
110
- rv += [item["id"] for item in r["items"]]
111
- if page + 1 >= r["pages"]:
112
- break
113
- return rv
114
-
115
- def _reply_chats(self) -> None:
116
- blacklisted = self._get_blacklisted()
117
- logger.debug(f"{blacklisted = }")
118
- me = self.me = self.api_client.get("/me")
119
-
120
- telemetry_data = {"links": []}
121
-
122
- basic_message_placeholders = {
123
- "first_name": me.get("first_name", ""),
124
- "last_name": me.get("last_name", ""),
125
- "email": me.get("email", ""),
126
- "phone": me.get("phone", ""),
121
+ self.reply_employers()
122
+
123
+ def reply_employers(self):
124
+ blacklist = set(self.tool.get_blacklisted())
125
+ me: datatypes.User = self.tool.get_me()
126
+ resumes = self.tool.get_resumes()
127
+ resumes = (
128
+ list(filter(lambda x: x["id"] == self.resume_id, resumes))
129
+ if self.resume_id
130
+ else resumes
131
+ )
132
+ resumes = list(
133
+ filter(
134
+ lambda resume: resume["status"]["id"] == "published", resumes
135
+ )
136
+ )
137
+ self._reply_chats(user=me, resumes=resumes, blacklist=blacklist)
138
+
139
+ def _reply_chats(
140
+ self,
141
+ user: datatypes.User,
142
+ resumes: list[datatypes.Resume],
143
+ blacklist: set[str],
144
+ ) -> None:
145
+ resume_map = {r["id"]: r for r in resumes}
146
+
147
+ base_placeholders = {
148
+ "first_name": user.get("first_name") or "",
149
+ "last_name": user.get("last_name") or "",
150
+ "email": user.get("email") or "",
151
+ "phone": user.get("phone") or "",
127
152
  }
128
153
 
129
- for negotiation in self._get_negotiations():
154
+ for negotiation in self.tool.get_negotiations():
130
155
  try:
131
- # Пропускаем другие резюме
132
- if self.resume_id != negotiation["resume"]["id"]:
156
+ # try:
157
+ # self.tool.storage.negotiations.save(negotiation)
158
+ # except RepositoryError as e:
159
+ # logger.exception(e)
160
+
161
+ if not (resume := resume_map.get(negotiation["resume"]["id"])):
133
162
  continue
134
163
 
135
- state_id = negotiation["state"]["id"]
164
+ updated_at = parse_api_datetime(negotiation["updated_at"])
165
+
166
+ # Пропуск откликов, которые не обновлялись более N дней (при просмотре они обновляются вроде)
167
+ if (
168
+ self.period
169
+ and (datetime().now(updated_at.tzinfo) - updated_at).days
170
+ > self.period
171
+ ):
172
+ continue
136
173
 
137
- # Пропускаем отказ
174
+ state_id = negotiation["state"]["id"]
138
175
  if state_id == "discard":
139
176
  continue
140
177
 
141
178
  if self.only_invitations and not state_id.startswith("inv"):
142
179
  continue
143
180
 
144
- logger.debug(negotiation)
145
181
  nid = negotiation["id"]
146
182
  vacancy = negotiation["vacancy"]
147
183
  employer = vacancy.get("employer") or {}
148
184
  salary = vacancy.get("salary") or {}
149
185
 
150
- if employer.get("id") in blacklisted:
186
+ if employer.get("id") in blacklist:
151
187
  print(
152
188
  "🚫 Пропускаем заблокированного работодателя",
153
189
  employer.get("alternate_url"),
154
190
  )
155
191
  continue
156
192
 
157
- message_placeholders = {
193
+ placeholders = {
158
194
  "vacancy_name": vacancy.get("name", ""),
159
195
  "employer_name": employer.get("name", ""),
160
- **basic_message_placeholders,
196
+ "resume_title": resume.get("title") or "",
197
+ **base_placeholders,
161
198
  }
162
199
 
163
200
  logger.debug(
164
201
  "Вакансия %(vacancy_name)s от %(employer_name)s"
165
- % message_placeholders
202
+ % placeholders
166
203
  )
167
204
 
168
205
  page: int = 0
169
- last_message: dict | None = None
206
+ last_message: datatypes.Message | None = None
170
207
  message_history: list[str] = []
171
208
  while True:
172
- messages_res = self.api_client.get(
209
+ messages_res: datatypes.PaginatedItems[
210
+ datatypes.Message
211
+ ] = self.api_client.get(
173
212
  f"/negotiations/{nid}/messages", page=page
174
213
  )
214
+ if not messages_res["items"]:
215
+ break
175
216
 
176
217
  last_message = messages_res["items"][-1]
177
- message_history.extend(
178
- (
179
- "<-"
180
- if item["author"]["participant_type"] == "employer"
181
- else "->"
218
+ for message in messages_res["items"]:
219
+ if not message.get("text"):
220
+ continue
221
+ author = (
222
+ "Работодатель"
223
+ if message["author"]["participant_type"]
224
+ == "employer"
225
+ else "Я"
182
226
  )
183
- + " "
184
- + item["text"]
185
- for item in messages_res["items"]
186
- if item.get("text")
187
- )
227
+ message_date = parse_api_datetime(
228
+ message.get("created_at")
229
+ ).strftime("%d.%m.%Y %H:%M:%S")
230
+
231
+ message_history.append(
232
+ f"[ {message_date} ] {author}: {message['text']}"
233
+ )
234
+
188
235
  if page + 1 >= messages_res["pages"]:
189
236
  break
190
-
191
237
  page = messages_res["pages"] - 1
192
238
 
193
- if self.enable_telemetry:
194
- # Собираем ссылки на тестовые задания
195
- for message in message_history:
196
- if message.startswith("-> "):
197
- continue
198
- # Тестовые задания и тп
199
- for link in GOOGLE_DOCS_RE.findall(message):
200
- document_data = {
201
- "vacancy_url": vacancy.get("alternate_url"),
202
- "vacancy_name": vacancy.get("name"),
203
- "salary": (
204
- f"{salary.get('from', '...')}-{salary.get('to', '...')} {salary.get('currency', 'RUR')}" # noqa: E501
205
- if salary
206
- else None
207
- ),
208
- "employer_url": vacancy.get("employer", {}).get(
209
- "alternate_url"
210
- ),
211
- "link": link,
212
- }
213
-
214
- telemetry_data["links"].append(document_data)
215
-
216
- if os.getenv("TEST_SEND_TELEMETRY") in ["1", "y", "Y"]:
239
+ if not last_message:
217
240
  continue
218
241
 
219
- logger.debug(last_message)
220
-
221
242
  is_employer_message = (
222
243
  last_message["author"]["participant_type"] == "employer"
223
244
  )
224
245
 
225
- if is_employer_message or not negotiation.get("viewed_by_opponent"):
246
+ if is_employer_message or not negotiation.get(
247
+ "viewed_by_opponent"
248
+ ):
249
+ send_message = ""
226
250
  if self.reply_message:
227
251
  send_message = (
228
- random_text(self.reply_message) % message_placeholders
252
+ rand_text(self.reply_message) % placeholders
229
253
  )
230
- logger.debug(send_message)
254
+ logger.debug(f"Template message: {send_message}")
255
+ elif self.openai_chat:
256
+ try:
257
+ ai_query = (
258
+ f"Вакансия: {placeholders['vacancy_name']}\n"
259
+ f"История переписки:\n"
260
+ + "\n".join(message_history[-10:])
261
+ + f"\n\nИнструкция: {self.pre_prompt}"
262
+ )
263
+ send_message = self.openai_chat.send_message(
264
+ ai_query
265
+ )
266
+ logger.debug(f"AI message: {send_message}")
267
+ except AIError as ex:
268
+ logger.warning(
269
+ f"Ошибка OpenAI для чата {nid}: {ex}"
270
+ )
271
+ continue
231
272
  else:
232
- print("🏢", message_placeholders["employer_name"])
233
- print("💼", message_placeholders["vacancy_name"])
234
- print("📅", vacancy["created_at"])
273
+ print("🏢", placeholders["employer_name"])
274
+ print("💼", placeholders["vacancy_name"])
235
275
  if salary:
236
- salary_from = salary.get("from") or "-"
237
- salary_to = salary.get("to") or "-"
238
- salary_currency = salary.get("currency")
239
276
  print(
240
- "💵 от", salary_from, "до", salary_to, salary_currency
277
+ "💵 от",
278
+ salary.get("from") or salary.get("to") or 0,
279
+ "до",
280
+ salary.get("to") or salary.get("from") or 0,
281
+ salary.get("currency", "RUR"),
241
282
  )
242
- print("")
243
- print("Последние сообщения:")
283
+
284
+ print("\nПоследние сообщения чата:")
285
+ print()
244
286
  for msg in (
245
- message_history[:1] + ["..."] + message_history[-3:]
287
+ message_history[-5:]
246
288
  if len(message_history) > 5
247
289
  else message_history
248
290
  ):
249
291
  print(msg)
292
+
250
293
  try:
251
- print("-" * 10)
252
- print()
294
+ print("-" * 40)
295
+ print("Активное резюме:", resume.get("title") or "")
253
296
  print(
254
- "Отмена отклика: /cancel <необязательное сообщение для отказа>"
297
+ "/ban, /cancel необязательное сообщение для отмены"
255
298
  )
256
- print("Заблокировать работодателя: /ban")
257
- print()
258
299
  send_message = input("Ваше сообщение: ").strip()
259
300
  except EOFError:
260
301
  continue
302
+
261
303
  if not send_message:
262
304
  print("🚶 Пропускаем чат")
263
305
  continue
264
306
 
307
+ if send_message.startswith("/ban"):
308
+ self.api_client.put(
309
+ f"/employers/blacklisted/{employer['id']}"
310
+ )
311
+ blacklist.add(employer["id"])
312
+ print(
313
+ "🚫 Работодатель заблокирован",
314
+ employer.get("alternate_url"),
315
+ )
316
+ continue
317
+ elif send_message.startswith("/cancel"):
318
+ _, decline_msg = send_message.split("/cancel", 1)
319
+ self.api_client.delete(
320
+ f"/negotiations/active/{nid}",
321
+ with_decline_message=decline_msg.strip(),
322
+ )
323
+ print("❌ Отмена заявки", vacancy["alternate_url"])
324
+ continue
325
+
326
+ # Финальная отправка текста
265
327
  if self.dry_run:
266
- logger.info(
267
- "Dry Run: Отправка сообщения в чат по вакансии %s: %s",
328
+ logger.debug(
329
+ "dry-run: отклик на",
268
330
  vacancy["alternate_url"],
269
331
  send_message,
270
332
  )
271
333
  continue
272
334
 
273
- time.sleep(
274
- random.uniform(
275
- self.reply_min_interval,
276
- self.reply_max_interval,
277
- )
335
+ self.api_client.post(
336
+ f"/negotiations/{nid}/messages",
337
+ message=send_message,
338
+ delay=random.uniform(1, 3),
278
339
  )
340
+ print(f"📨 Отправлено для {vacancy['alternate_url']}")
279
341
 
280
- if send_message.startswith("/ban"):
281
- self.api_client.put(f"/employers/blacklisted/{employer['id']}")
282
- blacklisted.append(employer["id"])
283
- print(
284
- "🚫 Работодатель добавлен в черный список",
285
- employer.get("alternate_url"),
286
- )
287
- elif send_message.startswith("/cancel"):
288
- _, decline_allowed = send_message.split("/cancel", 1)
289
- self.api_client.delete(
290
- f"/negotiations/active/{negotiation['id']}",
291
- with_decline_message=decline_allowed.strip(),
292
- )
293
- print("❌ Отменили заявку", vacancy["alternate_url"])
294
- else:
295
- self.api_client.post(
296
- f"/negotiations/{nid}/messages",
297
- message=send_message,
298
- )
299
- print(
300
- "📨 Отправили сообщение для",
301
- vacancy["alternate_url"],
302
- )
303
342
  except ApiError as ex:
304
343
  logger.error(ex)
305
344
 
306
- if self.enable_telemetry and len(telemetry_data["links"]) > 0:
307
- logger.debug(telemetry_data)
308
- try:
309
- self.telemetry_client.send_telemetry("/docs", telemetry_data)
310
- except TelemetryError as ex:
311
- logger.warning(ex, exc_info=True)
312
-
313
345
  print("📝 Сообщения разосланы!")
314
-
315
- def _get_negotiations(self) -> list[dict]:
316
- rv = []
317
- for page in range(self.max_pages):
318
- res = self.api_client.get("/negotiations", page=page, status="active")
319
- rv.extend(res["items"])
320
- if page >= res["pages"] - 1:
321
- break
322
- page += 1
323
-
324
- return rv
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from prettytable import PrettyTable
9
+
10
+ from .. import utils
11
+ from ..main import BaseNamespace, BaseOperation
12
+
13
+ if TYPE_CHECKING:
14
+ from ..main import HHApplicantTool
15
+
16
+
17
+ MISSING = type("Missing", (), {"__str__": lambda self: "Не установлено"})()
18
+
19
+
20
+ logger = logging.getLogger(__package__)
21
+
22
+
23
+ class Namespace(BaseNamespace):
24
+ key: str | None
25
+ value: str | None
26
+ delete: bool
27
+
28
+
29
+ def parse_value(v):
30
+ try:
31
+ return utils.json.loads(v)
32
+ except json.JSONDecodeError:
33
+ return v
34
+
35
+
36
+ class Operation(BaseOperation):
37
+ """Просмотр и управление настройками"""
38
+
39
+ __aliases__: list[str] = ["setting"]
40
+
41
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
42
+ parser.add_argument(
43
+ "-d",
44
+ "--delete",
45
+ action="store_true",
46
+ help="Удалить настройку по ключу либо удалить все насйтроки, если ключ не передан",
47
+ )
48
+ parser.add_argument(
49
+ "key", nargs="?", help="Ключ настройки", default=MISSING
50
+ )
51
+ parser.add_argument(
52
+ "value",
53
+ nargs="?",
54
+ type=parse_value,
55
+ help="Значение настройки",
56
+ default=MISSING,
57
+ )
58
+
59
+ def run(self, tool: HHApplicantTool) -> None:
60
+ args: Namespace = tool.args
61
+ settings = tool.storage.settings
62
+
63
+ if args.delete:
64
+ if args.key is not MISSING:
65
+ # Delete value
66
+ settings.delete_value(args.key)
67
+ print(f"🗑️ Настройка '{args.key}' удалена")
68
+ else:
69
+ settings.clear()
70
+ elif args.key is not MISSING and args.value is not MISSING:
71
+ settings.set_value(args.key, args.value)
72
+ print(f"✅ Установлено значение для '{args.key}'")
73
+ elif args.key is not MISSING:
74
+ # Get value
75
+ value = settings.get_value(args.key, MISSING)
76
+ if value is not MISSING:
77
+ # print(type(value).__name__, value)
78
+ print(value)
79
+ else:
80
+ print(f"⚠️ Настройка '{args.key}' не найдена")
81
+ else:
82
+ # List all settings
83
+ settings = settings.find()
84
+ t = PrettyTable(field_names=["Ключ", "Тип", "Значение"], align="l")
85
+ for setting in settings:
86
+ if setting.key.startswith("_"):
87
+ continue
88
+ t.add_row(
89
+ [
90
+ setting.key,
91
+ type(setting.value).__name__,
92
+ setting.value,
93
+ ]
94
+ )
95
+ print(t)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from runpy import run_module
7
+ from typing import TYPE_CHECKING
8
+
9
+ from ..main import BaseOperation
10
+
11
+ if TYPE_CHECKING:
12
+ from ..main import HHApplicantTool
13
+
14
+
15
+ logger = logging.getLogger(__package__)
16
+
17
+
18
+ class Operation(BaseOperation):
19
+ """Удалит Chromium и другие зависимости"""
20
+
21
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
+ pass
23
+
24
+ def run(self, tool: HHApplicantTool) -> None:
25
+ sys.argv = ["playwright", "uninstall", "chromium"]
26
+ run_module("playwright", run_name="__main__")