hpde-analytics-cli 1.0.0__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.
- hpde_analytics_cli/__init__.py +4 -0
- hpde_analytics_cli/api/__init__.py +1 -0
- hpde_analytics_cli/api/client.py +325 -0
- hpde_analytics_cli/auth/__init__.py +1 -0
- hpde_analytics_cli/auth/credentials.py +256 -0
- hpde_analytics_cli/auth/oauth.py +529 -0
- hpde_analytics_cli/main.py +360 -0
- hpde_analytics_cli/utils/__init__.py +1 -0
- hpde_analytics_cli/utils/data_export.py +366 -0
- hpde_analytics_cli/utils/field_discovery.py +351 -0
- hpde_analytics_cli/utils/report_generator.py +459 -0
- hpde_analytics_cli-1.0.0.dist-info/METADATA +309 -0
- hpde_analytics_cli-1.0.0.dist-info/RECORD +17 -0
- hpde_analytics_cli-1.0.0.dist-info/WHEEL +5 -0
- hpde_analytics_cli-1.0.0.dist-info/entry_points.txt +2 -0
- hpde_analytics_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- hpde_analytics_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Export Module
|
|
3
|
+
|
|
4
|
+
Exports raw API data to various formats for review.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Dict, List, Optional, Set
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataExporter:
|
|
15
|
+
"""Exports MSR API data to files for review."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, output_dir: str = "output", name: Optional[str] = None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the data exporter.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
output_dir: Base directory for output files
|
|
23
|
+
name: Optional custom name for the export folder
|
|
24
|
+
"""
|
|
25
|
+
self.output_dir = output_dir
|
|
26
|
+
self.export_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
27
|
+
self.custom_name = name
|
|
28
|
+
|
|
29
|
+
def _ensure_dir(self, path: str) -> None:
|
|
30
|
+
"""Ensure directory exists."""
|
|
31
|
+
os.makedirs(path, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
def _flatten_dict(self, d: Dict, parent_key: str = "", sep: str = ".") -> Dict:
|
|
34
|
+
"""
|
|
35
|
+
Flatten a nested dictionary.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
d: Dictionary to flatten
|
|
39
|
+
parent_key: Key prefix for nested items
|
|
40
|
+
sep: Separator between keys
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Flattened dictionary
|
|
44
|
+
"""
|
|
45
|
+
items: List[tuple] = []
|
|
46
|
+
for k, v in d.items():
|
|
47
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
|
48
|
+
if isinstance(v, dict):
|
|
49
|
+
items.extend(self._flatten_dict(v, new_key, sep).items())
|
|
50
|
+
elif isinstance(v, list):
|
|
51
|
+
# For lists, store as JSON string to preserve data
|
|
52
|
+
items.append((new_key, json.dumps(v) if v else ""))
|
|
53
|
+
else:
|
|
54
|
+
items.append((new_key, v))
|
|
55
|
+
return dict(items)
|
|
56
|
+
|
|
57
|
+
def export_json(self, data: Any, filename: str, include_timestamp: bool = True) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Export data to JSON file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Data to export
|
|
63
|
+
filename: Base filename (without extension)
|
|
64
|
+
include_timestamp: Whether to include timestamp in filename
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Path to exported file
|
|
68
|
+
"""
|
|
69
|
+
self._ensure_dir(self.output_dir)
|
|
70
|
+
|
|
71
|
+
if include_timestamp:
|
|
72
|
+
filepath = os.path.join(self.output_dir, f"{filename}_{self.export_timestamp}.json")
|
|
73
|
+
else:
|
|
74
|
+
filepath = os.path.join(self.output_dir, f"{filename}.json")
|
|
75
|
+
|
|
76
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(data, f, indent=2, default=str)
|
|
78
|
+
|
|
79
|
+
return filepath
|
|
80
|
+
|
|
81
|
+
def export_csv(self, data: List[Dict], filename: str, include_timestamp: bool = True) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Export list of dictionaries to CSV file.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
data: List of dictionaries to export
|
|
87
|
+
filename: Base filename (without extension)
|
|
88
|
+
include_timestamp: Whether to include timestamp in filename
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Path to exported file
|
|
92
|
+
"""
|
|
93
|
+
self._ensure_dir(self.output_dir)
|
|
94
|
+
|
|
95
|
+
if include_timestamp:
|
|
96
|
+
filepath = os.path.join(self.output_dir, f"{filename}_{self.export_timestamp}.csv")
|
|
97
|
+
else:
|
|
98
|
+
filepath = os.path.join(self.output_dir, f"{filename}.csv")
|
|
99
|
+
|
|
100
|
+
if not data:
|
|
101
|
+
# Create empty file with note
|
|
102
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
103
|
+
f.write("# No data available\n")
|
|
104
|
+
return filepath
|
|
105
|
+
|
|
106
|
+
# Flatten all records
|
|
107
|
+
flat_data = [self._flatten_dict(record) for record in data]
|
|
108
|
+
|
|
109
|
+
# Collect all unique keys
|
|
110
|
+
all_keys: Set[str] = set()
|
|
111
|
+
for record in flat_data:
|
|
112
|
+
all_keys.update(record.keys())
|
|
113
|
+
fieldnames = sorted(all_keys)
|
|
114
|
+
|
|
115
|
+
with open(filepath, "w", encoding="utf-8", newline="") as f:
|
|
116
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
|
117
|
+
writer.writeheader()
|
|
118
|
+
writer.writerows(flat_data)
|
|
119
|
+
|
|
120
|
+
return filepath
|
|
121
|
+
|
|
122
|
+
def export_all_data(self, client, event_id: str, verbose: bool = False) -> Dict[str, str]:
|
|
123
|
+
"""
|
|
124
|
+
Export all available data for an event.
|
|
125
|
+
|
|
126
|
+
Creates two sets of exports:
|
|
127
|
+
1. Full raw data (raw_data/ subfolder) - complete API responses for reference
|
|
128
|
+
2. Filtered data (main folder) - curated exports for Time Trials use
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
client: MSRClient instance
|
|
132
|
+
event_id: Event ID to export data for
|
|
133
|
+
verbose: Print progress messages
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary mapping endpoint names to exported file paths
|
|
137
|
+
"""
|
|
138
|
+
exported_files = {}
|
|
139
|
+
# Use custom name if provided, otherwise use timestamp
|
|
140
|
+
if self.custom_name:
|
|
141
|
+
folder_name = f"{self.custom_name}_{self.export_timestamp}"
|
|
142
|
+
else:
|
|
143
|
+
folder_name = f"export_{self.export_timestamp}"
|
|
144
|
+
export_subdir = os.path.join(self.output_dir, folder_name)
|
|
145
|
+
raw_data_subdir = os.path.join(export_subdir, "raw_data")
|
|
146
|
+
self._ensure_dir(export_subdir)
|
|
147
|
+
self._ensure_dir(raw_data_subdir)
|
|
148
|
+
|
|
149
|
+
# Store original output dir
|
|
150
|
+
original_output_dir = self.output_dir
|
|
151
|
+
|
|
152
|
+
# Store raw API responses for later use
|
|
153
|
+
raw_data = {}
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# ============================================================
|
|
157
|
+
# PHASE 1: Fetch all data and export full raw copies
|
|
158
|
+
# ============================================================
|
|
159
|
+
if verbose:
|
|
160
|
+
print("\n --- Exporting Full Raw Data (raw_data/) ---")
|
|
161
|
+
|
|
162
|
+
self.output_dir = raw_data_subdir
|
|
163
|
+
|
|
164
|
+
# 1. Export user profile (raw)
|
|
165
|
+
if verbose:
|
|
166
|
+
print(" Exporting user profile (raw)...")
|
|
167
|
+
try:
|
|
168
|
+
me_data = client.get_me()
|
|
169
|
+
raw_data["me"] = me_data
|
|
170
|
+
exported_files["raw_profile"] = self.export_json(
|
|
171
|
+
me_data, "profile_full", include_timestamp=False
|
|
172
|
+
)
|
|
173
|
+
if verbose:
|
|
174
|
+
print(f" [OK] {exported_files['raw_profile']}")
|
|
175
|
+
except Exception as e:
|
|
176
|
+
if verbose:
|
|
177
|
+
print(f" [ERROR] {e}")
|
|
178
|
+
|
|
179
|
+
# 2. Export organization calendar (raw)
|
|
180
|
+
if verbose:
|
|
181
|
+
print(" Exporting organization calendar (raw)...")
|
|
182
|
+
try:
|
|
183
|
+
calendar_data = client.get_organization_calendar()
|
|
184
|
+
raw_data["calendar"] = calendar_data
|
|
185
|
+
exported_files["raw_calendar"] = self.export_json(
|
|
186
|
+
calendar_data, "calendar_full", include_timestamp=False
|
|
187
|
+
)
|
|
188
|
+
events = calendar_data.get("events", [])
|
|
189
|
+
if events:
|
|
190
|
+
exported_files["raw_calendar_csv"] = self.export_csv(
|
|
191
|
+
events, "calendar_full", include_timestamp=False
|
|
192
|
+
)
|
|
193
|
+
if verbose:
|
|
194
|
+
print(f" [OK] {exported_files['raw_calendar']} ({len(events)} events)")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
if verbose:
|
|
197
|
+
print(f" [ERROR] {e}")
|
|
198
|
+
|
|
199
|
+
# 3. Export entry list (raw)
|
|
200
|
+
if verbose:
|
|
201
|
+
print(" Exporting entry list (raw)...")
|
|
202
|
+
try:
|
|
203
|
+
entrylist_data = client.get_event_entrylist(event_id)
|
|
204
|
+
raw_data["entrylist"] = entrylist_data
|
|
205
|
+
exported_files["raw_entrylist"] = self.export_json(
|
|
206
|
+
entrylist_data, "entrylist_full", include_timestamp=False
|
|
207
|
+
)
|
|
208
|
+
assignments = entrylist_data.get("assignments", [])
|
|
209
|
+
if assignments:
|
|
210
|
+
exported_files["raw_entrylist_csv"] = self.export_csv(
|
|
211
|
+
assignments, "entrylist_full", include_timestamp=False
|
|
212
|
+
)
|
|
213
|
+
if verbose:
|
|
214
|
+
print(
|
|
215
|
+
f" [OK] {exported_files['raw_entrylist']} ({len(assignments)} entries)"
|
|
216
|
+
)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
if verbose:
|
|
219
|
+
print(f" [ERROR] {e}")
|
|
220
|
+
|
|
221
|
+
# 4. Export attendees (raw)
|
|
222
|
+
if verbose:
|
|
223
|
+
print(" Exporting attendees (raw)...")
|
|
224
|
+
try:
|
|
225
|
+
attendees_data = client.get_event_attendees(event_id)
|
|
226
|
+
raw_data["attendees"] = attendees_data
|
|
227
|
+
exported_files["raw_attendees"] = self.export_json(
|
|
228
|
+
attendees_data, "attendees_full", include_timestamp=False
|
|
229
|
+
)
|
|
230
|
+
attendees = attendees_data.get("attendees", [])
|
|
231
|
+
if attendees:
|
|
232
|
+
exported_files["raw_attendees_csv"] = self.export_csv(
|
|
233
|
+
attendees, "attendees_full", include_timestamp=False
|
|
234
|
+
)
|
|
235
|
+
if verbose:
|
|
236
|
+
print(
|
|
237
|
+
f" [OK] {exported_files['raw_attendees']} ({len(attendees)} attendees)"
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
if verbose:
|
|
241
|
+
print(f" [ERROR] {e}")
|
|
242
|
+
|
|
243
|
+
# 5. Export assignments (raw)
|
|
244
|
+
if verbose:
|
|
245
|
+
print(" Exporting assignments (raw)...")
|
|
246
|
+
try:
|
|
247
|
+
assignments_data = client.get_event_assignments(event_id)
|
|
248
|
+
raw_data["assignments"] = assignments_data
|
|
249
|
+
exported_files["raw_assignments"] = self.export_json(
|
|
250
|
+
assignments_data, "assignments_full", include_timestamp=False
|
|
251
|
+
)
|
|
252
|
+
assignments = assignments_data.get("assignments", [])
|
|
253
|
+
if assignments:
|
|
254
|
+
exported_files["raw_assignments_csv"] = self.export_csv(
|
|
255
|
+
assignments, "assignments_full", include_timestamp=False
|
|
256
|
+
)
|
|
257
|
+
if verbose:
|
|
258
|
+
print(
|
|
259
|
+
f" [OK] {exported_files['raw_assignments']} ({len(assignments)} assignments)"
|
|
260
|
+
)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
if verbose:
|
|
263
|
+
print(f" [ERROR] {e}")
|
|
264
|
+
|
|
265
|
+
# ============================================================
|
|
266
|
+
# PHASE 2: Export filtered data for Time Trials use
|
|
267
|
+
# ============================================================
|
|
268
|
+
if verbose:
|
|
269
|
+
print("\n --- Exporting Filtered Data (Time Trials) ---")
|
|
270
|
+
|
|
271
|
+
self.output_dir = export_subdir
|
|
272
|
+
|
|
273
|
+
# 1. Export user profile (filtered - same as raw for this endpoint)
|
|
274
|
+
if verbose:
|
|
275
|
+
print(" Exporting user profile...")
|
|
276
|
+
if "me" in raw_data:
|
|
277
|
+
exported_files["profile"] = self.export_json(
|
|
278
|
+
raw_data["me"], "profile", include_timestamp=False
|
|
279
|
+
)
|
|
280
|
+
if verbose:
|
|
281
|
+
print(f" [OK] {exported_files['profile']}")
|
|
282
|
+
|
|
283
|
+
# 2. Export organization calendar (filtered)
|
|
284
|
+
if verbose:
|
|
285
|
+
print(" Exporting organization calendar...")
|
|
286
|
+
if "calendar" in raw_data:
|
|
287
|
+
exported_files["calendar"] = self.export_json(
|
|
288
|
+
raw_data["calendar"], "calendar", include_timestamp=False
|
|
289
|
+
)
|
|
290
|
+
events = raw_data["calendar"].get("events", [])
|
|
291
|
+
if events:
|
|
292
|
+
exported_files["calendar_csv"] = self.export_csv(
|
|
293
|
+
events, "calendar_events", include_timestamp=False
|
|
294
|
+
)
|
|
295
|
+
if verbose:
|
|
296
|
+
print(f" [OK] {exported_files['calendar']} ({len(events)} events)")
|
|
297
|
+
|
|
298
|
+
# 3. Export entry list (filtered)
|
|
299
|
+
if verbose:
|
|
300
|
+
print(" Exporting entry list...")
|
|
301
|
+
if "entrylist" in raw_data:
|
|
302
|
+
exported_files["entrylist"] = self.export_json(
|
|
303
|
+
raw_data["entrylist"], "entrylist", include_timestamp=False
|
|
304
|
+
)
|
|
305
|
+
assignments = raw_data["entrylist"].get("assignments", [])
|
|
306
|
+
if assignments:
|
|
307
|
+
exported_files["entrylist_csv"] = self.export_csv(
|
|
308
|
+
assignments, "entrylist", include_timestamp=False
|
|
309
|
+
)
|
|
310
|
+
if verbose:
|
|
311
|
+
print(f" [OK] {exported_files['entrylist']} ({len(assignments)} entries)")
|
|
312
|
+
|
|
313
|
+
# 4. Export attendees (filtered)
|
|
314
|
+
if verbose:
|
|
315
|
+
print(" Exporting attendees...")
|
|
316
|
+
if "attendees" in raw_data:
|
|
317
|
+
exported_files["attendees"] = self.export_json(
|
|
318
|
+
raw_data["attendees"], "attendees", include_timestamp=False
|
|
319
|
+
)
|
|
320
|
+
attendees = raw_data["attendees"].get("attendees", [])
|
|
321
|
+
if attendees:
|
|
322
|
+
exported_files["attendees_csv"] = self.export_csv(
|
|
323
|
+
attendees, "attendees", include_timestamp=False
|
|
324
|
+
)
|
|
325
|
+
if verbose:
|
|
326
|
+
print(f" [OK] {exported_files['attendees']} ({len(attendees)} attendees)")
|
|
327
|
+
|
|
328
|
+
# 5. Export assignments (filtered)
|
|
329
|
+
if verbose:
|
|
330
|
+
print(" Exporting assignments...")
|
|
331
|
+
if "assignments" in raw_data:
|
|
332
|
+
exported_files["assignments"] = self.export_json(
|
|
333
|
+
raw_data["assignments"], "assignments", include_timestamp=False
|
|
334
|
+
)
|
|
335
|
+
assignments = raw_data["assignments"].get("assignments", [])
|
|
336
|
+
if assignments:
|
|
337
|
+
exported_files["assignments_csv"] = self.export_csv(
|
|
338
|
+
assignments, "assignments", include_timestamp=False
|
|
339
|
+
)
|
|
340
|
+
if verbose:
|
|
341
|
+
print(
|
|
342
|
+
f" [OK] {exported_files['assignments']} ({len(assignments)} assignments)"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# 6. Create summary file
|
|
346
|
+
if verbose:
|
|
347
|
+
print(" Creating export summary...")
|
|
348
|
+
summary = {
|
|
349
|
+
"export_timestamp": self.export_timestamp,
|
|
350
|
+
"event_id": event_id,
|
|
351
|
+
"organization_id": client.organization_id,
|
|
352
|
+
"files_exported": list(exported_files.keys()),
|
|
353
|
+
"export_directory": export_subdir,
|
|
354
|
+
"raw_data_directory": raw_data_subdir,
|
|
355
|
+
}
|
|
356
|
+
exported_files["summary"] = self.export_json(
|
|
357
|
+
summary, "export_summary", include_timestamp=False
|
|
358
|
+
)
|
|
359
|
+
if verbose:
|
|
360
|
+
print(f" [OK] {exported_files['summary']}")
|
|
361
|
+
|
|
362
|
+
finally:
|
|
363
|
+
# Restore original output dir
|
|
364
|
+
self.output_dir = original_output_dir
|
|
365
|
+
|
|
366
|
+
return exported_files
|