netra-sdk 0.1.42__tar.gz → 0.1.44__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 netra-sdk might be problematic. Click here for more details.
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/PKG-INFO +5 -3
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/config.py +3 -1
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/session_manager.py +30 -6
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/tracer.py +63 -39
- netra_sdk-0.1.44/netra/utils.py +81 -0
- netra_sdk-0.1.44/netra/version.py +1 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/pyproject.toml +2 -1
- netra_sdk-0.1.42/netra/version.py +0 -1
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/LICENCE +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/README.md +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/anonymizer/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/anonymizer/anonymizer.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/anonymizer/base.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/anonymizer/fp_anonymizer.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/decorators.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/exceptions/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/exceptions/injection.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/exceptions/pii.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/input_scanner.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/aiohttp/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/aiohttp/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/cohere/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/cohere/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/fastapi/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/fastapi/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/google_genai/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/google_genai/config.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/google_genai/utils.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/google_genai/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/httpx/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/httpx/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/instruments.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/litellm/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/litellm/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/litellm/wrappers.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/mistralai/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/mistralai/config.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/mistralai/utils.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/mistralai/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/openai/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/openai/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/openai/wrappers.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/pydantic_ai/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/pydantic_ai/utils.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/pydantic_ai/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/pydantic_ai/wrappers.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/weaviate/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/instrumentation/weaviate/version.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/pii.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/processors/__init__.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/processors/instrumentation_span_processor.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/processors/scrubbing_span_processor.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/processors/session_span_processor.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/scanner.py +0 -0
- {netra_sdk-0.1.42 → netra_sdk-0.1.44}/netra/span_wrapper.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: netra-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.44
|
|
4
4
|
Summary: A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments.
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENCE
|
|
6
7
|
Keywords: netra,tracing,observability,sdk,ai,llm,vector,database
|
|
7
8
|
Author: Sooraj Thomas
|
|
8
9
|
Author-email: sooraj@keyvalue.systems
|
|
@@ -19,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
20
|
Classifier: Typing :: Typed
|
|
20
21
|
Provides-Extra: llm-guard
|
|
21
22
|
Provides-Extra: presidio
|
|
23
|
+
Requires-Dist: json-repair (==0.44.1)
|
|
22
24
|
Requires-Dist: llm-guard (==0.3.16) ; extra == "llm-guard"
|
|
23
25
|
Requires-Dist: opentelemetry-api (>=1.34.0,<2.0.0)
|
|
24
26
|
Requires-Dist: opentelemetry-instrumentation-aio-pika (>=0.55b1,<1.0.0)
|
|
@@ -28,7 +28,9 @@ class Config:
|
|
|
28
28
|
LIBRARY_NAME = "netra"
|
|
29
29
|
LIBRARY_VERSION = __version__
|
|
30
30
|
# Maximum length for any attribute value (strings and bytes). Processors should honor this.
|
|
31
|
-
ATTRIBUTE_MAX_LEN =
|
|
31
|
+
ATTRIBUTE_MAX_LEN = 2000
|
|
32
|
+
# Maximum length specifically for conversation entry content (strings or JSON when serialized)
|
|
33
|
+
CONVERSATION_CONTENT_MAX_LEN = 1000
|
|
32
34
|
|
|
33
35
|
def __init__(
|
|
34
36
|
self,
|
|
@@ -13,6 +13,7 @@ from opentelemetry import context as otel_context
|
|
|
13
13
|
from opentelemetry import trace
|
|
14
14
|
|
|
15
15
|
from netra.config import Config
|
|
16
|
+
from netra.utils import process_content_for_max_len
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -279,6 +280,10 @@ class SessionManager:
|
|
|
279
280
|
|
|
280
281
|
if not isinstance(role, str):
|
|
281
282
|
raise TypeError(f"role must be a string, got {type(role)}")
|
|
283
|
+
|
|
284
|
+
if not isinstance(content, (str, dict)):
|
|
285
|
+
raise TypeError(f"content must be a string or dict, got {type(content)}")
|
|
286
|
+
|
|
282
287
|
if not role:
|
|
283
288
|
raise ValueError("role must be a non-empty string")
|
|
284
289
|
|
|
@@ -286,11 +291,14 @@ class SessionManager:
|
|
|
286
291
|
raise ValueError("content must not be empty")
|
|
287
292
|
|
|
288
293
|
try:
|
|
294
|
+
|
|
295
|
+
# Get active recording span
|
|
289
296
|
span = trace.get_current_span()
|
|
290
297
|
if not (span and getattr(span, "is_recording", lambda: False)()):
|
|
291
298
|
logger.warning("No active span to add conversation attribute.")
|
|
292
299
|
return
|
|
293
300
|
|
|
301
|
+
# Load existing conversation (JSON string -> list)
|
|
294
302
|
existing: List[Dict[str, Any]] = []
|
|
295
303
|
raw_data = None
|
|
296
304
|
|
|
@@ -300,10 +308,12 @@ class SessionManager:
|
|
|
300
308
|
raw_data = attrs.get("conversation")
|
|
301
309
|
except Exception:
|
|
302
310
|
logger.exception("Failed to retrieve conversation attribute")
|
|
311
|
+
|
|
303
312
|
if raw_data:
|
|
304
313
|
try:
|
|
305
314
|
import json
|
|
306
315
|
|
|
316
|
+
parsed: Any = None
|
|
307
317
|
if isinstance(raw_data, str):
|
|
308
318
|
parsed = json.loads(raw_data)
|
|
309
319
|
if isinstance(parsed, list):
|
|
@@ -311,16 +321,30 @@ class SessionManager:
|
|
|
311
321
|
except Exception:
|
|
312
322
|
existing = []
|
|
313
323
|
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
# Enforce per-entry content length limit without breaking the entire conversation structure
|
|
325
|
+
max_len = Config.CONVERSATION_CONTENT_MAX_LEN
|
|
326
|
+
processed_content = process_content_for_max_len(content, max_len)
|
|
327
|
+
|
|
328
|
+
# Create a conversation entry
|
|
329
|
+
entry: Dict[str, Any] = {"type": normalized_type, "role": role, "content": processed_content}
|
|
330
|
+
|
|
331
|
+
# Add format based on processed value type for backend parsing
|
|
332
|
+
if isinstance(processed_content, str):
|
|
318
333
|
entry["format"] = "text"
|
|
319
|
-
elif isinstance(
|
|
334
|
+
elif isinstance(processed_content, dict):
|
|
320
335
|
entry["format"] = "json"
|
|
321
336
|
existing.append(entry)
|
|
322
337
|
|
|
323
|
-
|
|
338
|
+
# Bypass global attribute value truncation by writing directly to the span's
|
|
339
|
+
# private attribute store. We intentionally avoid span.set_attribute here.
|
|
340
|
+
try:
|
|
341
|
+
import json
|
|
342
|
+
|
|
343
|
+
payload = json.dumps(existing, default=str)
|
|
344
|
+
attrs = getattr(span, "_attributes", None)
|
|
345
|
+
attrs["conversation"] = payload # type: ignore[index]
|
|
346
|
+
except Exception:
|
|
347
|
+
logger.exception("Failed to set conversation attribute directly on span")
|
|
324
348
|
except Exception as e:
|
|
325
349
|
logger.exception("Failed to add conversation attribute: %s", e)
|
|
326
350
|
|
|
@@ -57,50 +57,74 @@ class FilteringSpanExporter(SpanExporter): # type: ignore[misc]
|
|
|
57
57
|
|
|
58
58
|
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
59
59
|
filtered: List[ReadableSpan] = []
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
blocked_parent_map: Dict[Any, Any] = {}
|
|
61
|
+
for span in spans:
|
|
62
|
+
name = getattr(span, "name", None)
|
|
63
|
+
if name is None or not self._is_blocked(name):
|
|
64
|
+
filtered.append(span)
|
|
64
65
|
continue
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try:
|
|
73
|
-
has_valid_parent = bool(is_valid_attr())
|
|
74
|
-
except Exception:
|
|
75
|
-
has_valid_parent = False
|
|
76
|
-
else:
|
|
77
|
-
has_valid_parent = bool(is_valid_attr)
|
|
78
|
-
|
|
79
|
-
is_root_span = parent is None or not has_valid_parent
|
|
80
|
-
|
|
81
|
-
if is_root_span:
|
|
82
|
-
# Apply name-based blocking only for root spans
|
|
83
|
-
if name in self._exact:
|
|
84
|
-
continue
|
|
85
|
-
blocked = False
|
|
86
|
-
for pref in self._prefixes:
|
|
87
|
-
if name.startswith(pref):
|
|
88
|
-
blocked = True
|
|
89
|
-
break
|
|
90
|
-
if not blocked and self._suffixes:
|
|
91
|
-
for suf in self._suffixes:
|
|
92
|
-
if name.endswith(suf):
|
|
93
|
-
blocked = True
|
|
94
|
-
break
|
|
95
|
-
if not blocked:
|
|
96
|
-
filtered.append(s)
|
|
97
|
-
else:
|
|
98
|
-
# Do not block child spans based on name
|
|
99
|
-
filtered.append(s)
|
|
66
|
+
|
|
67
|
+
span_context = getattr(span, "context", None)
|
|
68
|
+
span_id = getattr(span_context, "span_id", None) if span_context else None
|
|
69
|
+
if span_id is not None:
|
|
70
|
+
blocked_parent_map[span_id] = getattr(span, "parent", None)
|
|
71
|
+
if blocked_parent_map:
|
|
72
|
+
self._reparent_blocked_children(filtered, blocked_parent_map)
|
|
100
73
|
if not filtered:
|
|
101
74
|
return SpanExportResult.SUCCESS
|
|
102
75
|
return self._exporter.export(filtered)
|
|
103
76
|
|
|
77
|
+
def _is_blocked(self, name: str) -> bool:
|
|
78
|
+
if name in self._exact:
|
|
79
|
+
return True
|
|
80
|
+
for pref in self._prefixes:
|
|
81
|
+
if name.startswith(pref):
|
|
82
|
+
return True
|
|
83
|
+
for suf in self._suffixes:
|
|
84
|
+
if name.endswith(suf):
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def _reparent_blocked_children(
|
|
89
|
+
self,
|
|
90
|
+
spans: Sequence[ReadableSpan],
|
|
91
|
+
blocked_parent_map: Dict[Any, Any],
|
|
92
|
+
) -> None:
|
|
93
|
+
if not blocked_parent_map:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
for span in spans:
|
|
97
|
+
parent_context = getattr(span, "parent", None)
|
|
98
|
+
if parent_context is None:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
updated_parent = parent_context
|
|
102
|
+
visited: set[Any] = set()
|
|
103
|
+
changed = False
|
|
104
|
+
|
|
105
|
+
while updated_parent is not None:
|
|
106
|
+
parent_span_id = getattr(updated_parent, "span_id", None)
|
|
107
|
+
if parent_span_id not in blocked_parent_map or parent_span_id in visited:
|
|
108
|
+
break
|
|
109
|
+
visited.add(parent_span_id)
|
|
110
|
+
updated_parent = blocked_parent_map[parent_span_id]
|
|
111
|
+
changed = True
|
|
112
|
+
|
|
113
|
+
if changed:
|
|
114
|
+
self._set_span_parent(span, updated_parent)
|
|
115
|
+
|
|
116
|
+
def _set_span_parent(self, span: ReadableSpan, parent: Any) -> None:
|
|
117
|
+
if hasattr(span, "_parent"):
|
|
118
|
+
try:
|
|
119
|
+
span._parent = parent
|
|
120
|
+
return
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
try:
|
|
124
|
+
setattr(span, "parent", parent)
|
|
125
|
+
except Exception:
|
|
126
|
+
logger.debug("Failed to reparent span %s", getattr(span, "name", "<unknown>"), exc_info=True)
|
|
127
|
+
|
|
104
128
|
def shutdown(self) -> None:
|
|
105
129
|
try:
|
|
106
130
|
self._exporter.shutdown()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""General utility helpers for Netra SDK.
|
|
2
|
+
|
|
3
|
+
This module centralizes common helpers that can be reused across the codebase.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def truncate_string(value: str, max_len: int) -> str:
|
|
12
|
+
"""Truncate a string to max_len characters.
|
|
13
|
+
|
|
14
|
+
If value is not a string, it is returned unchanged.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
if not isinstance(value, str):
|
|
18
|
+
return value
|
|
19
|
+
return value if len(value) <= max_len else value[:max_len]
|
|
20
|
+
except Exception:
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def truncate_and_repair_json(content: Any, max_len: int) -> Any:
|
|
25
|
+
"""Truncate a dict/list by JSON-serializing and hard-cutting, then attempt repair.
|
|
26
|
+
|
|
27
|
+
The function will:
|
|
28
|
+
- json.dumps(content, default=str)
|
|
29
|
+
- hard-cut the string to max_len
|
|
30
|
+
- try to repair using `json-repair` (optional dependency)
|
|
31
|
+
- parse back with json.loads
|
|
32
|
+
|
|
33
|
+
On failure, returns a minimal safe container with a preview of the truncated text.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
import json
|
|
37
|
+
|
|
38
|
+
json_str = json.dumps(content, default=str)
|
|
39
|
+
if len(json_str) <= max_len:
|
|
40
|
+
return content
|
|
41
|
+
|
|
42
|
+
truncated = json_str[:max_len]
|
|
43
|
+
|
|
44
|
+
# Try json_repair if available
|
|
45
|
+
repaired_obj: Any = None
|
|
46
|
+
try:
|
|
47
|
+
try:
|
|
48
|
+
from json_repair import repair_json as _repair_json
|
|
49
|
+
except Exception: # pragma: no cover - optional dependency not installed
|
|
50
|
+
_repair_json = None
|
|
51
|
+
|
|
52
|
+
if _repair_json is not None:
|
|
53
|
+
repaired_str = _repair_json(truncated)
|
|
54
|
+
repaired_obj = json.loads(repaired_str)
|
|
55
|
+
except Exception:
|
|
56
|
+
repaired_obj = None
|
|
57
|
+
|
|
58
|
+
if repaired_obj is not None:
|
|
59
|
+
return repaired_obj
|
|
60
|
+
|
|
61
|
+
# Fallback: safe container preserving a preview
|
|
62
|
+
return {"__truncated__": True, "preview": truncated}
|
|
63
|
+
except Exception:
|
|
64
|
+
# If anything goes wrong, return original content as-is
|
|
65
|
+
return content
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def process_content_for_max_len(content: Any, max_len: int) -> Any:
|
|
69
|
+
"""Ensure the content fits within max_len when serialized.
|
|
70
|
+
|
|
71
|
+
- If content is a string: truncate to max_len.
|
|
72
|
+
- If content is a dict or list: attempt truncate+repair to keep it valid JSON.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
if isinstance(content, str):
|
|
76
|
+
return truncate_string(content, max_len)
|
|
77
|
+
if isinstance(content, (dict, list)):
|
|
78
|
+
return truncate_and_repair_json(content, max_len)
|
|
79
|
+
return content
|
|
80
|
+
except Exception:
|
|
81
|
+
return content
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.44"
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netra-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.44"
|
|
8
8
|
description = "A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Sooraj Thomas",email = "sooraj@keyvalue.systems"}
|
|
@@ -77,6 +77,7 @@ dependencies = [
|
|
|
77
77
|
"opentelemetry-instrumentation-tortoiseorm>=0.55b1,<1.0.0",
|
|
78
78
|
"opentelemetry-instrumentation-urllib>=0.55b1,<1.0.0",
|
|
79
79
|
"opentelemetry-instrumentation-urllib3>=0.55b1,<1.0.0",
|
|
80
|
+
"json-repair==0.44.1",
|
|
80
81
|
]
|
|
81
82
|
|
|
82
83
|
[project.urls]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.42"
|
|
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
|