hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.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 (75) 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 +23 -33
  5. hh_applicant_tool/api/client.py +50 -64
  6. hh_applicant_tool/api/errors.py +51 -7
  7. hh_applicant_tool/constants.py +0 -3
  8. hh_applicant_tool/datatypes.py +291 -0
  9. hh_applicant_tool/main.py +233 -111
  10. hh_applicant_tool/operations/apply_similar.py +266 -362
  11. hh_applicant_tool/operations/authorize.py +256 -120
  12. hh_applicant_tool/operations/call_api.py +18 -8
  13. hh_applicant_tool/operations/check_negotiations.py +102 -0
  14. hh_applicant_tool/operations/check_proxy.py +30 -0
  15. hh_applicant_tool/operations/config.py +119 -16
  16. hh_applicant_tool/operations/install.py +34 -0
  17. hh_applicant_tool/operations/list_resumes.py +24 -10
  18. hh_applicant_tool/operations/log.py +77 -0
  19. hh_applicant_tool/operations/migrate_db.py +65 -0
  20. hh_applicant_tool/operations/query.py +120 -0
  21. hh_applicant_tool/operations/refresh_token.py +14 -13
  22. hh_applicant_tool/operations/reply_employers.py +148 -167
  23. hh_applicant_tool/operations/settings.py +95 -0
  24. hh_applicant_tool/operations/uninstall.py +26 -0
  25. hh_applicant_tool/operations/update_resumes.py +21 -10
  26. hh_applicant_tool/operations/whoami.py +40 -7
  27. hh_applicant_tool/storage/__init__.py +4 -0
  28. hh_applicant_tool/storage/facade.py +24 -0
  29. hh_applicant_tool/storage/models/__init__.py +0 -0
  30. hh_applicant_tool/storage/models/base.py +169 -0
  31. hh_applicant_tool/storage/models/contact.py +16 -0
  32. hh_applicant_tool/storage/models/employer.py +12 -0
  33. hh_applicant_tool/storage/models/negotiation.py +16 -0
  34. hh_applicant_tool/storage/models/resume.py +19 -0
  35. hh_applicant_tool/storage/models/setting.py +6 -0
  36. hh_applicant_tool/storage/models/vacancy.py +36 -0
  37. hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
  38. hh_applicant_tool/storage/queries/schema.sql +119 -0
  39. hh_applicant_tool/storage/repositories/__init__.py +0 -0
  40. hh_applicant_tool/storage/repositories/base.py +176 -0
  41. hh_applicant_tool/storage/repositories/contacts.py +19 -0
  42. hh_applicant_tool/storage/repositories/employers.py +13 -0
  43. hh_applicant_tool/storage/repositories/negotiations.py +12 -0
  44. hh_applicant_tool/storage/repositories/resumes.py +14 -0
  45. hh_applicant_tool/storage/repositories/settings.py +34 -0
  46. hh_applicant_tool/storage/repositories/vacancies.py +8 -0
  47. hh_applicant_tool/storage/utils.py +49 -0
  48. hh_applicant_tool/utils/__init__.py +31 -0
  49. hh_applicant_tool/utils/attrdict.py +6 -0
  50. hh_applicant_tool/utils/binpack.py +167 -0
  51. hh_applicant_tool/utils/config.py +55 -0
  52. hh_applicant_tool/utils/dateutil.py +19 -0
  53. hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
  54. hh_applicant_tool/utils/jsonutil.py +61 -0
  55. hh_applicant_tool/utils/log.py +144 -0
  56. hh_applicant_tool/utils/misc.py +12 -0
  57. hh_applicant_tool/utils/mixins.py +220 -0
  58. hh_applicant_tool/utils/string.py +27 -0
  59. hh_applicant_tool/utils/terminal.py +19 -0
  60. hh_applicant_tool/utils/user_agent.py +17 -0
  61. hh_applicant_tool-1.4.7.dist-info/METADATA +628 -0
  62. hh_applicant_tool-1.4.7.dist-info/RECORD +67 -0
  63. hh_applicant_tool/ai/blackbox.py +0 -55
  64. hh_applicant_tool/color_log.py +0 -47
  65. hh_applicant_tool/mixins.py +0 -13
  66. hh_applicant_tool/operations/clear_negotiations.py +0 -109
  67. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  68. hh_applicant_tool/operations/get_employer_contacts.py +0 -348
  69. hh_applicant_tool/telemetry_client.py +0 -106
  70. hh_applicant_tool/types.py +0 -45
  71. hh_applicant_tool/utils.py +0 -119
  72. hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
  73. hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
  74. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/WHEEL +0 -0
  75. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.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 typing import TYPE_CHECKING
7
+
8
+ from .. import datatypes
9
+ from ..ai.base import AIError
10
+ from ..api import ApiError
11
+ from ..main import BaseNamespace, BaseOperation
12
+ from ..utils.dateutil 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,31 @@ 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
42
40
 
43
41
 
44
- class Operation(BaseOperation, GetResumeIdMixin):
42
+ class Operation(BaseOperation):
45
43
  """Ответ всем работодателям."""
46
44
 
45
+ __aliases__ = ["reply-chats", "reply-all"]
46
+
47
47
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
48
- # parser.add_argument(
49
- # "reply_message",
50
- # help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ. Если не передать, то его нужно будет вводить интерактивно.",
51
- # )
52
48
  parser.add_argument("--resume-id", help="Идентификатор резюме")
53
- parser.add_argument(
54
- "-i",
55
- "--reply-interval",
56
- help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
57
- default="5-10",
58
- type=parse_interval,
59
- )
60
49
  parser.add_argument(
61
50
  "-m",
62
51
  "--reply-message",
63
52
  "--reply",
64
- help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали ответ. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.",
53
+ help="Отправить сообщение во все чаты. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.", # noqa: E501
65
54
  )
66
55
  parser.add_argument(
67
56
  "-p",
@@ -77,7 +66,6 @@ class Operation(BaseOperation, GetResumeIdMixin):
77
66
  default=False,
78
67
  action=argparse.BooleanOptionalAction,
79
68
  )
80
-
81
69
  parser.add_argument(
82
70
  "--dry-run",
83
71
  "--dry",
@@ -85,39 +73,47 @@ class Operation(BaseOperation, GetResumeIdMixin):
85
73
  default=False,
86
74
  action=argparse.BooleanOptionalAction,
87
75
  )
76
+ parser.add_argument(
77
+ "--use-ai",
78
+ "--ai",
79
+ help="Использовать AI для автоматической генерации ответов",
80
+ action=argparse.BooleanOptionalAction,
81
+ )
82
+ parser.add_argument(
83
+ "--first-prompt",
84
+ help="Начальный промпт чата для AI",
85
+ default="Ты — соискатель на HeadHunter. Отвечай вежливо и кратко.",
86
+ )
87
+ parser.add_argument(
88
+ "--prompt",
89
+ help="Промпт для генерации сообщения",
90
+ default="Напиши короткий ответ работодателю на основе истории переписки.",
91
+ )
88
92
 
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` должен быть передан чеерез аргументы или настройки"
93
+ def run(self, tool: HHApplicantTool) -> None:
94
+ args: Namespace = tool.args
95
+ self.applicant_tool = tool
96
+ self.api_client = tool.api_client
97
+ self.resume_id = tool.first_resume_id()
98
+ self.reply_message = args.reply_message or tool.config.get(
99
+ "reply_message"
100
+ )
99
101
  self.max_pages = args.max_pages
100
102
  self.dry_run = args.dry_run
101
103
  self.only_invitations = args.only_invitations
104
+
105
+ self.pre_prompt = args.prompt
106
+ self.openai_chat = (
107
+ tool.get_openai_chat(args.first_prompt) if args.use_ai else None
108
+ )
109
+
102
110
  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")
111
+ self.reply_chats()
119
112
 
120
- telemetry_data = {"links": []}
113
+ def reply_chats(self) -> None:
114
+ blacklisted = self.applicant_tool.get_blacklisted()
115
+ logger.debug(f"{blacklisted = }")
116
+ me: datatypes.User = self.applicant_tool.get_me()
121
117
 
122
118
  basic_message_placeholders = {
123
119
  "first_name": me.get("first_name", ""),
@@ -126,22 +122,20 @@ class Operation(BaseOperation, GetResumeIdMixin):
126
122
  "phone": me.get("phone", ""),
127
123
  }
128
124
 
129
- for negotiation in self._get_negotiations():
125
+ for negotiation in self.applicant_tool.get_negotiations():
130
126
  try:
131
- # Пропускаем другие резюме
127
+ self.applicant_tool.storage.negotiations.save(negotiation)
128
+
132
129
  if self.resume_id != negotiation["resume"]["id"]:
133
130
  continue
134
131
 
135
132
  state_id = negotiation["state"]["id"]
136
-
137
- # Пропускаем отказ
138
133
  if state_id == "discard":
139
134
  continue
140
135
 
141
136
  if self.only_invitations and not state_id.startswith("inv"):
142
137
  continue
143
138
 
144
- logger.debug(negotiation)
145
139
  nid = negotiation["id"]
146
140
  vacancy = negotiation["vacancy"]
147
141
  employer = vacancy.get("employer") or {}
@@ -169,156 +163,143 @@ class Operation(BaseOperation, GetResumeIdMixin):
169
163
  last_message: dict | None = None
170
164
  message_history: list[str] = []
171
165
  while True:
172
- messages_res = self.api_client.get(
166
+ messages_res: datatypes.PaginatedItems[
167
+ datatypes.Message
168
+ ] = self.api_client.get(
173
169
  f"/negotiations/{nid}/messages", page=page
174
170
  )
171
+ if not messages_res["items"]:
172
+ break
175
173
 
176
174
  last_message = messages_res["items"][-1]
177
- message_history.extend(
178
- (
179
- "<-"
180
- if item["author"]["participant_type"] == "employer"
181
- else "->"
175
+ for message in messages_res["items"]:
176
+ if not message.get("text"):
177
+ continue
178
+ author = (
179
+ "Работодатель"
180
+ if message["author"]["participant_type"]
181
+ == "employer"
182
+ else "Я"
182
183
  )
183
- + " "
184
- + item["text"]
185
- for item in messages_res["items"]
186
- if item.get("text")
187
- )
184
+ message_date = parse_api_datetime(
185
+ message.get("created_at")
186
+ ).isoformat()
187
+ message_history.append(
188
+ f"[ {message_date} ] {author}: {message['text']}"
189
+ )
190
+
188
191
  if page + 1 >= messages_res["pages"]:
189
192
  break
190
-
191
193
  page = messages_res["pages"] - 1
192
194
 
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"]:
195
+ if not last_message:
217
196
  continue
218
197
 
219
- logger.debug(last_message)
220
-
221
198
  is_employer_message = (
222
199
  last_message["author"]["participant_type"] == "employer"
223
200
  )
224
201
 
225
- if is_employer_message or not negotiation.get("viewed_by_opponent"):
202
+ if is_employer_message or not negotiation.get(
203
+ "viewed_by_opponent"
204
+ ):
205
+ send_message = ""
226
206
  if self.reply_message:
227
207
  send_message = (
228
- random_text(self.reply_message) % message_placeholders
208
+ rand_text(self.reply_message) % message_placeholders
229
209
  )
230
- logger.debug(send_message)
210
+ logger.debug(f"Template message: {send_message}")
211
+ elif self.openai_chat:
212
+ try:
213
+ ai_query = (
214
+ f"Вакансия: {message_placeholders['vacancy_name']}\n"
215
+ f"История переписки:\n"
216
+ + "\n".join(message_history[-10:])
217
+ + f"\n\nИнструкция: {self.pre_prompt}"
218
+ )
219
+ send_message = self.openai_chat.send_message(
220
+ ai_query
221
+ )
222
+ logger.debug(f"AI message: {send_message}")
223
+ except AIError as ex:
224
+ logger.warning(
225
+ f"Ошибка OpenAI для чата {nid}: {ex}"
226
+ )
227
+ continue
231
228
  else:
232
- print("🏢", message_placeholders["employer_name"])
233
- print("💼", message_placeholders["vacancy_name"])
234
- print("📅", vacancy["created_at"])
229
+ print(
230
+ "\n🏢",
231
+ message_placeholders["employer_name"],
232
+ "| 💼",
233
+ message_placeholders["vacancy_name"],
234
+ )
235
235
  if salary:
236
- salary_from = salary.get("from") or "-"
237
- salary_to = salary.get("to") or "-"
238
- salary_currency = salary.get("currency")
239
236
  print(
240
- "💵 от", salary_from, "до", salary_to, salary_currency
237
+ "💵 от",
238
+ salary.get("from") or salary.get("to") or 0,
239
+ "до",
240
+ salary.get("to") or salary.get("from") or 0,
241
+ salary.get("currency", "RUR"),
241
242
  )
242
- print("")
243
- print("Последние сообщения:")
243
+
244
+ print("\nПоследние сообщения чата:")
245
+ print()
244
246
  for msg in (
245
- message_history[:1] + ["..."] + message_history[-3:]
247
+ message_history[-5:]
246
248
  if len(message_history) > 5
247
249
  else message_history
248
250
  ):
249
251
  print(msg)
252
+
250
253
  try:
251
254
  print("-" * 10)
252
- print()
253
255
  print(
254
- "Отмена отклика: /cancel <необязательное сообщение для отказа>"
256
+ "Команды: /ban, /cancel <опционально сообщение для отмены>"
255
257
  )
256
- print("Заблокировать работодателя: /ban")
257
- print()
258
258
  send_message = input("Ваше сообщение: ").strip()
259
259
  except EOFError:
260
260
  continue
261
+
261
262
  if not send_message:
262
263
  print("🚶 Пропускаем чат")
263
264
  continue
264
265
 
266
+ if send_message.startswith("/ban"):
267
+ self.applicant_tool.storage.employers.save(employer)
268
+ self.api_client.put(
269
+ f"/employers/blacklisted/{employer['id']}"
270
+ )
271
+ blacklisted.append(employer["id"])
272
+ print(
273
+ "🚫 Работодатель в ЧС",
274
+ employer.get("alternate_url"),
275
+ )
276
+ continue
277
+ elif send_message.startswith("/cancel"):
278
+ _, decline_msg = send_message.split("/cancel", 1)
279
+ self.api_client.delete(
280
+ f"/negotiations/active/{nid}",
281
+ with_decline_message=decline_msg.strip(),
282
+ )
283
+ print("❌ Отмена заявки", vacancy["alternate_url"])
284
+ continue
285
+
286
+ # Финальная отправка текста
265
287
  if self.dry_run:
266
- logger.info(
267
- "Dry Run: Отправка сообщения в чат по вакансии %s: %s",
288
+ logger.debug(
289
+ "dry-run: отклик на",
268
290
  vacancy["alternate_url"],
269
291
  send_message,
270
292
  )
271
293
  continue
272
294
 
273
- time.sleep(
274
- random.uniform(
275
- self.reply_min_interval,
276
- self.reply_max_interval,
277
- )
295
+ self.api_client.post(
296
+ f"/negotiations/{nid}/messages",
297
+ message=send_message,
298
+ delay=random.uniform(1, 3),
278
299
  )
300
+ print(f"📨 Отправлено для {vacancy['alternate_url']}")
279
301
 
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
302
  except ApiError as ex:
304
303
  logger.error(ex)
305
304
 
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
305
  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 ..main import BaseNamespace, BaseOperation
11
+ from ..utils import jsonutil
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 jsonutil.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__")
@@ -1,12 +1,19 @@
1
1
  # Этот модуль можно использовать как образец для других
2
+ from __future__ import annotations
3
+
2
4
  import argparse
3
5
  import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ..api import ApiError
9
+ from ..datatypes import PaginatedItems
10
+ from ..main import BaseNamespace, BaseOperation
11
+ from ..utils import print_err
12
+ from ..utils.string import shorten
13
+
14
+ if TYPE_CHECKING:
15
+ from ..main import HHApplicantTool
4
16
 
5
- from ..api import ApiClient, ApiError
6
- from ..main import BaseOperation
7
- from ..main import Namespace as BaseNamespace
8
- from ..types import ApiListResponse
9
- from ..utils import print_err, truncate_string
10
17
 
11
18
  logger = logging.getLogger(__package__)
12
19
 
@@ -18,15 +25,19 @@ class Namespace(BaseNamespace):
18
25
  class Operation(BaseOperation):
19
26
  """Обновить все резюме"""
20
27
 
28
+ __aliases__ = ["update"]
29
+
21
30
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
31
  pass
23
32
 
24
- def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
25
- resumes: ApiListResponse = api_client.get("/resumes/mine")
33
+ def run(self, tool: HHApplicantTool) -> None:
34
+ resumes: PaginatedItems = tool.get_resumes()
26
35
  for resume in resumes["items"]:
27
36
  try:
28
- res = api_client.post(f"/resumes/{resume['id']}/publish")
37
+ res = tool.api_client.post(
38
+ f"/resumes/{resume['id']}/publish",
39
+ )
29
40
  assert res == {}
30
- print("✅ Обновлено", truncate_string(resume["title"]))
41
+ print("✅ Обновлено", shorten(resume["title"]))
31
42
  except ApiError as ex:
32
- print_err("❗ Ошибка:", ex)
43
+ print_err("❗", ex)
@@ -1,11 +1,16 @@
1
1
  # Этот модуль можно использовать как образец для других
2
+ from __future__ import annotations
3
+
2
4
  import argparse
3
- import json
4
5
  import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from .. import datatypes
9
+ from ..main import BaseNamespace, BaseOperation
10
+
11
+ if TYPE_CHECKING:
12
+ from ..main import HHApplicantTool
5
13
 
6
- from ..api import ApiClient
7
- from ..main import BaseOperation
8
- from ..main import Namespace as BaseNamespace
9
14
 
10
15
  logger = logging.getLogger(__package__)
11
16
 
@@ -14,12 +19,40 @@ class Namespace(BaseNamespace):
14
19
  pass
15
20
 
16
21
 
22
+ def fmt_plus(n: int) -> str:
23
+ assert n >= 0
24
+ return f"+{n}" if n else "0"
25
+
26
+
17
27
  class Operation(BaseOperation):
18
28
  """Выведет текущего пользователя"""
19
29
 
30
+ __aliases__: list[str] = ["id"]
31
+
20
32
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
21
33
  pass
22
34
 
23
- def run(self, args: Namespace, api_client: ApiClient, _) -> None:
24
- result = api_client.get("/me")
25
- print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
35
+ def run(self, tool: HHApplicantTool) -> None:
36
+ api_client = tool.api_client
37
+ result: datatypes.User = api_client.get("me")
38
+ full_name = " ".join(
39
+ filter(
40
+ None,
41
+ [
42
+ result.get("last_name"),
43
+ result.get("first_name"),
44
+ result.get("middle_name"),
45
+ ],
46
+ )
47
+ )
48
+ with tool.storage.settings as s:
49
+ s.set_value("user.full_name", full_name)
50
+ s.set_value("user.email", result.get("email"))
51
+ s.set_value("user.phone", result.get("phone"))
52
+ counters = result["counters"]
53
+ print(
54
+ f"🆔 {result['id']} {full_name or 'Анонимный аккаунт'} "
55
+ f"[ 📄 {counters['resumes_count']} "
56
+ f"| 👁️ {fmt_plus(counters['new_resume_views'])} "
57
+ f"| ✉️ {fmt_plus(counters['unread_negotiations'])} ]"
58
+ )