hh-applicant-tool 0.2.1__py3-none-any.whl → 0.2.3__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.
Potentially problematic release.
This version of hh-applicant-tool might be problematic. Click here for more details.
- hh_applicant_tool/api/errors.py +6 -6
- hh_applicant_tool/constants.py +1 -1
- hh_applicant_tool/operations/add_handler.py +11 -0
- hh_applicant_tool/operations/apply_similar.py +54 -18
- hh_applicant_tool/operations/authorize.py +19 -0
- hh_applicant_tool/utils.py +1 -1
- {hh_applicant_tool-0.2.1.dist-info → hh_applicant_tool-0.2.3.dist-info}/METADATA +25 -9
- {hh_applicant_tool-0.2.1.dist-info → hh_applicant_tool-0.2.3.dist-info}/RECORD +10 -10
- {hh_applicant_tool-0.2.1.dist-info → hh_applicant_tool-0.2.3.dist-info}/WHEEL +1 -1
- {hh_applicant_tool-0.2.1.dist-info → hh_applicant_tool-0.2.3.dist-info}/entry_points.txt +0 -0
hh_applicant_tool/api/errors.py
CHANGED
|
@@ -37,11 +37,11 @@ class ApiError(Exception):
|
|
|
37
37
|
def response_headers(self) -> CaseInsensitiveDict:
|
|
38
38
|
return self._response.headers
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
# def __getattr__(self, name: str) -> Any:
|
|
41
|
+
# try:
|
|
42
|
+
# return self._raw[name]
|
|
43
|
+
# except KeyError as ex:
|
|
44
|
+
# raise AttributeError(name) from ex
|
|
45
45
|
|
|
46
46
|
def __str__(self) -> str:
|
|
47
47
|
return str(self._raw)
|
|
@@ -58,7 +58,7 @@ class ClientError(ApiError):
|
|
|
58
58
|
class BadRequest(ClientError):
|
|
59
59
|
@property
|
|
60
60
|
def limit_exceeded(self) -> bool:
|
|
61
|
-
return any(x["value"] == "limit_exceeded" for x in self.errors)
|
|
61
|
+
return any(x["value"] == "limit_exceeded" for x in self._raw["errors"])
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class Forbidden(ClientError):
|
hh_applicant_tool/constants.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
1
|
+
DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
|
2
2
|
ANDROID_CLIENT_ID = (
|
|
3
3
|
"HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD"
|
|
4
4
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from subprocess import check_call
|
|
5
6
|
|
|
@@ -36,6 +37,12 @@ class Operation(BaseOperation):
|
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
def run(self, args: Namespace) -> None:
|
|
40
|
+
# Проверка, запущен ли скрипт в WSL
|
|
41
|
+
if self.is_wsl():
|
|
42
|
+
print_err("⚠️ Предупреждение: Скрипт запущен в WSL 💩. Функциональность может быть ограничена или не работать вовсе.")
|
|
43
|
+
print_err("Рекомендуется запуск на нативных Linux-системах.")
|
|
44
|
+
return 1
|
|
45
|
+
|
|
39
46
|
# TODO: с root не будет работать
|
|
40
47
|
desktop_path = Path(
|
|
41
48
|
"~/.local/share/applications/hhandroid.desktop"
|
|
@@ -48,3 +55,7 @@ class Operation(BaseOperation):
|
|
|
48
55
|
else:
|
|
49
56
|
print_err("⛔ Обработчик уже существует!")
|
|
50
57
|
return 1
|
|
58
|
+
|
|
59
|
+
def is_wsl(self) -> bool:
|
|
60
|
+
"""Проверяет, запущен ли скрипт в WSL."""
|
|
61
|
+
return "WSL_DISTRO_NAME" in os.environ
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Этот модуль можно использовать как образец для других
|
|
2
1
|
import argparse
|
|
3
2
|
import logging
|
|
4
3
|
import random
|
|
5
|
-
|
|
4
|
+
import time
|
|
5
|
+
from typing import TextIO, Tuple
|
|
6
6
|
|
|
7
7
|
from ..api import ApiClient, ApiError, BadRequest
|
|
8
8
|
from ..main import BaseOperation
|
|
@@ -16,6 +16,9 @@ logger = logging.getLogger(__package__)
|
|
|
16
16
|
class Namespace(BaseNamespace):
|
|
17
17
|
resume_id: str | None
|
|
18
18
|
message_list: TextIO
|
|
19
|
+
force_message: bool
|
|
20
|
+
apply_interval: Tuple[float, float]
|
|
21
|
+
page_interval: Tuple[float, float]
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
class Operation(BaseOperation):
|
|
@@ -34,6 +37,27 @@ class Operation(BaseOperation):
|
|
|
34
37
|
default=False,
|
|
35
38
|
action=argparse.BooleanOptionalAction,
|
|
36
39
|
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--apply-interval",
|
|
42
|
+
help="Интервал между отправкой откликов в секундах (X, X-Y)",
|
|
43
|
+
default="1-5",
|
|
44
|
+
type=self._parse_interval,
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--page-interval",
|
|
48
|
+
help="Интервал между получением следующей страницы рекомендованных вакансий в секундах (X, X-Y)",
|
|
49
|
+
default="1-3",
|
|
50
|
+
type=self._parse_interval,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _parse_interval(interval: str) -> Tuple[float, float]:
|
|
55
|
+
"""Парсит строку интервала и возвращает кортеж с минимальным и максимальным значениями."""
|
|
56
|
+
if "-" in interval:
|
|
57
|
+
min_interval, max_interval = map(float, interval.split("-"))
|
|
58
|
+
else:
|
|
59
|
+
min_interval = max_interval = float(interval)
|
|
60
|
+
return min(min_interval, max_interval), max(min_interval, max_interval)
|
|
37
61
|
|
|
38
62
|
def run(self, args: Namespace) -> None:
|
|
39
63
|
assert args.config["token"]
|
|
@@ -45,8 +69,6 @@ class Operation(BaseOperation):
|
|
|
45
69
|
resume_id := args.resume_id or args.config["default_resume_id"]
|
|
46
70
|
):
|
|
47
71
|
resumes: ApiListResponse = api.get("/resumes/mine")
|
|
48
|
-
# Используем id первого резюме
|
|
49
|
-
# TODO: создать 10 резюме и рассылать по 2000 откликов в сутки
|
|
50
72
|
resume_id = resumes["items"][0]["id"]
|
|
51
73
|
if args.message_list:
|
|
52
74
|
application_messages = list(
|
|
@@ -54,33 +76,41 @@ class Operation(BaseOperation):
|
|
|
54
76
|
)
|
|
55
77
|
else:
|
|
56
78
|
application_messages = [
|
|
57
|
-
"Меня заинтересовала
|
|
58
|
-
"Прошу рассмотреть мою кандидатуру на вакансию %(name)s",
|
|
79
|
+
"Меня заинтересовала ваша вакансия %(name)s",
|
|
80
|
+
"Прошу рассмотреть мою жалкую кандидатуру на вакансию %(name)s",
|
|
81
|
+
"Ваша вакансия %(name)s соответствует моим навыкам и опыту",
|
|
82
|
+
"Хочу присоединиться к вашей успешной команде лидеров рынка в качестве %(name)s",
|
|
83
|
+
"Мое резюме содержит все баззворды, указанные в вашей вакансии %(name)s",
|
|
59
84
|
]
|
|
85
|
+
|
|
86
|
+
apply_min_interval, apply_max_interval = args.apply_interval
|
|
87
|
+
page_min_interval, page_max_interval = args.page_interval
|
|
88
|
+
|
|
60
89
|
self._apply_similar(
|
|
61
|
-
api, resume_id, args.force_message, application_messages
|
|
90
|
+
api, resume_id, args.force_message, application_messages, apply_min_interval, apply_max_interval, page_min_interval, page_max_interval
|
|
62
91
|
)
|
|
63
92
|
|
|
64
93
|
def _get_vacancies(
|
|
65
|
-
self, api: ApiClient, resume_id: str
|
|
94
|
+
self, api: ApiClient, resume_id: str, page_min_interval: float, page_max_interval: float
|
|
66
95
|
) -> list[VacancyItem]:
|
|
67
96
|
rv = []
|
|
68
|
-
# работает ограничение: глубина возвращаемых результатов не может быть больше 2000
|
|
69
|
-
# Номер страницы (считается от 0, по умолчанию - 0)
|
|
70
97
|
per_page = 100
|
|
71
98
|
for page in range(20):
|
|
72
99
|
res: ApiListResponse = api.get(
|
|
73
100
|
f"/resumes/{resume_id}/similar_vacancies",
|
|
74
101
|
page=page,
|
|
75
102
|
per_page=per_page,
|
|
76
|
-
# Мне кажется, что так поисковая выдача можно забиться неадекватами, которые по полгода кого-то ищут
|
|
77
|
-
# Но так откликается на что-то уж совсем нерелевантное
|
|
78
|
-
# order_by="publication_time",
|
|
79
103
|
order_by="relevance",
|
|
80
104
|
)
|
|
81
105
|
rv.extend(res["items"])
|
|
82
106
|
if page >= res["pages"] - 1:
|
|
83
107
|
break
|
|
108
|
+
|
|
109
|
+
# Задержка перед получением следующей страницы
|
|
110
|
+
if page > 0:
|
|
111
|
+
interval = random.uniform(page_min_interval, page_max_interval)
|
|
112
|
+
time.sleep(interval)
|
|
113
|
+
|
|
84
114
|
return rv
|
|
85
115
|
|
|
86
116
|
def _apply_similar(
|
|
@@ -89,15 +119,20 @@ class Operation(BaseOperation):
|
|
|
89
119
|
resume_id: str,
|
|
90
120
|
force_message: bool,
|
|
91
121
|
application_messages: list[str],
|
|
122
|
+
apply_min_interval: float,
|
|
123
|
+
apply_max_interval: float,
|
|
124
|
+
page_min_interval: float,
|
|
125
|
+
page_max_interval: float,
|
|
92
126
|
) -> None:
|
|
93
|
-
# Получаем список рекомендованных вакансий и отправляем заявки
|
|
94
|
-
# Проблема тут в том, что вакансии на которые мы отклимкались должны исчезать из поиска, но ОНИ ТАМ ПРИСУТСТВУЮТ. Так же есть вакансии с ебучими тестами, которые всегда вверху. Вроде можно отсортировать по дате, а потом постепенно уменьшать диапазон, но он не точный и округляется до 5 минут, а потому там повторы
|
|
95
127
|
item: VacancyItem
|
|
96
|
-
for item in self._get_vacancies(api, resume_id):
|
|
97
|
-
# В рот я ебал вас и ваши тесты, пидоры
|
|
128
|
+
for item in self._get_vacancies(api, resume_id, page_min_interval, page_max_interval):
|
|
98
129
|
if item["has_test"]:
|
|
99
130
|
continue
|
|
100
|
-
|
|
131
|
+
|
|
132
|
+
# Задержка перед отправкой отклика
|
|
133
|
+
interval = random.uniform(apply_min_interval, apply_max_interval)
|
|
134
|
+
time.sleep(interval)
|
|
135
|
+
|
|
101
136
|
params = {
|
|
102
137
|
"resume_id": resume_id,
|
|
103
138
|
"vacancy_id": item["id"],
|
|
@@ -121,4 +156,5 @@ class Operation(BaseOperation):
|
|
|
121
156
|
print_err("❗ Ошибка:", ex)
|
|
122
157
|
if isinstance(ex, BadRequest) and ex.limit_exceeded:
|
|
123
158
|
break
|
|
159
|
+
|
|
124
160
|
print("📝 Отклики на вакансии разосланы!")
|
|
@@ -61,6 +61,17 @@ class Operation(BaseOperation):
|
|
|
61
61
|
pass
|
|
62
62
|
|
|
63
63
|
def run(self, args: Namespace) -> None:
|
|
64
|
+
# Проверяем, установлен ли socat
|
|
65
|
+
if not self.is_socat_installed():
|
|
66
|
+
print("⚠️ Предупреждение: socat не установлен. Для работы с unix-сокетами рекомендуется установить socat.")
|
|
67
|
+
print()
|
|
68
|
+
print("Вы можете установить socat с помощью вашего пакетного менеджера, например:")
|
|
69
|
+
print()
|
|
70
|
+
print(" - Debian/Ubuntu: sudo apt-get install socat")
|
|
71
|
+
print(" - Fedora: sudo dnf install socat")
|
|
72
|
+
print(" - Arch/Manjaro: sudo pacman -S socat")
|
|
73
|
+
print()
|
|
74
|
+
|
|
64
75
|
oauth = OAuthClient(
|
|
65
76
|
user_agent=(
|
|
66
77
|
args.config["oauth_user_agent"] or args.config["user_agent"]
|
|
@@ -76,3 +87,11 @@ class Operation(BaseOperation):
|
|
|
76
87
|
HHANDROID_SOCKET_PATH, oauth_client=oauth, config=args.config
|
|
77
88
|
)
|
|
78
89
|
server.serve_forever()
|
|
90
|
+
|
|
91
|
+
def is_socat_installed(self) -> bool:
|
|
92
|
+
"""Проверяет, установлен ли socat в системе."""
|
|
93
|
+
try:
|
|
94
|
+
subprocess.run(["socat", "-h"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
95
|
+
return True
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
return False
|
hh_applicant_tool/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -8,13 +8,21 @@ Requires-Python: >=3.10,<4.0
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
12
|
Requires-Dist: prettytable (>=3.6.0,<4.0.0)
|
|
12
13
|
Requires-Dist: requests (>=2.28.2,<3.0.0)
|
|
13
14
|
Description-Content-Type: text/markdown
|
|
14
15
|
|
|
15
16
|
# HH Applicant Tool
|
|
16
17
|
|
|
17
|
-
!
|
|
18
|
+
> ! Наложен мораторий на доработки/переработки. Разработка будет возобновлена после 100 звезд 💫
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
[]()
|
|
22
|
+
[]()
|
|
23
|
+
[]()
|
|
24
|
+
[]()
|
|
25
|
+
[]()
|
|
18
26
|
|
|
19
27
|
Утилита для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме.
|
|
20
28
|
|
|
@@ -23,13 +31,23 @@ Description-Content-Type: text/markdown
|
|
|
23
31
|
- socat
|
|
24
32
|
- python >= 3.10
|
|
25
33
|
|
|
26
|
-
Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить.
|
|
34
|
+
Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить. В Arch-based:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
yay -S socat
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Пример работы:
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
|
|
27
45
|
|
|
28
|
-
Данная утилита не
|
|
46
|
+
Данная утилита работает только в Linux. Для авторизации требуется наличие графического окружения (это не совсем верно, но мне лень расписывать...), после нее вы можете перенести `~/.config/hh-applicant-tool/config.json` на сервер и запускать утилиту через systemd или cron, либо же вообще делать это через **WSL**. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
|
|
29
47
|
|
|
30
48
|
Предыстория.
|
|
31
49
|
|
|
32
|
-
Был один знакомый знакомого, который работал
|
|
50
|
+
Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отлчить Java от JavaScript, но я думаю, что в значительном числе случаев, тут имеют место такие вот рассылки... И я перенял эту порочную практику. Мне уже просто лень читать весь этот бред, что пишут в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое. Поэтому тупло спамлю в надежде на идеальную работу. Долгое время (пару недель в октябре 2022) я делал массовые заявки с помощью консоли браузера:
|
|
33
51
|
|
|
34
52
|
```js
|
|
35
53
|
$$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
|
|
@@ -71,9 +89,7 @@ $ hh-applicant-tool apply-similar
|
|
|
71
89
|
# Поднимаем резюме
|
|
72
90
|
$ hh-applicant-tool update-resumes
|
|
73
91
|
|
|
74
|
-
# Чистим заявки и баним за отказы
|
|
75
|
-
# астралохинь и прочих 3.14дарасов по блату, считающих, что погроммистом может
|
|
76
|
-
# быть только ДОЦЕНТ МАТЕМАТИЧЕСКИХ НАУК 🤡
|
|
92
|
+
# Чистим заявки и баним за отказы говноконторы
|
|
77
93
|
$ hh-applicant-tool clear-negotiations --blacklist-discard
|
|
78
94
|
```
|
|
79
95
|
|
|
@@ -119,7 +135,7 @@ https://hh.ru/employer/1918903
|
|
|
119
135
|
| **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
|
|
120
136
|
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
|
|
121
137
|
| **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
|
|
122
|
-
| **call-api** | Вызов произвольного метода API с
|
|
138
|
+
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
123
139
|
| **refresh-token** | Обновляет access_token. |
|
|
124
140
|
|
|
125
141
|
Для начала нужно добавить обработчик протокола `hhandroid`, который используется Android-приложением для усложнения жизни честным автоматизаторам:
|
|
@@ -2,14 +2,14 @@ hh_applicant_tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
2
2
|
hh_applicant_tool/__main__.py,sha256=cwKJAAML0RRKT9Qbzcwf07HHcuSd8oh7kx4P1apndWQ,84
|
|
3
3
|
hh_applicant_tool/api/__init__.py,sha256=kgFSHibRaAugN2BA3U1djEa20qgKJUUVouwJzjEB0DU,84
|
|
4
4
|
hh_applicant_tool/api/client.py,sha256=z_YMsd5zL4-1_aIbkEKqm_1m_mZkm3BMxlAQuCoNj2Y,7040
|
|
5
|
-
hh_applicant_tool/api/errors.py,sha256=
|
|
5
|
+
hh_applicant_tool/api/errors.py,sha256=0SoWKnsSUA0K2YgQA8GwGhe-pRMwtfK94MR6_MgbORQ,1722
|
|
6
6
|
hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3s,823
|
|
7
|
-
hh_applicant_tool/constants.py,sha256=
|
|
7
|
+
hh_applicant_tool/constants.py,sha256=EPtdv-60NSMUpYOUcPR2ZnLQ2P_rxrPuvbOqATw9w8w,512
|
|
8
8
|
hh_applicant_tool/main.py,sha256=Gzk1K1jJpO7aazgdXtXAgBhkfFtrteBRx895V5lf07E,3016
|
|
9
9
|
hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
hh_applicant_tool/operations/add_handler.py,sha256=
|
|
11
|
-
hh_applicant_tool/operations/apply_similar.py,sha256=
|
|
12
|
-
hh_applicant_tool/operations/authorize.py,sha256=
|
|
10
|
+
hh_applicant_tool/operations/add_handler.py,sha256=e28O0pSLIgSTwv5n0I2GHDMKYPV-dNWHYFmhbkVzq34,2288
|
|
11
|
+
hh_applicant_tool/operations/apply_similar.py,sha256=79kGig-hlLZR2rZ0YReP1CNWO9SMoGFvXJnNWa6kk2E,6590
|
|
12
|
+
hh_applicant_tool/operations/authorize.py,sha256=JEL8dlDQfy6i1YFWNUjvDpJ__Ea_PFazz0lYt_r9Afw,3782
|
|
13
13
|
hh_applicant_tool/operations/call_api.py,sha256=oWAfvy4LwbsQ8HsgI_3en3sMTlu3ZWU7NzpxssrUNSU,1472
|
|
14
14
|
hh_applicant_tool/operations/clear_negotiations.py,sha256=5ybdJMUfV9XYxnn2y4zvWdS_ZE8yXAbFJoXOMyqetw8,4041
|
|
15
15
|
hh_applicant_tool/operations/list_resumes.py,sha256=HYxjDALrWl_YBIitDldhhEs-WO_0e_2FGw586MSdsuE,1281
|
|
@@ -17,8 +17,8 @@ hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWs
|
|
|
17
17
|
hh_applicant_tool/operations/update_resumes.py,sha256=FKtwEL7i0vaOxLEn1nARi72SuMYB5VjN6Fihe6rlt-Y,1196
|
|
18
18
|
hh_applicant_tool/operations/whoami.py,sha256=kdLQ_FjYzpJPKxFlocxf7vgXhW1zocb0bd5IAWmsQuA,861
|
|
19
19
|
hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
|
|
20
|
-
hh_applicant_tool/utils.py,sha256=
|
|
21
|
-
hh_applicant_tool-0.2.
|
|
22
|
-
hh_applicant_tool-0.2.
|
|
23
|
-
hh_applicant_tool-0.2.
|
|
24
|
-
hh_applicant_tool-0.2.
|
|
20
|
+
hh_applicant_tool/utils.py,sha256=FVmE5U7eKONx7G7NIOvu5RJUqLvAr2ThOTOtch-txBs,1347
|
|
21
|
+
hh_applicant_tool-0.2.3.dist-info/METADATA,sha256=YJ25EGECBhEoinI3bRAvPp9LrvvPJs5p3i_T1I7x41k,15604
|
|
22
|
+
hh_applicant_tool-0.2.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
23
|
+
hh_applicant_tool-0.2.3.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
|
|
24
|
+
hh_applicant_tool-0.2.3.dist-info/RECORD,,
|
|
File without changes
|