hh-applicant-tool 0.1.9__tar.gz → 0.2.1__tar.gz

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.

Files changed (23) hide show
  1. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/PKG-INFO +13 -12
  2. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/README.md +12 -11
  3. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/api/client.py +14 -8
  4. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/api/errors.py +7 -3
  5. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/constants.py +2 -2
  6. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/apply_similar.py +5 -4
  7. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/call_api.py +11 -5
  8. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/clear_negotiations.py +2 -3
  9. hh_applicant_tool-0.2.1/hh_applicant_tool/operations/refresh_token.py +42 -0
  10. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/update_resumes.py +2 -2
  11. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/whoami.py +2 -2
  12. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/utils.py +4 -9
  13. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/pyproject.toml +1 -1
  14. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/__init__.py +0 -0
  15. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/__main__.py +0 -0
  16. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/api/__init__.py +0 -0
  17. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/color_log.py +0 -0
  18. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/main.py +0 -0
  19. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/__init__.py +0 -0
  20. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/add_handler.py +0 -0
  21. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/authorize.py +0 -0
  22. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/operations/list_resumes.py +0 -0
  23. {hh_applicant_tool-0.1.9 → hh_applicant_tool-0.2.1}/hh_applicant_tool/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.1.9
3
+ Version: 0.2.1
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -110,16 +110,17 @@ https://hh.ru/employer/1918903
110
110
  - `-v` используется для вывода отладочной информации. Два таких флага, например, выводят запросы к **API**.
111
111
  - `-c <path>` можно создать путь до конфига. С помощью этого флага можно одновременно использовать несколько профилей.
112
112
 
113
- | Операция | Описание |
114
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
115
- | **add-handler** | Добавляет обработчик протокола `hhandroid` |
116
- | **authorize** | Открывает сайт hh.ru для авторизации и перехватывает перенаправление на `hhadnroid://oauthresponse` |
117
- | **whoami** | Выводит информацию об авторизованном пользователе |
118
- | **list-resumes** | Список резюме |
119
- | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
120
- | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
121
- | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
122
- | **call-api** | Вызвать произвольный метод API м вывести результат. Я эту команду придумал чтобы собирать ссылки на сайты и загонять их в сканер уязвимостей. |
113
+ | Операция | Описание |
114
+ | ---------------------- | --------------------------------------------------------------------------------------------------- |
115
+ | **add-handler** | Добавляет обработчик протокола `hhandroid` |
116
+ | **authorize** | Открывает сайт hh.ru для авторизации и перехватывает перенаправление на `hhadnroid://oauthresponse` |
117
+ | **whoami** | Выводит информацию об авторизованном пользователе |
118
+ | **list-resumes** | Список резюме |
119
+ | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
120
+ | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
121
+ | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
122
+ | **call-api** | Вызов произвольного метода API с вводом результата. |
123
+ | **refresh-token** | Обновляет access_token. |
123
124
 
124
125
  Для начала нужно добавить обработчик протокола `hhandroid`, который используется Android-приложением для усложнения жизни честным автоматизаторам:
125
126
 
@@ -223,7 +224,7 @@ datetime.datetime(2023, 3, 23, 6, 36, 15, 596290)
223
224
  >>>
224
225
  ```
225
226
 
226
- После нужно авторизоваться по новой.
227
+ После нужно вызвать `refresh-token`.
227
228
 
228
229
  ![](https://user-images.githubusercontent.com/12753171/222870516-b29f2417-d11a-4122-8291-7d440a422a31.png)
229
230
 
@@ -96,16 +96,17 @@ https://hh.ru/employer/1918903
96
96
  - `-v` используется для вывода отладочной информации. Два таких флага, например, выводят запросы к **API**.
97
97
  - `-c <path>` можно создать путь до конфига. С помощью этого флага можно одновременно использовать несколько профилей.
98
98
 
99
- | Операция | Описание |
100
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
101
- | **add-handler** | Добавляет обработчик протокола `hhandroid` |
102
- | **authorize** | Открывает сайт hh.ru для авторизации и перехватывает перенаправление на `hhadnroid://oauthresponse` |
103
- | **whoami** | Выводит информацию об авторизованном пользователе |
104
- | **list-resumes** | Список резюме |
105
- | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
106
- | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
107
- | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
108
- | **call-api** | Вызвать произвольный метод API м вывести результат. Я эту команду придумал чтобы собирать ссылки на сайты и загонять их в сканер уязвимостей. |
99
+ | Операция | Описание |
100
+ | ---------------------- | --------------------------------------------------------------------------------------------------- |
101
+ | **add-handler** | Добавляет обработчик протокола `hhandroid` |
102
+ | **authorize** | Открывает сайт hh.ru для авторизации и перехватывает перенаправление на `hhadnroid://oauthresponse` |
103
+ | **whoami** | Выводит информацию об авторизованном пользователе |
104
+ | **list-resumes** | Список резюме |
105
+ | **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
106
+ | **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
107
+ | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
108
+ | **call-api** | Вызов произвольного метода API с вводом результата. |
109
+ | **refresh-token** | Обновляет access_token. |
109
110
 
110
111
  Для начала нужно добавить обработчик протокола `hhandroid`, который используется Android-приложением для усложнения жизни честным автоматизаторам:
111
112
 
@@ -209,7 +210,7 @@ datetime.datetime(2023, 3, 23, 6, 36, 15, 596290)
209
210
  >>>
210
211
  ```
211
212
 
212
- После нужно авторизоваться по новой.
213
+ После нужно вызвать `refresh-token`.
213
214
 
214
215
  ![](https://user-images.githubusercontent.com/12753171/222870516-b29f2417-d11a-4122-8291-7d440a422a31.png)
215
216
 
@@ -14,8 +14,8 @@ import requests
14
14
  from requests import Response, Session
15
15
 
16
16
  from ..constants import (
17
- HHANDROID_CLIENT_ID,
18
- HHANDROID_CLIENT_SECRET,
17
+ ANDROID_CLIENT_ID,
18
+ ANDROID_CLIENT_SECRET,
19
19
  DEFAULT_USER_AGENT,
20
20
  )
21
21
  from ..types import AccessToken
@@ -99,11 +99,12 @@ class BaseClient:
99
99
  "%d %-6s %s",
100
100
  response.status_code,
101
101
  method,
102
- url + (
102
+ url
103
+ + (
103
104
  "?" + urlencode(params)
104
105
  if not has_body and params
105
106
  else ""
106
- )
107
+ ),
107
108
  )
108
109
  self.previous_request_time = time.monotonic()
109
110
  self.raise_for_status(response, rv)
@@ -143,8 +144,8 @@ class BaseClient:
143
144
 
144
145
  @dataclass
145
146
  class OAuthClient(BaseClient):
146
- client_id: str = HHANDROID_CLIENT_ID
147
- client_secret: str = HHANDROID_CLIENT_SECRET
147
+ client_id: str = ANDROID_CLIENT_ID
148
+ client_secret: str = ANDROID_CLIENT_SECRET
148
149
  _: dataclasses.KW_ONLY
149
150
  base_url: str = "https://hh.ru/oauth"
150
151
  state: str = ""
@@ -181,7 +182,8 @@ class OAuthClient(BaseClient):
181
182
 
182
183
  @dataclass
183
184
  class ApiClient(BaseClient):
184
- access_token: str
185
+ # Например, для просмотра информации о компании токен не нужен
186
+ access_token: str | None = None
185
187
  refresh_token: str | None = None
186
188
  _: dataclasses.KW_ONLY
187
189
  base_url: str = "https://api.hh.ru/"
@@ -196,7 +198,11 @@ class ApiClient(BaseClient):
196
198
  def additional_headers(
197
199
  self,
198
200
  ) -> dict[str, str]:
199
- return {"Authorization": f"Bearer {self.access_token}"}
201
+ return (
202
+ {"Authorization": f"Bearer {self.access_token}"}
203
+ if self.access_token
204
+ else {}
205
+ )
200
206
 
201
207
  # def refresh_access(self) -> AccessToken:
202
208
  # tok = self.oauth_client.refresh_access(self.refresh_token)
@@ -1,7 +1,7 @@
1
- #from copy import deepcopy
1
+ # from copy import deepcopy
2
2
  from typing import Any
3
3
 
4
- from requests import Response, Request
4
+ from requests import Request, Response
5
5
  from requests.adapters import CaseInsensitiveDict
6
6
 
7
7
  __all__ = (
@@ -20,7 +20,11 @@ class ApiError(Exception):
20
20
  def __init__(self, response: Response, data: dict[str, Any]) -> None:
21
21
  self._response = response
22
22
  self._raw = data
23
-
23
+
24
+ @property
25
+ def data(self) -> dict:
26
+ return self._raw
27
+
24
28
  @property
25
29
  def request(self) -> Request:
26
30
  return self._response.request
@@ -1,8 +1,8 @@
1
1
  DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
2
- HHANDROID_CLIENT_ID = (
2
+ ANDROID_CLIENT_ID = (
3
3
  "HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
4
4
  )
5
- HHANDROID_CLIENT_SECRET = (
5
+ ANDROID_CLIENT_SECRET = (
6
6
  "V9M870DE342BGHFRUJ5FTCGCUA1482AN0DI8C5TFI9ULMA89H10N60NOP8I4JMVS"
7
7
  )
8
8
  HHANDROID_SOCKET_PATH = "/tmp/hhandroid.sock"
@@ -8,7 +8,7 @@ from ..api import ApiClient, ApiError, BadRequest
8
8
  from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
10
  from ..types import ApiListResponse, VacancyItem
11
- from ..utils import truncate_string
11
+ from ..utils import print_err, truncate_string
12
12
 
13
13
  logger = logging.getLogger(__package__)
14
14
 
@@ -74,8 +74,9 @@ class Operation(BaseOperation):
74
74
  page=page,
75
75
  per_page=per_page,
76
76
  # Мне кажется, что так поисковая выдача можно забиться неадекватами, которые по полгода кого-то ищут
77
- # order_by="relevance",
78
- order_by="publication_time",
77
+ # Но так откликается на что-то уж совсем нерелевантное
78
+ # order_by="publication_time",
79
+ order_by="relevance",
79
80
  )
80
81
  rv.extend(res["items"])
81
82
  if page >= res["pages"] - 1:
@@ -117,7 +118,7 @@ class Operation(BaseOperation):
117
118
  ")",
118
119
  )
119
120
  except ApiError as ex:
120
- logger.warning(ex)
121
+ print_err("❗ Ошибка:", ex)
121
122
  if isinstance(ex, BadRequest) and ex.limit_exceeded:
122
123
  break
123
124
  print("📝 Отклики на вакансии разосланы!")
@@ -1,11 +1,12 @@
1
1
  # Этот модуль можно использовать как образец для других
2
2
  import argparse
3
+ import json
3
4
  import logging
5
+ import sys
4
6
 
5
- from ..api import ApiClient
7
+ from ..api import ApiClient, ApiError
6
8
  from ..main import BaseOperation
7
9
  from ..main import Namespace as BaseNamespace
8
- from ..utils import dumps
9
10
 
10
11
  logger = logging.getLogger(__package__)
11
12
 
@@ -14,6 +15,7 @@ class Namespace(BaseNamespace):
14
15
  method: str
15
16
  endpoint: str
16
17
  params: list[str]
18
+ pretty_print: bool
17
19
 
18
20
 
19
21
  class Operation(BaseOperation):
@@ -24,7 +26,7 @@ class Operation(BaseOperation):
24
26
  parser.add_argument(
25
27
  "param",
26
28
  nargs="*",
27
- help="PARAM=VALUE. Значения можно оборачивать в кавычки.",
29
+ help="PARAM=VALUE",
28
30
  default=[],
29
31
  )
30
32
  parser.add_argument(
@@ -38,5 +40,9 @@ class Operation(BaseOperation):
38
40
  user_agent=args.config["user_agent"],
39
41
  )
40
42
  params = dict(x.split("=", 1) for x in args.param)
41
- result = api.request(args.method, args.endpoint, params=params)
42
- print(dumps(result))
43
+ try:
44
+ result = api.request(args.method, args.endpoint, params=params)
45
+ print(json.dumps(result, ensure_ascii=True))
46
+ except ApiError as ex:
47
+ json.dump(ex.data, sys.stderr, ensure_ascii=True)
48
+ return 1
@@ -8,7 +8,7 @@ from ..constants import INVALID_ISO8601_FORMAT
8
8
  from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
10
  from ..types import ApiListResponse
11
- from ..utils import truncate_string
11
+ from ..utils import print_err, truncate_string
12
12
 
13
13
  logger = logging.getLogger(__package__)
14
14
 
@@ -100,6 +100,5 @@ class Operation(BaseOperation):
100
100
  ")",
101
101
  )
102
102
  except ClientError as ex:
103
- logger.warning(ex)
104
-
103
+ print_err("❗ Ошибка:", ex)
105
104
  print("🧹 Чистка заявок завершена!")
@@ -0,0 +1,42 @@
1
+ # Этот модуль можно использовать как образец для других
2
+ import argparse
3
+ import logging
4
+
5
+ from ..api import ApiError, OAuthClient
6
+ from ..main import BaseOperation
7
+ from ..main import Namespace as BaseNamespace
8
+ from ..utils import print_err
9
+
10
+ logger = logging.getLogger(__package__)
11
+
12
+
13
+ class Namespace(BaseNamespace):
14
+ pass
15
+
16
+
17
+ class Operation(BaseOperation):
18
+ """Получает новый access_token."""
19
+
20
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
21
+ pass
22
+
23
+ def run(self, args: Namespace) -> None:
24
+ if (
25
+ not args.config["token"]
26
+ or not args.config["token"]["refresh_token"]
27
+ ):
28
+ print_err("❗ Необходим refresh_token!")
29
+ return 1
30
+ try:
31
+ oauth = OAuthClient(
32
+ user_agent=(
33
+ args.config["oauth_user_agent"]
34
+ or args.config["user_agent"]
35
+ ),
36
+ )
37
+ token = oauth.refresh_access(args.config["token"]["refresh_token"])
38
+ args.config.save(token=token)
39
+ print("✅ Токен обновлен!")
40
+ except ApiError as ex:
41
+ print_err("❗ Ошибка:", ex)
42
+ return 1
@@ -6,7 +6,7 @@ from ..api import ApiClient, ApiError
6
6
  from ..main import BaseOperation
7
7
  from ..main import Namespace as BaseNamespace
8
8
  from ..types import ApiListResponse
9
- from ..utils import truncate_string
9
+ from ..utils import print_err, truncate_string
10
10
 
11
11
  logger = logging.getLogger(__package__)
12
12
 
@@ -34,4 +34,4 @@ class Operation(BaseOperation):
34
34
  assert res == {}
35
35
  print("✅ Обновлено", truncate_string(resume["title"]))
36
36
  except ApiError as ex:
37
- logger.warning(ex)
37
+ print_err("❗ Ошибка:", ex)
@@ -1,11 +1,11 @@
1
1
  # Этот модуль можно использовать как образец для других
2
2
  import argparse
3
+ import json
3
4
  import logging
4
5
 
5
6
  from ..api import ApiClient
6
7
  from ..main import BaseOperation
7
8
  from ..main import Namespace as BaseNamespace
8
- from ..utils import dumps
9
9
 
10
10
  logger = logging.getLogger(__package__)
11
11
 
@@ -27,4 +27,4 @@ class Operation(BaseOperation):
27
27
  user_agent=args.config["user_agent"],
28
28
  )
29
29
  result = api.get("/me")
30
- print(dumps(result))
30
+ print(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True))
@@ -9,13 +9,6 @@ from typing import Any
9
9
 
10
10
  print_err = partial(print, file=sys.stderr)
11
11
 
12
- json_dump_kwargs = dict(
13
- indent=2, ensure_ascii=False, sort_keys=True, default=str
14
- )
15
-
16
- dump = partial(json.dump, **json_dump_kwargs)
17
- dumps = partial(json.dumps, **json_dump_kwargs)
18
-
19
12
 
20
13
  class AttrDict(dict):
21
14
  __getattr__ = dict.get
@@ -43,8 +36,10 @@ class Config(dict):
43
36
  self.update(*args, **kwargs)
44
37
  self._config_path.parent.mkdir(exist_ok=True, parents=True)
45
38
  with self._lock:
46
- with self._config_path.open("w+") as f:
47
- dump(self, f)
39
+ with self._config_path.open("w+") as fp:
40
+ json.dump(
41
+ self, fp, ensure_ascii=True, indent=2, sort_keys=True
42
+ )
48
43
 
49
44
  __getitem__ = dict.get
50
45
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hh-applicant-tool"
3
- version = "0.1.9"
3
+ version = "0.2.1"
4
4
  description = ""
5
5
  authors = ["Senior YAML Developer <yamldeveloper@proton.me>"]
6
6
  readme = "README.md"