mseep-agentops 0.4.18__py3-none-any.whl

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.
Files changed (94) hide show
  1. agentops/__init__.py +488 -0
  2. agentops/client/__init__.py +5 -0
  3. agentops/client/api/__init__.py +71 -0
  4. agentops/client/api/base.py +162 -0
  5. agentops/client/api/types.py +21 -0
  6. agentops/client/api/versions/__init__.py +10 -0
  7. agentops/client/api/versions/v3.py +65 -0
  8. agentops/client/api/versions/v4.py +104 -0
  9. agentops/client/client.py +211 -0
  10. agentops/client/http/__init__.py +0 -0
  11. agentops/client/http/http_adapter.py +116 -0
  12. agentops/client/http/http_client.py +215 -0
  13. agentops/config.py +268 -0
  14. agentops/enums.py +36 -0
  15. agentops/exceptions.py +38 -0
  16. agentops/helpers/__init__.py +44 -0
  17. agentops/helpers/dashboard.py +54 -0
  18. agentops/helpers/deprecation.py +50 -0
  19. agentops/helpers/env.py +52 -0
  20. agentops/helpers/serialization.py +137 -0
  21. agentops/helpers/system.py +178 -0
  22. agentops/helpers/time.py +11 -0
  23. agentops/helpers/version.py +36 -0
  24. agentops/instrumentation/__init__.py +598 -0
  25. agentops/instrumentation/common/__init__.py +82 -0
  26. agentops/instrumentation/common/attributes.py +278 -0
  27. agentops/instrumentation/common/instrumentor.py +147 -0
  28. agentops/instrumentation/common/metrics.py +100 -0
  29. agentops/instrumentation/common/objects.py +26 -0
  30. agentops/instrumentation/common/span_management.py +176 -0
  31. agentops/instrumentation/common/streaming.py +218 -0
  32. agentops/instrumentation/common/token_counting.py +177 -0
  33. agentops/instrumentation/common/version.py +71 -0
  34. agentops/instrumentation/common/wrappers.py +235 -0
  35. agentops/legacy/__init__.py +277 -0
  36. agentops/legacy/event.py +156 -0
  37. agentops/logging/__init__.py +4 -0
  38. agentops/logging/config.py +86 -0
  39. agentops/logging/formatters.py +34 -0
  40. agentops/logging/instrument_logging.py +91 -0
  41. agentops/sdk/__init__.py +27 -0
  42. agentops/sdk/attributes.py +151 -0
  43. agentops/sdk/core.py +607 -0
  44. agentops/sdk/decorators/__init__.py +51 -0
  45. agentops/sdk/decorators/factory.py +486 -0
  46. agentops/sdk/decorators/utility.py +216 -0
  47. agentops/sdk/exporters.py +87 -0
  48. agentops/sdk/processors.py +71 -0
  49. agentops/sdk/types.py +21 -0
  50. agentops/semconv/__init__.py +36 -0
  51. agentops/semconv/agent.py +29 -0
  52. agentops/semconv/core.py +19 -0
  53. agentops/semconv/enum.py +11 -0
  54. agentops/semconv/instrumentation.py +13 -0
  55. agentops/semconv/langchain.py +63 -0
  56. agentops/semconv/message.py +61 -0
  57. agentops/semconv/meters.py +24 -0
  58. agentops/semconv/resource.py +52 -0
  59. agentops/semconv/span_attributes.py +118 -0
  60. agentops/semconv/span_kinds.py +50 -0
  61. agentops/semconv/status.py +11 -0
  62. agentops/semconv/tool.py +15 -0
  63. agentops/semconv/workflow.py +69 -0
  64. agentops/validation.py +357 -0
  65. mseep_agentops-0.4.18.dist-info/METADATA +49 -0
  66. mseep_agentops-0.4.18.dist-info/RECORD +94 -0
  67. mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
  68. mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
  69. mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
  70. tests/__init__.py +0 -0
  71. tests/conftest.py +10 -0
  72. tests/unit/__init__.py +0 -0
  73. tests/unit/client/__init__.py +1 -0
  74. tests/unit/client/test_http_adapter.py +221 -0
  75. tests/unit/client/test_http_client.py +206 -0
  76. tests/unit/conftest.py +54 -0
  77. tests/unit/sdk/__init__.py +1 -0
  78. tests/unit/sdk/instrumentation_tester.py +207 -0
  79. tests/unit/sdk/test_attributes.py +392 -0
  80. tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
  81. tests/unit/sdk/test_decorators.py +763 -0
  82. tests/unit/sdk/test_exporters.py +241 -0
  83. tests/unit/sdk/test_factory.py +1188 -0
  84. tests/unit/sdk/test_internal_span_processor.py +397 -0
  85. tests/unit/sdk/test_resource_attributes.py +35 -0
  86. tests/unit/test_config.py +82 -0
  87. tests/unit/test_context_manager.py +777 -0
  88. tests/unit/test_events.py +27 -0
  89. tests/unit/test_host_env.py +54 -0
  90. tests/unit/test_init_py.py +501 -0
  91. tests/unit/test_serialization.py +433 -0
  92. tests/unit/test_session.py +676 -0
  93. tests/unit/test_user_agent.py +34 -0
  94. tests/unit/test_validation.py +405 -0
@@ -0,0 +1,433 @@
1
+ """Tests for serialization helpers."""
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime
6
+ from decimal import Decimal
7
+ from enum import Enum
8
+ from typing import Dict
9
+
10
+ import pytest
11
+
12
+ from agentops.helpers.serialization import (
13
+ filter_unjsonable,
14
+ is_jsonable,
15
+ model_to_dict,
16
+ safe_serialize,
17
+ serialize_uuid,
18
+ )
19
+
20
+
21
+ # Define test models and data structures
22
+ class SampleEnum(Enum):
23
+ ONE = 1
24
+ TWO = 2
25
+ THREE = "three"
26
+
27
+
28
+ class SimpleModel:
29
+ """A simple class with __dict__ but no model_dump or dict method."""
30
+
31
+ def __init__(self, value: str):
32
+ self.value = value
33
+
34
+
35
+ class ModelWithToJson:
36
+ """A class that implements to_json method."""
37
+
38
+ def __init__(self, data: Dict):
39
+ self.data = data
40
+
41
+ def to_json(self):
42
+ return self.data
43
+
44
+
45
+ class PydanticV1Model:
46
+ """Mock Pydantic v1 model with dict method."""
47
+
48
+ def __init__(self, **data):
49
+ self.__dict__.update(data)
50
+
51
+ def dict(self):
52
+ return self.__dict__
53
+
54
+
55
+ class PydanticV2Model:
56
+ """Mock Pydantic v2 model with model_dump method."""
57
+
58
+ def __init__(self, **data):
59
+ self.__dict__.update(data)
60
+
61
+ def model_dump(self):
62
+ return self.__dict__
63
+
64
+
65
+ class ModelWithParse:
66
+ """Mock model with parse method."""
67
+
68
+ def __init__(self, data):
69
+ self.data = data
70
+
71
+ def parse(self):
72
+ return self.data
73
+
74
+
75
+ class ModelWithoutDict:
76
+ """A class without __dict__ attribute."""
77
+
78
+ __slots__ = ["value"]
79
+
80
+ def __init__(self, value: str):
81
+ self.value = value
82
+
83
+
84
+ # Define test cases for is_jsonable
85
+ class TestIsJsonable:
86
+ def test_jsonable_types(self):
87
+ """Test that jsonable types return True."""
88
+ jsonable_objects = [
89
+ "string",
90
+ "",
91
+ 123,
92
+ 123.45,
93
+ True,
94
+ False,
95
+ None,
96
+ [1, 2, 3],
97
+ {"key": "value"},
98
+ [],
99
+ {},
100
+ ]
101
+
102
+ for obj in jsonable_objects:
103
+ assert is_jsonable(obj) is True
104
+
105
+ def test_unjsonable_types(self):
106
+ """Test that unjsonable types return False."""
107
+ unjsonable_objects = [
108
+ datetime.now(),
109
+ uuid.uuid4(),
110
+ Decimal("123.45"),
111
+ {1, 2, 3}, # set
112
+ SampleEnum.ONE,
113
+ lambda x: x, # function
114
+ object(), # generic object
115
+ ]
116
+
117
+ for obj in unjsonable_objects:
118
+ assert is_jsonable(obj) is False
119
+
120
+ def test_circular_reference(self):
121
+ """Test that circular references are not jsonable."""
122
+ a = {}
123
+ b = {}
124
+ a["b"] = b
125
+ b["a"] = a
126
+
127
+ # The current implementation doesn't handle ValueError from circular references
128
+ # So this will raise an exception instead of returning False
129
+ with pytest.raises(ValueError, match="Circular reference detected"):
130
+ is_jsonable(a)
131
+
132
+
133
+ # Define test cases for filter_unjsonable
134
+ class TestFilterUnjsonable:
135
+ def test_filter_simple_dict(self):
136
+ """Test filtering of simple dictionary."""
137
+ input_dict = {
138
+ "string": "value",
139
+ "number": 42,
140
+ "list": [1, 2, 3],
141
+ "dict": {"nested": "value"},
142
+ "uuid": uuid.uuid4(),
143
+ "datetime": datetime.now(),
144
+ "set": {1, 2, 3},
145
+ }
146
+
147
+ result = filter_unjsonable(input_dict)
148
+
149
+ # Check that jsonable values are preserved
150
+ assert result["string"] == "value"
151
+ assert result["number"] == 42
152
+ assert result["list"] == [1, 2, 3]
153
+ assert result["dict"] == {"nested": "value"}
154
+
155
+ # Check that unjsonable values are converted to strings or empty strings
156
+ assert isinstance(result["uuid"], str)
157
+ assert result["datetime"] == ""
158
+ assert result["set"] == ""
159
+
160
+ def test_filter_nested_dict(self):
161
+ """Test filtering of nested dictionaries."""
162
+ input_dict = {
163
+ "level1": {
164
+ "level2": {
165
+ "uuid": uuid.uuid4(),
166
+ "string": "preserved",
167
+ "datetime": datetime.now(),
168
+ }
169
+ },
170
+ "list_with_unjsonable": [
171
+ {"uuid": uuid.uuid4()},
172
+ "string",
173
+ datetime.now(),
174
+ ],
175
+ }
176
+
177
+ result = filter_unjsonable(input_dict)
178
+
179
+ # Check nested structure is preserved
180
+ assert result["level1"]["level2"]["string"] == "preserved"
181
+ assert isinstance(result["level1"]["level2"]["uuid"], str)
182
+ assert result["level1"]["level2"]["datetime"] == ""
183
+
184
+ # Check list filtering
185
+ assert result["list_with_unjsonable"][1] == "string"
186
+ assert isinstance(result["list_with_unjsonable"][0]["uuid"], str)
187
+ assert result["list_with_unjsonable"][2] == ""
188
+
189
+ def test_filter_list(self):
190
+ """Test filtering of lists."""
191
+ input_list = [
192
+ "string",
193
+ 42,
194
+ uuid.uuid4(),
195
+ datetime.now(),
196
+ [1, 2, uuid.uuid4()],
197
+ {"uuid": uuid.uuid4()},
198
+ ]
199
+
200
+ result = filter_unjsonable(input_list)
201
+
202
+ assert result[0] == "string"
203
+ assert result[1] == 42
204
+ assert isinstance(result[2], str) # UUID converted to string
205
+ assert result[3] == "" # datetime converted to empty string
206
+ assert isinstance(result[4][2], str) # nested UUID converted to string
207
+ assert isinstance(result[5]["uuid"], str) # nested UUID converted to string
208
+
209
+ def test_filter_empty_structures(self):
210
+ """Test filtering of empty structures."""
211
+ assert filter_unjsonable({}) == {}
212
+ assert filter_unjsonable([]) == []
213
+ assert filter_unjsonable({"empty": {}}) == {"empty": {}}
214
+
215
+
216
+ # Define test cases for serialize_uuid
217
+ class TestSerializeUuid:
218
+ def test_serialize_uuid(self):
219
+ """Test UUID serialization."""
220
+ test_uuid = uuid.uuid4()
221
+ result = serialize_uuid(test_uuid)
222
+
223
+ assert isinstance(result, str)
224
+ assert result == str(test_uuid)
225
+
226
+ def test_serialize_uuid_string(self):
227
+ """Test that UUID string representation is correct."""
228
+ test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000001")
229
+ result = serialize_uuid(test_uuid)
230
+
231
+ assert result == "00000000-0000-0000-0000-000000000001"
232
+
233
+
234
+ # Define test cases for safe_serialize
235
+ class TestSafeSerialize:
236
+ def test_strings_returned_untouched(self):
237
+ """Test that strings are returned untouched."""
238
+ test_strings = [
239
+ "simple string",
240
+ "",
241
+ "special chars: !@#$%^&*()",
242
+ '{"json": "string"}', # JSON as a string
243
+ "[1, 2, 3]", # JSON array as a string
244
+ "line 1\nline 2", # String with newlines
245
+ ]
246
+
247
+ for input_str in test_strings:
248
+ # The string should be returned exactly as is
249
+ assert safe_serialize(input_str) == input_str
250
+
251
+ def test_complex_objects_serialized(self):
252
+ """Test that complex objects are properly serialized."""
253
+ test_cases = [
254
+ # Test case, expected serialized form (or None for dict check)
255
+ ({"key": "value"}, '{"key": "value"}'),
256
+ ([1, 2, 3], "[1, 2, 3]"),
257
+ (123, "123"),
258
+ (123.45, "123.45"),
259
+ (True, "true"),
260
+ (False, "false"),
261
+ (None, "null"),
262
+ ]
263
+
264
+ for input_obj, expected in test_cases:
265
+ result = safe_serialize(input_obj)
266
+ if expected is not None:
267
+ # Check exact match for simple cases
268
+ assert json.loads(result) == json.loads(expected)
269
+ else:
270
+ # For complex cases just verify it's valid JSON
271
+ assert isinstance(result, str)
272
+ assert json.loads(result) is not None
273
+
274
+ def test_pydantic_models(self):
275
+ """Test serialization of Pydantic-like models."""
276
+ # V1 model with dict()
277
+ v1_model = PydanticV1Model(name="test", value=42)
278
+ v1_result = safe_serialize(v1_model)
279
+ assert json.loads(v1_result) == {"name": "test", "value": 42}
280
+
281
+ # V2 model with model_dump()
282
+ v2_model = PydanticV2Model(name="test", value=42)
283
+ v2_result = safe_serialize(v2_model)
284
+ assert json.loads(v2_result) == {"name": "test", "value": 42}
285
+
286
+ # Note: parse() method is currently not implemented due to recursion issues
287
+ # See TODO in serialization.py
288
+
289
+ def test_special_types(self):
290
+ """Test serialization of special types using AgentOpsJSONEncoder."""
291
+ test_cases = [
292
+ # Datetime
293
+ (datetime(2023, 1, 1, 12, 0, 0), '"2023-01-01T12:00:00"'),
294
+ # UUID
295
+ (uuid.UUID("00000000-0000-0000-0000-000000000001"), '"00000000-0000-0000-0000-000000000001"'),
296
+ # Decimal
297
+ (Decimal("123.45"), '"123.45"'),
298
+ # Set
299
+ ({1, 2, 3}, "[1, 2, 3]"),
300
+ # Enum
301
+ (SampleEnum.ONE, "1"),
302
+ (SampleEnum.THREE, '"three"'),
303
+ # Class with to_json
304
+ (ModelWithToJson({"key": "value"}), '{"key": "value"}'),
305
+ ]
306
+
307
+ for input_obj, expected in test_cases:
308
+ result = safe_serialize(input_obj)
309
+
310
+ # Handle list comparison for sets where order might vary
311
+ if isinstance(input_obj, set):
312
+ assert sorted(json.loads(result)) == sorted(json.loads(expected))
313
+ else:
314
+ assert json.loads(result) == json.loads(expected)
315
+
316
+ def test_nested_objects(self):
317
+ """Test serialization of nested objects."""
318
+ nested_obj = {
319
+ "string": "value",
320
+ "number": 42,
321
+ "list": [1, 2, {"inner": "value"}],
322
+ "dict": {"inner": {"deeper": [1, 2, 3]}},
323
+ "model": PydanticV2Model(name="test"),
324
+ }
325
+
326
+ result = safe_serialize(nested_obj)
327
+
328
+ # Verify it's valid JSON
329
+ parsed = json.loads(result)
330
+ assert parsed["string"] == "value"
331
+ assert parsed["number"] == 42
332
+ assert parsed["list"][2]["inner"] == "value"
333
+ assert parsed["dict"]["inner"]["deeper"] == [1, 2, 3]
334
+
335
+ # Just verify we have the model in some form
336
+ assert "model" in parsed
337
+ # And verify it contains the expected data in some form
338
+ assert "test" in str(parsed["model"])
339
+
340
+ def test_fallback_to_str(self):
341
+ """Test fallback to str() for unserializable objects."""
342
+
343
+ class Unserializable:
344
+ def __str__(self):
345
+ return "Unserializable object"
346
+
347
+ obj = Unserializable()
348
+ result = safe_serialize(obj)
349
+ # The string is wrapped in quotes because it's serialized as a JSON string
350
+ assert result == '"Unserializable object"'
351
+
352
+ def test_serialization_error_handling(self):
353
+ """Test handling of serialization errors."""
354
+
355
+ # Create an object that causes JSON serialization to fail
356
+ class BadObject:
357
+ def __init__(self):
358
+ self.recursive = None
359
+
360
+ def __getitem__(self, key):
361
+ # This will cause infinite recursion during JSON serialization
362
+ return self.recursive
363
+
364
+ def __str__(self):
365
+ return "BadObject representation"
366
+
367
+ bad_obj = BadObject()
368
+ bad_obj.recursive = bad_obj
369
+
370
+ result = safe_serialize(bad_obj)
371
+ assert result == '"BadObject representation"'
372
+
373
+ def test_value_error_handling(self):
374
+ """Test handling of ValueError during JSON serialization."""
375
+
376
+ # Create an object that causes a ValueError during JSON serialization
377
+ class ValueErrorObject:
378
+ def to_json(self):
379
+ raise ValueError("Cannot serialize this object")
380
+
381
+ def __str__(self):
382
+ return "ValueErrorObject representation"
383
+
384
+ obj = ValueErrorObject()
385
+ result = safe_serialize(obj)
386
+ assert result == "ValueErrorObject representation"
387
+
388
+
389
+ class TestModelToDict:
390
+ def test_none_returns_empty_dict(self):
391
+ """Test that None returns an empty dict."""
392
+ assert model_to_dict(None) == {}
393
+
394
+ def test_dict_returns_unchanged(self):
395
+ """Test that a dict is returned unchanged."""
396
+ test_dict = {"key": "value"}
397
+ assert model_to_dict(test_dict) is test_dict
398
+
399
+ def test_pydantic_models(self):
400
+ """Test conversion of Pydantic-like models to dicts."""
401
+ # V1 model with dict()
402
+ v1_model = PydanticV1Model(name="test", value=42)
403
+ assert model_to_dict(v1_model) == {"name": "test", "value": 42}
404
+
405
+ # V2 model with model_dump()
406
+ v2_model = PydanticV2Model(name="test", value=42)
407
+ assert model_to_dict(v2_model) == {"name": "test", "value": 42}
408
+
409
+ @pytest.mark.skip(reason="parse() method handling is currently commented out in the implementation")
410
+ def test_parse_method(self):
411
+ """Test models with parse method."""
412
+ parse_model = ModelWithParse({"name": "test", "value": 42})
413
+ assert model_to_dict(parse_model) == {"name": "test", "value": 42}
414
+
415
+ def test_dict_fallback(self):
416
+ """Test fallback to __dict__."""
417
+ simple_model = SimpleModel("test value")
418
+ assert model_to_dict(simple_model) == {"value": "test value"}
419
+
420
+ def test_dict_fallback_exception_handling(self):
421
+ """Test exception handling in dict fallback."""
422
+ # Test with object that has no __dict__ attribute
423
+ model_without_dict = ModelWithoutDict("test value")
424
+ assert model_to_dict(model_without_dict) == {}
425
+
426
+ # Test with object that raises exception when accessing __dict__
427
+ class BadModel:
428
+ @property
429
+ def __dict__(self):
430
+ raise AttributeError("No dict for you!")
431
+
432
+ bad_model = BadModel()
433
+ assert model_to_dict(bad_model) == {}