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

@@ -1,6 +1,5 @@
1
1
  import argparse
2
2
  import logging
3
- import time
4
3
  from urllib.parse import parse_qs, urlsplit
5
4
  import sys
6
5
  from typing import Any
@@ -35,9 +34,8 @@ except ImportError:
35
34
  pass
36
35
 
37
36
 
38
- from ..api import OAuthClient
39
- from ..main import BaseOperation, Namespace
40
- from ..utils import Config
37
+ from ..api import ApiClient # noqa: E402
38
+ from ..main import BaseOperation, Namespace # noqa: E402
41
39
 
42
40
  logger = logging.getLogger(__package__)
43
41
 
@@ -54,10 +52,9 @@ class HHAndroidUrlSchemeHandler(QWebEngineUrlSchemeHandler):
54
52
 
55
53
 
56
54
  class WebViewWindow(QMainWindow):
57
- def __init__(self, url: str, oauth_client: OAuthClient, config: Config) -> None:
55
+ def __init__(self, api_client: ApiClient) -> None:
58
56
  super().__init__()
59
- self.oauth_client = oauth_client
60
- self.config = config
57
+ self.api_client = api_client
61
58
  # Настройка WebEngineView
62
59
  self.web_view = QWebEngineView()
63
60
  self.setCentralWidget(self.web_view)
@@ -68,16 +65,15 @@ class WebViewWindow(QMainWindow):
68
65
  profile.installUrlSchemeHandler(b"hhandroid", self.hhandroid_handler)
69
66
  # Настройки окна для мобильного вида
70
67
  self.resize(480, 800)
71
- self.web_view.setUrl(QUrl(url))
68
+ self.web_view.setUrl(QUrl(api_client.oauth_client.authorize_url))
72
69
 
73
70
  def handle_redirect_uri(self, redirect_uri: str) -> None:
74
71
  logger.debug(f"handle redirect uri: {redirect_uri}")
75
72
  sp = urlsplit(redirect_uri)
76
73
  code = parse_qs(sp.query).get("code", [None])[0]
77
74
  if code:
78
- token = self.oauth_client.authenticate(code)
79
- logger.debug("Сохраняем токен")
80
- self.config.save(token=dict(token, created_at=int(time.time())))
75
+ token = self.api_client.oauth_client.authenticate(code)
76
+ self.api_client.handle_access_token(token)
81
77
  print("🔓 Авторизация прошла успешно!")
82
78
  self.close()
83
79
 
@@ -88,21 +84,15 @@ class Operation(BaseOperation):
88
84
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
89
85
  pass
90
86
 
91
- def run(self, args: Namespace) -> None:
87
+ def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
92
88
  if not QT_IMPORTED:
93
89
  print_err(
94
90
  "❗Критиническая Ошибка: PyQt6 не был импортирован, возможно, вы долбоеб и забыли его установить, либо же криворукие разрабы этой либы опять все сломали..."
95
91
  )
96
92
  sys.exit(1)
97
93
 
98
- oauth = OAuthClient(
99
- user_agent=(args.config["oauth_user_agent"] or args.config["user_agent"]),
100
- )
101
-
102
94
  app = QApplication(sys.argv)
103
- window = WebViewWindow(
104
- oauth.authorize_url, oauth_client=oauth, config=args.config
105
- )
95
+ window = WebViewWindow(api_client=api_client)
106
96
  window.show()
107
97
 
108
98
  app.exec()
@@ -4,8 +4,8 @@ import json
4
4
  import logging
5
5
  import sys
6
6
 
7
- from ..api import ApiError
8
- from ..main import BaseOperation, get_api
7
+ from ..api import ApiError, ApiClient
8
+ from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
10
 
11
11
  logger = logging.getLogger(__package__)
@@ -22,23 +22,22 @@ class Operation(BaseOperation):
22
22
  """Вызвать произвольный метод API <https://github.com/hhru/api>."""
23
23
 
24
24
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
25
- parser.add_argument("endpoint")
25
+ parser.add_argument("endpoint", help="Путь до эндпоинта API")
26
26
  parser.add_argument(
27
27
  "param",
28
28
  nargs="*",
29
- help="PARAM=VALUE",
29
+ help="Параметры указываются в виде PARAM=VALUE",
30
30
  default=[],
31
31
  )
32
32
  parser.add_argument(
33
33
  "-m", "--method", "--meth", default="GET", help="HTTP Метод"
34
34
  )
35
35
 
36
- def run(self, args: Namespace) -> None:
37
- api = get_api(args)
36
+ def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
38
37
  params = dict(x.split("=", 1) for x in args.param)
39
38
  try:
40
- result = api.request(args.method, args.endpoint, params=params)
41
- print(json.dumps(result, ensure_ascii=True))
39
+ result = api_client.request(args.method, args.endpoint, params=params)
40
+ print(json.dumps(result, ensure_ascii=False))
42
41
  except ApiError as ex:
43
- json.dump(ex.data, sys.stderr, ensure_ascii=True)
42
+ json.dump(ex.data, sys.stderr, ensure_ascii=False)
44
43
  return 1
@@ -7,7 +7,6 @@ from ..api import ApiClient, ClientError
7
7
  from ..constants import INVALID_ISO8601_FORMAT
8
8
  from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
- from ..main import get_api
11
10
  from ..types import ApiListResponse
12
11
  from ..utils import print_err, truncate_string
13
12
 
@@ -44,12 +43,12 @@ class Operation(BaseOperation):
44
43
  action=argparse.BooleanOptionalAction,
45
44
  )
46
45
 
47
- def _get_active_negotiations(self, api: ApiClient) -> list[dict]:
46
+ def _get_active_negotiations(self, api_client: ApiClient) -> list[dict]:
48
47
  rv = []
49
48
  page = 0
50
49
  per_page = 100
51
50
  while True:
52
- r: ApiListResponse = api.get(
51
+ r: ApiListResponse = api_client.get(
53
52
  "/negotiations", page=page, per_page=per_page, status="active"
54
53
  )
55
54
  rv.extend(r["items"])
@@ -58,9 +57,8 @@ class Operation(BaseOperation):
58
57
  break
59
58
  return rv
60
59
 
61
- def run(self, args: Namespace) -> None:
62
- api = get_api(args)
63
- negotiations = self._get_active_negotiations(api)
60
+ def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
61
+ negotiations = self._get_active_negotiations(api_client)
64
62
  print("Всего активных:", len(negotiations))
65
63
  for item in negotiations:
66
64
  state = item["state"]
@@ -69,20 +67,18 @@ class Operation(BaseOperation):
69
67
  # hidden True
70
68
  is_discard = state["id"] == "discard"
71
69
  if not item["hidden"] and (
72
- args.all
70
+ args.all
73
71
  or is_discard
74
72
  or (
75
73
  state["id"] == "response"
76
- and (
77
- datetime.utcnow() - timedelta(days=args.older_than)
78
- ).replace(tzinfo=timezone.utc)
79
- > datetime.strptime(
80
- item["updated_at"], INVALID_ISO8601_FORMAT
74
+ and (datetime.utcnow() - timedelta(days=args.older_than)).replace(
75
+ tzinfo=timezone.utc
81
76
  )
77
+ > datetime.strptime(item["updated_at"], INVALID_ISO8601_FORMAT)
82
78
  )
83
79
  ):
84
80
  decline_allowed = item.get("decline_allowed") or False
85
- r = api.delete(
81
+ r = api_client.delete(
86
82
  f"/negotiations/active/{item['id']}",
87
83
  with_decline_message=decline_allowed,
88
84
  )
@@ -99,7 +95,7 @@ class Operation(BaseOperation):
99
95
  if is_discard and args.blacklist_discard:
100
96
  employer = vacancy["employer"]
101
97
  try:
102
- r = api.put(f"/employers/blacklisted/{employer['id']}")
98
+ r = api_client.put(f"/employers/blacklisted/{employer['id']}")
103
99
  assert not r
104
100
  print(
105
101
  "🚫 Заблокировали",
@@ -2,6 +2,7 @@ import argparse
2
2
  import logging
3
3
  import os
4
4
  import subprocess
5
+ from typing import Any
5
6
 
6
7
  from ..main import BaseOperation
7
8
  from ..main import Namespace as BaseNamespace
@@ -12,25 +13,39 @@ EDITOR = os.getenv("EDITOR", "nano")
12
13
 
13
14
 
14
15
  class Namespace(BaseNamespace):
15
- print: bool
16
+ show_path: bool
17
+ key: str
18
+
19
+
20
+ def get_value(data: dict[str, Any], path: str) -> Any:
21
+ for key in path.split("."):
22
+ if key not in data:
23
+ return None
24
+ data = data[key]
25
+ return data
16
26
 
17
27
 
18
28
  class Operation(BaseOperation):
19
- """Редактировать конфигурационный файл или показать путь до него"""
29
+ """Операции с конфигурационным файлом"""
20
30
 
21
31
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
32
  parser.add_argument(
23
33
  "-p",
24
- "--print",
34
+ "--show-path",
35
+ "--path",
25
36
  type=bool,
26
37
  default=False,
27
38
  action=argparse.BooleanOptionalAction,
28
- help="Напечатать путь и выйти",
39
+ help="Вывести полный путь к конфигу",
29
40
  )
41
+ parser.add_argument("-k", "--key", help="Вывести отдельное значение из конфига")
30
42
 
31
- def run(self, args: Namespace) -> None:
43
+ def run(self, args: Namespace, *_) -> None:
44
+ if args.key:
45
+ print(get_value(args.config, args.key))
46
+ return
32
47
  config_path = str(args.config._config_path)
33
- if args.print:
48
+ if args.show_path:
34
49
  print(config_path)
35
50
  else:
36
51
  subprocess.call([EDITOR, config_path])
@@ -0,0 +1,30 @@
1
+ # Этот модуль можно использовать как образец для других
2
+ import argparse
3
+ import logging
4
+
5
+ from ..telemetry_client import TelemetryClient, TelemetryError
6
+
7
+ from ..main import BaseOperation
8
+ from ..main import Namespace as BaseNamespace
9
+ from ..utils import print_err
10
+
11
+ logger = logging.getLogger(__package__)
12
+
13
+
14
+ class Namespace(BaseNamespace):
15
+ pass
16
+
17
+
18
+ class Operation(BaseOperation):
19
+ """Удалить всю телеметрию, сохраненную на сервере."""
20
+
21
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
+ pass
23
+
24
+ def run(self, a, b, telemetry_client: TelemetryClient) -> None:
25
+ try:
26
+ telemetry_client.send_telemetry("/delete")
27
+ print("✅ Вся телеметрия, сохраненная на сервере, была успешно удалена!")
28
+ except TelemetryError as ex:
29
+ print_err("❗ Ошибка:", ex)
30
+ return 1
@@ -4,7 +4,6 @@ from os import getenv
4
4
 
5
5
  from ..main import BaseOperation
6
6
  from ..main import Namespace as BaseNamespace
7
- from ..main import get_proxies
8
7
  from ..telemetry_client import TelemetryClient
9
8
 
10
9
  logger = logging.getLogger(__package__)
@@ -48,27 +47,27 @@ class Operation(BaseOperation):
48
47
  help="Номер страницы в выдаче",
49
48
  )
50
49
 
51
- def run(self, args: Namespace) -> None:
52
- proxies = get_proxies(args)
53
- client = TelemetryClient(proxies=proxies)
54
- auth = (
55
- (args.username, args.password)
56
- if args.username and args.password
57
- else None
58
- )
59
- # Аутентификация пользователя
60
- results = client.get_telemetry(
50
+ def run(self, args: Namespace, _, telemetry_client: TelemetryClient) -> None:
51
+ results = telemetry_client.get_telemetry(
61
52
  "/contact/persons",
62
53
  {"search": args.search, "per_page": 10, "page": args.page},
63
- auth=auth,
64
54
  )
55
+ if "contact_persons" not in results:
56
+ print("❌", results)
57
+ return 1
58
+
59
+ print(
60
+ "Тут отображаются только данные, собранные с вашего telemetry_client_id. Вы так же можете их удалить с помощью команды delete-telemetry."
61
+ )
62
+ print()
63
+
65
64
  self._print_contacts(results)
66
65
 
67
66
  def _print_contacts(self, data: dict) -> None:
68
67
  """Вывод всех контактов в древовидной структуре."""
69
68
  page = data["page"]
70
69
  pages = (data["total"] // data["per_page"]) + 1
71
- print(f"📋 Контакты ({page}/{pages}):")
70
+ print(f"Страница {page}/{pages}:")
72
71
  contacts = data.get("contact_persons", [])
73
72
  for idx, contact in enumerate(contacts):
74
73
  is_last_contact = idx == len(contacts) - 1
@@ -78,26 +77,11 @@ class Operation(BaseOperation):
78
77
  def _print_contact(self, contact: dict, is_last_contact: bool) -> None:
79
78
  """Вывод информации о конкретном контакте."""
80
79
  prefix = "└──" if is_last_contact else "├──"
81
- print(f" {prefix} 🧑 {contact.get('name', 'н/д')}")
80
+ print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}")
82
81
  prefix2 = " " if is_last_contact else " │ "
83
82
  print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
84
83
  employer = contact.get("employer") or {}
85
84
  print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
86
85
  print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
87
- print(f"{prefix2}├── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
88
-
89
- phones = contact["phone_numbers"] or [{"phone_number": "(нет номеров)"}]
90
- print(f"{prefix2}├── 📞 Телефоны:")
91
- last_phone = len(phones) - 1
92
- for i, phone in enumerate(phones):
93
- sub_prefix = "└──" if i == last_phone else "├──"
94
- print(f"{prefix2}│ {sub_prefix} {phone['phone_number']}")
95
-
96
- telegrams = contact["telegram_usernames"] or [
97
- {"username": "(нет аккаунтов)"}
98
- ]
99
- print(f"{prefix2}└── 📱 Telegram:")
100
- last_telegram = len(telegrams) - 1
101
- for i, telegram in enumerate(telegrams):
102
- sub_prefix = "└──" if i == last_telegram else "├──"
103
- print(f"{prefix2} {sub_prefix} {telegram['username']}")
86
+ print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
87
+ print(prefix2)
@@ -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, get_api
8
+ from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
10
  from ..types import ApiListResponse
11
11
  from ..utils import truncate_string
@@ -23,12 +23,9 @@ class Operation(BaseOperation):
23
23
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
24
24
  pass
25
25
 
26
- def run(self, args: Namespace) -> None:
27
- api = get_api(args)
28
- resumes: ApiListResponse = api.get("/resumes/mine")
29
- t = PrettyTable(
30
- field_names=["ID", "Название", "Статус"], align="l", valign="t"
31
- )
26
+ def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
27
+ resumes: ApiListResponse = api_client.get("/resumes/mine")
28
+ t = PrettyTable(field_names=["ID", "Название", "Статус"], align="l", valign="t")
32
29
  t.add_rows(
33
30
  [
34
31
  (
@@ -1,8 +1,8 @@
1
1
  # Этот модуль можно использовать как образец для других
2
2
  import argparse
3
3
  import logging
4
-
5
- from ..api import ApiError, OAuthClient
4
+ from typing import Any
5
+ from ..api import ApiError, ApiClient
6
6
  from ..main import BaseOperation
7
7
  from ..main import Namespace as BaseNamespace
8
8
  from ..utils import print_err
@@ -20,22 +20,9 @@ class Operation(BaseOperation):
20
20
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
21
21
  pass
22
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
23
+ def run(self, _, api_client: ApiClient, *args: Any) -> None:
30
24
  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)
25
+ api_client.refresh_access_token()
39
26
  print("✅ Токен обновлен!")
40
27
  except ApiError as ex:
41
28
  print_err("❗ Ошибка:", ex)
@@ -4,10 +4,9 @@ import random
4
4
  import time
5
5
  from typing import Tuple
6
6
 
7
- from ..api import ApiError
7
+ from ..api import ApiError, ApiClient
8
8
  from ..main import BaseOperation
9
9
  from ..main import Namespace as BaseNamespace
10
- from ..main import get_api
11
10
  from ..mixins import GetResumeIdMixin
12
11
  from ..utils import parse_interval, random_text
13
12
 
@@ -67,8 +66,8 @@ class Operation(BaseOperation, GetResumeIdMixin):
67
66
  action=argparse.BooleanOptionalAction,
68
67
  )
69
68
 
70
- def run(self, args: Namespace) -> None:
71
- self.api = get_api(args)
69
+ def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
70
+ self.api_client = api_client
72
71
  self.resume_id = self._get_resume_id()
73
72
  self.reply_min_interval, self.reply_max_interval = args.reply_interval
74
73
  self.reply_message = args.reply_message or args.config["reply_message"]
@@ -80,7 +79,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
80
79
  self._reply_chats()
81
80
 
82
81
  def _reply_chats(self) -> None:
83
- me = self.me = self.api.get("/me")
82
+ me = self.me = self.api_client.get("/me")
84
83
 
85
84
  basic_message_placeholders = {
86
85
  "first_name": me.get("first_name", ""),
@@ -124,7 +123,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
124
123
  last_message: dict | None = None
125
124
  message_history: list[str] = []
126
125
  while True:
127
- messages_res = self.api.get(
126
+ messages_res = self.api_client.get(
128
127
  f"/negotiations/{nid}/messages", page=page
129
128
  )
130
129
 
@@ -160,10 +159,12 @@ class Operation(BaseOperation, GetResumeIdMixin):
160
159
  print("💼", message_placeholders["vacancy_name"])
161
160
  print("📅", vacancy["created_at"])
162
161
  if salary:
163
- salary_from = salary.get("from")or "-"
164
- salary_to = salary.get("to")or "-"
162
+ salary_from = salary.get("from") or "-"
163
+ salary_to = salary.get("to") or "-"
165
164
  salary_currency = salary.get("currency")
166
- print("💵 от", salary_from, "до", salary_to, salary_currency)
165
+ print(
166
+ "💵 от", salary_from, "до", salary_to, salary_currency
167
+ )
167
168
  print("")
168
169
  print("Последние сообщения:")
169
170
  for msg in (
@@ -192,7 +193,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
192
193
  self.reply_max_interval,
193
194
  )
194
195
  )
195
- self.api.post(
196
+ self.api_client.post(
196
197
  f"/negotiations/{nid}/messages",
197
198
  message=message,
198
199
  )
@@ -208,7 +209,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
208
209
  def _get_negotiations(self) -> list[dict]:
209
210
  rv = []
210
211
  for page in range(self.max_pages):
211
- res = self.api.get("/negotiations", page=page, status="active")
212
+ res = self.api_client.get("/negotiations", page=page, status="active")
212
213
  rv.extend(res["items"])
213
214
  if page >= res["pages"] - 1:
214
215
  break
@@ -3,7 +3,7 @@ import argparse
3
3
  import logging
4
4
 
5
5
  from ..api import ApiClient, ApiError
6
- from ..main import BaseOperation, get_api
6
+ from ..main import BaseOperation
7
7
  from ..main import Namespace as BaseNamespace
8
8
  from ..types import ApiListResponse
9
9
  from ..utils import print_err, truncate_string
@@ -21,12 +21,11 @@ class Operation(BaseOperation):
21
21
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
22
22
  pass
23
23
 
24
- def run(self, args: Namespace) -> None:
25
- api = get_api(args)
26
- resumes: ApiListResponse = api.get("/resumes/mine")
24
+ def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
25
+ resumes: ApiListResponse = api_client.get("/resumes/mine")
27
26
  for resume in resumes["items"]:
28
27
  try:
29
- res = api.post(f"/resumes/{resume['id']}/publish")
28
+ res = api_client.post(f"/resumes/{resume['id']}/publish")
30
29
  assert res == {}
31
30
  print("✅ Обновлено", truncate_string(resume["title"]))
32
31
  except ApiError as ex:
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
 
6
6
  from ..api import ApiClient
7
- from ..main import BaseOperation, get_api
7
+ from ..main import BaseOperation
8
8
  from ..main import Namespace as BaseNamespace
9
9
 
10
10
  logger = logging.getLogger(__package__)
@@ -20,7 +20,6 @@ class Operation(BaseOperation):
20
20
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
21
21
  pass
22
22
 
23
- def run(self, args: Namespace) -> None:
24
- api = get_api(args)
25
- result = api.get("/me")
26
- print(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True))
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))
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import logging
3
5
  import os
@@ -6,9 +8,10 @@ import warnings
6
8
  from functools import partialmethod
7
9
  from typing import Any, Dict, Optional
8
10
  from urllib.parse import urljoin
9
-
10
11
  import requests
12
+ from .utils import Config
11
13
 
14
+ # Сертификат на сервере давно истек, но его обновлять мне лень...
12
15
  warnings.filterwarnings("ignore", message="Unverified HTTPS request")
13
16
 
14
17
  logger = logging.getLogger(__package__)
@@ -28,6 +31,7 @@ class TelemetryClient:
28
31
 
29
32
  def __init__(
30
33
  self,
34
+ telemetry_client_id: str,
31
35
  server_address: Optional[str] = None,
32
36
  *,
33
37
  session: Optional[requests.Session] = None,
@@ -35,6 +39,7 @@ class TelemetryClient:
35
39
  proxies: dict | None = None,
36
40
  delay: Optional[float] = None,
37
41
  ) -> None:
42
+ self.send_telemetry_id = telemetry_client_id
38
43
  self.server_address = os.getenv(
39
44
  "TELEMETRY_SERVER", server_address or self.server_address
40
45
  )
@@ -67,7 +72,10 @@ class TelemetryClient:
67
72
  response = self.session.request(
68
73
  method,
69
74
  url,
70
- headers={"User-Agent": self.user_agent},
75
+ headers={
76
+ "User-Agent": self.user_agent,
77
+ "X-Telemetry-Client-ID": self.send_telemetry_id,
78
+ },
71
79
  proxies=self.proxies,
72
80
  params=data if not has_body else None,
73
81
  json=data if has_body else None,
@@ -91,3 +99,8 @@ class TelemetryClient:
91
99
 
92
100
  get_telemetry = partialmethod(request, "GET")
93
101
  send_telemetry = partialmethod(request, "POST")
102
+
103
+ @classmethod
104
+ def create_from_config(cls, config: Config) -> "TelemetryClient":
105
+ assert "telemetry_client_id" in config
106
+ return cls(config["telemetry_client_id"])