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

@@ -12,11 +12,12 @@ from urllib.parse import urlencode
12
12
 
13
13
  import requests
14
14
  from requests import Response, Session
15
+ import random
15
16
 
16
17
  from ..constants import (
17
18
  ANDROID_CLIENT_ID,
18
19
  ANDROID_CLIENT_SECRET,
19
- DEFAULT_USER_AGENT,
20
+ USER_AGENT_TEMPLATE,
20
21
  )
21
22
  from ..types import AccessToken
22
23
  from . import errors
@@ -38,6 +39,7 @@ class BaseClient:
38
39
  user_agent: str | None = None
39
40
  session: Session | None = None
40
41
  previous_request_time: float = 0.0
42
+ delay: float = 0.334
41
43
 
42
44
  def __post_init__(self) -> None:
43
45
  self.lock = Lock()
@@ -46,11 +48,31 @@ class BaseClient:
46
48
  session.headers.update(
47
49
  {
48
50
  **self.additional_headers(),
49
- "User-Agent": self.user_agent or DEFAULT_USER_AGENT,
51
+ "User-Agent": self.user_agent or self.default_user_agent(),
50
52
  }
51
53
  )
52
54
  logger.debug("Default Headers: %r", session.headers)
53
55
 
56
+ def default_user_agent(self) -> str:
57
+ return USER_AGENT_TEMPLATE % (
58
+ random.choice(["8.0", "8.1", "9", "10", "11", "12"]),
59
+ random.choice(
60
+ [
61
+ "SM-G998B", # Samsung Galaxy S21 Ultra
62
+ "Pixel 6", # Google Pixel 6
63
+ "Mi 11", # Xiaomi Mi 11
64
+ "OnePlus 9", # OnePlus 9
65
+ "P40", # Huawei P40
66
+ "LG G8", # LG G8
67
+ "Xperia 1 II", # Sony Xperia 1 II
68
+ "Moto G Power", # Motorola Moto G Power
69
+ "HTC U12+", # HTC U12+
70
+ "ROG Phone 5", # Asus ROG Phone 5
71
+ ]
72
+ ),
73
+ random.randint(88, 130),
74
+ )
75
+
54
76
  def additional_headers(
55
77
  self,
56
78
  ) -> dict[str, str]:
@@ -61,7 +83,7 @@ class BaseClient:
61
83
  method: ALLOWED_METHODS,
62
84
  endpoint: str,
63
85
  params: dict | None = None,
64
- delay: float = 0.34,
86
+ delay: float | None = None,
65
87
  **kwargs: Any,
66
88
  ) -> dict:
67
89
  # Не знаю насколько это "правильно"
@@ -72,7 +94,9 @@ class BaseClient:
72
94
  with self.lock:
73
95
  # На серваке какая-то анти-DDOS система
74
96
  if (
75
- delay := delay - time.monotonic() + self.previous_request_time
97
+ delay := (self.delay if delay is None else delay)
98
+ - time.monotonic()
99
+ + self.previous_request_time
76
100
  ) > 0:
77
101
  logger.debug("wait %fs before request", delay)
78
102
  time.sleep(delay)
@@ -1,4 +1,4 @@
1
- DEFAULT_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36"
1
+ USER_AGENT_TEMPLATE = "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Mobile Safari/537.36"
2
2
 
3
3
  ANDROID_CLIENT_ID = (
4
4
  "HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
hh_applicant_tool/main.py CHANGED
@@ -9,14 +9,12 @@ from os import getenv
9
9
  from pathlib import Path
10
10
  from pkgutil import iter_modules
11
11
  from typing import Sequence
12
-
12
+ from .api import ApiClient
13
13
  from .color_log import ColorHandler
14
14
  from .utils import Config, get_config_path
15
15
 
16
16
  DEFAULT_CONFIG_PATH = (
17
- get_config_path()
18
- / __package__.replace("_", "-")
19
- / "config.json"
17
+ get_config_path() / __package__.replace("_", "-") / "config.json"
20
18
  )
21
19
 
22
20
  logger = logging.getLogger(__package__)
@@ -35,6 +33,18 @@ OPERATIONS = "operations"
35
33
  class Namespace(argparse.Namespace):
36
34
  config: Config
37
35
  verbosity: int
36
+ delay: float
37
+
38
+
39
+ def get_api(args: Namespace) -> ApiClient:
40
+ token = args.config.get("token", {})
41
+ api = ApiClient(
42
+ access_token=token.get("access_token"),
43
+ refresh_token=token.get("refresh_token"),
44
+ user_agent=args.config["user_agent"],
45
+ delay=args.delay,
46
+ )
47
+ return api
38
48
 
39
49
 
40
50
  class HHApplicantTool:
@@ -45,32 +55,46 @@ class HHApplicantTool:
45
55
  Группа поддержки: <https://t.me/+aSjr8qM_AP85ZDBi>
46
56
  """
47
57
 
58
+ class ArgumentFormatter(
59
+ argparse.ArgumentDefaultsHelpFormatter,
60
+ argparse.RawDescriptionHelpFormatter,
61
+ ):
62
+ pass
63
+
48
64
  def create_parser(self) -> argparse.ArgumentParser:
49
65
  parser = argparse.ArgumentParser(
50
66
  description=self.__doc__,
51
- formatter_class=argparse.RawDescriptionHelpFormatter,
67
+ formatter_class=self.ArgumentFormatter,
52
68
  )
53
69
  parser.add_argument(
54
70
  "-c",
55
71
  "--config",
56
- help="config path",
72
+ help="Путь до файла конфигурации",
57
73
  type=Config,
58
74
  default=Config(DEFAULT_CONFIG_PATH),
59
75
  )
60
76
  parser.add_argument(
61
77
  "-v",
62
78
  "--verbosity",
63
- help="increase verbosity",
79
+ help="При использовании от одного и более раз увеличивает количество отладочной информации в выводе",
64
80
  action="count",
65
81
  default=0,
66
82
  )
83
+ parser.add_argument(
84
+ "-d",
85
+ "--delay",
86
+ type=float,
87
+ default=0.334,
88
+ help="Задержка между запросами к API HH",
89
+ )
67
90
  subparsers = parser.add_subparsers(help="commands")
68
91
  package_dir = Path(__file__).resolve().parent / OPERATIONS
69
92
  for _, module_name, _ in iter_modules([str(package_dir)]):
70
93
  mod = import_module(f"{__package__}.{OPERATIONS}.{module_name}")
71
94
  op: BaseOperation = mod.Operation()
72
95
  op_parser = subparsers.add_parser(
73
- module_name.replace("_", "-"), description=op.__doc__
96
+ module_name.replace("_", "-"),
97
+ description=op.__doc__, formatter_class=self.ArgumentFormatter
74
98
  )
75
99
  op_parser.set_defaults(run=op.run)
76
100
  op.setup_parser(op_parser)
@@ -8,7 +8,7 @@ from typing import TextIO, Tuple
8
8
 
9
9
  from ..api import ApiClient, ApiError, BadRequest
10
10
  from ..main import BaseOperation
11
- from ..main import Namespace as BaseNamespace
11
+ from ..main import Namespace as BaseNamespace, get_api
12
12
  from ..telemetry_client import TelemetryError
13
13
  from ..telemetry_client import get_client as get_telemetry_client
14
14
  from ..types import ApiListResponse, VacancyItem
@@ -25,8 +25,9 @@ class Namespace(BaseNamespace):
25
25
  page_interval: Tuple[float, float]
26
26
 
27
27
 
28
+ # https://api.hh.ru/openapi/redoc
28
29
  class Operation(BaseOperation):
29
- """Откликнуться на все подходящие вакансии"""
30
+ """Откликнуться на все подходящие вакансии. По умолчанию применяются значения, которые были отмечены галочками в форме для поиска на сайте"""
30
31
 
31
32
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
32
33
  parser.add_argument("--resume-id", help="Идентефикатор резюме")
@@ -53,6 +54,24 @@ class Operation(BaseOperation):
53
54
  default="1-3",
54
55
  type=self._parse_interval,
55
56
  )
57
+ parser.add_argument(
58
+ "--order-by",
59
+ help="Сортировка вакансий",
60
+ choices=[
61
+ "publication_time",
62
+ "salary_desc",
63
+ "salary_asc",
64
+ "relevance",
65
+ "distance",
66
+ ],
67
+ default="relevance",
68
+ )
69
+ parser.add_argument(
70
+ "--search",
71
+ help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500', те можно и город указать, и ожидаемую зряплату",
72
+ type=str,
73
+ default=None,
74
+ )
56
75
 
57
76
  @staticmethod
58
77
  def _parse_interval(interval: str) -> Tuple[float, float]:
@@ -64,11 +83,7 @@ class Operation(BaseOperation):
64
83
  return min(min_interval, max_interval), max(min_interval, max_interval)
65
84
 
66
85
  def run(self, args: Namespace) -> None:
67
- assert args.config["token"]
68
- api = ApiClient(
69
- access_token=args.config["token"]["access_token"],
70
- user_agent=args.config["user_agent"],
71
- )
86
+ api = get_api(args)
72
87
  resume_id = self._get_resume_id(args, api)
73
88
  application_messages = self._get_application_messages(args)
74
89
 
@@ -84,6 +99,8 @@ class Operation(BaseOperation):
84
99
  apply_max_interval,
85
100
  page_min_interval,
86
101
  page_max_interval,
102
+ args.order_by,
103
+ args.search,
87
104
  )
88
105
 
89
106
  def _get_resume_id(self, args: Namespace, api: ApiClient) -> str:
@@ -119,12 +136,20 @@ class Operation(BaseOperation):
119
136
  apply_max_interval: float,
120
137
  page_min_interval: float,
121
138
  page_max_interval: float,
139
+ order_by: str,
140
+ search: str | None = None,
122
141
  ) -> None:
123
142
  telemetry_client = get_telemetry_client()
124
143
  telemetry_data = defaultdict(dict)
125
144
 
126
145
  vacancies = self._get_vacancies(
127
- api, resume_id, page_min_interval, page_max_interval, per_page=100
146
+ api,
147
+ resume_id,
148
+ page_min_interval,
149
+ page_max_interval,
150
+ per_page=100,
151
+ order_by=order_by,
152
+ search=search,
128
153
  )
129
154
 
130
155
  self._collect_vacancy_telemetry(telemetry_data, vacancies)
@@ -149,7 +174,7 @@ class Operation(BaseOperation):
149
174
 
150
175
  try:
151
176
  employer_id = vacancy["employer"]["id"]
152
- except IndexError:
177
+ except KeyError:
153
178
  logger.warning(
154
179
  f"Вакансия без работодателя: {vacancy['alternate_url']}"
155
180
  )
@@ -173,13 +198,23 @@ class Operation(BaseOperation):
173
198
  params = {
174
199
  "resume_id": resume_id,
175
200
  "vacancy_id": vacancy["id"],
176
- "message": (
177
- random.choice(application_messages) % vacancy
178
- if force_message or vacancy["response_letter_required"]
179
- else ""
180
- ),
201
+ "message": "",
181
202
  }
182
203
 
204
+ if vacancy.get("response_letter_required"):
205
+ message_template = random.choice(application_messages)
206
+
207
+ try:
208
+ params["message"] = message_template % vacancy
209
+ except TypeError as ex:
210
+ # TypeError: not enough arguments for format string
211
+ # API HH все кривое, иногда нет идентификатора работодателя, иногда у вакансии нет названия.
212
+ # И это типа рашн хайлоад, где из-за дрочки на аджайл слепили кривую говнину.
213
+ logger.error(
214
+ f"Ошибка форматирования шаблона сообщения {template_message!r} для {vacancy = }"
215
+ )
216
+ continue
217
+
183
218
  res = api.post("/negotiations", params)
184
219
  assert res == {}
185
220
  print(
@@ -205,14 +240,20 @@ class Operation(BaseOperation):
205
240
  page_min_interval: float,
206
241
  page_max_interval: float,
207
242
  per_page: int,
243
+ order_by: str,
244
+ search: str | None = None,
208
245
  ) -> list[VacancyItem]:
209
246
  rv = []
210
247
  for page in range(20):
248
+ params = {
249
+ "page": page,
250
+ "per_page": per_page,
251
+ "order_by": order_by,
252
+ }
253
+ if search:
254
+ params["text"] = search
211
255
  res: ApiListResponse = api.get(
212
- f"/resumes/{resume_id}/similar_vacancies",
213
- page=page,
214
- per_page=per_page,
215
- order_by="relevance",
256
+ f"/resumes/{resume_id}/similar_vacancies", params
216
257
  )
217
258
  rv.extend(res["items"])
218
259
 
@@ -4,8 +4,8 @@ import json
4
4
  import logging
5
5
  import sys
6
6
 
7
- from ..api import ApiClient, ApiError
8
- from ..main import BaseOperation
7
+ from ..api import ApiError
8
+ from ..main import BaseOperation, get_api
9
9
  from ..main import Namespace as BaseNamespace
10
10
 
11
11
  logger = logging.getLogger(__package__)
@@ -34,11 +34,7 @@ class Operation(BaseOperation):
34
34
  )
35
35
 
36
36
  def run(self, args: Namespace) -> None:
37
- assert args.config["token"]
38
- api = ApiClient(
39
- access_token=args.config["token"]["access_token"],
40
- user_agent=args.config["user_agent"],
41
- )
37
+ api = get_api(args)
42
38
  params = dict(x.split("=", 1) for x in args.param)
43
39
  try:
44
40
  result = api.request(args.method, args.endpoint, params=params)
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
5
5
 
6
6
  from ..api import ApiClient, ClientError
7
7
  from ..constants import INVALID_ISO8601_FORMAT
8
- from ..main import BaseOperation
8
+ from ..main import BaseOperation, get_api
9
9
  from ..main import Namespace as BaseNamespace
10
10
  from ..types import ApiListResponse
11
11
  from ..utils import print_err, truncate_string
@@ -51,11 +51,7 @@ class Operation(BaseOperation):
51
51
  return rv
52
52
 
53
53
  def run(self, args: Namespace) -> None:
54
- assert args.config["token"]
55
- api = ApiClient(
56
- access_token=args.config["token"]["access_token"],
57
- user_agent=args.config["user_agent"],
58
- )
54
+ api = get_api(args)
59
55
  negotiations = self._get_active_negotiations(api)
60
56
  print("Всего активных:", len(negotiations))
61
57
  for item in negotiations:
@@ -5,7 +5,7 @@ import logging
5
5
  from prettytable import PrettyTable
6
6
 
7
7
  from ..api import ApiClient
8
- from ..main import BaseOperation
8
+ from ..main import BaseOperation, get_api
9
9
  from ..main import Namespace as BaseNamespace
10
10
  from ..types import ApiListResponse
11
11
  from ..utils import truncate_string
@@ -24,11 +24,7 @@ class Operation(BaseOperation):
24
24
  pass
25
25
 
26
26
  def run(self, args: Namespace) -> None:
27
- assert args.config["token"]
28
- api = ApiClient(
29
- access_token=args.config["token"]["access_token"],
30
- user_agent=args.config["user_agent"],
31
- )
27
+ api = get_api(args)
32
28
  resumes: ApiListResponse = api.get("/resumes/mine")
33
29
  t = PrettyTable(
34
30
  field_names=["ID", "Название", "Статус"], align="l", valign="t"
@@ -3,7 +3,7 @@ import argparse
3
3
  import logging
4
4
 
5
5
  from ..api import ApiClient, ApiError
6
- from ..main import BaseOperation
6
+ from ..main import BaseOperation, get_api
7
7
  from ..main import Namespace as BaseNamespace
8
8
  from ..types import ApiListResponse
9
9
  from ..utils import print_err, truncate_string
@@ -22,11 +22,7 @@ class Operation(BaseOperation):
22
22
  pass
23
23
 
24
24
  def run(self, args: Namespace) -> None:
25
- assert args.config["token"]
26
- api = ApiClient(
27
- access_token=args.config["token"]["access_token"],
28
- user_agent=args.config["user_agent"],
29
- )
25
+ api = get_api(args)
30
26
  resumes: ApiListResponse = api.get("/resumes/mine")
31
27
  for resume in resumes["items"]:
32
28
  try:
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
 
6
6
  from ..api import ApiClient
7
- from ..main import BaseOperation
7
+ from ..main import BaseOperation, get_api
8
8
  from ..main import Namespace as BaseNamespace
9
9
 
10
10
  logger = logging.getLogger(__package__)
@@ -21,10 +21,6 @@ class Operation(BaseOperation):
21
21
  pass
22
22
 
23
23
  def run(self, args: Namespace) -> None:
24
- assert args.config["token"]
25
- api = ApiClient(
26
- access_token=args.config["token"]["access_token"],
27
- user_agent=args.config["user_agent"],
28
- )
24
+ api = get_api(args)
29
25
  result = api.get("/me")
30
26
  print(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True))
@@ -51,8 +51,6 @@ class TelemetryClient:
51
51
  :raises TelemetryError: Если произошла ошибка при отправке или декодировании JSON.
52
52
  """
53
53
  url = urljoin(self.server_address, endpoint)
54
- logger.debug(data)
55
-
56
54
  try:
57
55
  response = self.session.post(url, json=data)
58
56
  # response.raise_for_status()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -19,8 +19,6 @@ Description-Content-Type: text/markdown
19
19
 
20
20
  ## HH Applicant Tool
21
21
 
22
- > ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
23
-
24
22
  ![Publish to PyPI](https://github.com/s3rgeym/hh-applicant-tool/actions/workflows/publish.yml/badge.svg)
25
23
  [![PyPi Version](https://img.shields.io/pypi/v/hh-applicant-tool)]()
26
24
  [![Python Versions](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg)]()
@@ -37,7 +35,7 @@ Description-Content-Type: text/markdown
37
35
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
38
36
  asdf/pyenv/conda и что-то еще...
39
37
 
40
- Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (я не знаю, где винда что хранит) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
38
+ Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (`C:\Users\%username%\AppData\Roaming\hh-applicant-tool\config.json` в Windows) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
41
39
 
42
40
  Пример работы:
43
41
 
@@ -67,9 +65,34 @@ $ pipx install 'hh-applicant-tool[qt]'
67
65
  $ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
68
66
 
69
67
  # Для обновления до новой версии
70
- $ pipx upgrade 'hh-applicant-tool'
68
+ $ pipx upgrade hh-applicant-tool
71
69
  ```
72
70
 
71
+ Отдельно я распишу процесс установки в **Windows** в подробностях:
72
+
73
+ * Для начала поставьте последнюю версию **Python 3** любым удобным способом.
74
+ * Запустите **Terminal** или **PowerShell** от Администратора и выполните:
75
+ ```ps
76
+ Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
77
+ ```
78
+ Данная политика разрешает текущему пользователю (от которого зашли) запускать скрипты. Без нее не будут работать виртуальные окружения.
79
+ * Создайте и активируйте виртуальное окружение:
80
+ ```ps
81
+ PS> python -m pip venv hh-applicant-venv
82
+ PS> .\hh-applicant-venv\Scripts\activate
83
+ ```
84
+ * Поставьте все пакеты в виртуальное окружение `hh-applicant-venv`:
85
+ ```ps
86
+ (hh-applicant-venv) PS> pip install hh-applicant-tool[qt]
87
+ ```
88
+ * Проверьте работает ли оно:
89
+ ```ps
90
+ (hh-applicant-venv) PS> hh-applicant-tool -h
91
+ ```
92
+ * В случае неудачи вернитесь к первому шагу.
93
+ * Для последующих запусков сначала активируйте виртуальное окружение.
94
+
95
+
73
96
  Использование:
74
97
 
75
98
  ```bash
@@ -133,10 +156,11 @@ https://hh.ru/employer/1918903
133
156
  | **whoami** | Выводит информацию об авторизованном пользователе |
134
157
  | **list-resumes** | Список резюме |
135
158
  | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
136
- | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
159
+ | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками. |
137
160
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
138
- | **call-api** | Вызов произвольного метода API с выводом результата. |
161
+ | **call-api** | Вызов произвольного метода API с выводом результата. |
139
162
  | **refresh-token** | Обновляет access_token. |
163
+ | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, сайта фирмы и могут быть удалены по просьбе упал_намоченный лицо. Данная функция готова и будет доступна после 100 ⭐ |
140
164
 
141
165
  Авторизуемся:
142
166
 
@@ -146,6 +170,10 @@ $ hh-applicant-tool -vv authorize
146
170
 
147
171
  ![image](https://github.com/user-attachments/assets/88961e31-4ea3-478f-8c43-914d6785bc3b)
148
172
 
173
+ > В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
174
+
175
+
176
+
149
177
  В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
150
178
 
151
179
  ```json
@@ -233,7 +261,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
233
261
 
234
262
  Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
235
263
 
236
- Утилита собирает и передает на сервер разработчика следующую ифнормацию:
264
+ Утилита собирает и передает на сервер разработчика следующую информацию:
237
265
 
238
266
  1. Название вакансии.
239
267
  1. Тип вакансии (открытая/закрытая).
@@ -242,7 +270,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
242
270
  1. Прямая ссылка на вакансию.
243
271
  1. Дата создания вакансии.
244
272
  1. Дата публикации вакансии.
245
- 1. Контактная информация хрюши (ее телефон, email и тп).
273
+ 1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, хранящаеся в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля (может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и росписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова).
246
274
  1. Название компании.
247
275
  1. Тип компании.
248
276
  1. Описание компании.
@@ -0,0 +1,24 @@
1
+ hh_applicant_tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ hh_applicant_tool/__main__.py,sha256=cwKJAAML0RRKT9Qbzcwf07HHcuSd8oh7kx4P1apndWQ,84
3
+ hh_applicant_tool/api/__init__.py,sha256=kgFSHibRaAugN2BA3U1djEa20qgKJUUVouwJzjEB0DU,84
4
+ hh_applicant_tool/api/client.py,sha256=c0XBEQIS-kPi2JeS9TmgcO8ZyOjV6HsgiwZRcKUOQCI,7927
5
+ hh_applicant_tool/api/errors.py,sha256=0SoWKnsSUA0K2YgQA8GwGhe-pRMwtfK94MR6_MgbORQ,1722
6
+ hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3s,823
7
+ hh_applicant_tool/constants.py,sha256=lpgKkP2chWgTnBXvzxbSPWpKcfzp8VxMTlkssFcQhH4,469
8
+ hh_applicant_tool/main.py,sha256=sL9eSWUkOz-NJbkq8PxRluvXUh5AqLVi7qmrdN1YAKY,3996
9
+ hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ hh_applicant_tool/operations/apply_similar.py,sha256=fNiQlxjdRzaG2w-mWhZoaCo8OyAp71NM3PJamyTGyi8,12726
11
+ hh_applicant_tool/operations/authorize.py,sha256=TyUTCSOGwSYVJMEd5vSI981LRRI-RZf8hnlVYhtRVwA,3184
12
+ hh_applicant_tool/operations/call_api.py,sha256=qwPDrWP9tiqHkaYYWYBZtymj9AaxObB86Eny-Bf5q_c,1314
13
+ hh_applicant_tool/operations/clear_negotiations.py,sha256=98Yuw9xP4dN5sUnUKlZxqfhU40TQ-aCjK4vj4LRTeYo,3894
14
+ hh_applicant_tool/operations/list_resumes.py,sha256=XBrVFTnl45BUtuvjVm70h_CXZrOvAULnLkLkyUh7gxw,1134
15
+ hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
16
+ hh_applicant_tool/operations/update_resumes.py,sha256=gGxMYMoT9GqJjwn4AgrOAEJCZu4sdhaV0VmPr3jG-q8,1049
17
+ hh_applicant_tool/operations/whoami.py,sha256=sg0r7m6oKkpMEmGt4QdtYdS-1gf-1KKdnk32yblbRJs,714
18
+ hh_applicant_tool/telemetry_client.py,sha256=1jgbc8oMfLhbEi2pTA2fF0pKlHSWekHY3oEJCDI8Uas,2268
19
+ hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
20
+ hh_applicant_tool/utils.py,sha256=wXcxs5IinK7PH6ipCCpngw7faUiboEpi9a8j1wDEPKw,2282
21
+ hh_applicant_tool-0.3.6.dist-info/METADATA,sha256=gxwj6cYpX-LDDSgoEQeIGAkeBuxrsSelggU47MOg4hw,18509
22
+ hh_applicant_tool-0.3.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
+ hh_applicant_tool-0.3.6.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
+ hh_applicant_tool-0.3.6.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- hh_applicant_tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- hh_applicant_tool/__main__.py,sha256=cwKJAAML0RRKT9Qbzcwf07HHcuSd8oh7kx4P1apndWQ,84
3
- hh_applicant_tool/api/__init__.py,sha256=kgFSHibRaAugN2BA3U1djEa20qgKJUUVouwJzjEB0DU,84
4
- hh_applicant_tool/api/client.py,sha256=z_YMsd5zL4-1_aIbkEKqm_1m_mZkm3BMxlAQuCoNj2Y,7040
5
- hh_applicant_tool/api/errors.py,sha256=0SoWKnsSUA0K2YgQA8GwGhe-pRMwtfK94MR6_MgbORQ,1722
6
- hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3s,823
7
- hh_applicant_tool/constants.py,sha256=YdNz0CF4swJ9OO5vVpOZ39RP-05xSU0Ghw4Y6BhISoE,468
8
- hh_applicant_tool/main.py,sha256=XLKsd8mpCKuFoP3ESMpgJKdGL3m6tOQ8kKrOzCN-dwA,3075
9
- hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- hh_applicant_tool/operations/apply_similar.py,sha256=V8oLm200m_QCtnjgJ0gY86hBKwec1GDUP8R0Cm5yv_U,10838
11
- hh_applicant_tool/operations/authorize.py,sha256=TyUTCSOGwSYVJMEd5vSI981LRRI-RZf8hnlVYhtRVwA,3184
12
- hh_applicant_tool/operations/call_api.py,sha256=oWAfvy4LwbsQ8HsgI_3en3sMTlu3ZWU7NzpxssrUNSU,1472
13
- hh_applicant_tool/operations/clear_negotiations.py,sha256=5ybdJMUfV9XYxnn2y4zvWdS_ZE8yXAbFJoXOMyqetw8,4041
14
- hh_applicant_tool/operations/list_resumes.py,sha256=HYxjDALrWl_YBIitDldhhEs-WO_0e_2FGw586MSdsuE,1281
15
- hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
16
- hh_applicant_tool/operations/update_resumes.py,sha256=FKtwEL7i0vaOxLEn1nARi72SuMYB5VjN6Fihe6rlt-Y,1196
17
- hh_applicant_tool/operations/whoami.py,sha256=kdLQ_FjYzpJPKxFlocxf7vgXhW1zocb0bd5IAWmsQuA,861
18
- hh_applicant_tool/telemetry_client.py,sha256=TlsNKlclPyJqLPO0xHkHKBIhT8bmgx1ZBup4PjE8w5E,2296
19
- hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
20
- hh_applicant_tool/utils.py,sha256=wXcxs5IinK7PH6ipCCpngw7faUiboEpi9a8j1wDEPKw,2282
21
- hh_applicant_tool-0.3.4.dist-info/METADATA,sha256=0z_iJf0g4i9SGWTLmNd9zlOEGrWV5JonQwoyviXhykM,15881
22
- hh_applicant_tool-0.3.4.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
- hh_applicant_tool-0.3.4.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
- hh_applicant_tool-0.3.4.dist-info/RECORD,,