hh-applicant-tool 0.3.3__tar.gz → 0.3.5__tar.gz
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-0.3.3 → hh_applicant_tool-0.3.5}/PKG-INFO +33 -4
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/README.md +32 -3
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/apply_similar.py +81 -23
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/pyproject.toml +1 -1
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/__init__.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/__main__.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/api/__init__.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/api/client.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/api/errors.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/color_log.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/constants.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/main.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/__init__.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/authorize.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/call_api.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/clear_negotiations.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/list_resumes.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/refresh_token.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/update_resumes.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/whoami.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/telemetry_client.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/types.py +0 -0
- {hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hh-applicant-tool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Senior YAML Developer
|
|
6
6
|
Author-email: yamldeveloper@proton.me
|
|
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
38
38
|
asdf/pyenv/conda и что-то еще...
|
|
39
39
|
|
|
40
|
-
Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (
|
|
40
|
+
Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (`C:\Users\%username%\AppData\Roaming\hh-applicant-tool\config.json` — в Windows) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
|
|
41
41
|
|
|
42
42
|
Пример работы:
|
|
43
43
|
|
|
@@ -67,9 +67,34 @@ $ pipx install 'hh-applicant-tool[qt]'
|
|
|
67
67
|
$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
|
|
68
68
|
|
|
69
69
|
# Для обновления до новой версии
|
|
70
|
-
$ pipx upgrade
|
|
70
|
+
$ pipx upgrade hh-applicant-tool
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Отдельно я распишу процесс установки в **Windows** в подробностях:
|
|
74
|
+
|
|
75
|
+
* Для начала поставьте Python 3 любым удобным способом.
|
|
76
|
+
* Запустите терминал/консоль от Администратора и выполните:
|
|
77
|
+
```ps
|
|
78
|
+
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
|
|
79
|
+
```
|
|
80
|
+
Без этой настройки не будут работать виртуальные окружения.
|
|
81
|
+
* Создайте и активируйте виртуальное окружение:
|
|
82
|
+
```ps
|
|
83
|
+
PS> python -m pip venv hh-applicant-venv
|
|
84
|
+
PS> .\hh-applicant-venv\Scripts\activate
|
|
85
|
+
```
|
|
86
|
+
* Поставьте все пакеты в виртуальное окружение `hh-applicant-tool`:
|
|
87
|
+
```ps
|
|
88
|
+
(hh-applicant-venv) PS> pip install hh-applicant-tool[qt]
|
|
89
|
+
```
|
|
90
|
+
* Проверьте работает ли оно:
|
|
91
|
+
```ps
|
|
92
|
+
(hh-applicant-venv) PS> hh-applicant-tool -h
|
|
93
|
+
```
|
|
94
|
+
* В случае неудачи, вернитесь к первому шагу. Для последующих запусков сперва активируйте виртуальное окружение.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
73
98
|
Использование:
|
|
74
99
|
|
|
75
100
|
```bash
|
|
@@ -135,7 +160,7 @@ https://hh.ru/employer/1918903
|
|
|
135
160
|
| **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
|
|
136
161
|
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
|
|
137
162
|
| **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
|
|
138
|
-
| **call-api** | Вызов произвольного метода API с выводом результата.
|
|
163
|
+
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
139
164
|
| **refresh-token** | Обновляет access_token. |
|
|
140
165
|
|
|
141
166
|
Авторизуемся:
|
|
@@ -146,6 +171,10 @@ $ hh-applicant-tool -vv authorize
|
|
|
146
171
|
|
|
147
172
|

|
|
148
173
|
|
|
174
|
+
> В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
149
178
|
В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
|
|
150
179
|
|
|
151
180
|
```json
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
Работает с Python >= 3.10. Нужную версию Python можно поставить через
|
|
19
19
|
asdf/pyenv/conda и что-то еще...
|
|
20
20
|
|
|
21
|
-
Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (
|
|
21
|
+
Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (`C:\Users\%username%\AppData\Roaming\hh-applicant-tool\config.json` — в Windows) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
|
|
22
22
|
|
|
23
23
|
Пример работы:
|
|
24
24
|
|
|
@@ -48,9 +48,34 @@ $ pipx install 'hh-applicant-tool[qt]'
|
|
|
48
48
|
$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool
|
|
49
49
|
|
|
50
50
|
# Для обновления до новой версии
|
|
51
|
-
$ pipx upgrade
|
|
51
|
+
$ pipx upgrade hh-applicant-tool
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
Отдельно я распишу процесс установки в **Windows** в подробностях:
|
|
55
|
+
|
|
56
|
+
* Для начала поставьте Python 3 любым удобным способом.
|
|
57
|
+
* Запустите терминал/консоль от Администратора и выполните:
|
|
58
|
+
```ps
|
|
59
|
+
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
|
|
60
|
+
```
|
|
61
|
+
Без этой настройки не будут работать виртуальные окружения.
|
|
62
|
+
* Создайте и активируйте виртуальное окружение:
|
|
63
|
+
```ps
|
|
64
|
+
PS> python -m pip venv hh-applicant-venv
|
|
65
|
+
PS> .\hh-applicant-venv\Scripts\activate
|
|
66
|
+
```
|
|
67
|
+
* Поставьте все пакеты в виртуальное окружение `hh-applicant-tool`:
|
|
68
|
+
```ps
|
|
69
|
+
(hh-applicant-venv) PS> pip install hh-applicant-tool[qt]
|
|
70
|
+
```
|
|
71
|
+
* Проверьте работает ли оно:
|
|
72
|
+
```ps
|
|
73
|
+
(hh-applicant-venv) PS> hh-applicant-tool -h
|
|
74
|
+
```
|
|
75
|
+
* В случае неудачи, вернитесь к первому шагу. Для последующих запусков сперва активируйте виртуальное окружение.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
54
79
|
Использование:
|
|
55
80
|
|
|
56
81
|
```bash
|
|
@@ -116,7 +141,7 @@ https://hh.ru/employer/1918903
|
|
|
116
141
|
| **update-resumes** | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату». |
|
|
117
142
|
| **apply-similar** | Откликнуться на все подходящие вакансии. Лимит = 200 в день |
|
|
118
143
|
| **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
|
|
119
|
-
| **call-api** | Вызов произвольного метода API с выводом результата.
|
|
144
|
+
| **call-api** | Вызов произвольного метода API с выводом результата. |
|
|
120
145
|
| **refresh-token** | Обновляет access_token. |
|
|
121
146
|
|
|
122
147
|
Авторизуемся:
|
|
@@ -127,6 +152,10 @@ $ hh-applicant-tool -vv authorize
|
|
|
127
152
|
|
|
128
153
|

|
|
129
154
|
|
|
155
|
+
> В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
130
159
|
В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
|
|
131
160
|
|
|
132
161
|
```json
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/apply_similar.py
RENAMED
|
@@ -25,8 +25,9 @@ class Namespace(BaseNamespace):
|
|
|
25
25
|
page_interval: Tuple[float, float]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
# https://api.hh.ru/openapi/redoc
|
|
28
29
|
class Operation(BaseOperation):
|
|
29
|
-
"""Откликнуться на все подходящие
|
|
30
|
+
"""Откликнуться на все подходящие вакансии. По умолчанию применяются значения, которые были отмечены галочками в форме для поиска на сайте"""
|
|
30
31
|
|
|
31
32
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
32
33
|
parser.add_argument("--resume-id", help="Идентефикатор резюме")
|
|
@@ -53,6 +54,24 @@ class Operation(BaseOperation):
|
|
|
53
54
|
default="1-3",
|
|
54
55
|
type=self._parse_interval,
|
|
55
56
|
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--order-by",
|
|
59
|
+
help="Сортировка вакансий",
|
|
60
|
+
choices=[
|
|
61
|
+
"publication_time",
|
|
62
|
+
"salary_desc",
|
|
63
|
+
"salary_asc",
|
|
64
|
+
"relevance",
|
|
65
|
+
"distance",
|
|
66
|
+
],
|
|
67
|
+
default="relevance",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--search",
|
|
71
|
+
help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500', те можно и город указать, и ожидаемую зряплату",
|
|
72
|
+
type=str,
|
|
73
|
+
default=None,
|
|
74
|
+
)
|
|
56
75
|
|
|
57
76
|
@staticmethod
|
|
58
77
|
def _parse_interval(interval: str) -> Tuple[float, float]:
|
|
@@ -84,6 +103,8 @@ class Operation(BaseOperation):
|
|
|
84
103
|
apply_max_interval,
|
|
85
104
|
page_min_interval,
|
|
86
105
|
page_max_interval,
|
|
106
|
+
args.order_by,
|
|
107
|
+
args.search,
|
|
87
108
|
)
|
|
88
109
|
|
|
89
110
|
def _get_resume_id(self, args: Namespace, api: ApiClient) -> str:
|
|
@@ -119,12 +140,20 @@ class Operation(BaseOperation):
|
|
|
119
140
|
apply_max_interval: float,
|
|
120
141
|
page_min_interval: float,
|
|
121
142
|
page_max_interval: float,
|
|
143
|
+
order_by: str,
|
|
144
|
+
search: str | None = None,
|
|
122
145
|
) -> None:
|
|
123
146
|
telemetry_client = get_telemetry_client()
|
|
124
147
|
telemetry_data = defaultdict(dict)
|
|
125
148
|
|
|
126
149
|
vacancies = self._get_vacancies(
|
|
127
|
-
api,
|
|
150
|
+
api,
|
|
151
|
+
resume_id,
|
|
152
|
+
page_min_interval,
|
|
153
|
+
page_max_interval,
|
|
154
|
+
per_page=100,
|
|
155
|
+
order_by=order_by,
|
|
156
|
+
search=search,
|
|
128
157
|
)
|
|
129
158
|
|
|
130
159
|
self._collect_vacancy_telemetry(telemetry_data, vacancies)
|
|
@@ -134,7 +163,7 @@ class Operation(BaseOperation):
|
|
|
134
163
|
if getenv("TEST_TELEMETRY"):
|
|
135
164
|
break
|
|
136
165
|
|
|
137
|
-
if vacancy
|
|
166
|
+
if vacancy.get("has_test"):
|
|
138
167
|
print("🚫 Пропускаем тест", vacancy["alternate_url"])
|
|
139
168
|
continue
|
|
140
169
|
|
|
@@ -147,16 +176,23 @@ class Operation(BaseOperation):
|
|
|
147
176
|
)
|
|
148
177
|
continue
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
|
|
179
|
+
try:
|
|
180
|
+
employer_id = vacancy["employer"]["id"]
|
|
181
|
+
except IndexError:
|
|
182
|
+
logger.warning(
|
|
183
|
+
f"Вакансия без работодателя: {vacancy['alternate_url']}"
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
employer = api.get(f"/employers/{employer_id}")
|
|
187
|
+
|
|
188
|
+
telemetry_data["employers"][employer_id] = {
|
|
189
|
+
"name": employer.get("name"),
|
|
190
|
+
"type": employer.get("type"),
|
|
191
|
+
"description": employer.get("description"),
|
|
192
|
+
"site_url": employer.get("site_url"),
|
|
193
|
+
"area": employer.get("area", {}).get("name"), # город
|
|
194
|
+
}
|
|
152
195
|
|
|
153
|
-
telemetry_data["employers"][employer_id] = {
|
|
154
|
-
"name": employer.get("name"),
|
|
155
|
-
"type": employer.get("type"),
|
|
156
|
-
"description": employer.get("description"),
|
|
157
|
-
"site_url": employer.get("site_url"),
|
|
158
|
-
"area": employer.get("area", {}).get("name"), # город
|
|
159
|
-
}
|
|
160
196
|
# Задержка перед отправкой отклика
|
|
161
197
|
interval = random.uniform(
|
|
162
198
|
apply_min_interval, apply_max_interval
|
|
@@ -166,13 +202,23 @@ class Operation(BaseOperation):
|
|
|
166
202
|
params = {
|
|
167
203
|
"resume_id": resume_id,
|
|
168
204
|
"vacancy_id": vacancy["id"],
|
|
169
|
-
"message":
|
|
170
|
-
random.choice(application_messages) % vacancy
|
|
171
|
-
if force_message or vacancy["response_letter_required"]
|
|
172
|
-
else ""
|
|
173
|
-
),
|
|
205
|
+
"message": "",
|
|
174
206
|
}
|
|
175
207
|
|
|
208
|
+
if vacancy.get("response_letter_required"):
|
|
209
|
+
message_template = random.choice(application_messages)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
params["message"] = template_message % vacancy
|
|
213
|
+
except TypeError as ex:
|
|
214
|
+
# TypeError: not enough arguments for format string
|
|
215
|
+
# API HH все кривое, иногда нет идентификатора работодателя, иногда у вакансии нет названия.
|
|
216
|
+
# И это типа рашн хайлоад, где из-за дрочки на аджайл слепили кривую говнину.
|
|
217
|
+
logger.error(
|
|
218
|
+
f"Ошибка форматирования шаблона сообщения {template_message!r} для {vacancy = }"
|
|
219
|
+
)
|
|
220
|
+
continue
|
|
221
|
+
|
|
176
222
|
res = api.post("/negotiations", params)
|
|
177
223
|
assert res == {}
|
|
178
224
|
print(
|
|
@@ -198,14 +244,20 @@ class Operation(BaseOperation):
|
|
|
198
244
|
page_min_interval: float,
|
|
199
245
|
page_max_interval: float,
|
|
200
246
|
per_page: int,
|
|
247
|
+
order_by: str,
|
|
248
|
+
search: str | None = None,
|
|
201
249
|
) -> list[VacancyItem]:
|
|
202
250
|
rv = []
|
|
203
251
|
for page in range(20):
|
|
252
|
+
params = {
|
|
253
|
+
"page": page,
|
|
254
|
+
"per_page": per_page,
|
|
255
|
+
"order_by": order_by,
|
|
256
|
+
}
|
|
257
|
+
if search:
|
|
258
|
+
params["text"] = search
|
|
204
259
|
res: ApiListResponse = api.get(
|
|
205
|
-
f"/resumes/{resume_id}/similar_vacancies",
|
|
206
|
-
page=page,
|
|
207
|
-
per_page=per_page,
|
|
208
|
-
order_by="relevance",
|
|
260
|
+
f"/resumes/{resume_id}/similar_vacancies", params
|
|
209
261
|
)
|
|
210
262
|
rv.extend(res["items"])
|
|
211
263
|
|
|
@@ -242,7 +294,11 @@ class Operation(BaseOperation):
|
|
|
242
294
|
"contacts": vacancy.get(
|
|
243
295
|
"contacts"
|
|
244
296
|
), # пиздорванки там телеграм для связи указывают
|
|
245
|
-
|
|
297
|
+
# HH с точки зрения перфикциониста — кусок говна, где кривые
|
|
298
|
+
# форматы даты, у вакансий может не быть работодателя...
|
|
299
|
+
"employer_id": int(vacancy["employer"]["id"])
|
|
300
|
+
if "employer" in vacancy and "id" in vacancy["employer"]
|
|
301
|
+
else None,
|
|
246
302
|
# Остальное неинтересно
|
|
247
303
|
}
|
|
248
304
|
|
|
@@ -250,7 +306,9 @@ class Operation(BaseOperation):
|
|
|
250
306
|
self, telemetry_client, telemetry_data: defaultdict
|
|
251
307
|
) -> None:
|
|
252
308
|
try:
|
|
253
|
-
res = telemetry_client.send_telemetry(
|
|
309
|
+
res = telemetry_client.send_telemetry(
|
|
310
|
+
"/collect", dict(telemetry_data)
|
|
311
|
+
)
|
|
254
312
|
logger.debug(res)
|
|
255
313
|
except TelemetryError as ex:
|
|
256
314
|
logger.error(ex)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/__init__.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/authorize.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/call_api.py
RENAMED
|
File without changes
|
|
File without changes
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/list_resumes.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/refresh_token.py
RENAMED
|
File without changes
|
{hh_applicant_tool-0.3.3 → hh_applicant_tool-0.3.5}/hh_applicant_tool/operations/update_resumes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|