rapidpro-api 0.1.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.
Files changed (65) hide show
  1. rapidpro_api/__init__.py +0 -0
  2. rapidpro_api/contact_processors.py +343 -0
  3. rapidpro_api/contact_processors_pl.py +106 -0
  4. rapidpro_api/datasets/BaseDataset.py +551 -0
  5. rapidpro_api/datasets/ContactsDataset.py +31 -0
  6. rapidpro_api/datasets/DeltaLakeDataset.py +270 -0
  7. rapidpro_api/datasets/FieldsDataset.py +31 -0
  8. rapidpro_api/datasets/FlowsDataset.py +31 -0
  9. rapidpro_api/datasets/GroupsDataset.py +31 -0
  10. rapidpro_api/datasets/MessagesDataset.py +31 -0
  11. rapidpro_api/datasets/RawDataset.py +649 -0
  12. rapidpro_api/datasets/RunsDataset.py +32 -0
  13. rapidpro_api/datasets/__init__.py +8 -0
  14. rapidpro_api/field_processors.py +34 -0
  15. rapidpro_api/flow_processors.py +140 -0
  16. rapidpro_api/group_processors.py +101 -0
  17. rapidpro_api/message_processors.py +110 -0
  18. rapidpro_api/run_processors_pl.py +87 -0
  19. rapidpro_api/stats/__init__.py +5 -0
  20. rapidpro_api/stats/download_stats.py +112 -0
  21. rapidpro_api/stats/download_stats_list.py +115 -0
  22. rapidpro_api/stats/process_stats.py +318 -0
  23. rapidpro_api/stats/process_stats_list.py +266 -0
  24. rapidpro_api/stats/stats.py +643 -0
  25. rapidpro_api/telegram.py +37 -0
  26. rapidpro_api/time_utils.py +224 -0
  27. rapidpro_api/ureport_processors_pl.py +331 -0
  28. rapidpro_api/utils.py +72 -0
  29. rapidpro_api/validators.py +123 -0
  30. rapidpro_api/version.py +8 -0
  31. rapidpro_api/workspaces.py +278 -0
  32. rapidpro_api-0.1.0.dist-info/METADATA +95 -0
  33. rapidpro_api-0.1.0.dist-info/RECORD +65 -0
  34. rapidpro_api-0.1.0.dist-info/WHEEL +5 -0
  35. rapidpro_api-0.1.0.dist-info/top_level.txt +2 -0
  36. tests/__init__.py +0 -0
  37. tests/datasets/__init__.py +0 -0
  38. tests/datasets/test_base_dataset.py +528 -0
  39. tests/datasets/test_contacts_dataset.py +27 -0
  40. tests/datasets/test_fields_dataset.py +27 -0
  41. tests/datasets/test_flows_dataset.py +27 -0
  42. tests/datasets/test_groups_dataset.py +27 -0
  43. tests/datasets/test_messages_dataset.py +27 -0
  44. tests/datasets/test_raw_dataset.py +400 -0
  45. tests/datasets/test_runs_dataset.py +27 -0
  46. tests/stats/__init__.py +0 -0
  47. tests/stats/test_download_stats.py +186 -0
  48. tests/stats/test_download_stats_list.py +366 -0
  49. tests/stats/test_process_stats.py +0 -0
  50. tests/stats/test_process_stats_list.py +531 -0
  51. tests/stats/test_stats.py +711 -0
  52. tests/test_contact_processors.py +199 -0
  53. tests/test_contact_processors_pl.py +188 -0
  54. tests/test_field_processors.py +101 -0
  55. tests/test_flow_processor.py +90 -0
  56. tests/test_group_processors.py +190 -0
  57. tests/test_message_processors.py +129 -0
  58. tests/test_run_processors_pl.py +248 -0
  59. tests/test_telegram.py +61 -0
  60. tests/test_time_utils.py +318 -0
  61. tests/test_ureport_processors_pl.py +198 -0
  62. tests/test_utils.py +182 -0
  63. tests/test_validators.py +127 -0
  64. tests/test_version.py +20 -0
  65. tests/test_workspaces.py +576 -0
@@ -0,0 +1,266 @@
1
+ import pandas as pd
2
+ import logging
3
+ from deltalake import write_deltalake
4
+ from typing import List, Dict, Any
5
+ from rapidpro_api.stats import ProcessStats
6
+
7
+
8
+ class ProcessStatsList:
9
+ """
10
+ Manages a collection of ProcessStats for multiple workspaces.
11
+
12
+ This class provides functionality to:
13
+ - Collect and manage processing statistics from multiple workspaces
14
+ - Track successful and failed processing operations
15
+ - Calculate aggregate metrics and success rates
16
+ - Save statistics to Delta Lake tables
17
+ - Generate comprehensive summaries
18
+
19
+ Example usage:
20
+ # Initialize the stats list
21
+ stats_list = ProcessStatsList("flows", "process_all")
22
+
23
+ # Process workspaces and collect stats
24
+ for workspace in workspaces:
25
+ success, process_stats = process_workspace_data(workspace)
26
+ stats_list.add_stats(process_stats)
27
+
28
+ # Get summary and save results
29
+ summary = stats_list.get_summary()
30
+ stats_list.save_stats_to_delta("/path/to/stats/table")
31
+
32
+ # Print results
33
+ print(f"Processed {summary['successful_processing']} workspaces successfully")
34
+ print(f"Failed to process {summary['failed_processing']} workspaces")
35
+ """
36
+
37
+ def __init__(self, dataset: str, action: str):
38
+ """
39
+ Initialize the processing stats list.
40
+
41
+ Args:
42
+ dataset: Name of the dataset being processed (e.g., "flows", "contacts")
43
+ action: Action being performed (e.g., "process_all", "process_update")
44
+ """
45
+ self.dataset = dataset
46
+ self.action = action
47
+ self.workspace_stats: List[ProcessStats] = []
48
+ self.processed_ok: List[str] = []
49
+ self.processed_error: List[str] = []
50
+
51
+ def add_stats(self, process_stats: ProcessStats):
52
+ """
53
+ Add a ProcessStats instance to the collection.
54
+
55
+ Args:
56
+ process_stats: The ProcessStats instance to add
57
+ """
58
+ self.workspace_stats.append(process_stats)
59
+
60
+ if process_stats.completed:
61
+ self.processed_ok.append(process_stats.ws_code)
62
+ else:
63
+ self.processed_error.append(process_stats.ws_code)
64
+
65
+ def get_successful_workspaces(self) -> List[str]:
66
+ """Get list of successfully processed workspace codes."""
67
+ return self.processed_ok.copy()
68
+
69
+ def get_failed_workspaces(self) -> List[str]:
70
+ """Get list of failed workspace codes."""
71
+ return self.processed_error.copy()
72
+
73
+ def get_total_count(self) -> int:
74
+ """Get total number of processed workspaces."""
75
+ return len(self.workspace_stats)
76
+
77
+ def get_success_count(self) -> int:
78
+ """Get number of successful processing operations."""
79
+ return len(self.processed_ok)
80
+
81
+ def get_failure_count(self) -> int:
82
+ """Get number of failed processing operations."""
83
+ return len(self.processed_error)
84
+
85
+ def get_success_rate(self) -> float:
86
+ """Get success rate as a percentage."""
87
+ total = self.get_total_count()
88
+ return (self.get_success_count() / total * 100) if total > 0 else 0.0
89
+
90
+ def get_total_files_processed(self) -> int:
91
+ """Get total number of files processed across all workspaces."""
92
+ return sum(stats.get_files_processed_count() for stats in self.workspace_stats)
93
+
94
+ def get_total_rows_processed(self) -> int:
95
+ """Get total number of rows processed across all workspaces."""
96
+ return sum(stats.get_rows_processed_count() for stats in self.workspace_stats)
97
+
98
+ def get_total_processing_time(self) -> float:
99
+ """Get total processing time across all workspaces in seconds."""
100
+ return sum(stats.get_total_processing_time() for stats in self.workspace_stats)
101
+
102
+ def get_average_processing_rate(self) -> float:
103
+ """
104
+ Get average processing rate across all successful workspaces in rows per second.
105
+
106
+ Returns:
107
+ Average rows per second, or 0.0 if no successful processing
108
+ """
109
+ successful_stats = [stats for stats in self.workspace_stats if stats.completed]
110
+ if not successful_stats:
111
+ return 0.0
112
+
113
+ rates = []
114
+ for stats in successful_stats:
115
+ rate = stats.get_processing_rate()
116
+ if rate is not None and rate > 0:
117
+ rates.append(rate)
118
+
119
+ return sum(rates) / len(rates) if rates else 0.0
120
+
121
+ def get_processing_summary_by_workspace(self) -> List[Dict[str, Any]]:
122
+ """
123
+ Get a summary of processing statistics for each workspace.
124
+
125
+ Returns:
126
+ List of dictionaries with workspace processing summaries
127
+ """
128
+ summaries = []
129
+ for stats in self.workspace_stats:
130
+ summaries.append({
131
+ "workspace_code": stats.ws_code,
132
+ "completed": stats.completed,
133
+ "files_processed": stats.get_files_processed_count(),
134
+ "rows_processed": stats.get_rows_processed_count(),
135
+ "processing_time": stats.get_total_processing_time(),
136
+ "processing_rate": stats.get_processing_rate()
137
+ })
138
+ return summaries
139
+
140
+ def save_stats_to_delta(self, delta_path: str):
141
+ """
142
+ Save all collected stats to a Delta Lake table.
143
+
144
+ Args:
145
+ delta_path: Path to the Delta Lake table
146
+ """
147
+ if not self.workspace_stats:
148
+ logging.warning("No processing stats to save")
149
+ return
150
+
151
+ stats_df = pd.DataFrame([ws_stats.to_dict() for ws_stats in self.workspace_stats])
152
+ logging.info("Saving processing stats to %s...", delta_path)
153
+ write_deltalake(table_or_uri=delta_path, data=stats_df, mode="append")
154
+
155
+ def get_summary(self) -> Dict[str, Any]:
156
+ """
157
+ Get a comprehensive summary of processing statistics.
158
+
159
+ Returns:
160
+ Dictionary containing summary statistics
161
+ """
162
+ return {
163
+ "dataset": self.dataset,
164
+ "action": self.action,
165
+ "total_workspaces": self.get_total_count(),
166
+ "successful_processing": self.get_success_count(),
167
+ "failed_processing": self.get_failure_count(),
168
+ "success_rate": self.get_success_rate(),
169
+ "total_files_processed": self.get_total_files_processed(),
170
+ "total_rows_processed": self.get_total_rows_processed(),
171
+ "total_processing_time": self.get_total_processing_time(),
172
+ "average_processing_rate": self.get_average_processing_rate(),
173
+ "successful_workspaces": self.get_successful_workspaces(),
174
+ "failed_workspaces": self.get_failed_workspaces()
175
+ }
176
+
177
+ def get_stats_dataframe(self) -> pd.DataFrame:
178
+ """
179
+ Get all stats as a pandas DataFrame.
180
+
181
+ Returns:
182
+ DataFrame containing all workspace processing statistics
183
+ """
184
+ if not self.workspace_stats:
185
+ return pd.DataFrame()
186
+ return pd.DataFrame([ws_stats.to_dict() for ws_stats in self.workspace_stats])
187
+
188
+ def print_summary(self):
189
+ """Print a formatted summary to the console."""
190
+ summary = self.get_summary()
191
+ print(f"\n=== Processing Summary for {summary['dataset']} - {summary['action']} ===")
192
+ print(f"Total workspaces processed: {summary['total_workspaces']}")
193
+ print(f"Successful processing: {summary['successful_processing']}")
194
+ print(f"Failed processing: {summary['failed_processing']}")
195
+ print(f"Success rate: {summary['success_rate']:.1f}%")
196
+ print(f"Total files processed: {summary['total_files_processed']}")
197
+ print(f"Total rows processed: {summary['total_rows_processed']:,}")
198
+ print(f"Total processing time: {summary['total_processing_time']:.2f} seconds")
199
+ print(f"Average processing rate: {summary['average_processing_rate']:.2f} rows/second")
200
+
201
+ if summary['successful_workspaces']:
202
+ print(f"Successful workspaces: {', '.join(summary['successful_workspaces'])}")
203
+ if summary['failed_workspaces']:
204
+ print(f"Failed workspaces: {', '.join(summary['failed_workspaces'])}")
205
+
206
+ def print_detailed_summary(self):
207
+ """Print a detailed summary including per-workspace statistics."""
208
+ self.print_summary()
209
+
210
+ workspace_summaries = self.get_processing_summary_by_workspace()
211
+ if workspace_summaries:
212
+ print(f"\n=== Per-Workspace Details ===")
213
+ for ws_summary in workspace_summaries:
214
+ status = "✅" if ws_summary['completed'] else "❌"
215
+ rate_str = f"{ws_summary['processing_rate']:.2f}" if ws_summary['processing_rate'] else "N/A"
216
+ print(f"{status} {ws_summary['workspace_code']}: "
217
+ f"{ws_summary['files_processed']} files, "
218
+ f"{ws_summary['rows_processed']:,} rows, "
219
+ f"{ws_summary['processing_time']:.2f}s, "
220
+ f"{rate_str} rows/s")
221
+
222
+ def get_top_performers(self, limit: int = 5) -> List[Dict[str, Any]]:
223
+ """
224
+ Get the top performing workspaces by processing rate.
225
+
226
+ Args:
227
+ limit: Maximum number of workspaces to return
228
+
229
+ Returns:
230
+ List of workspace summaries sorted by processing rate (descending)
231
+ """
232
+ summaries = self.get_processing_summary_by_workspace()
233
+ # Filter successful workspaces with valid processing rates
234
+ successful_summaries = [
235
+ s for s in summaries
236
+ if s['completed'] and s['processing_rate'] is not None and s['processing_rate'] > 0
237
+ ]
238
+ # Sort by processing rate in descending order
239
+ sorted_summaries = sorted(successful_summaries, key=lambda x: x['processing_rate'], reverse=True)
240
+ return sorted_summaries[:limit]
241
+
242
+ def get_slowest_performers(self, limit: int = 5) -> List[Dict[str, Any]]:
243
+ """
244
+ Get the slowest performing workspaces by processing rate.
245
+
246
+ Args:
247
+ limit: Maximum number of workspaces to return
248
+
249
+ Returns:
250
+ List of workspace summaries sorted by processing rate (ascending)
251
+ """
252
+ summaries = self.get_processing_summary_by_workspace()
253
+ # Filter successful workspaces with valid processing rates
254
+ successful_summaries = [
255
+ s for s in summaries
256
+ if s['completed'] and s['processing_rate'] is not None and s['processing_rate'] > 0
257
+ ]
258
+ # Sort by processing rate in ascending order
259
+ sorted_summaries = sorted(successful_summaries, key=lambda x: x['processing_rate'])
260
+ return sorted_summaries[:limit]
261
+
262
+ def __str__(self) -> str:
263
+ """Return a string representation of the processing stats list."""
264
+ return (f"ProcessStatsList(dataset={self.dataset}, action={self.action}, "
265
+ f"total={self.get_total_count()}, successful={self.get_success_count()}, "
266
+ f"failed={self.get_failure_count()}, success_rate={self.get_success_rate():.1f}%)")