hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.7__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 (75) 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 +23 -33
  5. hh_applicant_tool/api/client.py +50 -64
  6. hh_applicant_tool/api/errors.py +51 -7
  7. hh_applicant_tool/constants.py +0 -3
  8. hh_applicant_tool/datatypes.py +291 -0
  9. hh_applicant_tool/main.py +233 -111
  10. hh_applicant_tool/operations/apply_similar.py +266 -362
  11. hh_applicant_tool/operations/authorize.py +256 -120
  12. hh_applicant_tool/operations/call_api.py +18 -8
  13. hh_applicant_tool/operations/check_negotiations.py +102 -0
  14. hh_applicant_tool/operations/check_proxy.py +30 -0
  15. hh_applicant_tool/operations/config.py +119 -16
  16. hh_applicant_tool/operations/install.py +34 -0
  17. hh_applicant_tool/operations/list_resumes.py +24 -10
  18. hh_applicant_tool/operations/log.py +77 -0
  19. hh_applicant_tool/operations/migrate_db.py +65 -0
  20. hh_applicant_tool/operations/query.py +120 -0
  21. hh_applicant_tool/operations/refresh_token.py +14 -13
  22. hh_applicant_tool/operations/reply_employers.py +148 -167
  23. hh_applicant_tool/operations/settings.py +95 -0
  24. hh_applicant_tool/operations/uninstall.py +26 -0
  25. hh_applicant_tool/operations/update_resumes.py +21 -10
  26. hh_applicant_tool/operations/whoami.py +40 -7
  27. hh_applicant_tool/storage/__init__.py +4 -0
  28. hh_applicant_tool/storage/facade.py +24 -0
  29. hh_applicant_tool/storage/models/__init__.py +0 -0
  30. hh_applicant_tool/storage/models/base.py +169 -0
  31. hh_applicant_tool/storage/models/contact.py +16 -0
  32. hh_applicant_tool/storage/models/employer.py +12 -0
  33. hh_applicant_tool/storage/models/negotiation.py +16 -0
  34. hh_applicant_tool/storage/models/resume.py +19 -0
  35. hh_applicant_tool/storage/models/setting.py +6 -0
  36. hh_applicant_tool/storage/models/vacancy.py +36 -0
  37. hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
  38. hh_applicant_tool/storage/queries/schema.sql +119 -0
  39. hh_applicant_tool/storage/repositories/__init__.py +0 -0
  40. hh_applicant_tool/storage/repositories/base.py +176 -0
  41. hh_applicant_tool/storage/repositories/contacts.py +19 -0
  42. hh_applicant_tool/storage/repositories/employers.py +13 -0
  43. hh_applicant_tool/storage/repositories/negotiations.py +12 -0
  44. hh_applicant_tool/storage/repositories/resumes.py +14 -0
  45. hh_applicant_tool/storage/repositories/settings.py +34 -0
  46. hh_applicant_tool/storage/repositories/vacancies.py +8 -0
  47. hh_applicant_tool/storage/utils.py +49 -0
  48. hh_applicant_tool/utils/__init__.py +31 -0
  49. hh_applicant_tool/utils/attrdict.py +6 -0
  50. hh_applicant_tool/utils/binpack.py +167 -0
  51. hh_applicant_tool/utils/config.py +55 -0
  52. hh_applicant_tool/utils/dateutil.py +19 -0
  53. hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
  54. hh_applicant_tool/utils/jsonutil.py +61 -0
  55. hh_applicant_tool/utils/log.py +144 -0
  56. hh_applicant_tool/utils/misc.py +12 -0
  57. hh_applicant_tool/utils/mixins.py +220 -0
  58. hh_applicant_tool/utils/string.py +27 -0
  59. hh_applicant_tool/utils/terminal.py +19 -0
  60. hh_applicant_tool/utils/user_agent.py +17 -0
  61. hh_applicant_tool-1.4.7.dist-info/METADATA +628 -0
  62. hh_applicant_tool-1.4.7.dist-info/RECORD +67 -0
  63. hh_applicant_tool/ai/blackbox.py +0 -55
  64. hh_applicant_tool/color_log.py +0 -47
  65. hh_applicant_tool/mixins.py +0 -13
  66. hh_applicant_tool/operations/clear_negotiations.py +0 -109
  67. hh_applicant_tool/operations/delete_telemetry.py +0 -30
  68. hh_applicant_tool/operations/get_employer_contacts.py +0 -348
  69. hh_applicant_tool/telemetry_client.py +0 -106
  70. hh_applicant_tool/types.py +0 -45
  71. hh_applicant_tool/utils.py +0 -119
  72. hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
  73. hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
  74. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/WHEEL +0 -0
  75. {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,67 @@
1
+ hh_applicant_tool/__init__.py,sha256=ic_GIg9QtlrtgbXEDdhFg3J52VJRRu_OoEsFoRmZ_wk,34
2
+ hh_applicant_tool/__main__.py,sha256=o6kDuEJML3MnZW3HUKRBjaMeTjYjBQAwqRkavfwQSQ8,92
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=R82CiGxklm5WxLVUNpXzdOVrKVRI5uT7Lzd9puT-FqY,1655
6
+ hh_applicant_tool/api/__init__.py,sha256=kgFSHibRaAugN2BA3U1djEa20qgKJUUVouwJzjEB0DU,84
7
+ hh_applicant_tool/api/client.py,sha256=iy4W2ubaDMxkcWqjRU6cH7V2PcSxAy5IYKU-dStEra0,9392
8
+ hh_applicant_tool/api/errors.py,sha256=u6ogOo0QijQ-UCi6aApeR2v4bqh1neSQ-184owiPWiI,3399
9
+ hh_applicant_tool/constants.py,sha256=OkVtQXRIyrZvgJ0N7Oxe-L5BSPyJL5DhyUU7exAHIuI,622
10
+ hh_applicant_tool/datatypes.py,sha256=YAeqY8khKolBFUt-bNuUXQlmpxkkEGLef5f07QtTSuM,5939
11
+ hh_applicant_tool/main.py,sha256=b3CKwewS5RduUGf7PLG2sJDXjTzVUIoj0x52Gm78Mac,9999
12
+ hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ hh_applicant_tool/operations/apply_similar.py,sha256=nE5GKg4g5uOoV8FE7KT9ctGf_MUTg5yr1M_pBvmPxgM,20030
14
+ hh_applicant_tool/operations/authorize.py,sha256=rg3ks71_LuQrfFoIFEb03bHAp29q00sT5NcGwzuuAYs,10728
15
+ hh_applicant_tool/operations/call_api.py,sha256=9bnyakrQ3f5DiAL7Qr6bhkVqQeFFSloslSFNPkj0CGU,1519
16
+ hh_applicant_tool/operations/check_negotiations.py,sha256=LHrgVCfJF4My-l1YUBDTV_c-0kDEem5HylvkZBq-41c,3629
17
+ hh_applicant_tool/operations/check_proxy.py,sha256=HUbDN_CacFEQLGq8B2SBZ_F7-appdMeIrblFrxvt5MI,777
18
+ hh_applicant_tool/operations/config.py,sha256=eNQik_sBobrNMBB3tZ2tuCveCqH-XCub26WInd6GYT0,4725
19
+ hh_applicant_tool/operations/install.py,sha256=e626LnUB5OVsZ1M6cMXfW94hSlN9BDwPfKYcXMtPEgc,794
20
+ hh_applicant_tool/operations/list_resumes.py,sha256=OU5s197LGAGvcvNTaVGxU-TAhiu0hkC3RCGoN5sdJ5Y,1330
21
+ hh_applicant_tool/operations/log.py,sha256=VwnKYDYOOMJKZPGe3X2auo89rAIdbR_7KzXzbt4ve7Y,2542
22
+ hh_applicant_tool/operations/migrate_db.py,sha256=CMWpkhnvJvyGb4QSrtfxrWfEehmHyZ_flevMMgobkfI,2073
23
+ hh_applicant_tool/operations/query.py,sha256=sJz6yenn3eCs2U3clhe9ipC03SniGeer_lIf5ZN8qZg,3660
24
+ hh_applicant_tool/operations/refresh_token.py,sha256=K-L8GMC9azu3tngdVsb8zub9SmzKpuCjbCYAwNgp6Uo,640
25
+ hh_applicant_tool/operations/reply_employers.py,sha256=h0-8d62fRG71DiDkpNDFAiBfTeU4t39B7Zwq4zxl3SE,11979
26
+ hh_applicant_tool/operations/settings.py,sha256=HYxx81vIVbyXOf--tMpA3BrrHjqWNx8F8YDByaWTnwc,2924
27
+ hh_applicant_tool/operations/uninstall.py,sha256=IfFTCzvMT1FfoVj-yvIXwa-UaxV5ai8mmRs9hVDiWa4,626
28
+ hh_applicant_tool/operations/update_resumes.py,sha256=GqNZ5yDIAVW4C8QfzxTliF9CIY-PPDccMcZ2pajDCag,1177
29
+ hh_applicant_tool/operations/whoami.py,sha256=_kkz8pFRF9ChsoxqVyJ4dokABu-ELeld9RSLwoxPdlA,1687
30
+ hh_applicant_tool/storage/__init__.py,sha256=ggelL-_xA2foBurkYP6OfgXBl-9uhfGX1PXPnZxlPSc,153
31
+ hh_applicant_tool/storage/facade.py,sha256=j-GurkPFA5megmGXosWShR6GwmQsiyo5QQdEu1_VRKs,910
32
+ hh_applicant_tool/storage/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ hh_applicant_tool/storage/models/base.py,sha256=iO9ZpMYpRHNOFsDtsgqDPJFZ56wt7WXVMlQ_Qr4iBOA,5511
34
+ hh_applicant_tool/storage/models/contact.py,sha256=4zfriRYyWLBL0Suiams7Qn5RxuJPPjywEc6ICFdTrIQ,502
35
+ hh_applicant_tool/storage/models/employer.py,sha256=QH9F-SIUWweeHZzWoptgIdlFQy4wxqakLmpVs7Fg9nI,344
36
+ hh_applicant_tool/storage/models/negotiation.py,sha256=HSSXD2YMvaVkNqHBLmFxVcpbpOpQzaCPraL-7j7m7Fk,454
37
+ hh_applicant_tool/storage/models/resume.py,sha256=R3FcVhlUvG-5K23OvLtOWbf0zGQLx7Nods25L6xq-1c,548
38
+ hh_applicant_tool/storage/models/setting.py,sha256=DIRvW1IX-1E3Af-JJY8RZwxn-E39OSHHwNDk-N4Yh-4,123
39
+ hh_applicant_tool/storage/models/vacancy.py,sha256=97OkoCh6kvjRuQoKYRGAHJE9L1ncELL427L-TXcnqbA,1097
40
+ hh_applicant_tool/storage/queries/migrations/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ hh_applicant_tool/storage/queries/schema.sql,sha256=zOikp5dpqUk8QGqcNolA51yPhaL-3uRUM3pmy6SfrSs,3893
42
+ hh_applicant_tool/storage/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
+ hh_applicant_tool/storage/repositories/base.py,sha256=aSHJZKCi-MB1LZKZKZXmPvikPi31z5FTBoSozFPEkfk,5734
44
+ hh_applicant_tool/storage/repositories/contacts.py,sha256=0ijj_wpyaHUQtJTlts5ar8SX-fe7ZGywa0y788zYZgs,461
45
+ hh_applicant_tool/storage/repositories/employers.py,sha256=WWN6K-Qk7_PJfSUl8BOvNmbyO4Xjy2jaagCmvdnbAzY,309
46
+ hh_applicant_tool/storage/repositories/negotiations.py,sha256=IcTGXx0FK1FeGC49WBfvKOFwARSD6OpBTn_l0iZTKU4,259
47
+ hh_applicant_tool/storage/repositories/resumes.py,sha256=jYJ5O5FVyR8F-T6SXvgmfFEw5k0d9K-UjaPSHCRChHc,354
48
+ hh_applicant_tool/storage/repositories/settings.py,sha256=-amBuLNVQDjdqcCGbyWrbQmON_XdettkEDKJW0OwQYk,808
49
+ hh_applicant_tool/storage/repositories/vacancies.py,sha256=ykrn204H5hkbXVo_Ga-9Ry9BJ-b6EKHm-nht713B3p0,181
50
+ hh_applicant_tool/storage/utils.py,sha256=tjQDlsB1p7_mvXAipuxddWKpjV-V5G1YGCTu-52OXB4,1610
51
+ hh_applicant_tool/utils/__init__.py,sha256=OD_dtkJUt81bGKobilsxs1BBwxqfmAu-Ga3PNjaWWRQ,744
52
+ hh_applicant_tool/utils/attrdict.py,sha256=wlbow24rovk9kZ0lc0xA4ULlgZ-UWj4GUAmZhAbW9Wg,155
53
+ hh_applicant_tool/utils/binpack.py,sha256=qW55IPleip_wi1GyNBhPwCYP3GGQzi8BASGKD3sOIw8,4658
54
+ hh_applicant_tool/utils/config.py,sha256=cZUVt5F15BbY47X4fAvOLtpBUp9hxwzEMOG30jS30Ro,1600
55
+ hh_applicant_tool/utils/dateutil.py,sha256=foPghTLlgThL-oiMu_LedaG8KS5IMfjGo1wsrAanhPM,456
56
+ hh_applicant_tool/utils/jsonc.py,sha256=WATp8MeKglEI6MRdXWBmqvW3q8IRNpt7UPjAKIf5VBs,4072
57
+ hh_applicant_tool/utils/jsonutil.py,sha256=uW5Ajdv_IYCfVRHJiUVjQnb5nI7XRMva-s3QcVWruvY,1701
58
+ hh_applicant_tool/utils/log.py,sha256=-RtmzoSY9TD4JC2tG2k01x-nJRuYg2eYFXSIH5GGGCA,4084
59
+ hh_applicant_tool/utils/misc.py,sha256=BW0oUEozsMS8WBBIH6y-lUC_MBkuwhO2zbL0nM_Fi50,238
60
+ hh_applicant_tool/utils/mixins.py,sha256=kCMO4oehmWXkrmz8ogJUpUQ1Mk5W-eBNz75pmWoZaas,7282
61
+ hh_applicant_tool/utils/string.py,sha256=dQn1uhwYXesjRmqkDn5HAHOdeaGF2v1HXpT1Quff6Eg,618
62
+ hh_applicant_tool/utils/terminal.py,sha256=pn5aKU3H5t4RXepdNynM-QX8imgxKsriM0EL7AnZ7oM,676
63
+ hh_applicant_tool/utils/user_agent.py,sha256=4Qmql_E9wxhWfZ_ohmr1cqi2bzgYvbKvcsOXrB1KBL8,498
64
+ hh_applicant_tool-1.4.7.dist-info/METADATA,sha256=5ARmoKG8tGX_YpfJiZjTCsIjnVWFbq0MMr9r7JV3Vdk,43268
65
+ hh_applicant_tool-1.4.7.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
66
+ hh_applicant_tool-1.4.7.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
67
+ hh_applicant_tool-1.4.7.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,109 +0,0 @@
1
- # Этот модуль можно использовать как образец для других
2
- import argparse
3
- import logging
4
- from datetime import datetime, timedelta, timezone
5
-
6
- from ..api import ApiClient, ClientError
7
- from ..constants import INVALID_ISO8601_FORMAT
8
- from ..main import BaseOperation
9
- from ..main import Namespace as BaseNamespace
10
- from ..types import ApiListResponse
11
- from ..utils import print_err, truncate_string
12
-
13
- logger = logging.getLogger(__package__)
14
-
15
-
16
- class Namespace(BaseNamespace):
17
- older_than: int
18
- blacklist_discard: bool
19
- all: bool
20
-
21
-
22
- class Operation(BaseOperation):
23
- """Отменяет старые отклики, скрывает отказы с опциональной блокировкой работодателя."""
24
-
25
- def setup_parser(self, parser: argparse.ArgumentParser) -> None:
26
- parser.add_argument(
27
- "--older-than",
28
- type=int,
29
- default=30,
30
- help="Удалить отклики старше опр. кол-ва дней. По умолчанию: %(default)d",
31
- )
32
- parser.add_argument(
33
- "--all",
34
- action=argparse.BooleanOptionalAction,
35
- help="Удалить все отклики в тч с приглашениями",
36
- )
37
- parser.add_argument(
38
- "--blacklist-discard",
39
- help="Если установлен, то заблокирует работодателя в случае отказа, чтобы его вакансии не отображались в возможных",
40
- action=argparse.BooleanOptionalAction,
41
- )
42
-
43
- def _get_active_negotiations(self, api_client: ApiClient) -> list[dict]:
44
- rv = []
45
- page = 0
46
- per_page = 100
47
- while True:
48
- r: ApiListResponse = api_client.get(
49
- "/negotiations", page=page, per_page=per_page, status="active"
50
- )
51
- rv.extend(r["items"])
52
- page += 1
53
- if page >= r["pages"]:
54
- break
55
- return rv
56
-
57
- def run(self, args: Namespace, api_client: ApiClient, *_) -> None:
58
- negotiations = self._get_active_negotiations(api_client)
59
- print("Всего активных:", len(negotiations))
60
- for item in negotiations:
61
- state = item["state"]
62
- # messaging_status archived
63
- # decline_allowed False
64
- # hidden True
65
- is_discard = state["id"] == "discard"
66
- if not item["hidden"] and (
67
- args.all
68
- or is_discard
69
- or (
70
- state["id"] == "response"
71
- and (datetime.utcnow() - timedelta(days=args.older_than)).replace(
72
- tzinfo=timezone.utc
73
- )
74
- > datetime.strptime(item["updated_at"], INVALID_ISO8601_FORMAT)
75
- )
76
- ):
77
- decline_allowed = item.get("decline_allowed") or False
78
- r = api_client.delete(
79
- f"/negotiations/active/{item['id']}",
80
- with_decline_message=decline_allowed,
81
- )
82
- assert {} == r
83
- vacancy = item["vacancy"]
84
- print(
85
- "❌ Удалили",
86
- state["name"].lower(),
87
- vacancy["alternate_url"],
88
- "(",
89
- truncate_string(vacancy["name"]),
90
- ")",
91
- )
92
- if is_discard and args.blacklist_discard:
93
- employer = vacancy.get("employer", {})
94
- if not employer or 'id' not in employer:
95
- # Работодатель удален или скрыт
96
- continue
97
- try:
98
- r = api_client.put(f"/employers/blacklisted/{employer['id']}")
99
- assert not r
100
- print(
101
- "🚫 Заблокировали",
102
- employer["alternate_url"],
103
- "(",
104
- truncate_string(employer["name"]),
105
- ")",
106
- )
107
- except ClientError as ex:
108
- print_err("❗ Ошибка:", ex)
109
- print("🧹 Чистка заявок завершена!")
@@ -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)