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,643 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Dict, Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
class Stats:
|
|
5
|
+
"""
|
|
6
|
+
A statistics collection class for tracking timers, counters, ratios, and attributes.
|
|
7
|
+
|
|
8
|
+
This class provides functionality to:
|
|
9
|
+
- Track execution times with named timers
|
|
10
|
+
- Count events with optional units
|
|
11
|
+
- Calculate ratios between timers/counters
|
|
12
|
+
- Store arbitrary attributes
|
|
13
|
+
- Export all data as a flat dictionary
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
>>> stats = Stats()
|
|
17
|
+
|
|
18
|
+
# Timer usage
|
|
19
|
+
>>> stats.start_timer("processing")
|
|
20
|
+
>>> # ... do some work ...
|
|
21
|
+
>>> stats.end_timer("processing")
|
|
22
|
+
|
|
23
|
+
# Cumulative timer usage
|
|
24
|
+
>>> stats.start_cumulative_timer("data_load")
|
|
25
|
+
>>> # ... load some data ...
|
|
26
|
+
>>> stats.end_cumulative_timer("data_load")
|
|
27
|
+
>>> # ... do other work ...
|
|
28
|
+
>>> stats.start_cumulative_timer("data_load")
|
|
29
|
+
>>> # ... load even more data ...
|
|
30
|
+
>>> accumulated_time = stats.end_cumulative_timer("data_load")
|
|
31
|
+
|
|
32
|
+
# Counter usage
|
|
33
|
+
>>> stats.count("items_processed", 10, "items")
|
|
34
|
+
>>> stats.count("items_processed", 5) # adds to existing
|
|
35
|
+
|
|
36
|
+
# Ratio calculation
|
|
37
|
+
>>> stats.ratio("success_rate", "successful_items", "total_items")
|
|
38
|
+
|
|
39
|
+
# Attributes
|
|
40
|
+
>>> stats.add_attribute("version", "1.0.0")
|
|
41
|
+
|
|
42
|
+
# Export all data
|
|
43
|
+
>>> data = stats.to_dict()
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self._timers = {} # {name: {'start': time, 'end': time, 'elapsed': duration}}
|
|
48
|
+
self._cumulative_timers = {} # {name: {'start': time, 'accumulated': total_duration}}
|
|
49
|
+
self._counters = {} # {name: {'value': amount, 'unit': unit}}
|
|
50
|
+
self._ratios = {} # {name: {'numerator': name, 'denominator': name, 'value': ratio}}
|
|
51
|
+
self._attributes = {} # {name: value}
|
|
52
|
+
self._names_used = set() # Track all names to ensure uniqueness
|
|
53
|
+
|
|
54
|
+
def set_timer(self, name: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Defines a new timer with the given name, but does not start it.
|
|
57
|
+
This is useful for pre-defining timers before starting them, for example if you use them
|
|
58
|
+
for a ratio (which requires numerator and denominator to be pre-defined).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: Unique name for the timer
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If the timer name is already used by a timer or counter
|
|
64
|
+
Examples:
|
|
65
|
+
>>> stats = Stats()
|
|
66
|
+
>>> stats.set_timer("api_call")
|
|
67
|
+
>>> stats.start_timer("api_call")
|
|
68
|
+
>>> # ... do some work ...
|
|
69
|
+
>>> elapsed = stats.end_timer("api_call")
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
if name in self._names_used:
|
|
73
|
+
raise ValueError(f"Name '{name}' is already in use")
|
|
74
|
+
|
|
75
|
+
self._names_used.add(name)
|
|
76
|
+
self._timers[name] = {
|
|
77
|
+
'start': None,
|
|
78
|
+
'end': None,
|
|
79
|
+
'elapsed': 0.0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def start_timer(self, name: str) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Start a timer with the given name.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: Unique name for the timer
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If the name is already used by a timer or counter
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
>>> stats = Stats()
|
|
94
|
+
>>> stats.start_timer("api_call")
|
|
95
|
+
"""
|
|
96
|
+
if name in self._names_used and name not in self._timers:
|
|
97
|
+
raise ValueError(f"Name '{name}' is already in use")
|
|
98
|
+
|
|
99
|
+
current_time = time.time()
|
|
100
|
+
self._timers[name] = {
|
|
101
|
+
'start': current_time,
|
|
102
|
+
'end': None,
|
|
103
|
+
'elapsed': 0.0
|
|
104
|
+
}
|
|
105
|
+
self._names_used.add(name)
|
|
106
|
+
|
|
107
|
+
def stop_timer(self, name: str) -> float:
|
|
108
|
+
"""
|
|
109
|
+
Alias for end_timer to stop a timer and get elapsed time.
|
|
110
|
+
"""
|
|
111
|
+
return self.end_timer(name)
|
|
112
|
+
|
|
113
|
+
def end_timer(self, name: str) -> float:
|
|
114
|
+
"""
|
|
115
|
+
End a timer and calculate elapsed time.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name: Name of the timer to end
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Elapsed time in seconds
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If timer doesn't exist or wasn't started
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
>>> stats = Stats()
|
|
128
|
+
>>> stats.start_timer("task")
|
|
129
|
+
>>> elapsed = stats.end_timer("task")
|
|
130
|
+
"""
|
|
131
|
+
if name not in self._timers:
|
|
132
|
+
raise ValueError(f"Timer '{name}' does not exist")
|
|
133
|
+
|
|
134
|
+
timer = self._timers[name]
|
|
135
|
+
if timer['start'] is None:
|
|
136
|
+
raise ValueError(f"Timer '{name}' was not started")
|
|
137
|
+
|
|
138
|
+
current_time = time.time()
|
|
139
|
+
elapsed = current_time - timer['start']
|
|
140
|
+
|
|
141
|
+
timer['end'] = current_time
|
|
142
|
+
timer['elapsed'] = elapsed
|
|
143
|
+
|
|
144
|
+
return elapsed
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def set_cumulative_timer(self, name: str) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Defines a new cumulative timer with the given name, but does not start it.
|
|
150
|
+
This is useful for pre-defining timers before starting them, for example if you use them
|
|
151
|
+
for a ratio (which requires numerator and denominator to be pre-defined).
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Unique name for the cumulative timer
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If the timer name is already used by a timer or counter
|
|
157
|
+
Examples:
|
|
158
|
+
>>> stats = Stats()
|
|
159
|
+
>>> stats.set_cumulative_timer("data_load")
|
|
160
|
+
>>> stats.start_cumulative_timer("data_load")
|
|
161
|
+
>>> # ... load some data ...
|
|
162
|
+
>>> stats.end_cumulative_timer("data_load")
|
|
163
|
+
>>> # ... do other work ...
|
|
164
|
+
>>> stats.start_cumulative_timer("data_load")
|
|
165
|
+
>>> # ... load even more data ...
|
|
166
|
+
>>> accumulated_time = stats.end_cumulative_timer("data_load")
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if name in self._names_used:
|
|
170
|
+
raise ValueError(f"Name '{name}' is already in use")
|
|
171
|
+
self._names_used.add(name)
|
|
172
|
+
self._cumulative_timers[name] = {
|
|
173
|
+
'start': None,
|
|
174
|
+
'accumulated': 0.0 # Initialize elapsed to 0 for accumulation
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def start_cumulative_timer(self, name: str ) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Start an cumulative timer. This allows multiple start-end cycles to accumulate total time.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
name: Unique name for the cumulative timer
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: If the name is already used by a timer or counter
|
|
186
|
+
Examples:
|
|
187
|
+
>>> stats = Stats()
|
|
188
|
+
>>> stats.start_cumulative_timer("data_processing")
|
|
189
|
+
>>> # ... do some work ...
|
|
190
|
+
>>> stats.end_cumulative_timer("data_processing")
|
|
191
|
+
>>> # ... do more work ...
|
|
192
|
+
>>> stats.start_cumulative_timer("data_processing")
|
|
193
|
+
>>> # ... do even more work ...
|
|
194
|
+
>>> stats.end_cumulative_timer("data_processing")
|
|
195
|
+
"""
|
|
196
|
+
if name in self._names_used and name not in self._cumulative_timers:
|
|
197
|
+
raise ValueError(f"Name '{name}' is already in use")
|
|
198
|
+
|
|
199
|
+
current_time = time.time()
|
|
200
|
+
if name not in self._cumulative_timers:
|
|
201
|
+
self._cumulative_timers[name] = {
|
|
202
|
+
'start': current_time,
|
|
203
|
+
'accumulated': 0.0 # Initialize elapsed to 0 for accumulation
|
|
204
|
+
}
|
|
205
|
+
self._names_used.add(name)
|
|
206
|
+
else:
|
|
207
|
+
# if timer not started
|
|
208
|
+
if self._cumulative_timers[name]['start'] is None:
|
|
209
|
+
self._cumulative_timers[name]['start'] = current_time
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
def get_elapsed_time(self, name: str) -> float:
|
|
213
|
+
"""
|
|
214
|
+
Get the elapsed time for a timer. The timer can be either cumulative or "normal".
|
|
215
|
+
if the timer is still running, returns the elapsed time up to now (does not stop the timer).
|
|
216
|
+
if the timer was ended, returns the total elapsed time.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
name: Name of the timer
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Elapsed time in seconds
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: If timer doesn't exist.
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
>>> stats = Stats()
|
|
229
|
+
>>> # for a normal timer
|
|
230
|
+
>>> stats.start_timer("task")
|
|
231
|
+
>>> # ... do some work ...
|
|
232
|
+
>>> stats.get_elapsed_time("task") # returns elapsed time so far
|
|
233
|
+
>>> # ... do some more work ...
|
|
234
|
+
>>> stats.end_timer("task")
|
|
235
|
+
>>> elapsed = stats.get_elapsed_time("task")
|
|
236
|
+
>>> # for a cumulative timer
|
|
237
|
+
>>> stats.start_cumulative_timer("data_load")
|
|
238
|
+
>>> # some time passes...
|
|
239
|
+
>>> elapsed = stats.get_elapsed_time("data_load")
|
|
240
|
+
"""
|
|
241
|
+
if (name not in self._timers) and (name not in self._cumulative_timers):
|
|
242
|
+
raise ValueError(f"Timer '{name}' does not exist")
|
|
243
|
+
|
|
244
|
+
if name in self._cumulative_timers:
|
|
245
|
+
return self._get_elapsed_for_cumulative_timer(name)
|
|
246
|
+
return self._get_elapsed_for_timer(name)
|
|
247
|
+
|
|
248
|
+
def _get_elapsed_for_timer(self, name:str) -> float:
|
|
249
|
+
"""Helper method to get elapsed time for a normal timer.
|
|
250
|
+
NOTE: Does not check if timer exists.
|
|
251
|
+
Args:
|
|
252
|
+
name: Name of the timer
|
|
253
|
+
Returns:
|
|
254
|
+
Elapsed time in seconds since start, or 0 if not started
|
|
255
|
+
"""
|
|
256
|
+
timer = self._timers[name]
|
|
257
|
+
if timer['start'] is None:
|
|
258
|
+
return 0.0 # Timer was never started
|
|
259
|
+
|
|
260
|
+
if timer['elapsed'] != 0.0: # timer completed -> return elapsed
|
|
261
|
+
return timer['elapsed']
|
|
262
|
+
# if timer not ended, calculate elapsed time up to now
|
|
263
|
+
if timer['end'] is None:
|
|
264
|
+
return time.time() - timer['start']
|
|
265
|
+
else:
|
|
266
|
+
return timer['end'] - timer['start']
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_elapsed_for_cumulative_timer(self, name: str) -> float:
|
|
270
|
+
"""Helper method to get elapsed time for a cumulative timer.
|
|
271
|
+
NOTE: Does not check if timer exists.
|
|
272
|
+
Args:
|
|
273
|
+
name: Name of the cumulative timer
|
|
274
|
+
Returns:
|
|
275
|
+
Elapsed time in seconds since last start, or 0 if not running
|
|
276
|
+
"""
|
|
277
|
+
timer = self._cumulative_timers[name]
|
|
278
|
+
if timer['start'] is None:
|
|
279
|
+
return 0.0 # Timer not running
|
|
280
|
+
return time.time() - timer['start']
|
|
281
|
+
|
|
282
|
+
def get_accumulated_time(self, name: str) -> float:
|
|
283
|
+
"""Helper method to get total accumulated time for a cumulative timer.
|
|
284
|
+
NOTE: Does not check if timer exists.
|
|
285
|
+
Args:
|
|
286
|
+
name: Name of the cumulative timer
|
|
287
|
+
Returns:
|
|
288
|
+
Total accumulated time in seconds
|
|
289
|
+
"""
|
|
290
|
+
elapsed = self._get_elapsed_for_cumulative_timer(name)
|
|
291
|
+
return self._cumulative_timers[name]['accumulated'] + elapsed
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def end_cumulative_timer(self, name: str, return_elapsed=False) -> float:
|
|
295
|
+
"""
|
|
296
|
+
End an cumulative timer and add to total elapsed time.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
name: Name of the cumulative timer to end
|
|
300
|
+
return_elapsed: If True, returns the elapsed time for this session only. If False
|
|
301
|
+
(default), returns the total accumulated time.
|
|
302
|
+
Returns:
|
|
303
|
+
Total accumulated time or this session elapsed time if return_elapsed is True
|
|
304
|
+
Raises:
|
|
305
|
+
ValueError: If timer doesn't exist or wasn't started
|
|
306
|
+
Examples:
|
|
307
|
+
>>> stats = Stats()
|
|
308
|
+
>>> stats.start_cumulative_timer("data_processing")
|
|
309
|
+
>>> # ... do some work (10 seconds)...
|
|
310
|
+
>>> total_elapsed = stats.end_cumulative_timer("data_processing")
|
|
311
|
+
>>> # total_elapsed is 10 seconds
|
|
312
|
+
>>> #... do more work ...
|
|
313
|
+
>>> stats.start_cumulative_timer("data_processing")
|
|
314
|
+
>>> # ... do more work (5 seconds)...
|
|
315
|
+
>>> session_elapsed = stats.end_cumulative_timer("data_processing", return_elapsed=True)
|
|
316
|
+
>>> # session_elapsed is 5 seconds
|
|
317
|
+
>>> total = stats.get_accumulated_time("data_processing")
|
|
318
|
+
>>> # total is 15 seconds
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
if name not in self._cumulative_timers:
|
|
322
|
+
raise ValueError(f"Timer '{name}' does not exist")
|
|
323
|
+
elapsed = self._get_elapsed_for_cumulative_timer(name)
|
|
324
|
+
self._cumulative_timers[name]['accumulated'] = self._cumulative_timers[name]['accumulated'] + elapsed
|
|
325
|
+
self._cumulative_timers[name]['start'] = None # Reset start to None to indicate it's not running
|
|
326
|
+
return elapsed if return_elapsed else self._cumulative_timers[name]['accumulated']
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def count(self, counter_name: str, amount: Union[int, float], unit: Optional[str] = None) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Add to a counter or create a new one.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
counter_name: Unique name for the counter
|
|
335
|
+
amount: Amount to add to the counter
|
|
336
|
+
unit: Optional unit description
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
ValueError: If the name is already used by a timer
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
>>> stats = Stats()
|
|
343
|
+
>>> stats.count("requests", 1, "requests")
|
|
344
|
+
>>> stats.count("requests", 5, "requests") # now total is 6
|
|
345
|
+
>>> stats.count("bytes", 1024, "bytes")
|
|
346
|
+
"""
|
|
347
|
+
if counter_name in self._timers:
|
|
348
|
+
raise ValueError(f"Name '{counter_name}' is already used by a timer")
|
|
349
|
+
|
|
350
|
+
if counter_name not in self._counters:
|
|
351
|
+
self._counters[counter_name] = {'value': 0, 'unit': unit}
|
|
352
|
+
self._names_used.add(counter_name)
|
|
353
|
+
|
|
354
|
+
self._counters[counter_name]['value'] += amount
|
|
355
|
+
|
|
356
|
+
# Update unit if provided
|
|
357
|
+
if unit is not None:
|
|
358
|
+
self._counters[counter_name]['unit'] = unit
|
|
359
|
+
|
|
360
|
+
def get_counter_value(self, counter_name: str) -> Union[int, float]:
|
|
361
|
+
"""
|
|
362
|
+
Get the current value of a counter.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
counter_name: Name of the counter
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Current counter value
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
ValueError: If counter doesn't exist
|
|
372
|
+
|
|
373
|
+
Examples:
|
|
374
|
+
>>> stats = Stats()
|
|
375
|
+
>>> stats.count("items", 10)
|
|
376
|
+
>>> value = stats.get_counter_value("items") # 10
|
|
377
|
+
"""
|
|
378
|
+
if counter_name not in self._counters:
|
|
379
|
+
raise ValueError(f"Counter '{counter_name}' does not exist")
|
|
380
|
+
|
|
381
|
+
return self._counters[counter_name]['value']
|
|
382
|
+
|
|
383
|
+
def get_counter_unit(self, counter_name: str) -> Optional[str]:
|
|
384
|
+
"""
|
|
385
|
+
Get the unit of a counter.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
counter_name: Name of the counter
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Counter unit or None if not set
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
ValueError: If counter doesn't exist
|
|
395
|
+
Examples:
|
|
396
|
+
>>> stats = Stats()
|
|
397
|
+
>>> stats.count("items", 10, "pieces")
|
|
398
|
+
>>> unit = stats.get_counter_unit("items") # "pieces"
|
|
399
|
+
"""
|
|
400
|
+
if counter_name not in self._counters:
|
|
401
|
+
raise ValueError(f"Counter '{counter_name}' does not exist")
|
|
402
|
+
|
|
403
|
+
return self._counters[counter_name]['unit']
|
|
404
|
+
|
|
405
|
+
def set_counter_unit(self, counter_name: str, unit: str) -> None:
|
|
406
|
+
"""
|
|
407
|
+
Set or update the unit of a counter.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
counter_name: Name of the counter
|
|
411
|
+
unit: Unit description to set for the counter
|
|
412
|
+
Raises:
|
|
413
|
+
ValueError: If counter doesn't exist
|
|
414
|
+
Examples:
|
|
415
|
+
>>> stats = Stats()
|
|
416
|
+
>>> stats.count("items", 10)
|
|
417
|
+
>>> stats.set_counter_unit("items", "pieces")
|
|
418
|
+
>>> unit = stats.get_counter_unit("items") # "pieces"
|
|
419
|
+
"""
|
|
420
|
+
if counter_name not in self._counters:
|
|
421
|
+
raise ValueError(f"Counter '{counter_name}' does not exist")
|
|
422
|
+
|
|
423
|
+
self._counters[counter_name]['unit'] = unit
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def set_ratio(self, ratio_name: str, numerator: str, denominator: str) -> None:
|
|
427
|
+
"""
|
|
428
|
+
Add a ratio definition between two timers or counters.
|
|
429
|
+
The ratio is not calculated until requested.
|
|
430
|
+
Before using a ratio, ensure that both numerator and denominator timers/counters
|
|
431
|
+
have been defined.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
ratio_name: Name for this ratio
|
|
435
|
+
numerator: Name of timer/counter to use as numerator
|
|
436
|
+
denominator: Name of timer/counter to use as denominator
|
|
437
|
+
|
|
438
|
+
Raises:
|
|
439
|
+
ValueError: If numerator or denominator don't exist
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
>>> stats = Stats()
|
|
443
|
+
>>> stats.count("success", 80)
|
|
444
|
+
>>> stats.count("total", 100)
|
|
445
|
+
>>> stats.set_ratio("success_rate", "success", "total")
|
|
446
|
+
>>> #
|
|
447
|
+
>>> # with timers, you can do:
|
|
448
|
+
>>> stats.start_timer("num")
|
|
449
|
+
>>> stats.end_timer("den")
|
|
450
|
+
>>> stats.set_ratio("ratio", "num", "den")
|
|
451
|
+
>>> #
|
|
452
|
+
>>> # or set them first
|
|
453
|
+
>>> stats.set_timer("num")
|
|
454
|
+
>>> stats.set_timer("den")
|
|
455
|
+
>>> stats.set_ratio("ratio", "num", "den")
|
|
456
|
+
>>> #
|
|
457
|
+
>>> # then to calculate the ratio:
|
|
458
|
+
>>> ratio = stats.calculate_ratio("ratio")
|
|
459
|
+
"""
|
|
460
|
+
if numerator not in self._timers and numerator not in self._counters and numerator not in self._cumulative_timers:
|
|
461
|
+
raise ValueError(f"Numerator '{numerator}' not found in timers or counters")
|
|
462
|
+
|
|
463
|
+
if denominator not in self._timers and denominator not in self._counters and denominator not in self._cumulative_timers:
|
|
464
|
+
raise ValueError(f"Denominator '{denominator}' not found in timers or counters")
|
|
465
|
+
|
|
466
|
+
self._ratios[ratio_name] = {
|
|
467
|
+
'numerator': numerator,
|
|
468
|
+
'denominator': denominator,
|
|
469
|
+
'value': None
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def calculate_ratio(self, ratio_name: str) -> float:
|
|
474
|
+
"""
|
|
475
|
+
Calculate the value of a previously defined ratio.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
ratio_name: Name of the ratio to calculate
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Calculated ratio value
|
|
482
|
+
|
|
483
|
+
Raises:
|
|
484
|
+
ValueError: If ratio doesn't exist, timer not completed, or denominator is zero
|
|
485
|
+
|
|
486
|
+
Examples:
|
|
487
|
+
>>> stats = Stats()
|
|
488
|
+
>>> stats.count("success", 80)
|
|
489
|
+
>>> stats.count("total", 100)
|
|
490
|
+
>>> stats.add_ratio("success_rate", "success", "total")
|
|
491
|
+
>>> ratio = stats.calculate_ratio("success_rate") # 0.8
|
|
492
|
+
"""
|
|
493
|
+
if ratio_name not in self._ratios:
|
|
494
|
+
raise ValueError(f"Ratio '{ratio_name}' does not exist")
|
|
495
|
+
|
|
496
|
+
ratio_def = self._ratios[ratio_name]
|
|
497
|
+
numerator = ratio_def['numerator']
|
|
498
|
+
denominator = ratio_def['denominator']
|
|
499
|
+
|
|
500
|
+
def get_value(name: str) -> float:
|
|
501
|
+
if name in self._timers:
|
|
502
|
+
timer = self._timers[name]
|
|
503
|
+
return self.get_elapsed_time(name)
|
|
504
|
+
elif name in self._counters:
|
|
505
|
+
return self._counters[name]['value']
|
|
506
|
+
elif name in self._cumulative_timers:
|
|
507
|
+
return self.get_accumulated_time(name)
|
|
508
|
+
else:
|
|
509
|
+
raise ValueError(f"'{name}' not found in timers or counters")
|
|
510
|
+
|
|
511
|
+
num_value = get_value(numerator)
|
|
512
|
+
den_value = get_value(denominator)
|
|
513
|
+
|
|
514
|
+
if den_value == 0:
|
|
515
|
+
return 0.0
|
|
516
|
+
#raise ValueError(f"Denominator '{denominator}' cannot be zero")
|
|
517
|
+
|
|
518
|
+
ratio_value = num_value / den_value
|
|
519
|
+
self._ratios[ratio_name]['value'] = ratio_value
|
|
520
|
+
|
|
521
|
+
return ratio_value
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def set_attribute(self, name: str, value: str) -> None:
|
|
525
|
+
"""
|
|
526
|
+
Add an attribute (key-value pair).
|
|
527
|
+
If the attribute already exists, it will be overwritten.
|
|
528
|
+
Args:
|
|
529
|
+
name: Attribute name
|
|
530
|
+
value: Attribute value
|
|
531
|
+
|
|
532
|
+
Examples:
|
|
533
|
+
>>> stats = Stats()
|
|
534
|
+
>>> stats.add_attribute("version", "1.2.3")
|
|
535
|
+
>>> stats.add_attribute("environment", "production")
|
|
536
|
+
"""
|
|
537
|
+
self._attributes[name] = value
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
541
|
+
"""
|
|
542
|
+
Export all statistics as a flat dictionary.
|
|
543
|
+
|
|
544
|
+
For time accumulators, returns the total accumulated time. If the timer is currently running,
|
|
545
|
+
it adds the elapsed time since it was started to the accumulated time.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Dictionary containing all timers, counters, ratios, and attributes
|
|
549
|
+
|
|
550
|
+
Examples:
|
|
551
|
+
>>> stats = Stats()
|
|
552
|
+
>>> stats.start_timer("task")
|
|
553
|
+
>>> stats.end_timer("task")
|
|
554
|
+
>>> stats.count("items", 5, "pieces")
|
|
555
|
+
>>> stats.add_attribute("version", "1.0")
|
|
556
|
+
>>> data = stats.to_dict()
|
|
557
|
+
>>> # Returns: {
|
|
558
|
+
>>> # 'start_task_time': 1234567890.123,
|
|
559
|
+
>>> # 'end_task_time': 1234567891.456,
|
|
560
|
+
>>> # 'items_count': 5,
|
|
561
|
+
>>> # 'items_count_unit': 'pieces',
|
|
562
|
+
>>> # 'version': '1.0'
|
|
563
|
+
>>> # }
|
|
564
|
+
"""
|
|
565
|
+
result = {}
|
|
566
|
+
|
|
567
|
+
# Add timer data
|
|
568
|
+
for name, timer in self._timers.items():
|
|
569
|
+
if timer['start'] is not None:
|
|
570
|
+
result[f'start_{name}'] = timer['start']
|
|
571
|
+
result[f'start_{name}_date'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timer['start']))
|
|
572
|
+
|
|
573
|
+
if timer['end'] is None:
|
|
574
|
+
result[f'end_{name}'] = time.time()
|
|
575
|
+
# convert to iso datetime string in UTC
|
|
576
|
+
result[f'end_{name}_date'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result[f'end_{name}']))
|
|
577
|
+
else:
|
|
578
|
+
result[f'end_{name}'] = timer['end']
|
|
579
|
+
result[f'end_{name}_date'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timer['end']))
|
|
580
|
+
result[f'{name}_elapsed'] = self.get_elapsed_time(name)
|
|
581
|
+
|
|
582
|
+
# Add counter data
|
|
583
|
+
for name, counter in self._counters.items():
|
|
584
|
+
result[f'{name}'] = counter['value']
|
|
585
|
+
result[f'{name}_unit'] = counter['unit'] or "unit"
|
|
586
|
+
|
|
587
|
+
# Add ratio data
|
|
588
|
+
for name, ratio in self._ratios.items():
|
|
589
|
+
result[f'{name}'] = self.calculate_ratio(name)
|
|
590
|
+
# Add unit as combination of numerator and denominator units if available
|
|
591
|
+
num_unit = self._get_unit_for_name(ratio['numerator'])
|
|
592
|
+
den_unit = self._get_unit_for_name(ratio['denominator'])
|
|
593
|
+
if num_unit and den_unit and num_unit == den_unit:
|
|
594
|
+
result[f'{name}_unit'] = ""
|
|
595
|
+
else:
|
|
596
|
+
result[f'{name}_unit'] = f"{num_unit}s/{den_unit}"
|
|
597
|
+
|
|
598
|
+
for name, timer in self._cumulative_timers.items():
|
|
599
|
+
# if timer started, add elapsed time to accumulated
|
|
600
|
+
result[f'{name}'] = self.get_accumulated_time(name)
|
|
601
|
+
# Add attributes
|
|
602
|
+
result.update(self._attributes)
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
def _get_unit_for_name(self, name: str) -> Optional[str]:
|
|
606
|
+
"""Helper method to get unit for a timer or counter name."""
|
|
607
|
+
if name in self._counters:
|
|
608
|
+
return self._counters[name]['unit'] or "unit"
|
|
609
|
+
if name in self._timers:
|
|
610
|
+
return "second"
|
|
611
|
+
if name in self._cumulative_timers:
|
|
612
|
+
return "second"
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def __str__(self) -> str:
|
|
617
|
+
"""
|
|
618
|
+
Return a human-readable string representation of all statistics.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Formatted string containing all timers, counters, ratios, and attributes
|
|
622
|
+
|
|
623
|
+
Examples:
|
|
624
|
+
>>> stats = Stats()
|
|
625
|
+
>>> stats.start_timer("task")
|
|
626
|
+
>>> stats.end_timer("task")
|
|
627
|
+
>>> stats.count("items", 5, "pieces")
|
|
628
|
+
>>> stats.set_attribute("version", "1.0")
|
|
629
|
+
>>> print(stats)
|
|
630
|
+
"""
|
|
631
|
+
data = self.to_dict()
|
|
632
|
+
if not data:
|
|
633
|
+
return "Stats: (empty)"
|
|
634
|
+
|
|
635
|
+
lines = ["Stats:"]
|
|
636
|
+
for key, value in sorted(data.items()):
|
|
637
|
+
if isinstance(value, float):
|
|
638
|
+
formatted_value = f"{value:.4f}"
|
|
639
|
+
else:
|
|
640
|
+
formatted_value = str(value)
|
|
641
|
+
lines.append(f" {key}: {formatted_value}")
|
|
642
|
+
|
|
643
|
+
return "\n".join(lines)
|
rapidpro_api/telegram.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def message(text: str) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Sends a message to a Telegram chat using the bot token and chat ID from environment variables.
|
|
11
|
+
Expects the following environment variables to be set:
|
|
12
|
+
- TELEGRAM_BOT_TOKEN: The token of the Telegram bot.
|
|
13
|
+
- TELEGRAM_CHAT_ID: The chat ID where the message will be sent.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text (str): The message to send.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
bool: True if the message was sent successfully, False otherwise.
|
|
20
|
+
"""
|
|
21
|
+
logging.info(text)
|
|
22
|
+
bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
23
|
+
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
|
24
|
+
if not bot_token or not chat_id:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
|
28
|
+
payload = {
|
|
29
|
+
"chat_id": chat_id,
|
|
30
|
+
"text": text
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
response = requests.post(url, data=payload, timeout=10)
|
|
35
|
+
return response.status_code == 200 and response.json().get("ok", False)
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|