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.
@@ -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