devlogs 2.0.1__tar.gz → 2.0.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.
- {devlogs-2.0.1/src/devlogs.egg-info → devlogs-2.0.2}/PKG-INFO +1 -1
- {devlogs-2.0.1 → devlogs-2.0.2}/pyproject.toml +1 -1
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/devlogs_client.py +113 -10
- {devlogs-2.0.1 → devlogs-2.0.2/src/devlogs.egg-info}/PKG-INFO +1 -1
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_devlogs_client.py +99 -1
- {devlogs-2.0.1 → devlogs-2.0.2}/LICENSE +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/MANIFEST.in +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/README.md +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/setup.cfg +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/__init__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/__main__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/build_info.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/cli.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/__init__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/auth.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/cli.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/errors.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/forwarder.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/ingestor.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/schema.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/collector/server.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/config.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/context.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/demo.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/formatting.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/handler.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/jenkins/__init__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/jenkins/cli.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/jenkins/core.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/levels.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/mcp/__init__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/mcp/server.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/opensearch/__init__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/opensearch/client.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/opensearch/indexing.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/opensearch/mappings.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/opensearch/queries.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/retention.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/scrub.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/time_utils.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/web/__init__.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/web/server.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/web/static/devlogs.css +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/web/static/devlogs.js +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/web/static/index.html +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs/wrapper.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs.egg-info/SOURCES.txt +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs.egg-info/dependency_links.txt +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs.egg-info/entry_points.txt +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs.egg-info/requires.txt +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/src/devlogs.egg-info/top_level.txt +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_build_info.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_cli.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_collector_auth.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_collector_config.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_collector_schema.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_collector_server.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_config.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_context.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_formatting.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_handler.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_indexing.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_levels.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_mappings.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_mcp_server.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_opensearch_client.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_opensearch_queries.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_retention.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_scrub.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_time_utils.py +0 -0
- {devlogs-2.0.1 → devlogs-2.0.2}/tests/test_web.py +0 -0
|
@@ -8,18 +8,104 @@ import urllib.request
|
|
|
8
8
|
import urllib.error
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
|
-
from typing import Any, Dict, List, Optional
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
from urllib.parse import urlparse, unquote, urlunparse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_collector_url(url: str) -> Tuple[str, Optional[str]]:
|
|
16
|
+
"""Parse a URL and extract auth token if it's a collector URL.
|
|
17
|
+
|
|
18
|
+
Distinguishes between OpenSearch URLs and collector URLs:
|
|
19
|
+
- OpenSearch URL: has BOTH username AND password - keep credentials in URL
|
|
20
|
+
- Collector URL: has only token in username position - extract for Bearer auth
|
|
21
|
+
|
|
22
|
+
Collector URL format: http://token@host:port
|
|
23
|
+
OpenSearch URL format: http://user:password@host:port
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
url: The URL, optionally with credentials in userinfo
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (url, token):
|
|
30
|
+
- For OpenSearch URLs (user:pass): returns original URL, None
|
|
31
|
+
- For collector URLs (token only): returns clean URL without userinfo, token
|
|
32
|
+
- For plain URLs: returns original URL, None
|
|
33
|
+
"""
|
|
34
|
+
if not url:
|
|
35
|
+
return url, None
|
|
36
|
+
|
|
37
|
+
parsed = urlparse(url)
|
|
38
|
+
|
|
39
|
+
# If no userinfo, return as-is
|
|
40
|
+
if not parsed.username and not parsed.password:
|
|
41
|
+
return url, None
|
|
42
|
+
|
|
43
|
+
# OpenSearch URL: has BOTH username AND password
|
|
44
|
+
# Keep the URL as-is with credentials, no Bearer token
|
|
45
|
+
if parsed.username and parsed.password:
|
|
46
|
+
return url, None
|
|
47
|
+
|
|
48
|
+
# Collector URL: token in username position only (no password)
|
|
49
|
+
# Extract token and strip userinfo from URL
|
|
50
|
+
token = unquote(parsed.username) if parsed.username else None
|
|
51
|
+
|
|
52
|
+
# Rebuild URL without userinfo
|
|
53
|
+
# netloc without userinfo is just host:port
|
|
54
|
+
if parsed.port:
|
|
55
|
+
netloc = f"{parsed.hostname}:{parsed.port}"
|
|
56
|
+
else:
|
|
57
|
+
netloc = parsed.hostname or ""
|
|
58
|
+
|
|
59
|
+
clean_url = urlunparse((
|
|
60
|
+
parsed.scheme,
|
|
61
|
+
netloc,
|
|
62
|
+
parsed.path,
|
|
63
|
+
parsed.params,
|
|
64
|
+
parsed.query,
|
|
65
|
+
parsed.fragment,
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
return clean_url, token
|
|
12
69
|
|
|
13
70
|
|
|
14
71
|
@dataclass
|
|
15
72
|
class DevlogsClient:
|
|
16
|
-
"""Client for sending logs to a devlogs collector.
|
|
73
|
+
"""Client for sending logs to a devlogs collector or OpenSearch.
|
|
74
|
+
|
|
75
|
+
URL Types:
|
|
76
|
+
This client distinguishes between collector URLs and OpenSearch URLs:
|
|
77
|
+
|
|
78
|
+
Collector URL (token in username position):
|
|
79
|
+
http://dl1_myapp_secret@localhost:8080
|
|
80
|
+
- Token is extracted and sent as Bearer auth header
|
|
81
|
+
- Userinfo is stripped from the request URL
|
|
82
|
+
|
|
83
|
+
OpenSearch URL (both username AND password):
|
|
84
|
+
https://admin:password@opensearch.example.com:9200
|
|
85
|
+
- Credentials remain in the URL for HTTP Basic auth
|
|
86
|
+
- No Bearer token is used
|
|
17
87
|
|
|
18
88
|
Usage:
|
|
89
|
+
# Collector URL with token:
|
|
90
|
+
client = DevlogsClient(
|
|
91
|
+
collector_url="http://dl1_myapp_secret@localhost:8080",
|
|
92
|
+
application="my-app",
|
|
93
|
+
component="api-server",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# OpenSearch URL with credentials:
|
|
97
|
+
client = DevlogsClient(
|
|
98
|
+
collector_url="https://admin:password@opensearch.example.com:9200",
|
|
99
|
+
application="my-app",
|
|
100
|
+
component="api-server",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Or with explicit auth_token parameter:
|
|
19
104
|
client = DevlogsClient(
|
|
20
105
|
collector_url="http://localhost:8080",
|
|
21
106
|
application="my-app",
|
|
22
107
|
component="api-server",
|
|
108
|
+
auth_token="dl1_myapp_secret",
|
|
23
109
|
)
|
|
24
110
|
|
|
25
111
|
# Send a single log
|
|
@@ -34,6 +120,8 @@ class DevlogsClient:
|
|
|
34
120
|
{"message": "Event 1", "level": "info"},
|
|
35
121
|
{"message": "Event 2", "level": "warning"},
|
|
36
122
|
])
|
|
123
|
+
|
|
124
|
+
If both URL token and auth_token parameter are provided, auth_token takes precedence.
|
|
37
125
|
"""
|
|
38
126
|
|
|
39
127
|
collector_url: str
|
|
@@ -44,16 +132,27 @@ class DevlogsClient:
|
|
|
44
132
|
auth_token: Optional[str] = None
|
|
45
133
|
timeout: int = 30
|
|
46
134
|
|
|
135
|
+
# Internal fields set by __post_init__
|
|
136
|
+
_clean_url: str = field(default="", init=False, repr=False)
|
|
137
|
+
_resolved_token: Optional[str] = field(default=None, init=False, repr=False)
|
|
138
|
+
|
|
139
|
+
def __post_init__(self):
|
|
140
|
+
"""Parse collector URL and extract token if present."""
|
|
141
|
+
clean_url, url_token = _parse_collector_url(self.collector_url)
|
|
142
|
+
self._clean_url = clean_url
|
|
143
|
+
# Explicit auth_token parameter takes precedence over URL token
|
|
144
|
+
self._resolved_token = self.auth_token if self.auth_token else url_token
|
|
145
|
+
|
|
47
146
|
def _get_endpoint(self) -> str:
|
|
48
147
|
"""Get the collector endpoint URL."""
|
|
49
|
-
base = self.
|
|
148
|
+
base = self._clean_url.rstrip("/")
|
|
50
149
|
return f"{base}/v1/logs"
|
|
51
150
|
|
|
52
151
|
def _get_headers(self) -> Dict[str, str]:
|
|
53
152
|
"""Get request headers."""
|
|
54
153
|
headers = {"Content-Type": "application/json"}
|
|
55
|
-
if self.
|
|
56
|
-
headers["Authorization"] = f"Bearer {self.
|
|
154
|
+
if self._resolved_token:
|
|
155
|
+
headers["Authorization"] = f"Bearer {self._resolved_token}"
|
|
57
156
|
return headers
|
|
58
157
|
|
|
59
158
|
def _now(self) -> str:
|
|
@@ -209,15 +308,17 @@ def create_client(
|
|
|
209
308
|
version: Optional[str] = None,
|
|
210
309
|
auth_token: Optional[str] = None,
|
|
211
310
|
) -> DevlogsClient:
|
|
212
|
-
"""Create
|
|
311
|
+
"""Create a Devlogs client.
|
|
213
312
|
|
|
214
313
|
Args:
|
|
215
|
-
collector_url: The
|
|
314
|
+
collector_url: The endpoint URL. URL type is auto-detected:
|
|
315
|
+
- Collector URL: http://token@host:port (token becomes Bearer auth)
|
|
316
|
+
- OpenSearch URL: http://user:pass@host:port (credentials kept in URL)
|
|
216
317
|
application: Application name
|
|
217
318
|
component: Component name within the application
|
|
218
319
|
environment: Deployment environment (optional)
|
|
219
320
|
version: Application version (optional)
|
|
220
|
-
auth_token: Bearer token for authentication (optional)
|
|
321
|
+
auth_token: Bearer token for authentication (optional, overrides URL token)
|
|
221
322
|
|
|
222
323
|
Returns:
|
|
223
324
|
Configured DevlogsClient instance
|
|
@@ -248,7 +349,9 @@ def emit_log(
|
|
|
248
349
|
For repeated logging, use create_client() instead.
|
|
249
350
|
|
|
250
351
|
Args:
|
|
251
|
-
collector_url: The
|
|
352
|
+
collector_url: The endpoint URL. URL type is auto-detected:
|
|
353
|
+
- Collector URL: http://token@host:port (token becomes Bearer auth)
|
|
354
|
+
- OpenSearch URL: http://user:pass@host:port (credentials kept in URL)
|
|
252
355
|
application: Application name
|
|
253
356
|
component: Component name
|
|
254
357
|
message: Log message
|
|
@@ -256,7 +359,7 @@ def emit_log(
|
|
|
256
359
|
fields: Custom fields
|
|
257
360
|
environment: Deployment environment
|
|
258
361
|
version: Application version
|
|
259
|
-
auth_token: Bearer token
|
|
362
|
+
auth_token: Bearer token (optional, overrides URL token)
|
|
260
363
|
|
|
261
364
|
Returns:
|
|
262
365
|
True if accepted, False on error
|
|
@@ -4,7 +4,65 @@ import json
|
|
|
4
4
|
import pytest
|
|
5
5
|
from unittest.mock import Mock, patch
|
|
6
6
|
|
|
7
|
-
from devlogs.devlogs_client import DevlogsClient, create_client, emit_log
|
|
7
|
+
from devlogs.devlogs_client import DevlogsClient, create_client, emit_log, _parse_collector_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestParseCollectorUrl:
|
|
11
|
+
"""Tests for URL parsing - distinguishes collector vs OpenSearch URLs."""
|
|
12
|
+
|
|
13
|
+
def test_no_userinfo(self):
|
|
14
|
+
"""Plain URL without credentials."""
|
|
15
|
+
url, token = _parse_collector_url("http://localhost:8080")
|
|
16
|
+
assert url == "http://localhost:8080"
|
|
17
|
+
assert token is None
|
|
18
|
+
|
|
19
|
+
def test_collector_url_token_in_username(self):
|
|
20
|
+
"""Collector URL: token in username position only."""
|
|
21
|
+
url, token = _parse_collector_url("http://mytoken@localhost:8080")
|
|
22
|
+
assert url == "http://localhost:8080"
|
|
23
|
+
assert token == "mytoken"
|
|
24
|
+
|
|
25
|
+
def test_opensearch_url_user_and_password(self):
|
|
26
|
+
"""OpenSearch URL: both username and password - keep credentials in URL."""
|
|
27
|
+
url, token = _parse_collector_url("http://admin:secretpass@localhost:9200")
|
|
28
|
+
assert url == "http://admin:secretpass@localhost:9200"
|
|
29
|
+
assert token is None
|
|
30
|
+
|
|
31
|
+
def test_opensearch_url_preserves_all_parts(self):
|
|
32
|
+
"""OpenSearch URL preserves scheme, path, query."""
|
|
33
|
+
url, token = _parse_collector_url("https://user:pass@opensearch.example.com:9200/_bulk?refresh=true")
|
|
34
|
+
assert url == "https://user:pass@opensearch.example.com:9200/_bulk?refresh=true"
|
|
35
|
+
assert token is None
|
|
36
|
+
|
|
37
|
+
def test_collector_url_preserves_port(self):
|
|
38
|
+
url, token = _parse_collector_url("http://token@localhost:9999")
|
|
39
|
+
assert url == "http://localhost:9999"
|
|
40
|
+
assert token == "token"
|
|
41
|
+
|
|
42
|
+
def test_collector_url_preserves_path(self):
|
|
43
|
+
url, token = _parse_collector_url("http://token@localhost:8080/path/to/api")
|
|
44
|
+
assert url == "http://localhost:8080/path/to/api"
|
|
45
|
+
assert token == "token"
|
|
46
|
+
|
|
47
|
+
def test_collector_url_https_scheme(self):
|
|
48
|
+
url, token = _parse_collector_url("https://token@example.com")
|
|
49
|
+
assert url == "https://example.com"
|
|
50
|
+
assert token == "token"
|
|
51
|
+
|
|
52
|
+
def test_collector_url_encoded_token(self):
|
|
53
|
+
url, token = _parse_collector_url("http://my%3Atoken%40special@localhost:8080")
|
|
54
|
+
assert url == "http://localhost:8080"
|
|
55
|
+
assert token == "my:token@special"
|
|
56
|
+
|
|
57
|
+
def test_empty_url(self):
|
|
58
|
+
url, token = _parse_collector_url("")
|
|
59
|
+
assert url == ""
|
|
60
|
+
assert token is None
|
|
61
|
+
|
|
62
|
+
def test_devlogs_token_format(self):
|
|
63
|
+
url, token = _parse_collector_url("http://dl1_myapp_abcdefghijklmnopqrstuvwxyz123456@localhost:8080")
|
|
64
|
+
assert url == "http://localhost:8080"
|
|
65
|
+
assert token == "dl1_myapp_abcdefghijklmnopqrstuvwxyz123456"
|
|
8
66
|
|
|
9
67
|
|
|
10
68
|
class TestDevlogsClient:
|
|
@@ -116,6 +174,46 @@ class TestDevlogsClient:
|
|
|
116
174
|
headers = client._get_headers()
|
|
117
175
|
assert headers["Authorization"] == "Bearer my-secret-token"
|
|
118
176
|
|
|
177
|
+
def test_get_headers_with_token_in_url(self):
|
|
178
|
+
"""Collector URL: token in username position extracts Bearer auth."""
|
|
179
|
+
client = DevlogsClient(
|
|
180
|
+
collector_url="http://url-token@localhost:8080",
|
|
181
|
+
application="test-app",
|
|
182
|
+
component="api",
|
|
183
|
+
)
|
|
184
|
+
headers = client._get_headers()
|
|
185
|
+
assert headers["Authorization"] == "Bearer url-token"
|
|
186
|
+
|
|
187
|
+
def test_auth_token_param_overrides_url_token(self):
|
|
188
|
+
client = DevlogsClient(
|
|
189
|
+
collector_url="http://url-token@localhost:8080",
|
|
190
|
+
application="test-app",
|
|
191
|
+
component="api",
|
|
192
|
+
auth_token="param-token",
|
|
193
|
+
)
|
|
194
|
+
headers = client._get_headers()
|
|
195
|
+
assert headers["Authorization"] == "Bearer param-token"
|
|
196
|
+
|
|
197
|
+
def test_get_endpoint_strips_userinfo_for_collector_url(self):
|
|
198
|
+
"""Collector URL: userinfo is stripped from endpoint."""
|
|
199
|
+
client = DevlogsClient(
|
|
200
|
+
collector_url="http://mytoken@localhost:8080",
|
|
201
|
+
application="test-app",
|
|
202
|
+
component="api",
|
|
203
|
+
)
|
|
204
|
+
assert client._get_endpoint() == "http://localhost:8080/v1/logs"
|
|
205
|
+
|
|
206
|
+
def test_opensearch_url_keeps_credentials(self):
|
|
207
|
+
"""OpenSearch URL: credentials remain in URL, no Bearer token."""
|
|
208
|
+
client = DevlogsClient(
|
|
209
|
+
collector_url="https://admin:password@opensearch.example.com:9200",
|
|
210
|
+
application="test-app",
|
|
211
|
+
component="api",
|
|
212
|
+
)
|
|
213
|
+
headers = client._get_headers()
|
|
214
|
+
assert "Authorization" not in headers
|
|
215
|
+
assert client._get_endpoint() == "https://admin:password@opensearch.example.com:9200/v1/logs"
|
|
216
|
+
|
|
119
217
|
def test_emit_sends_single_record(self):
|
|
120
218
|
client = DevlogsClient(
|
|
121
219
|
collector_url="http://localhost:8080",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|