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
@@ -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"])
@@ -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
@@ -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
- )