Skip to content

Commit e878120

Browse files
committed
feat: add DataExporter utility class
1 parent 5d60871 commit e878120

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

src/gradient/_utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
ResponseCache as ResponseCache,
3636
RateLimiter as RateLimiter,
3737
BatchProcessor as BatchProcessor,
38+
DataExporter as DataExporter,
3839
)
3940
from ._compat import (
4041
get_args as get_args,

src/gradient/_utils/_utils.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,109 @@ def is_empty(self) -> bool:
612612
return len(self._batch) == 0
613613

614614

615+
# Data Export Classes
616+
class DataExporter:
617+
"""Utility for exporting API response data to JSON/CSV formats."""
618+
619+
def __init__(self) -> None:
620+
"""Initialize data exporter."""
621+
pass
622+
623+
def _flatten_response(self, data: Any, prefix: str = "") -> dict[str, Any]:
624+
"""Flatten nested response data for CSV export."""
625+
flattened = {}
626+
627+
if isinstance(data, dict):
628+
for key, value in data.items():
629+
new_key = f"{prefix}.{key}" if prefix else key
630+
if isinstance(value, (dict, list)):
631+
flattened.update(self._flatten_response(value, new_key))
632+
else:
633+
flattened[new_key] = value
634+
elif isinstance(data, list):
635+
# For lists, create indexed keys
636+
for i, item in enumerate(data):
637+
new_key = f"{prefix}[{i}]" if prefix else f"[{i}]"
638+
if isinstance(item, (dict, list)):
639+
flattened.update(self._flatten_response(item, new_key))
640+
else:
641+
flattened[new_key] = item
642+
else:
643+
flattened[prefix] = data
644+
645+
return flattened
646+
647+
def export_json(self, data: Any, file_path: str | None = None, indent: int = 2) -> str | None:
648+
"""Export data to JSON format.
649+
650+
Args:
651+
data: Data to export
652+
file_path: Optional file path to save to
653+
indent: JSON indentation level
654+
655+
Returns:
656+
JSON string if no file_path provided, None otherwise
657+
"""
658+
import json
659+
660+
json_str = json.dumps(data, indent=indent, default=str)
661+
662+
if file_path:
663+
with open(file_path, 'w', encoding='utf-8') as f:
664+
f.write(json_str)
665+
return None
666+
667+
return json_str
668+
669+
def export_csv(self, data: Any, file_path: str | None = None, headers: list[str] | None = None) -> str | None:
670+
"""Export data to CSV format.
671+
672+
Args:
673+
data: Data to export (list of dicts or single dict)
674+
file_path: Optional file path to save to
675+
headers: Optional custom headers
676+
677+
Returns:
678+
CSV string if no file_path provided, None otherwise
679+
"""
680+
import csv
681+
import io
682+
683+
# Ensure data is a list
684+
if not isinstance(data, list):
685+
data = [data]
686+
687+
# Flatten each item in the data
688+
flattened_data = [self._flatten_response(item) for item in data]
689+
690+
# Determine headers
691+
if not headers:
692+
all_keys = set()
693+
for item in flattened_data:
694+
all_keys.update(item.keys())
695+
headers = sorted(all_keys)
696+
697+
# Create CSV content
698+
output = io.StringIO() if not file_path else open(file_path, 'w', newline='', encoding='utf-8')
699+
700+
try:
701+
writer = csv.DictWriter(output, fieldnames=headers)
702+
writer.writeheader()
703+
704+
for item in flattened_data:
705+
# Fill missing keys with empty strings
706+
row = {header: item.get(header, '') for header in headers}
707+
writer.writerow(row)
708+
709+
if not file_path:
710+
return output.getvalue()
711+
return None
712+
713+
finally:
714+
if file_path:
715+
output.close()
716+
717+
615718
# API Key Validation Functions
616719
def validate_api_key(api_key: str | None) -> bool:
617720
"""Validate an API key format.

tests/test_data_exporter.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Tests for data export functionality."""
2+
3+
import tempfile
4+
import os
5+
import pytest
6+
from gradient._utils import DataExporter
7+
8+
9+
class TestDataExporter:
10+
"""Test data export functionality."""
11+
12+
def test_export_json_string(self):
13+
"""Test JSON export to string."""
14+
exporter = DataExporter()
15+
data = {"name": "test", "value": 123}
16+
17+
result = exporter.export_json(data)
18+
assert result is not None
19+
assert '"name": "test"' in result
20+
assert '"value": 123' in result
21+
22+
def test_export_json_file(self):
23+
"""Test JSON export to file."""
24+
exporter = DataExporter()
25+
data = {"items": [1, 2, 3]}
26+
27+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
28+
temp_path = f.name
29+
30+
try:
31+
result = exporter.export_json(data, temp_path)
32+
assert result is None # Should return None when saving to file
33+
34+
# Verify file contents
35+
with open(temp_path, 'r') as f:
36+
content = f.read()
37+
assert '"items": [1, 2, 3]' in content
38+
39+
finally:
40+
os.unlink(temp_path)
41+
42+
def test_export_csv_string(self):
43+
"""Test CSV export to string."""
44+
exporter = DataExporter()
45+
data = [
46+
{"name": "Alice", "age": 30},
47+
{"name": "Bob", "age": 25}
48+
]
49+
50+
result = exporter.export_csv(data)
51+
assert result is not None
52+
assert "name,age" in result
53+
assert "Alice,30" in result
54+
assert "Bob,25" in result
55+
56+
def test_export_csv_file(self):
57+
"""Test CSV export to file."""
58+
exporter = DataExporter()
59+
data = [{"id": 1, "status": "active"}]
60+
61+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as f:
62+
temp_path = f.name
63+
64+
try:
65+
result = exporter.export_csv(data, temp_path)
66+
assert result is None
67+
68+
# Verify file contents
69+
with open(temp_path, 'r') as f:
70+
content = f.read()
71+
assert "id,status" in content
72+
assert "1,active" in content
73+
74+
finally:
75+
os.unlink(temp_path)
76+
77+
def test_flatten_response(self):
78+
"""Test response flattening for nested data."""
79+
exporter = DataExporter()
80+
81+
nested_data = {
82+
"user": {
83+
"name": "John",
84+
"profile": {
85+
"age": 30,
86+
"hobbies": ["reading", "coding"]
87+
}
88+
},
89+
"active": True
90+
}
91+
92+
flattened = exporter._flatten_response(nested_data)
93+
94+
# Check flattened keys exist
95+
assert "user.name" in flattened
96+
assert "user.profile.age" in flattened
97+
assert "user.profile.hobbies[0]" in flattened
98+
assert "user.profile.hobbies[1]" in flattened
99+
assert "active" in flattened
100+
101+
# Check values
102+
assert flattened["user.name"] == "John"
103+
assert flattened["user.profile.age"] == 30
104+
assert flattened["active"] is True

0 commit comments

Comments
 (0)