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.
- 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 +23 -33
- hh_applicant_tool/api/client.py +50 -64
- hh_applicant_tool/api/errors.py +51 -7
- hh_applicant_tool/constants.py +0 -3
- hh_applicant_tool/datatypes.py +291 -0
- hh_applicant_tool/main.py +233 -111
- hh_applicant_tool/operations/apply_similar.py +266 -362
- hh_applicant_tool/operations/authorize.py +256 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_negotiations.py +102 -0
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +24 -10
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +120 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +148 -167
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +21 -10
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +4 -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/contact.py +16 -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 +119 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +176 -0
- hh_applicant_tool/storage/repositories/contacts.py +19 -0
- hh_applicant_tool/storage/repositories/employers.py +13 -0
- hh_applicant_tool/storage/repositories/negotiations.py +12 -0
- hh_applicant_tool/storage/repositories/resumes.py +14 -0
- hh_applicant_tool/storage/repositories/settings.py +34 -0
- hh_applicant_tool/storage/repositories/vacancies.py +8 -0
- hh_applicant_tool/storage/utils.py +49 -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/dateutil.py +19 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/jsonutil.py +61 -0
- hh_applicant_tool/utils/log.py +144 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +220 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +19 -0
- hh_applicant_tool/utils/user_agent.py +17 -0
- hh_applicant_tool-1.4.7.dist-info/METADATA +628 -0
- hh_applicant_tool-1.4.7.dist-info/RECORD +67 -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/clear_negotiations.py +0 -109
- 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.7.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.7.dist-info}/entry_points.txt +0 -0
|
@@ -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"])
|
hh_applicant_tool/types.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from typing import TypedDict, Literal
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class AccessToken(TypedDict):
|
|
5
|
-
access_token: str
|
|
6
|
-
refresh_token: str
|
|
7
|
-
expires_in: int
|
|
8
|
-
token_type: Literal["bearer"]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ApiListResponse(TypedDict):
|
|
12
|
-
...
|
|
13
|
-
items: list
|
|
14
|
-
found: int
|
|
15
|
-
page: int
|
|
16
|
-
pages: int
|
|
17
|
-
per_page: int
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class VacancyItem(TypedDict):
|
|
21
|
-
accept_incomplete_resumes: bool
|
|
22
|
-
address: dict
|
|
23
|
-
alternate_url: str
|
|
24
|
-
apply_alternate_url: str
|
|
25
|
-
area: dict
|
|
26
|
-
contacts: dict
|
|
27
|
-
counters: dict
|
|
28
|
-
department: dict
|
|
29
|
-
employer: dict
|
|
30
|
-
has_test: bool
|
|
31
|
-
id: int
|
|
32
|
-
insider_interview: dict
|
|
33
|
-
name: str
|
|
34
|
-
professional_roles: list
|
|
35
|
-
published_at: str
|
|
36
|
-
relations: list
|
|
37
|
-
response_letter_required: bool
|
|
38
|
-
response_url: str | None
|
|
39
|
-
salary: dict
|
|
40
|
-
schedule: dict
|
|
41
|
-
snippet: dict
|
|
42
|
-
sort_point_distance: float
|
|
43
|
-
type: dict
|
|
44
|
-
url: str
|
|
45
|
-
experience: dict
|
hh_applicant_tool/utils.py
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import json
|
|
5
|
-
import platform
|
|
6
|
-
import random
|
|
7
|
-
import re
|
|
8
|
-
import sys
|
|
9
|
-
import uuid
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
from functools import partial
|
|
12
|
-
from os import getenv
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from threading import Lock
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
from .constants import INVALID_ISO8601_FORMAT
|
|
18
|
-
|
|
19
|
-
print_err = partial(print, file=sys.stderr, flush=True)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_config_path() -> Path:
|
|
23
|
-
match platform.system():
|
|
24
|
-
case "Windows":
|
|
25
|
-
return Path(getenv("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
26
|
-
case "Darwin": # macOS
|
|
27
|
-
return Path.home() / "Library" / "Application Support"
|
|
28
|
-
case _: # Linux and etc
|
|
29
|
-
return Path(getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class AttrDict(dict):
|
|
33
|
-
__getattr__ = dict.get
|
|
34
|
-
__setattr__ = dict.__setitem__
|
|
35
|
-
__delattr__ = dict.__delitem__
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# TODO: добавить defaults
|
|
39
|
-
class Config(dict):
|
|
40
|
-
def __init__(self, config_path: str | Path | None = None):
|
|
41
|
-
self._config_path = Path(config_path or get_config_path())
|
|
42
|
-
self._lock = Lock()
|
|
43
|
-
self.load()
|
|
44
|
-
|
|
45
|
-
def load(self) -> None:
|
|
46
|
-
if self._config_path.exists():
|
|
47
|
-
with self._lock:
|
|
48
|
-
with self._config_path.open(
|
|
49
|
-
"r", encoding="utf-8", errors="replace"
|
|
50
|
-
) as f:
|
|
51
|
-
self.update(json.load(f))
|
|
52
|
-
|
|
53
|
-
def save(self, *args: Any, **kwargs: Any) -> None:
|
|
54
|
-
self.update(*args, **kwargs)
|
|
55
|
-
self._config_path.parent.mkdir(exist_ok=True, parents=True)
|
|
56
|
-
with self._lock:
|
|
57
|
-
with self._config_path.open("w+", encoding="utf-8", errors="replace") as fp:
|
|
58
|
-
json.dump(
|
|
59
|
-
self,
|
|
60
|
-
fp,
|
|
61
|
-
ensure_ascii=True,
|
|
62
|
-
indent=2,
|
|
63
|
-
sort_keys=True,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
__getitem__ = dict.get
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def truncate_string(s: str, limit: int = 75, ellipsis: str = "…") -> str:
|
|
70
|
-
return s[:limit] + bool(s[limit:]) * ellipsis
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def make_hash(data: str) -> str:
|
|
74
|
-
# Вычисляем хеш SHA-256
|
|
75
|
-
return hashlib.sha256(data.encode()).hexdigest()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def parse_invalid_datetime(dt: str) -> datetime:
|
|
79
|
-
return datetime.strptime(dt, INVALID_ISO8601_FORMAT)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def fix_datetime(dt: str | None) -> str | None:
|
|
83
|
-
return parse_invalid_datetime(dt).isoformat() if dt is not None else None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def random_text(s: str) -> str:
|
|
87
|
-
while (
|
|
88
|
-
temp := re.sub(
|
|
89
|
-
r"{([^{}]+)}",
|
|
90
|
-
lambda m: random.choice(
|
|
91
|
-
m.group(1).split("|"),
|
|
92
|
-
),
|
|
93
|
-
s,
|
|
94
|
-
)
|
|
95
|
-
) != s:
|
|
96
|
-
s = temp
|
|
97
|
-
return s
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def parse_interval(interval: str) -> tuple[float, float]:
|
|
101
|
-
"""Парсит строку интервала и возвращает кортеж с минимальным и максимальным значениями."""
|
|
102
|
-
if "-" in interval:
|
|
103
|
-
min_interval, max_interval = map(float, interval.split("-"))
|
|
104
|
-
else:
|
|
105
|
-
min_interval = max_interval = float(interval)
|
|
106
|
-
return min(min_interval, max_interval), max(min_interval, max_interval)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def android_user_agent() -> str:
|
|
110
|
-
"""Android Default"""
|
|
111
|
-
devices = "23053RN02A, 23053RN02Y, 23053RN02I, 23053RN02L, 23077RABDC".split(", ")
|
|
112
|
-
device = random.choice(devices)
|
|
113
|
-
minor = random.randint(100, 150)
|
|
114
|
-
patch = random.randint(10000, 15000)
|
|
115
|
-
android = random.randint(11, 15)
|
|
116
|
-
return (
|
|
117
|
-
f"ru.hh.android/7.{minor}.{patch}, Device: {device}, "
|
|
118
|
-
f"Android OS: {android} (UUID: {uuid.uuid4()})"
|
|
119
|
-
)
|