hh-applicant-tool 0.5.0__py3-none-any.whl → 0.5.1__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.

Potentially problematic release.


This version of hh-applicant-tool might be problematic. Click here for more details.

@@ -6,9 +6,10 @@ from typing import Tuple
6
6
 
7
7
  from ..api import ApiError
8
8
  from ..main import BaseOperation
9
- from ..main import Namespace as BaseNamespace, get_api
10
- from ..utils import parse_interval, random_text
9
+ from ..main import Namespace as BaseNamespace
10
+ from ..main import get_api
11
11
  from ..mixins import GetResumeIdMixin
12
+ from ..utils import parse_interval, random_text
12
13
 
13
14
  logger = logging.getLogger(__package__)
14
15
 
@@ -17,6 +18,7 @@ class Namespace(BaseNamespace):
17
18
  reply_message: str
18
19
  reply_interval: Tuple[float, float]
19
20
  max_pages: int
21
+ only_invitations: bool
20
22
  dry_run: bool
21
23
 
22
24
 
@@ -24,25 +26,42 @@ class Operation(BaseOperation, GetResumeIdMixin):
24
26
  """Ответ всем работодателям."""
25
27
 
26
28
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
29
+ # parser.add_argument(
30
+ # "reply_message",
31
+ # help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ. Если не передать, то его нужно будет вводить интерактивно.",
32
+ # )
33
+ parser.add_argument("--resume-id", help="Идентификатор резюме")
27
34
  parser.add_argument(
28
- "reply_message",
29
- help="Сообщение для отправки во все чаты с работодателями, где ожидают ответа либо не прочитали ответ",
30
- )
31
- parser.add_argument('--resume-id', help="Идентификатор резюме")
32
- parser.add_argument(
35
+ "-i",
33
36
  "--reply-interval",
34
37
  help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
35
38
  default="5-10",
36
39
  type=parse_interval,
37
40
  )
38
41
  parser.add_argument(
42
+ "-m",
39
43
  "--reply-message",
40
44
  "--reply",
41
- help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали ответ",
45
+ help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали ответ. Еслм не передать сообщение, то нужно будет вводить его в интерактивном режиме.",
46
+ )
47
+ parser.add_argument(
48
+ "-p",
49
+ "--max-pages",
50
+ type=int,
51
+ default=25,
52
+ help="Максимальное количество страниц для проверки",
42
53
  )
43
- parser.add_argument('--max-pages', type=int, default=25, help='Максимальное количество страниц для проверки')
54
+ parser.add_argument(
55
+ "-oi",
56
+ "--only-invitations",
57
+ help="Отвечать только на приглашения",
58
+ default=False,
59
+ action=argparse.BooleanOptionalAction,
60
+ )
61
+
44
62
  parser.add_argument(
45
63
  "--dry-run",
64
+ "--dry",
46
65
  help="Не отправлять сообщения, а только выводить параметры запроса",
47
66
  default=False,
48
67
  action=argparse.BooleanOptionalAction,
@@ -52,14 +71,16 @@ class Operation(BaseOperation, GetResumeIdMixin):
52
71
  self.api = get_api(args)
53
72
  self.resume_id = self._get_resume_id()
54
73
  self.reply_min_interval, self.reply_max_interval = args.reply_interval
55
- self.reply_message = args.reply_message
74
+ self.reply_message = args.reply_message or args.config["reply_message"]
75
+ # assert self.reply_message, "`reply_message` должен быть передан чеерез аргументы или настройки"
56
76
  self.max_pages = args.max_pages
57
77
  self.dry_run = args.dry_run
58
- logger.debug(f'{self.reply_message = }')
78
+ self.only_invitations = args.only_invitations
79
+ logger.debug(f"{self.reply_message = }")
59
80
  self._reply_chats()
60
81
 
61
82
  def _reply_chats(self) -> None:
62
- me =self.me= self.api.get("/me")
83
+ me = self.me = self.api.get("/me")
63
84
 
64
85
  basic_message_placeholders = {
65
86
  "first_name": me.get("first_name", ""),
@@ -71,17 +92,26 @@ class Operation(BaseOperation, GetResumeIdMixin):
71
92
  for negotiation in self._get_negotiations():
72
93
  try:
73
94
  # Пропускаем другие резюме
74
- if self.resume_id != negotiation['resume']['id']:
95
+ if self.resume_id != negotiation["resume"]["id"]:
75
96
  continue
76
97
 
98
+ state_id = negotiation["state"]["id"]
99
+
100
+ # Пропускаем отказ
101
+ if state_id == "discard":
102
+ continue
103
+
104
+ if self.only_invitations and not state_id.startswith("inv"):
105
+ continue
106
+
107
+ logger.debug(negotiation)
77
108
  nid = negotiation["id"]
78
109
  vacancy = negotiation["vacancy"]
110
+ salary = vacancy.get("salary") or {}
79
111
 
80
112
  message_placeholders = {
81
113
  "vacancy_name": vacancy.get("name", ""),
82
- "employer_name": vacancy.get("employer", {}).get(
83
- "name", ""
84
- ),
114
+ "employer_name": vacancy.get("employer", {}).get("name", ""),
85
115
  **basic_message_placeholders,
86
116
  }
87
117
 
@@ -92,28 +122,61 @@ class Operation(BaseOperation, GetResumeIdMixin):
92
122
 
93
123
  page: int = 0
94
124
  last_message: dict | None = None
125
+ message_history: list[str] = []
95
126
  while True:
96
127
  messages_res = self.api.get(
97
128
  f"/negotiations/{nid}/messages", page=page
98
129
  )
130
+
99
131
  last_message = messages_res["items"][-1]
132
+ message_history.extend(
133
+ (
134
+ "<-"
135
+ if item["author"]["participant_type"] == "employer"
136
+ else "->"
137
+ )
138
+ + " "
139
+ + item["text"]
140
+ for item in messages_res["items"]
141
+ if item.get("text")
142
+ )
100
143
  if page + 1 >= messages_res["pages"]:
101
144
  break
102
145
 
103
146
  page = messages_res["pages"] - 1
104
147
 
105
- logger.debug(last_message["text"])
148
+ logger.debug(last_message)
106
149
 
107
- if last_message["author"][
108
- "participant_type"
109
- ] == "employer" or not negotiation.get(
110
- "viewed_by_opponent"
111
- ):
112
- message = (
113
- random_text(self.reply_message)
114
- % message_placeholders
115
- )
116
- logger.debug(message)
150
+ is_employer_message = (
151
+ last_message["author"]["participant_type"] == "employer"
152
+ )
153
+
154
+ if is_employer_message or not negotiation.get("viewed_by_opponent"):
155
+ if self.reply_message:
156
+ message = random_text(self.reply_message) % message_placeholders
157
+ logger.debug(message)
158
+ else:
159
+ print("🏢", message_placeholders["employer_name"])
160
+ print("💼", message_placeholders["vacancy_name"])
161
+ print("📅", vacancy["created_at"])
162
+ if salary:
163
+ salary_from = salary.get("from", "-")
164
+ salary_to = salary.get("to", "-")
165
+ salary_currency = salary.get("currency")
166
+ print("💵 от", salary_from, "до", salary_to, salary_currency)
167
+ print("")
168
+ print("Последние сообщения:")
169
+ for msg in (
170
+ message_history[:1] + ["..."] + message_history[-2:]
171
+ if len(message_history) > 3
172
+ else message_history
173
+ ):
174
+ print(msg)
175
+ print("-" * 10)
176
+ message = input("Ваше сообщение: ").strip()
177
+ if not message:
178
+ print("🚶 Пропускаем чат")
179
+ continue
117
180
 
118
181
  if self.dry_run:
119
182
  logger.info(
@@ -145,7 +208,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
145
208
  def _get_negotiations(self) -> list[dict]:
146
209
  rv = []
147
210
  for page in range(self.max_pages):
148
- res = self.api.get("/negotiations", page=page, status='active')
211
+ res = self.api.get("/negotiations", page=page, status="active")
149
212
  rv.extend(res["items"])
150
213
  if page >= res["pages"] - 1:
151
214
  break
@@ -85,7 +85,7 @@ def fix_datetime(dt: str | None) -> str | None:
85
85
 
86
86
  def random_text(s: str) -> str:
87
87
  while (
88
- s1 := re.sub(
88
+ temp := re.sub(
89
89
  r"{([^{}]+)}",
90
90
  lambda m: random.choice(
91
91
  m.group(1).split("|"),
@@ -93,7 +93,7 @@ def random_text(s: str) -> str:
93
93
  s,
94
94
  )
95
95
  ) != s:
96
- s = s1
96
+ s = temp
97
97
  return s
98
98
 
99
99
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  ### Описание
34
34
 
35
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Обсуждения без отзывов удаляются. Данные на сервер передаются анонимно. Для этого собираются данные о работодателях и их вакансиях. Никакие персональные данные пользователя утилиты под которыми, вы авторизуетесь никуда не отправляются только работодателей и рекрутеров. Отправку этих данных можно отключить (флаг `--disable-telemetry`), но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги отзывы удаляет, или мудак его запугает и жалоб в РКН накидает, а последний всегда найдет за что сайт заблокировать. Единственное место где можно написать отзыв — это **Telegram**.
35
+ Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Для этого собираются данные о работодателях и их вакансиях (персональные данные пользователя не передаются ни в каком виде). Отправку данных на сервер разработчика можно отключить, но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги или после угроз судом удаляют отзывы. Единственное место где можно написать отзыв — это **Telegram**.
36
36
 
37
37
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
38
38
  asdf/pyenv/conda и что-то еще.
@@ -41,7 +41,7 @@ asdf/pyenv/conda и что-то еще.
41
41
 
42
42
  Пример работы:
43
43
 
44
- ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
44
+ ![image](https://github.com/user-attachments/assets/a0cce1aa-884b-4d84-905a-3bb207eba4a3)
45
45
 
46
46
 
47
47
  ### Предыстория
@@ -15,13 +15,13 @@ hh_applicant_tool/operations/clear_negotiations.py,sha256=98Yuw9xP4dN5sUnUKlZxqf
15
15
  hh_applicant_tool/operations/get_employer_contacts.py,sha256=7BEzEnNAp87RlOP6HX0LR6cbtud2FuKCKK5sq6fINq8,4066
16
16
  hh_applicant_tool/operations/list_resumes.py,sha256=XBrVFTnl45BUtuvjVm70h_CXZrOvAULnLkLkyUh7gxw,1134
17
17
  hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
18
- hh_applicant_tool/operations/reply_employers.py,sha256=wwDcI9YeZGUwadWQYFBwNpXb8qSAejaJ4KAuQTfFIuk,5686
18
+ hh_applicant_tool/operations/reply_employers.py,sha256=pcD1Uxmaixw7LSebDE2bUot67HRxbgghR9mvuW2MBUk,8660
19
19
  hh_applicant_tool/operations/update_resumes.py,sha256=gGxMYMoT9GqJjwn4AgrOAEJCZu4sdhaV0VmPr3jG-q8,1049
20
20
  hh_applicant_tool/operations/whoami.py,sha256=sg0r7m6oKkpMEmGt4QdtYdS-1gf-1KKdnk32yblbRJs,714
21
21
  hh_applicant_tool/telemetry_client.py,sha256=wYLbKnx3sOmESFHqjLt-0Gww1O3lJiXFYdWnsorIhK8,3261
22
22
  hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
23
- hh_applicant_tool/utils.py,sha256=vjSRbwU8mduFgORcyO2sQj-2B6klzQCtg_CFVCsgCo4,3067
24
- hh_applicant_tool-0.5.0.dist-info/METADATA,sha256=xoA2y1wx6s-oTOS8GMw-9o78NpUy34SuLaf85w8eGos,19969
25
- hh_applicant_tool-0.5.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
26
- hh_applicant_tool-0.5.0.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
27
- hh_applicant_tool-0.5.0.dist-info/RECORD,,
23
+ hh_applicant_tool/utils.py,sha256=OsdjzUa76VC8vihHu27rD5df9DWqRzpvJr3PsdfJzNE,3071
24
+ hh_applicant_tool-0.5.1.dist-info/METADATA,sha256=-xswxfCHwrU43KuTRfJHFHLz4TRqRn3AqaCPXKmNrQk,19553
25
+ hh_applicant_tool-0.5.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
26
+ hh_applicant_tool-0.5.1.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
27
+ hh_applicant_tool-0.5.1.dist-info/RECORD,,