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.
- rapidpro_api/__init__.py +0 -0
- rapidpro_api/contact_processors.py +343 -0
- rapidpro_api/contact_processors_pl.py +106 -0
- rapidpro_api/datasets/BaseDataset.py +551 -0
- rapidpro_api/datasets/ContactsDataset.py +31 -0
- rapidpro_api/datasets/DeltaLakeDataset.py +270 -0
- rapidpro_api/datasets/FieldsDataset.py +31 -0
- rapidpro_api/datasets/FlowsDataset.py +31 -0
- rapidpro_api/datasets/GroupsDataset.py +31 -0
- rapidpro_api/datasets/MessagesDataset.py +31 -0
- rapidpro_api/datasets/RawDataset.py +649 -0
- rapidpro_api/datasets/RunsDataset.py +32 -0
- rapidpro_api/datasets/__init__.py +8 -0
- rapidpro_api/field_processors.py +34 -0
- rapidpro_api/flow_processors.py +140 -0
- rapidpro_api/group_processors.py +101 -0
- rapidpro_api/message_processors.py +110 -0
- rapidpro_api/run_processors_pl.py +87 -0
- rapidpro_api/stats/__init__.py +5 -0
- rapidpro_api/stats/download_stats.py +112 -0
- rapidpro_api/stats/download_stats_list.py +115 -0
- rapidpro_api/stats/process_stats.py +318 -0
- rapidpro_api/stats/process_stats_list.py +266 -0
- rapidpro_api/stats/stats.py +643 -0
- rapidpro_api/telegram.py +37 -0
- rapidpro_api/time_utils.py +224 -0
- rapidpro_api/ureport_processors_pl.py +331 -0
- rapidpro_api/utils.py +72 -0
- rapidpro_api/validators.py +123 -0
- rapidpro_api/version.py +8 -0
- rapidpro_api/workspaces.py +278 -0
- rapidpro_api-0.1.0.dist-info/METADATA +95 -0
- rapidpro_api-0.1.0.dist-info/RECORD +65 -0
- rapidpro_api-0.1.0.dist-info/WHEEL +5 -0
- rapidpro_api-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/datasets/__init__.py +0 -0
- tests/datasets/test_base_dataset.py +528 -0
- tests/datasets/test_contacts_dataset.py +27 -0
- tests/datasets/test_fields_dataset.py +27 -0
- tests/datasets/test_flows_dataset.py +27 -0
- tests/datasets/test_groups_dataset.py +27 -0
- tests/datasets/test_messages_dataset.py +27 -0
- tests/datasets/test_raw_dataset.py +400 -0
- tests/datasets/test_runs_dataset.py +27 -0
- tests/stats/__init__.py +0 -0
- tests/stats/test_download_stats.py +186 -0
- tests/stats/test_download_stats_list.py +366 -0
- tests/stats/test_process_stats.py +0 -0
- tests/stats/test_process_stats_list.py +531 -0
- tests/stats/test_stats.py +711 -0
- tests/test_contact_processors.py +199 -0
- tests/test_contact_processors_pl.py +188 -0
- tests/test_field_processors.py +101 -0
- tests/test_flow_processor.py +90 -0
- tests/test_group_processors.py +190 -0
- tests/test_message_processors.py +129 -0
- tests/test_run_processors_pl.py +248 -0
- tests/test_telegram.py +61 -0
- tests/test_time_utils.py +318 -0
- tests/test_ureport_processors_pl.py +198 -0
- tests/test_utils.py +182 -0
- tests/test_validators.py +127 -0
- tests/test_version.py +20 -0
- 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}%)")
|