hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.12__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 (76) 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 +25 -35
  5. hh_applicant_tool/api/__init__.py +4 -2
  6. hh_applicant_tool/api/client.py +65 -68
  7. hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
  8. hh_applicant_tool/api/datatypes.py +293 -0
  9. hh_applicant_tool/api/errors.py +57 -7
  10. hh_applicant_tool/api/user_agent.py +17 -0
  11. hh_applicant_tool/main.py +234 -113
  12. hh_applicant_tool/operations/apply_similar.py +353 -371
  13. hh_applicant_tool/operations/authorize.py +313 -120
  14. hh_applicant_tool/operations/call_api.py +18 -8
  15. hh_applicant_tool/operations/check_proxy.py +30 -0
  16. hh_applicant_tool/operations/clear_negotiations.py +90 -82
  17. hh_applicant_tool/operations/config.py +119 -16
  18. hh_applicant_tool/operations/install.py +34 -0
  19. hh_applicant_tool/operations/list_resumes.py +23 -11
  20. hh_applicant_tool/operations/log.py +77 -0
  21. hh_applicant_tool/operations/migrate_db.py +65 -0
  22. hh_applicant_tool/operations/query.py +122 -0
  23. hh_applicant_tool/operations/refresh_token.py +14 -13
  24. hh_applicant_tool/operations/reply_employers.py +201 -180
  25. hh_applicant_tool/operations/settings.py +95 -0
  26. hh_applicant_tool/operations/uninstall.py +26 -0
  27. hh_applicant_tool/operations/update_resumes.py +23 -11
  28. hh_applicant_tool/operations/whoami.py +40 -7
  29. hh_applicant_tool/storage/__init__.py +8 -0
  30. hh_applicant_tool/storage/facade.py +24 -0
  31. hh_applicant_tool/storage/models/__init__.py +0 -0
  32. hh_applicant_tool/storage/models/base.py +169 -0
  33. hh_applicant_tool/storage/models/contacts.py +28 -0
  34. hh_applicant_tool/storage/models/employer.py +12 -0
  35. hh_applicant_tool/storage/models/negotiation.py +16 -0
  36. hh_applicant_tool/storage/models/resume.py +19 -0
  37. hh_applicant_tool/storage/models/setting.py +6 -0
  38. hh_applicant_tool/storage/models/vacancy.py +36 -0
  39. hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
  40. hh_applicant_tool/storage/queries/schema.sql +132 -0
  41. hh_applicant_tool/storage/repositories/__init__.py +0 -0
  42. hh_applicant_tool/storage/repositories/base.py +230 -0
  43. hh_applicant_tool/storage/repositories/contacts.py +14 -0
  44. hh_applicant_tool/storage/repositories/employers.py +14 -0
  45. hh_applicant_tool/storage/repositories/errors.py +19 -0
  46. hh_applicant_tool/storage/repositories/negotiations.py +13 -0
  47. hh_applicant_tool/storage/repositories/resumes.py +9 -0
  48. hh_applicant_tool/storage/repositories/settings.py +35 -0
  49. hh_applicant_tool/storage/repositories/vacancies.py +9 -0
  50. hh_applicant_tool/storage/utils.py +40 -0
  51. hh_applicant_tool/utils/__init__.py +31 -0
  52. hh_applicant_tool/utils/attrdict.py +6 -0
  53. hh_applicant_tool/utils/binpack.py +167 -0
  54. hh_applicant_tool/utils/config.py +55 -0
  55. hh_applicant_tool/utils/date.py +19 -0
  56. hh_applicant_tool/utils/json.py +61 -0
  57. hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
  58. hh_applicant_tool/utils/log.py +147 -0
  59. hh_applicant_tool/utils/misc.py +12 -0
  60. hh_applicant_tool/utils/mixins.py +221 -0
  61. hh_applicant_tool/utils/string.py +27 -0
  62. hh_applicant_tool/utils/terminal.py +32 -0
  63. hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
  64. hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
  65. hh_applicant_tool/ai/blackbox.py +0 -55
  66. hh_applicant_tool/color_log.py +0 -47
  67. hh_applicant_tool/mixins.py +0 -13
  68. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  69. hh_applicant_tool/operations/get_employer_contacts.py +0 -348
  70. hh_applicant_tool/telemetry_client.py +0 -106
  71. hh_applicant_tool/types.py +0 -45
  72. hh_applicant_tool/utils.py +0 -119
  73. hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
  74. hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
  75. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
  76. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,68 @@
1
+ hh_applicant_tool/__init__.py,sha256=ic_GIg9QtlrtgbXEDdhFg3J52VJRRu_OoEsFoRmZ_wk,34
2
+ hh_applicant_tool/__main__.py,sha256=iPpiz09xKqtAjrhONS99OYp6R2dQ6Anbhw1qPIN8ELo,80
3
+ hh_applicant_tool/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ hh_applicant_tool/ai/base.py,sha256=wrccJEO5FNfW6LQTj-pLZAu0VCcs8QXdzf74erBuTew,35
5
+ hh_applicant_tool/ai/openai.py,sha256=m6iRZ3Q-FhzJRttWoriwisq6Pl5o72QLws2JbGCuutw,1657
6
+ hh_applicant_tool/api/__init__.py,sha256=HJtIt4Mp4poqRdHwxsLF0DzD_pC6z1Kk25GfKm6rfOo,152
7
+ hh_applicant_tool/api/client.py,sha256=BzSpTZyzroxiRTTCMlnV2ED4hsRcq9C8epoY0w7hL48,9765
8
+ hh_applicant_tool/api/client_keys.py,sha256=LsMdYNaGNMUV9kvUBqGBd-oMwuUyyLkObOU_wfEnNMQ,491
9
+ hh_applicant_tool/api/datatypes.py,sha256=m5oorneUkc-B1031mGU8cWRDC2X-IaszLiZwHahr_rc,6093
10
+ hh_applicant_tool/api/errors.py,sha256=I-b_MriyjlGMnAvCRNu0pbPxpuWRUwCf5bqWfq7kwAE,3500
11
+ hh_applicant_tool/api/user_agent.py,sha256=ZbbaGcLSrMyP7ATtHPCxIrTFSR4MAjpvDfqZR9YtcVE,504
12
+ hh_applicant_tool/main.py,sha256=POyWgXPqbIsvhtuwzBFxbxP2BCvaGiy-4aYqeX70i8E,9801
13
+ hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ hh_applicant_tool/operations/apply_similar.py,sha256=CF6TBM_O5qv0MT75kMLHJ_jkvTU5elHTGjP2LDJywSo,22933
15
+ hh_applicant_tool/operations/authorize.py,sha256=vdVFT81DbeTUBEhj1ScdolAFuPE9pSjhn7fAgRwh2Xs,12703
16
+ hh_applicant_tool/operations/call_api.py,sha256=J15RLJIIYmoaHL8icNKVvSlDuqovjXOujFaZXTc7CMY,1489
17
+ hh_applicant_tool/operations/check_proxy.py,sha256=HUbDN_CacFEQLGq8B2SBZ_F7-appdMeIrblFrxvt5MI,777
18
+ hh_applicant_tool/operations/clear_negotiations.py,sha256=NBiMmRQ56t-YxeM1Hd1ED-d2Zsv5PCAocE7pCmgVuUY,4246
19
+ hh_applicant_tool/operations/config.py,sha256=eNQik_sBobrNMBB3tZ2tuCveCqH-XCub26WInd6GYT0,4725
20
+ hh_applicant_tool/operations/install.py,sha256=e626LnUB5OVsZ1M6cMXfW94hSlN9BDwPfKYcXMtPEgc,794
21
+ hh_applicant_tool/operations/list_resumes.py,sha256=Xf_Hf1B6cu8OGbGUZcI-hUT7QQXHJtRuA3KSMMcpRNk,1227
22
+ hh_applicant_tool/operations/log.py,sha256=VwnKYDYOOMJKZPGe3X2auo89rAIdbR_7KzXzbt4ve7Y,2542
23
+ hh_applicant_tool/operations/migrate_db.py,sha256=CMWpkhnvJvyGb4QSrtfxrWfEehmHyZ_flevMMgobkfI,2073
24
+ hh_applicant_tool/operations/query.py,sha256=noINFgTBHpcTq9Gu5xPIvtSI-y57HX9py-oAcULQ3aE,3705
25
+ hh_applicant_tool/operations/refresh_token.py,sha256=K-L8GMC9azu3tngdVsb8zub9SmzKpuCjbCYAwNgp6Uo,640
26
+ hh_applicant_tool/operations/reply_employers.py,sha256=zOzJr0Jlu4JM36u-xTadvKvldbTFpsLrTlRWJUzo64g,13481
27
+ hh_applicant_tool/operations/settings.py,sha256=_dZ-EOypRf8gLQx8PGH_LvFned5UVA5bn3s1gKcYcvI,2918
28
+ hh_applicant_tool/operations/uninstall.py,sha256=IfFTCzvMT1FfoVj-yvIXwa-UaxV5ai8mmRs9hVDiWa4,626
29
+ hh_applicant_tool/operations/update_resumes.py,sha256=_YgQvfEapwsqHIXeo6X_Z4WYr1O17mhr5_jCd5h_0co,1253
30
+ hh_applicant_tool/operations/whoami.py,sha256=hHhBscBRh2MIFrhmhDwB4BI1jAoA0DRr8nSlqcG9a7s,1690
31
+ hh_applicant_tool/storage/__init__.py,sha256=lj5dMbmk5y3j050VYNp_No9cPVAEr_J3T6MXGC9y__Y,168
32
+ hh_applicant_tool/storage/facade.py,sha256=CnAKKp7wJOHqORPBUEzK-26A6xl3U_bOfgceYeFJ4B0,907
33
+ hh_applicant_tool/storage/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ hh_applicant_tool/storage/models/base.py,sha256=cBbsQKFt4S_-TDk3xGUofUQHOU72KimFmqbr_ytHH70,5495
35
+ hh_applicant_tool/storage/models/contacts.py,sha256=OrP5czEbGB4reBbH7PaW3oguGMaA1UiXWJt5rlg34nk,1155
36
+ hh_applicant_tool/storage/models/employer.py,sha256=QH9F-SIUWweeHZzWoptgIdlFQy4wxqakLmpVs7Fg9nI,344
37
+ hh_applicant_tool/storage/models/negotiation.py,sha256=HSSXD2YMvaVkNqHBLmFxVcpbpOpQzaCPraL-7j7m7Fk,454
38
+ hh_applicant_tool/storage/models/resume.py,sha256=R3FcVhlUvG-5K23OvLtOWbf0zGQLx7Nods25L6xq-1c,548
39
+ hh_applicant_tool/storage/models/setting.py,sha256=DIRvW1IX-1E3Af-JJY8RZwxn-E39OSHHwNDk-N4Yh-4,123
40
+ hh_applicant_tool/storage/models/vacancy.py,sha256=97OkoCh6kvjRuQoKYRGAHJE9L1ncELL427L-TXcnqbA,1097
41
+ hh_applicant_tool/storage/queries/migrations/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ hh_applicant_tool/storage/queries/schema.sql,sha256=s0DwiTdr2FxMvfke7FkBAuSIECwbTMvbjfwQLClrfEU,4265
43
+ hh_applicant_tool/storage/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ hh_applicant_tool/storage/repositories/base.py,sha256=DMcxh5K7OvKLHi2PkyfLe1Cz4Hfd4gzrUCsOqdduh18,7176
45
+ hh_applicant_tool/storage/repositories/contacts.py,sha256=zUbGdgc-oD6nDDk0KfJnNVSZSlJU7iWGA0JOyxAcH5U,343
46
+ hh_applicant_tool/storage/repositories/employers.py,sha256=dscO_W2__zjIQmmyr9Sf00e1XqyYJtzyQDrLwmIyXwo,337
47
+ hh_applicant_tool/storage/repositories/errors.py,sha256=tAm5p8l1JsWCTfUSdrSdtt-fw_TWiThYyPm2wfjTIrA,394
48
+ hh_applicant_tool/storage/repositories/negotiations.py,sha256=jGw8k3FAZqAq9ltvUDJ2vwPY7lbR9fK7yPaKe9RAhCI,290
49
+ hh_applicant_tool/storage/repositories/resumes.py,sha256=-k9F_sPvkSSR75vGudhhKdQcgP_TYLra-xi3YiUZY_k,202
50
+ hh_applicant_tool/storage/repositories/settings.py,sha256=puidy8aDfp0zWINbF-vzxUIYfHtSczqEc3flSXFPYqE,835
51
+ hh_applicant_tool/storage/repositories/vacancies.py,sha256=EIWrxykKDJu3ueOKn-MTZfNtVO83-PeQSPvJkX2s5N4,209
52
+ hh_applicant_tool/storage/utils.py,sha256=KDVnb6Osx0HCh3EzzoEXPiK_KKysAR6F7ek9sBsQN7Y,1220
53
+ hh_applicant_tool/utils/__init__.py,sha256=-GZwAZSYGLO5D8Gzmdqlo1CrBjCx6l-lfTAs44olNdU,757
54
+ hh_applicant_tool/utils/attrdict.py,sha256=wlbow24rovk9kZ0lc0xA4ULlgZ-UWj4GUAmZhAbW9Wg,155
55
+ hh_applicant_tool/utils/binpack.py,sha256=qW55IPleip_wi1GyNBhPwCYP3GGQzi8BASGKD3sOIw8,4658
56
+ hh_applicant_tool/utils/config.py,sha256=4npQLjhi5d89gEebYjoFq16jTO8-Q4RMMwvVgRnWmXU,1588
57
+ hh_applicant_tool/utils/date.py,sha256=foPghTLlgThL-oiMu_LedaG8KS5IMfjGo1wsrAanhPM,456
58
+ hh_applicant_tool/utils/json.py,sha256=uW5Ajdv_IYCfVRHJiUVjQnb5nI7XRMva-s3QcVWruvY,1701
59
+ hh_applicant_tool/utils/jsonc.py,sha256=WATp8MeKglEI6MRdXWBmqvW3q8IRNpt7UPjAKIf5VBs,4072
60
+ hh_applicant_tool/utils/log.py,sha256=EbAvCnKMDQTWIPnYyIVY9T3KOkR3ljA9I5yk1yRmAFE,4094
61
+ hh_applicant_tool/utils/misc.py,sha256=BW0oUEozsMS8WBBIH6y-lUC_MBkuwhO2zbL0nM_Fi50,238
62
+ hh_applicant_tool/utils/mixins.py,sha256=p3I9VOCcfRwjH2hcELP8irCb1-_GVx8L2LvCqaqOW54,7408
63
+ hh_applicant_tool/utils/string.py,sha256=dQn1uhwYXesjRmqkDn5HAHOdeaGF2v1HXpT1Quff6Eg,618
64
+ hh_applicant_tool/utils/terminal.py,sha256=rCzAZj6Q0LYTCj1et16BfifvK8OMTsORoMP2vxG-JCk,1164
65
+ hh_applicant_tool-1.4.12.dist-info/METADATA,sha256=9rGEB-9QDSfac8SL0k5qTD51tZ4sYx6qauMLaIpRWEs,46122
66
+ hh_applicant_tool-1.4.12.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
67
+ hh_applicant_tool-1.4.12.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
68
+ hh_applicant_tool-1.4.12.dist-info/RECORD,,
@@ -1,55 +0,0 @@
1
- import logging
2
- from copy import deepcopy
3
-
4
- import requests
5
-
6
- logger = logging.getLogger(__package__)
7
-
8
-
9
- class BlackboxError(Exception):
10
- pass
11
-
12
-
13
- class BlackboxChat:
14
- chat_endpoint: str = "https://www.blackbox.ai/api/chat"
15
-
16
- def __init__(
17
- self,
18
- session_id: str,
19
- chat_payload: dict,
20
- proxies: dict[str, str] = {},
21
- session: requests.Session | None = None,
22
- ):
23
- self.session_id = session_id
24
- self.chat_payload = chat_payload
25
- self.proxies = proxies
26
- self.session = session or requests.session()
27
-
28
- def default_headers(self) -> dict[str, str]:
29
- return {
30
- "Accept": "*/*",
31
- "Accept-Language": "en-US,en;q=0.9,ru;q=0.8",
32
- "Content-Type": "application/json",
33
- "Origin": "https://www.blackbox.ai",
34
- "Priority": "u=0",
35
- "Referer": "https://www.blackbox.ai/",
36
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
37
- }
38
-
39
- def send_message(self, message: str) -> str:
40
- payload = deepcopy(self.chat_payload)
41
- payload["messages"].append(
42
- {**payload["messages"][0], "content": message}
43
- )
44
-
45
- try:
46
- response = self.session.post(
47
- self.chat_endpoint,
48
- json=payload,
49
- cookies={"sessionId": self.session_id},
50
- headers=self.default_headers(),
51
- proxies=self.proxies,
52
- )
53
- return response.text
54
- except requests.exceptions.RequestException as ex:
55
- raise BlackboxError(str(ex)) from ex
@@ -1,47 +0,0 @@
1
- import enum
2
- import logging
3
- from enum import auto
4
- import os, sys
5
-
6
-
7
- if sys.platform == "win32":
8
- import ctypes
9
- kernel32 = ctypes.windll.kernel32
10
- # 0x0004 = ENABLE_VIRTUAL_TERMINAL_PROCESSING
11
- # Берем дескриптор стандартного вывода (stdout)
12
- handle = kernel32.GetStdHandle(-11)
13
- mode = ctypes.c_uint()
14
- kernel32.GetConsoleMode(handle, ctypes.byref(mode))
15
- kernel32.SetConsoleMode(handle, mode.value | 0x0004)
16
-
17
-
18
- class Color(enum.Enum):
19
- BLACK = 30
20
- RED = auto()
21
- GREEN = auto()
22
- YELLOW = auto()
23
- BLUE = auto()
24
- PURPLE = auto()
25
- CYAN = auto()
26
- WHITE = auto()
27
-
28
- def __str__(self) -> str:
29
- return str(self.value)
30
-
31
-
32
- class ColorHandler(logging.StreamHandler):
33
- _color_map = {
34
- "CRITICAL": Color.RED,
35
- "ERROR": Color.RED,
36
- "WARNING": Color.RED,
37
- "INFO": Color.GREEN,
38
- "DEBUG": Color.BLUE,
39
- }
40
-
41
- def format(self, record: logging.LogRecord) -> str:
42
- message = super().format(record)
43
- isatty = getattr(self.stream, "isatty", None)
44
- if isatty and isatty():
45
- color_code = self._color_map[record.levelname]
46
- return f"\033[{color_code}m{message}\033[0m"
47
- return message
@@ -1,13 +0,0 @@
1
- from .api import ApiError
2
- from .types import ApiListResponse
3
-
4
-
5
- class GetResumeIdMixin:
6
- def _get_resume_id(self) -> str:
7
- try:
8
- resumes: ApiListResponse = self.api_client.get("/resumes/mine")
9
- return resumes["items"][0]["id"]
10
- except (ApiError, KeyError, IndexError) as ex:
11
- raise Exception("Не могу получить идентификатор резюме") from ex
12
-
13
-
@@ -1,30 +0,0 @@
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
@@ -1,348 +0,0 @@
1
- import argparse
2
- import logging
3
- from os import getenv
4
- import pathlib
5
- from ..main import BaseOperation
6
- from ..main import Namespace as BaseNamespace
7
- from ..telemetry_client import TelemetryClient
8
-
9
- logger = logging.getLogger(__package__)
10
-
11
-
12
- class Namespace(BaseNamespace):
13
- username: str | None
14
- password: str | None
15
- search: str | None
16
- export: bool
17
-
18
-
19
- class Operation(BaseOperation):
20
- """Выведет контакты работодателей, которые высылали вам приглашения"""
21
-
22
- def setup_parser(self, parser: argparse.ArgumentParser) -> None:
23
- # parser.add_argument(
24
- # "-u",
25
- # "--username",
26
- # type=str,
27
- # help="Имя пользователя для аутентификации",
28
- # default=getenv("AUTH_USERNAME"),
29
- # )
30
- # parser.add_argument(
31
- # "-P",
32
- # "--password",
33
- # type=str,
34
- # help="Пароль для аутентификации",
35
- # default=getenv("AUTH_PASSWORD"),
36
- # )
37
- parser.add_argument(
38
- "-s",
39
- "--search",
40
- type=str,
41
- default="",
42
- help="Строка поиска контактов работодателя (email, имя, название компании)",
43
- )
44
- parser.add_argument(
45
- "-p",
46
- "--page",
47
- default=1,
48
- help="Номер страницы в выдаче. Игнорируется при экспорте.",
49
- )
50
- parser.add_argument(
51
- "--export",
52
- action=argparse.BooleanOptionalAction,
53
- default=False,
54
- help="Экспортировать контакты работодателей.",
55
- )
56
- parser.add_argument(
57
- "-f",
58
- "--format",
59
- default="html",
60
- choices=["html", "json", "jsonl"],
61
- help="Формат вывода",
62
- )
63
-
64
- def run(self, args: Namespace, _, telemetry_client: TelemetryClient) -> None:
65
- if args.export:
66
- contact_persons = []
67
- page = 1
68
- per_page = 100
69
- while True:
70
- res = telemetry_client.get_telemetry(
71
- "/contact/persons",
72
- {"search": args.search, "per_page": per_page, "page": page},
73
- )
74
- assert "contact_persons" in res
75
- contact_persons += res["contact_persons"]
76
- if per_page * page >= res["total"]:
77
- break
78
- page += 1
79
- if args.format.startswith("json"):
80
- import json, sys
81
-
82
- is_json = args.format == "json"
83
- total_contacts = len(contact_persons)
84
-
85
- if is_json:
86
- sys.stdout.write("[")
87
-
88
- for index, contact in enumerate(contact_persons):
89
- if is_json and index > 0:
90
- sys.stdout.write(",")
91
-
92
- json.dump(contact, sys.stdout, ensure_ascii=False)
93
-
94
- if not is_json:
95
- sys.stdout.write("\n")
96
-
97
- if is_json:
98
- sys.stdout.write("]\n")
99
- else:
100
- print(generate_html_report(contact_persons))
101
- return
102
-
103
- res = telemetry_client.get_telemetry(
104
- "/contact/persons",
105
- {"search": args.search, "per_page": 10, "page": args.page},
106
- )
107
- if "contact_persons" not in res:
108
- print("❌", res)
109
- return 1
110
-
111
- print(
112
- "Тут отображаются только данные, собранные с вашего telemetry_client_id. Вы так же можете их удалить с помощью команды delete-telemetry."
113
- )
114
- print()
115
-
116
- print_contacts(res)
117
-
118
-
119
- def generate_html_report(data: list[dict]) -> str:
120
- """
121
- Генерирует HTML-отчет на основе предоставленных данных.
122
- """
123
- html_content = """\
124
- <!DOCTYPE html>
125
- <html lang="ru">
126
- <head>
127
- <meta charset="UTF-8">
128
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
129
- <title>Контакты работодателей</title>
130
- <style>
131
- body {
132
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
133
- margin: 20px;
134
- background-color: #f4f7f6;
135
- color: #333;
136
- }
137
- .container {
138
- max-width: 900px;
139
- margin: 20px auto;
140
- background-color: #ffffff;
141
- padding: 30px;
142
- border-radius: 10px;
143
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
144
- }
145
- h1 {
146
- color: #0056b3;
147
- text-align: center;
148
- margin-bottom: 30px;
149
- }
150
- .person-card {
151
- background-color: #e9f0f8;
152
- border: 1px solid #cce5ff;
153
- border-radius: 8px;
154
- padding: 20px;
155
- margin-bottom: 25px;
156
- transition: transform 0.2s ease-in-out;
157
- }
158
- .person-card:hover {
159
- transform: translateY(-5px);
160
- }
161
- .person-card h2 {
162
- color: #004085;
163
- margin-top: 0;
164
- margin-bottom: 10px;
165
- border-bottom: 2px solid #0056b3;
166
- padding-bottom: 5px;
167
- }
168
- .person-card p {
169
- margin: 5px 0;
170
- }
171
- .person-card strong {
172
- color: #004085;
173
- }
174
- .employer-info {
175
- background-color: #d1ecf1;
176
- border-left: 5px solid #007bff;
177
- padding: 15px;
178
- margin-top: 15px;
179
- border-radius: 5px;
180
- }
181
- .employer-info h3 {
182
- color: #0056b3;
183
- margin-top: 0;
184
- margin-bottom: 10px;
185
- }
186
- ul {
187
- list-style-type: none;
188
- padding: 0;
189
- }
190
- ul li {
191
- background-color: #f8fafd;
192
- padding: 8px 12px;
193
- margin-bottom: 5px;
194
- border-radius: 4px;
195
- border: 1px solid #e0e9f1;
196
- }
197
- a {
198
- color: #007bff;
199
- text-decoration: none;
200
- }
201
- a:hover {
202
- text-decoration: underline;
203
- }
204
- .no-data {
205
- color: #6c757d;
206
- font-style: italic;
207
- }
208
- .scam-warning {
209
- background-color: #f8d7da;
210
- color: #721c24;
211
- border: 1px solid #f5c6cb;
212
- padding: 10px;
213
- border-radius: 5px;
214
- margin-bottom: 15px;
215
- font-weight: bold;
216
- text-align: center;
217
- text-transform: uppercase;
218
- }
219
- </style>
220
- </head>
221
- <body>
222
- <div class="container">
223
- <h1>Полученные контакты</h1>
224
- """
225
-
226
- for item in data:
227
- name = item.get("name", "N/A")
228
- email = item.get("email", "N/A")
229
- employer = item.get("employer") or {}
230
-
231
- employer_name = employer.get("name", "N/A")
232
- employer_area = employer.get("area", "N/A")
233
- employer_site_url = employer.get("site_url", "")
234
-
235
- phone_numbers = [
236
- pn["phone_number"]
237
- for pn in item.get("phone_numbers", [])
238
- if "phone_number" in pn
239
- ]
240
- telegram_usernames = [
241
- tu["username"]
242
- for tu in item.get("telegram_usernames", [])
243
- if "username" in tu
244
- ]
245
-
246
- html_content += '<div class="person-card">'
247
-
248
- if item.get("is_scam"):
249
- html_content += '<div class="scam-warning">⚠️ ВНИМАНИЕ: Подозрение на мошенничество!</div>'
250
-
251
- html_content += f"""\
252
- <h2>{name}</h2>
253
- <p><strong>Email:</strong> <a href="mailto:{email}">{email}</a></p>
254
- """
255
-
256
- if employer_name != "N/A":
257
- html_content += f"""\
258
- <div class="employer-info">
259
- <h3>Работодатель: {employer_name}</h3>
260
- <p><strong>Город:</strong> {employer_area}</p>
261
- """
262
- if employer_site_url:
263
- html_content += f"""\
264
- <p><strong>Сайт:</strong> <a href="{employer_site_url}" target="_blank">{employer_site_url}</a></p>
265
- """
266
- html_content += "</div>" # Закрываем employer-info
267
- else:
268
- html_content += (
269
- '<p class="no-data">Информация о работодателе отсутствует.</p>'
270
- )
271
-
272
- if phone_numbers:
273
- html_content += "<p><strong>Номера телефонов:</strong></p><ul>"
274
- for phone in phone_numbers:
275
- html_content += f"<li><a href='tel:{phone}'>{phone}</a></li>"
276
- html_content += "</ul>"
277
- else:
278
- html_content += '<p class="no-data">Номера телефонов отсутствуют.</p>'
279
-
280
- if telegram_usernames:
281
- html_content += "<p><strong>Имена пользователей Telegram:</strong></p><ul>"
282
- for username in telegram_usernames:
283
- html_content += f"<li><a href='https://t.me/{username}' target='_blank'>@{username}</a></li>"
284
- html_content += "</ul>"
285
- else:
286
- html_content += (
287
- '<p class="no-data">Имена пользователей Telegram отсутствуют.</p>'
288
- )
289
-
290
- html_content += "</div>" # Закрываем person-card
291
-
292
- html_content += """\
293
- </div>
294
- </body>
295
- </html>"""
296
- return html_content
297
-
298
-
299
- def print_contacts(data: dict) -> None:
300
- """Вывод всех контактов в древовидной структуре."""
301
- page = data["page"]
302
- pages = (data["total"] // data["per_page"]) + 1
303
- print(f"Страница {page}/{pages}:")
304
- contacts = data.get("contact_persons", [])
305
- for idx, contact in enumerate(contacts):
306
- is_last_contact = idx == len(contacts) - 1
307
- print_contact(contact, is_last_contact)
308
- print()
309
-
310
-
311
- def print_contact(contact: dict, is_last_contact: bool) -> None:
312
- """Вывод информации о конкретном контакте."""
313
- is_scam = contact.get("is_scam", False)
314
- prefix = "└──" if is_last_contact else "├──"
315
- scam_label = " ⚠️ [МОШЕННИК]" if is_scam else ""
316
-
317
- print(f" {prefix} 🧑 {contact.get('name', 'Имя скрыто')}{scam_label}")
318
-
319
- prefix2 = " " if is_last_contact else " │ "
320
-
321
- print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
322
-
323
- # 📞 Телефоны (вложенный список)
324
- phones = contact.get("phone_numbers") or []
325
- print(f"{prefix2}├── 📞 Телефоны:")
326
- if phones:
327
- for i, phone in enumerate(phones):
328
- p = "└──" if i == len(phones) - 1 else "├──"
329
- print(f"{prefix2}│ {p} {phone['phone_number']}")
330
- else:
331
- print(f"{prefix2}│ └── н/д")
332
-
333
- # 💬 Telegram (вложенный список)
334
- telegram_usernames = contact.get("telegram_usernames") or []
335
- print(f"{prefix2}├── 💬 Telegram:")
336
- if telegram_usernames:
337
- for i, tg in enumerate(telegram_usernames):
338
- p = "└──" if i == len(telegram_usernames) - 1 else "├──"
339
- print(f"{prefix2}│ {p} {tg['username']}")
340
- else:
341
- print(f"{prefix2}│ └── н/д")
342
-
343
- employer = contact.get("employer") or {}
344
- print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
345
- print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
346
- print(f"{prefix2}└── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
347
-
348
- print(prefix2)
@@ -1,106 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import logging
5
- import os
6
- import time
7
- import warnings
8
- from functools import partialmethod
9
- from typing import Any, Dict, Optional
10
- from urllib.parse import urljoin
11
- import requests
12
- from .utils import Config
13
-
14
- # Сертификат на сервере давно истек, но его обновлять мне лень...
15
- warnings.filterwarnings("ignore", message="Unverified HTTPS request")
16
-
17
- logger = logging.getLogger(__package__)
18
-
19
-
20
- class TelemetryError(Exception):
21
- """Исключение, возникающее при ошибках в работе TelemetryClient."""
22
-
23
- pass
24
-
25
-
26
- class TelemetryClient:
27
- """Клиент для отправки телеметрии на сервер."""
28
-
29
- server_address: str = "https://hh-applicant-tool.mooo.com:54157/"
30
- default_delay: float = 0.56 # Задержка по умолчанию в секундах
31
-
32
- def __init__(
33
- self,
34
- telemetry_client_id: str,
35
- server_address: Optional[str] = None,
36
- *,
37
- session: Optional[requests.Session] = None,
38
- user_agent: str = "Mozilla/5.0 (HHApplicantTelemetry/1.0)",
39
- proxies: dict | None = None,
40
- delay: Optional[float] = None,
41
- ) -> None:
42
- self.send_telemetry_id = telemetry_client_id
43
- self.server_address = os.getenv(
44
- "TELEMETRY_SERVER", server_address or self.server_address
45
- )
46
- self.session = session or requests.Session()
47
- self.user_agent = user_agent
48
- self.proxies = proxies
49
- self.delay = delay if delay is not None else self.default_delay
50
- self.last_request_time = time.monotonic() # Время последнего запроса
51
-
52
- def request(
53
- self,
54
- method: str,
55
- endpoint: str,
56
- data: Dict[str, Any] | None = None,
57
- **kwargs: Any,
58
- ) -> Dict[str, Any]:
59
- method = method.upper()
60
- url = urljoin(self.server_address, endpoint)
61
- has_body = method in ["POST", "PUT", "PATCH"]
62
-
63
- # Вычисляем время, прошедшее с последнего запроса
64
- current_time = time.monotonic()
65
- time_since_last_request = current_time - self.last_request_time
66
-
67
- # Если прошло меньше времени, чем задержка, ждем оставшееся время
68
- if time_since_last_request < self.delay:
69
- time.sleep(self.delay - time_since_last_request)
70
-
71
- try:
72
- response = self.session.request(
73
- method,
74
- url,
75
- headers={
76
- "User-Agent": self.user_agent,
77
- "X-Telemetry-Client-ID": self.send_telemetry_id,
78
- },
79
- proxies=self.proxies,
80
- params=data if not has_body else None,
81
- json=data if has_body else None,
82
- verify=False, # Игнорирование истекшего сертификата
83
- **kwargs,
84
- )
85
- # response.raise_for_status()
86
- result = response.json()
87
- if 200 > response.status_code >= 300:
88
- raise TelemetryError(result)
89
- return result
90
-
91
- except (
92
- requests.exceptions.RequestException,
93
- json.JSONDecodeError,
94
- ) as ex:
95
- raise TelemetryError(str(ex)) from ex
96
- finally:
97
- # Обновляем время последнего запроса
98
- self.last_request_time = time.monotonic()
99
-
100
- get_telemetry = partialmethod(request, "GET")
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"])