Files
comet-ml--opik/sdks/python/tests/unit/message_processing/test_encoder_helpers.py
T
wehub-resource-sync 5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:44 +08:00

411 lines
14 KiB
Python

from typing import Dict, Optional, Any
from opik.anonymizer import anonymizer, recursive_anonymizer
from opik.message_processing import encoder_helpers
class MockAnonymizer(anonymizer.Anonymizer):
"""Mock anonymizer for testing purposes."""
def anonymize(self, data, **kwargs):
"""Mock anonymization that replaces strings with '[ANONYMIZED]'."""
if isinstance(data, str):
return "[ANONYMIZED]"
return data
class TestEncodeAndAnonymize:
"""Test suite for anonymize_encoded_obj functionality."""
def test_anonymize_encoded_obj__no_anonymizers__returns_encoded_only(self):
"""Test that with an empty anonymizers list, only encoding is performed."""
obj = {"name": "John Doe", "email": "john@example.com"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj, anonymizers=[], fields_to_anonymize=set(), object_type="span"
)
expected = {"name": "John Doe", "email": "john@example.com"}
assert result == expected
def test_anonymize_encoded_obj__with_anonymizers_no_fields__no_error(self):
"""Test that providing anonymizers with empty fields works."""
obj = {"name": "John Doe"}
mock_anonymizer = MockAnonymizer()
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=set(),
object_type="span",
)
expected = {"name": "John Doe"}
assert result == expected
def test_anonymize_encoded_obj__dict_with_matching_fields(self):
"""Test anonymization of a dictionary with matching field names."""
obj = {
"name": "John Doe",
"email": "john@example.com",
"phone": "123-456-7890",
"age": 30,
}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email", "phone"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
expected = {
"name": "John Doe",
"email": "[ANONYMIZED]",
"phone": "[ANONYMIZED]",
"age": 30,
}
assert result == expected
def test_anonymize_encoded_obj__dict_with_no_matching_fields(self):
"""Test that fields not in dict are ignored."""
obj = {"name": "John Doe", "age": 30}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email", "phone"} # These fields don't exist in obj
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
expected = {"name": "John Doe", "age": 30}
assert result == expected
def test_anonymize_encoded_obj__dict_partial_field_match(self):
"""Test anonymization when only some specified fields exist."""
obj = {"name": "John Doe", "email": "john@example.com", "age": 30}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email", "phone", "ssn"} # Only email exists
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
expected = {"name": "John Doe", "email": "[ANONYMIZED]", "age": 30}
assert result == expected
def test_anonymize_encoded_obj__non_dict_object__no_anonymization(self):
"""Test that non-dict objects are not anonymized."""
obj = ["item1", "item2", "item3"]
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"item1"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="trace",
)
# Should return an encoded list without anonymization
assert result == ["item1", "item2", "item3"]
def test_anonymize_encoded_obj__string_object__no_anonymization(self):
"""Test that string objects are not anonymized."""
obj = "This is a sensitive string"
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"field1"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="trace",
)
assert result == "This is a sensitive string"
def test_anonymize_encoded_obj__complex_nested_object(self):
"""Test encoding complex nested objects before anonymization."""
import dataclasses
from opik import jsonable_encoder
@dataclasses.dataclass
class Person:
name: str
email: str
age: int
address: Dict[str, str] = dataclasses.field(default_factory=dict)
person = Person(name="John Doe", email="john@example.com", age=30)
person.address["street"] = "123 Main Street"
person.address["city"] = "New York"
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email"}
# Encode the object first, as expected by anonymize_encoded_obj
encoded_person = jsonable_encoder.encode(person)
result = encoder_helpers.anonymize_encoded_obj(
obj=encoded_person,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="trace",
)
expected = {
"name": "John Doe",
"email": "[ANONYMIZED]",
"age": 30,
"address": {
"city": "New York",
"street": "123 Main Street",
},
}
assert result == expected
def test_anonymize_encoded_obj__nested_dict_in_encoded_result(self):
"""Test that only top-level fields are anonymized in nested structures."""
obj = {
"user_info": {"email": "nested@example.com", "name": "Nested User"},
"email": "top@example.com",
"id": "12345",
}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email"} # Only top-level email should be anonymized
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
expected = {
"user_info": {
"email": "nested@example.com", # Not anonymized (nested)
"name": "Nested User",
},
"email": "[ANONYMIZED]", # Anonymized (top-level)
"id": "12345",
}
assert result == expected
def test_anonymize_encoded_obj__empty_dict(self):
"""Test handling of empty dictionary."""
obj = {}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
assert result == {}
def test_anonymize_encoded_obj__empty_fields_set(self):
"""Test with an empty fields_to_anonymize set."""
obj = {"name": "John", "email": "john@example.com"}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = set() # Empty set
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
# No fields should be anonymized
expected = {"name": "John", "email": "john@example.com"}
assert result == expected
def test_anonymize_encoded_obj__various_field_types(self):
"""Test anonymization of fields with various data types."""
obj = {
"string_field": "test string",
"int_field": 42,
"float_field": 3.14,
"bool_field": True,
"none_field": None,
"list_field": [1, 2, 3],
}
# Create an anonymizer that just adds a prefix
class PrefixAnonymizer(anonymizer.Anonymizer):
def anonymize(self, data, **kwargs):
return f"ANON_{data}"
prefix_anonymizer = PrefixAnonymizer()
fields_to_anonymize = {"string_field", "int_field", "bool_field", "none_field"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[prefix_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
expected = {
"string_field": "ANON_test string",
"int_field": "ANON_42",
"float_field": 3.14, # Not anonymized
"bool_field": "ANON_True",
"none_field": "ANON_None",
"list_field": [1, 2, 3], # Not anonymized
}
assert result == expected
def test_anonymize_encoded_obj__integration_with_actual_encoder_features(self):
"""Test integration with actual encoder features like datetime serialization."""
from datetime import datetime, timezone
from opik import jsonable_encoder
obj = {
"timestamp": datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
"email": "test@example.com",
"data": {"nested": "value"},
}
mock_anonymizer = MockAnonymizer()
fields_to_anonymize = {"email"}
# Encode the object first, as expected by anonymize_encoded_obj
encoded_obj = jsonable_encoder.encode(obj)
result = encoder_helpers.anonymize_encoded_obj(
obj=encoded_obj,
anonymizers=[mock_anonymizer],
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
# Should have encoded datetime and anonymized email
assert "timestamp" in result
assert result["timestamp"] == "2023-01-01T12:00:00Z" # Encoded datetime
assert result["email"] == "[ANONYMIZED]" # Anonymized field
assert result["data"] == {"nested": "value"} # Unchanged nested data
def test_anonymize_encoded_obj__remove_sensitive_dictionary_key(self):
"""Test that sensitive keys can be removed from the result."""
class ApiKeyAnonymizer(anonymizer.Anonymizer):
def anonymize(self, data, **kwargs):
if "api_key" in data:
del data["api_key"]
return data
obj = {
"metadata": {
"api_key": "12345",
"email": "test@example.com",
"data": {"nested": "value"},
},
"input": {"role": "user", "question": "What is LLM?"},
}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[ApiKeyAnonymizer()],
fields_to_anonymize={"metadata"},
object_type="span",
)
# should remove api_key
assert "api_key" not in result["metadata"]
def test_anonymize_encoded_obj__field_name_passed_to_anonymizer(self):
"""Test that sensitive field names and auxiliary information are passed to the anonymizer."""
class ApiKeyAnonymizer(anonymizer.Anonymizer):
def anonymize(self, data, **kwargs):
field_name = kwargs.get("field_name")
object_type = kwargs.get("object_type")
if (
field_name == "metadata"
and object_type == "span"
and "api_key" in data
):
del data["api_key"]
return data
class SSNAnonymizer(recursive_anonymizer.RecursiveAnonymizer):
def anonymize_text(
self, data: str, field_name: Optional[str] = None, **kwargs: Any
) -> str:
object_type = kwargs.get("object_type")
if field_name == "input.ssn" and object_type == "span":
return "[SSN_REMOVED]"
return data
obj = {
"metadata": {
"api_key": "12345",
"email": "test@example.com",
"data": {"nested": "value"},
},
"input": {
"api_key": "12345",
"ssn": "123-4567-789",
"role": "user",
"question": "What is LLM?",
},
}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=[ApiKeyAnonymizer(), SSNAnonymizer()],
fields_to_anonymize={"metadata", "input"},
object_type="span",
)
# should remove api_key from metadata
assert "api_key" not in result["metadata"]
# should not remove api_key from input
assert "api_key" in result["input"]
# should have SSN removed
assert result["input"]["ssn"] == "[SSN_REMOVED]"
def test_anonymize_encoded_obj__multiple_anonymizers(self):
"""Test that multiple anonymizers are applied in a sequence."""
class PrefixAnonymizer(anonymizer.Anonymizer):
def anonymize(self, data, **kwargs):
if isinstance(data, str):
return f"PREFIX_{data}"
return data
class SuffixAnonymizer(anonymizer.Anonymizer):
def anonymize(self, data, **kwargs):
if isinstance(data, str):
return f"{data}_SUFFIX"
return data
obj = {"email": "test@example.com", "name": "John Doe"}
anonymizers = [PrefixAnonymizer(), SuffixAnonymizer()]
fields_to_anonymize = {"email"}
result = encoder_helpers.anonymize_encoded_obj(
obj=obj,
anonymizers=anonymizers,
fields_to_anonymize=fields_to_anonymize,
object_type="span",
)
# Should apply both anonymizers in order: first prefix, then suffix
expected = {"email": "PREFIX_test@example.com_SUFFIX", "name": "John Doe"}
assert result == expected