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.
- hh_applicant_tool/__init__.py +1 -0
- hh_applicant_tool/__main__.py +1 -1
- hh_applicant_tool/ai/base.py +2 -0
- hh_applicant_tool/ai/openai.py +25 -35
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +65 -68
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
- hh_applicant_tool/api/datatypes.py +293 -0
- hh_applicant_tool/api/errors.py +57 -7
- hh_applicant_tool/api/user_agent.py +17 -0
- hh_applicant_tool/main.py +234 -113
- hh_applicant_tool/operations/apply_similar.py +353 -371
- hh_applicant_tool/operations/authorize.py +313 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/clear_negotiations.py +90 -82
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +23 -11
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +122 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +201 -180
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +23 -11
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +8 -0
- hh_applicant_tool/storage/facade.py +24 -0
- hh_applicant_tool/storage/models/__init__.py +0 -0
- hh_applicant_tool/storage/models/base.py +169 -0
- hh_applicant_tool/storage/models/contacts.py +28 -0
- hh_applicant_tool/storage/models/employer.py +12 -0
- hh_applicant_tool/storage/models/negotiation.py +16 -0
- hh_applicant_tool/storage/models/resume.py +19 -0
- hh_applicant_tool/storage/models/setting.py +6 -0
- hh_applicant_tool/storage/models/vacancy.py +36 -0
- hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
- hh_applicant_tool/storage/queries/schema.sql +132 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +230 -0
- hh_applicant_tool/storage/repositories/contacts.py +14 -0
- hh_applicant_tool/storage/repositories/employers.py +14 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +13 -0
- hh_applicant_tool/storage/repositories/resumes.py +9 -0
- hh_applicant_tool/storage/repositories/settings.py +35 -0
- hh_applicant_tool/storage/repositories/vacancies.py +9 -0
- hh_applicant_tool/storage/utils.py +40 -0
- hh_applicant_tool/utils/__init__.py +31 -0
- hh_applicant_tool/utils/attrdict.py +6 -0
- hh_applicant_tool/utils/binpack.py +167 -0
- hh_applicant_tool/utils/config.py +55 -0
- hh_applicant_tool/utils/date.py +19 -0
- hh_applicant_tool/utils/json.py +61 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/log.py +147 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +221 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +32 -0
- hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
- hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
- hh_applicant_tool/ai/blackbox.py +0 -55
- hh_applicant_tool/color_log.py +0 -47
- hh_applicant_tool/mixins.py +0 -13
- hh_applicant_tool/operations/delete_telemetry.py +0 -30
- hh_applicant_tool/operations/get_employer_contacts.py +0 -348
- hh_applicant_tool/telemetry_client.py +0 -106
- hh_applicant_tool/types.py +0 -45
- hh_applicant_tool/utils.py +0 -119
- hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
- hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
- {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,,
|
hh_applicant_tool/ai/blackbox.py
DELETED
|
@@ -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
|
hh_applicant_tool/color_log.py
DELETED
|
@@ -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
|
hh_applicant_tool/mixins.py
DELETED
|
@@ -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"])
|