nginx-lens 0.3.1__tar.gz → 0.3.2__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 nginx-lens might be problematic. Click here for more details.

Files changed (50) hide show
  1. {nginx_lens-0.3.1/nginx_lens.egg-info → nginx_lens-0.3.2}/PKG-INFO +2 -1
  2. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/health.py +5 -1
  3. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/resolve.py +5 -1
  4. {nginx_lens-0.3.1 → nginx_lens-0.3.2/nginx_lens.egg-info}/PKG-INFO +2 -1
  5. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/nginx_lens.egg-info/requires.txt +1 -0
  6. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/setup.py +2 -1
  7. nginx_lens-0.3.2/upstream_checker/checker.py +255 -0
  8. nginx_lens-0.3.1/upstream_checker/checker.py +0 -164
  9. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/LICENSE +0 -0
  10. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/README.md +0 -0
  11. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/__init__.py +0 -0
  12. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/base.py +0 -0
  13. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/conflicts.py +0 -0
  14. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/dead_locations.py +0 -0
  15. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/diff.py +0 -0
  16. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/duplicates.py +0 -0
  17. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/empty_blocks.py +0 -0
  18. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/include.py +0 -0
  19. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/rewrite.py +0 -0
  20. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/route.py +0 -0
  21. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/unused.py +0 -0
  22. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/analyzer/warnings.py +0 -0
  23. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/__init__.py +0 -0
  24. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/analyze.py +0 -0
  25. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/cli.py +0 -0
  26. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/diff.py +0 -0
  27. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/graph.py +0 -0
  28. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/include.py +0 -0
  29. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/logs.py +0 -0
  30. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/route.py +0 -0
  31. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/syntax.py +0 -0
  32. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/commands/tree.py +0 -0
  33. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/exporter/__init__.py +0 -0
  34. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/exporter/graph.py +0 -0
  35. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/exporter/html.py +0 -0
  36. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/exporter/markdown.py +0 -0
  37. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/nginx_lens.egg-info/SOURCES.txt +0 -0
  38. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/nginx_lens.egg-info/dependency_links.txt +0 -0
  39. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/nginx_lens.egg-info/entry_points.txt +0 -0
  40. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/nginx_lens.egg-info/top_level.txt +0 -0
  41. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/parser/__init__.py +0 -0
  42. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/parser/nginx_parser.py +0 -0
  43. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/pyproject.toml +0 -0
  44. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/setup.cfg +0 -0
  45. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/tests/test_conflicts.py +0 -0
  46. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/tests/test_duplicates.py +0 -0
  47. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/tests/test_empty_blocks.py +0 -0
  48. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/tests/test_health.py +0 -0
  49. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/tests/test_parser.py +0 -0
  50. {nginx_lens-0.3.1 → nginx_lens-0.3.2}/upstream_checker/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -9,6 +9,7 @@ License-File: LICENSE
9
9
  Requires-Dist: typer[all]>=0.9.0
10
10
  Requires-Dist: rich>=13.0.0
11
11
  Requires-Dist: requests>=2.25.0
12
+ Requires-Dist: dnspython>=2.0.0
12
13
  Dynamic: author
13
14
  Dynamic: author-email
14
15
  Dynamic: license-file
@@ -61,7 +61,11 @@ def health(
61
61
  if resolved_list:
62
62
  # Показываем все IP-адреса через запятую
63
63
  resolved_str = ", ".join(resolved_list)
64
- table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
64
+ # Если есть "invalid resolve", показываем красным, иначе зеленым
65
+ if any("invalid resolve" in r for r in resolved_list):
66
+ table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[red]{resolved_str}[/red]")
67
+ else:
68
+ table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
65
69
  else:
66
70
  table.add_row(srv["address"], f"[{color}]{status}[/{color}]", "[yellow]Failed to resolve[/yellow]")
67
71
  else:
@@ -44,7 +44,11 @@ def resolve(
44
44
  if resolved_list:
45
45
  # Показываем все IP-адреса через запятую
46
46
  resolved_str = ", ".join(resolved_list)
47
- table.add_row(upstream_name, srv["address"], f"[green]{resolved_str}[/green]")
47
+ # Если есть "invalid resolve", показываем красным, иначе зеленым
48
+ if any("invalid resolve" in r for r in resolved_list):
49
+ table.add_row(upstream_name, srv["address"], f"[red]{resolved_str}[/red]")
50
+ else:
51
+ table.add_row(upstream_name, srv["address"], f"[green]{resolved_str}[/green]")
48
52
  else:
49
53
  table.add_row(upstream_name, srv["address"], "[red]Failed to resolve[/red]")
50
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -9,6 +9,7 @@ License-File: LICENSE
9
9
  Requires-Dist: typer[all]>=0.9.0
10
10
  Requires-Dist: rich>=13.0.0
11
11
  Requires-Dist: requests>=2.25.0
12
+ Requires-Dist: dnspython>=2.0.0
12
13
  Dynamic: author
13
14
  Dynamic: author-email
14
15
  Dynamic: license-file
@@ -1,3 +1,4 @@
1
1
  typer[all]>=0.9.0
2
2
  rich>=13.0.0
3
3
  requests>=2.25.0
4
+ dnspython>=2.0.0
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="nginx-lens",
5
- version="0.3.1",
5
+ version="0.3.2",
6
6
  description="CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx",
7
7
  author="Daniil Astrouski",
8
8
  author_email="shelovesuastra@gmail.com",
@@ -12,6 +12,7 @@ setup(
12
12
  "typer[all]>=0.9.0",
13
13
  "rich>=13.0.0",
14
14
  "requests>=2.25.0",
15
+ "dnspython>=2.0.0",
15
16
  ],
16
17
  entry_points={
17
18
  "console_scripts": [
@@ -0,0 +1,255 @@
1
+ # upstream_checker/checker.py
2
+
3
+ import socket
4
+ import time
5
+ import http.client
6
+ from typing import Dict, List
7
+ try:
8
+ import dns.resolver
9
+ import dns.exception
10
+ DNS_AVAILABLE = True
11
+ except ImportError:
12
+ DNS_AVAILABLE = False
13
+
14
+
15
+ def check_tcp(address: str, timeout: float, retries: int) -> bool:
16
+ """
17
+ Проверка доступности сервера по TCP.
18
+ Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
19
+ """
20
+ # Берем только host:port, игнорируем параметры
21
+ host_port = address.split()[0]
22
+ host, port = host_port.split(":")
23
+ port = int(port)
24
+
25
+ for _ in range(retries):
26
+ try:
27
+ with socket.create_connection((host, port), timeout=timeout):
28
+ return True
29
+ except (socket.timeout, ConnectionRefusedError, OSError):
30
+ time.sleep(0.2)
31
+ return False
32
+
33
+
34
+ def check_http(address: str, timeout: float, retries: int) -> bool:
35
+ """
36
+ Проверка доступности сервера по HTTP (GET /).
37
+ Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
38
+ """
39
+ host_port = address.split()[0]
40
+ host, port = host_port.split(":")
41
+ port = int(port)
42
+
43
+ for _ in range(retries):
44
+ try:
45
+ conn = http.client.HTTPConnection(host, port, timeout=timeout)
46
+ conn.request("GET", "/")
47
+ resp = conn.getresponse()
48
+ healthy = resp.status < 500
49
+ conn.close()
50
+ if healthy:
51
+ return True
52
+ except Exception:
53
+ time.sleep(0.2)
54
+ continue
55
+ return False
56
+
57
+
58
+ def resolve_address(address: str) -> List[str]:
59
+ """
60
+ Резолвит адрес upstream сервера в IP-адреса с информацией о CNAME.
61
+
62
+ Args:
63
+ address: Адрес в формате "host:port" или "host:port параметры"
64
+
65
+ Returns:
66
+ Список строк в формате:
67
+ - "ip:port" - если резолвинг успешен без CNAME
68
+ - "ip:port (via cname.example.com)" - если есть CNAME и все ок
69
+ - "invalid resolve (via cname.example.com -> TXT)" - если CNAME ведет на невалидную запись
70
+ Пустой список, если резолвинг не удался
71
+ """
72
+ try:
73
+ host_port = address.split()[0]
74
+
75
+ if ":" not in host_port:
76
+ return []
77
+
78
+ parts = host_port.rsplit(":", 1)
79
+ if len(parts) != 2:
80
+ return []
81
+ host, port = parts
82
+
83
+ # Если это уже IPv4 адрес, возвращаем как есть
84
+ try:
85
+ socket.inet_aton(host)
86
+ return [host_port]
87
+ except socket.error:
88
+ pass
89
+
90
+ # Проверяем IPv6 (в квадратных скобках)
91
+ if host.startswith("[") and host.endswith("]"):
92
+ ipv6_host = host[1:-1]
93
+ try:
94
+ socket.inet_pton(socket.AF_INET6, ipv6_host)
95
+ return [host_port]
96
+ except (socket.error, OSError):
97
+ pass
98
+
99
+ # Используем dnspython для детального DNS резолвинга
100
+ if DNS_AVAILABLE:
101
+ return _resolve_with_dns(host, port)
102
+ else:
103
+ # Fallback на стандартный socket, если dnspython недоступен
104
+ return _resolve_with_socket(host, port)
105
+ except (ValueError, IndexError, AttributeError):
106
+ return []
107
+
108
+
109
+ def _resolve_with_dns(host: str, port: str) -> List[str]:
110
+ """Резолвит DNS с использованием dnspython для получения информации о CNAME."""
111
+ try:
112
+ cname_info = None
113
+ invalid_type = None
114
+
115
+ # Сначала проверяем CNAME для исходного хоста
116
+ try:
117
+ cname_answer = dns.resolver.resolve(host, 'CNAME', raise_on_no_answer=False)
118
+ if cname_answer:
119
+ cname_target = str(cname_answer[0].target).rstrip('.')
120
+ cname_info = cname_target
121
+
122
+ # Если есть CNAME, проверяем A записи для CNAME цели (не для исходного хоста)
123
+ try:
124
+ a_answer = dns.resolver.resolve(cname_target, 'A', raise_on_no_answer=False)
125
+ if a_answer:
126
+ # CNAME ведет на валидные A записи
127
+ resolved_ips = []
128
+ for rdata in a_answer:
129
+ ip = str(rdata.address)
130
+ resolved_ips.append(f"{ip}:{port} (via {cname_info})")
131
+ return resolved_ips
132
+ else:
133
+ # Нет A записей для CNAME цели, проверяем другие типы
134
+ try:
135
+ txt_answer = dns.resolver.resolve(cname_target, 'TXT', raise_on_no_answer=False)
136
+ if txt_answer:
137
+ invalid_type = 'TXT'
138
+ except:
139
+ pass
140
+ if not invalid_type:
141
+ try:
142
+ mx_answer = dns.resolver.resolve(cname_target, 'MX', raise_on_no_answer=False)
143
+ if mx_answer:
144
+ invalid_type = 'MX'
145
+ except:
146
+ pass
147
+ if not invalid_type:
148
+ # Проверяем другие невалидные типы
149
+ try:
150
+ ns_answer = dns.resolver.resolve(cname_target, 'NS', raise_on_no_answer=False)
151
+ if ns_answer:
152
+ invalid_type = 'NS'
153
+ except:
154
+ pass
155
+ if invalid_type:
156
+ return [f"invalid resolve (via {cname_info} -> {invalid_type})"]
157
+ else:
158
+ return [f"invalid resolve (via {cname_info})"]
159
+ except Exception:
160
+ return [f"invalid resolve (via {cname_info})"]
161
+ except Exception:
162
+ pass
163
+
164
+ # Если нет CNAME, получаем A записи для исходного хоста
165
+ try:
166
+ a_answer = dns.resolver.resolve(host, 'A', raise_on_no_answer=False)
167
+ if a_answer:
168
+ resolved_ips = []
169
+ for rdata in a_answer:
170
+ ip = str(rdata.address)
171
+ resolved_ips.append(f"{ip}:{port}")
172
+ return resolved_ips if resolved_ips else []
173
+ except Exception:
174
+ pass
175
+
176
+ return []
177
+ except Exception:
178
+ return []
179
+
180
+
181
+ def _resolve_with_socket(host: str, port: str) -> List[str]:
182
+ """Fallback резолвинг через socket (без информации о CNAME)."""
183
+ try:
184
+ # gethostbyname_ex возвращает (hostname, aliaslist, ipaddrlist)
185
+ _, _, ipaddrlist = socket.gethostbyname_ex(host)
186
+ # Фильтруем только IPv4 адреса (IPv6 обрабатываются отдельно)
187
+ resolved_ips = []
188
+ for ip in ipaddrlist:
189
+ try:
190
+ # Проверяем, что это IPv4
191
+ socket.inet_aton(ip)
192
+ resolved_ips.append(f"{ip}:{port}")
193
+ except socket.error:
194
+ pass
195
+ return resolved_ips if resolved_ips else []
196
+ except (socket.gaierror, OSError):
197
+ return []
198
+
199
+
200
+ def resolve_upstreams(
201
+ upstreams: Dict[str, List[str]]
202
+ ) -> Dict[str, List[dict]]:
203
+ """
204
+ Резолвит DNS имена upstream-серверов в IP-адреса.
205
+
206
+ Возвращает:
207
+ {
208
+ "backend": [
209
+ {"address": "example.com:8080", "resolved": ["192.168.1.1:8080", "192.168.1.2:8080"]},
210
+ {"address": "127.0.0.1:8080", "resolved": ["127.0.0.1:8080"]},
211
+ {"address": "badhost:80", "resolved": []},
212
+ ...
213
+ ]
214
+ }
215
+ """
216
+ results = {}
217
+ for name, servers in upstreams.items():
218
+ results[name] = []
219
+ for srv in servers:
220
+ resolved = resolve_address(srv)
221
+ results[name].append({
222
+ "address": srv,
223
+ "resolved": resolved
224
+ })
225
+ return results
226
+
227
+
228
+ def check_upstreams(
229
+ upstreams: Dict[str, List[str]],
230
+ timeout: float = 2.0,
231
+ retries: int = 1,
232
+ mode: str = "tcp"
233
+ ) -> Dict[str, List[dict]]:
234
+ """
235
+ Проверяет доступность upstream-серверов.
236
+ mode: "tcp" (по умолчанию) или "http"
237
+
238
+ Возвращает:
239
+ {
240
+ "backend": [
241
+ {"address": "127.0.0.1:8080", "healthy": True},
242
+ ...
243
+ ]
244
+ }
245
+ """
246
+ results = {}
247
+ for name, servers in upstreams.items():
248
+ results[name] = []
249
+ for srv in servers:
250
+ if mode.lower() == "http":
251
+ healthy = check_http(srv, timeout, retries)
252
+ else:
253
+ healthy = check_tcp(srv, timeout, retries)
254
+ results[name].append({"address": srv, "healthy": healthy})
255
+ return results
@@ -1,164 +0,0 @@
1
- # upstream_checker/checker.py
2
-
3
- import socket
4
- import time
5
- import http.client
6
- from typing import Dict, List
7
-
8
-
9
- def check_tcp(address: str, timeout: float, retries: int) -> bool:
10
- """
11
- Проверка доступности сервера по TCP.
12
- Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
13
- """
14
- # Берем только host:port, игнорируем параметры
15
- host_port = address.split()[0]
16
- host, port = host_port.split(":")
17
- port = int(port)
18
-
19
- for _ in range(retries):
20
- try:
21
- with socket.create_connection((host, port), timeout=timeout):
22
- return True
23
- except (socket.timeout, ConnectionRefusedError, OSError):
24
- time.sleep(0.2)
25
- return False
26
-
27
-
28
- def check_http(address: str, timeout: float, retries: int) -> bool:
29
- """
30
- Проверка доступности сервера по HTTP (GET /).
31
- Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
32
- """
33
- host_port = address.split()[0]
34
- host, port = host_port.split(":")
35
- port = int(port)
36
-
37
- for _ in range(retries):
38
- try:
39
- conn = http.client.HTTPConnection(host, port, timeout=timeout)
40
- conn.request("GET", "/")
41
- resp = conn.getresponse()
42
- healthy = resp.status < 500
43
- conn.close()
44
- if healthy:
45
- return True
46
- except Exception:
47
- time.sleep(0.2)
48
- continue
49
- return False
50
-
51
-
52
- def resolve_address(address: str) -> List[str]:
53
- """
54
- Резолвит адрес upstream сервера в IP-адреса.
55
-
56
- Args:
57
- address: Адрес в формате "host:port" или "host:port параметры"
58
-
59
- Returns:
60
- Список IP-адресов в формате "ip:port" или пустой список, если резолвинг не удался
61
- """
62
- try:
63
- host_port = address.split()[0]
64
-
65
- if ":" not in host_port:
66
- return []
67
-
68
- parts = host_port.rsplit(":", 1)
69
- if len(parts) != 2:
70
- return []
71
- host, port = parts
72
-
73
- # Если это уже IPv4 адрес, возвращаем как есть
74
- try:
75
- socket.inet_aton(host)
76
- return [host_port]
77
- except socket.error:
78
- pass
79
-
80
- # Проверяем IPv6 (в квадратных скобках)
81
- if host.startswith("[") and host.endswith("]"):
82
- ipv6_host = host[1:-1]
83
- try:
84
- socket.inet_pton(socket.AF_INET6, ipv6_host)
85
- return [host_port]
86
- except (socket.error, OSError):
87
- pass
88
-
89
- # Пытаемся резолвить DNS имя - получаем все IP-адреса
90
- try:
91
- # gethostbyname_ex возвращает (hostname, aliaslist, ipaddrlist)
92
- _, _, ipaddrlist = socket.gethostbyname_ex(host)
93
- # Фильтруем только IPv4 адреса (IPv6 обрабатываются отдельно)
94
- resolved_ips = []
95
- for ip in ipaddrlist:
96
- try:
97
- # Проверяем, что это IPv4
98
- socket.inet_aton(ip)
99
- resolved_ips.append(f"{ip}:{port}")
100
- except socket.error:
101
- pass
102
- return resolved_ips if resolved_ips else []
103
- except (socket.gaierror, OSError):
104
- return []
105
- except (ValueError, IndexError, AttributeError):
106
- return []
107
-
108
-
109
- def resolve_upstreams(
110
- upstreams: Dict[str, List[str]]
111
- ) -> Dict[str, List[dict]]:
112
- """
113
- Резолвит DNS имена upstream-серверов в IP-адреса.
114
-
115
- Возвращает:
116
- {
117
- "backend": [
118
- {"address": "example.com:8080", "resolved": ["192.168.1.1:8080", "192.168.1.2:8080"]},
119
- {"address": "127.0.0.1:8080", "resolved": ["127.0.0.1:8080"]},
120
- {"address": "badhost:80", "resolved": []},
121
- ...
122
- ]
123
- }
124
- """
125
- results = {}
126
- for name, servers in upstreams.items():
127
- results[name] = []
128
- for srv in servers:
129
- resolved = resolve_address(srv)
130
- results[name].append({
131
- "address": srv,
132
- "resolved": resolved
133
- })
134
- return results
135
-
136
-
137
- def check_upstreams(
138
- upstreams: Dict[str, List[str]],
139
- timeout: float = 2.0,
140
- retries: int = 1,
141
- mode: str = "tcp"
142
- ) -> Dict[str, List[dict]]:
143
- """
144
- Проверяет доступность upstream-серверов.
145
- mode: "tcp" (по умолчанию) или "http"
146
-
147
- Возвращает:
148
- {
149
- "backend": [
150
- {"address": "127.0.0.1:8080", "healthy": True},
151
- ...
152
- ]
153
- }
154
- """
155
- results = {}
156
- for name, servers in upstreams.items():
157
- results[name] = []
158
- for srv in servers:
159
- if mode.lower() == "http":
160
- healthy = check_http(srv, timeout, retries)
161
- else:
162
- healthy = check_tcp(srv, timeout, retries)
163
- results[name].append({"address": srv, "healthy": healthy})
164
- return results
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes