logdetective 2.13.0__py3-none-any.whl → 3.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.
@@ -1,432 +0,0 @@
1
- import datetime
2
- from typing import Optional, Union, Dict
3
-
4
- import numpy
5
- from numpy.typing import ArrayLike
6
- from matplotlib import dates, colormaps, axes, pyplot, figure
7
-
8
- from logdetective.server.models import TimePeriod
9
- from logdetective.server.database.models import (
10
- AnalyzeRequestMetrics,
11
- EndpointType,
12
- Reactions,
13
- )
14
-
15
-
16
- class Definition:
17
- """Define plot details, given a time period."""
18
-
19
- def __init__(self, time_period: TimePeriod):
20
- self.time_period = time_period
21
- self.days_diff = time_period.get_time_period().days
22
- if self.time_period.hours:
23
- self._freq = "H"
24
- self._time_format = "%Y-%m-%d %H"
25
- self._locator = dates.HourLocator(interval=2)
26
- self._time_unit = "hour"
27
- self._time_delta = datetime.timedelta(hours=1)
28
- elif self.time_period.days:
29
- self._freq = "D"
30
- self._time_format = "%Y-%m-%d"
31
- self._locator = dates.DayLocator(interval=1)
32
- self._time_unit = "day"
33
- self._time_delta = datetime.timedelta(days=1)
34
- elif self.time_period.weeks:
35
- self._freq = "W"
36
- self._time_format = "%Y-%m-%d"
37
- self._locator = dates.WeekdayLocator(interval=1)
38
- self._time_unit = "week"
39
- self._time_delta = datetime.timedelta(weeks=1)
40
-
41
- @property
42
- def freq(self):
43
- # pylint: disable=missing-function-docstring
44
- return self._freq
45
-
46
- @property
47
- def time_format(self):
48
- # pylint: disable=missing-function-docstring
49
- return self._time_format
50
-
51
- @property
52
- def locator(self):
53
- # pylint: disable=missing-function-docstring
54
- return self._locator
55
-
56
- @property
57
- def time_unit(self):
58
- # pylint: disable=missing-function-docstring
59
- return self._time_unit
60
-
61
- @property
62
- def time_delta(self):
63
- # pylint: disable=missing-function-docstring
64
- return self._time_delta
65
-
66
-
67
- def create_time_series_arrays(
68
- values_dict: dict[datetime.datetime, int],
69
- plot_def: Definition,
70
- start_time: datetime.datetime,
71
- end_time: datetime.datetime,
72
- value_type: Optional[Union[type[int], type[float]]] = int,
73
- ) -> tuple[numpy.ndarray, numpy.ndarray]:
74
- """Create time series arrays from a dictionary of values.
75
-
76
- This function generates two aligned numpy arrays:
77
- 1. An array of timestamps from start_time to end_time
78
- 2. A corresponding array of valuesfor each timestamp
79
-
80
- The timestamps are truncated to the precision specified by time_format.
81
- If a timestamp in values_dict matches a generated timestamp, its values is used;
82
- otherwise, the value defaults to zero.
83
-
84
- Args:
85
- values_dict: Dictionary mapping timestamps to their respective values
86
- start_time: The starting timestamp of the time series
87
- end_time: The ending timestamp of the time series
88
- time_delta: The time interval between consecutive timestamps
89
- time_format: String format for datetime truncation (e.g., '%Y-%m-%d %H:%M')
90
-
91
- Returns:
92
- A tuple containing:
93
- - numpy.ndarray: Array of timestamps
94
- - numpy.ndarray: Array of corresponding values
95
- """
96
- num_intervals = int((end_time - start_time) / plot_def.time_delta) + 1
97
-
98
- timestamps = numpy.array(
99
- [
100
- datetime.datetime.strptime(
101
- (start_time + i * plot_def.time_delta).strftime(
102
- format=plot_def.time_format
103
- ),
104
- plot_def.time_format,
105
- )
106
- for i in range(num_intervals)
107
- ]
108
- )
109
- values = numpy.zeros(num_intervals, dtype=value_type)
110
-
111
- timestamp_to_index = {timestamp: i for i, timestamp in enumerate(timestamps)}
112
-
113
- for timestamp, count in values_dict.items():
114
- if timestamp in timestamp_to_index:
115
- values[timestamp_to_index[timestamp]] = count
116
-
117
- return timestamps, values
118
-
119
-
120
- def _add_bar_chart(
121
- ax: axes.Axes,
122
- plot_def: Definition,
123
- timestamps: ArrayLike,
124
- values: ArrayLike,
125
- label: str,
126
- ) -> None:
127
- """Add a blue bar chart"""
128
- bar_width = (
129
- 0.8 * plot_def.time_delta.total_seconds() / 86400
130
- ) # Convert to days for matplotlib
131
- ax.bar(
132
- timestamps,
133
- values,
134
- width=bar_width,
135
- alpha=0.7,
136
- color="skyblue",
137
- label=label,
138
- )
139
- ax.set_xlabel("Time")
140
- ax.set_ylabel(label, color="blue")
141
- ax.tick_params(axis="y", labelcolor="blue")
142
-
143
- ax.xaxis.set_major_formatter(dates.DateFormatter(plot_def.time_format))
144
- ax.xaxis.set_major_locator(plot_def.locator)
145
-
146
- pyplot.xticks(rotation=45)
147
-
148
- ax.grid(True, alpha=0.3)
149
-
150
-
151
- def _add_line_chart( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
152
- ax: axes.Axes,
153
- timestamps: ArrayLike,
154
- values: ArrayLike,
155
- label: str,
156
- color: str = "red",
157
- set_label: bool = True,
158
- ):
159
- """Add a red line chart"""
160
- ax.plot(timestamps, values, color=color, linestyle="-", linewidth=2, label=label)
161
- if set_label:
162
- ax.set_ylabel(label, color=color)
163
- ax.tick_params(axis="y", labelcolor=color)
164
-
165
-
166
- async def requests_per_time(
167
- period_of_time: TimePeriod,
168
- endpoint: EndpointType = EndpointType.ANALYZE,
169
- end_time: Optional[datetime.datetime] = None,
170
- ) -> figure.Figure:
171
- """
172
- Generate a visualization of request counts over a specified time period.
173
-
174
- This function creates a dual-axis plot showing:
175
- 1. A bar chart of request counts per time interval
176
- 2. A line chart showing the cumulative request count
177
-
178
- The time intervals are determined by the provided TimePeriod object, which defines
179
- the granularity and formatting of the time axis.
180
-
181
- Args:
182
- period_of_time: A TimePeriod object that defines the time period and interval
183
- for the analysis (e.g., hourly, daily, weekly)
184
- endpoint: One of the API endpoints
185
- end_time: The end time for the analysis period. If None, defaults to the current
186
- UTC time
187
-
188
- Returns:
189
- A matplotlib Figure object containing the generated visualization
190
- """
191
- end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
192
- start_time = period_of_time.get_period_start_time(end_time)
193
- plot_def = Definition(period_of_time)
194
- requests_counts = await AnalyzeRequestMetrics.get_requests_in_period(
195
- start_time, end_time, plot_def.time_format, endpoint
196
- )
197
- timestamps, counts = create_time_series_arrays(
198
- requests_counts, plot_def, start_time, end_time
199
- )
200
-
201
- fig, ax1 = pyplot.subplots(figsize=(12, 6))
202
- _add_bar_chart(ax1, plot_def, timestamps, counts, "Requests")
203
-
204
- ax2 = ax1.twinx()
205
- _add_line_chart(ax2, timestamps, numpy.cumsum(counts), "Cumulative Requests")
206
-
207
- pyplot.title(
208
- f"Requests received for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
209
- f"to {end_time.strftime(plot_def.time_format)})"
210
- )
211
-
212
- lines1, labels1 = ax1.get_legend_handles_labels()
213
- lines2, labels2 = ax2.get_legend_handles_labels()
214
- ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
215
-
216
- pyplot.tight_layout()
217
-
218
- return fig
219
-
220
-
221
- async def average_time_per_responses( # pylint: disable=too-many-locals
222
- period_of_time: TimePeriod,
223
- endpoint: EndpointType = EndpointType.ANALYZE,
224
- end_time: Optional[datetime.datetime] = None,
225
- ) -> figure.Figure:
226
- """
227
- Generate a visualization of average response time and length over a specified time period.
228
-
229
- This function creates a dual-axis plot showing:
230
- 1. A bar chart of average response time per time interval
231
- 1. A line chart of average response length per time interval
232
-
233
- The time intervals are determined by the provided TimePeriod object, which defines
234
- the granularity and formatting of the time axis.
235
-
236
- Args:
237
- period_of_time: A TimePeriod object that defines the time period and interval
238
- for the analysis (e.g., hourly, daily, weekly)
239
- endpoint: One of the API endpoints
240
- end_time: The end time for the analysis period. If None, defaults to the current
241
- UTC time
242
-
243
- Returns:
244
- A matplotlib Figure object containing the generated visualization
245
- """
246
- end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
247
- start_time = period_of_time.get_period_start_time(end_time)
248
- plot_def = Definition(period_of_time)
249
- responses_average_time = (
250
- await AnalyzeRequestMetrics.get_responses_average_time_in_period(
251
- start_time, end_time, plot_def.time_format, endpoint
252
- )
253
- )
254
- timestamps, average_time = create_time_series_arrays(
255
- responses_average_time,
256
- plot_def,
257
- start_time,
258
- end_time,
259
- float,
260
- )
261
-
262
- fig, ax1 = pyplot.subplots(figsize=(12, 6))
263
- _add_bar_chart(
264
- ax1, plot_def, timestamps, average_time, "average response time (seconds)"
265
- )
266
-
267
- responses_average_length = (
268
- await AnalyzeRequestMetrics.get_responses_average_length_in_period(
269
- start_time, end_time, plot_def.time_format, endpoint
270
- )
271
- )
272
- timestamps, average_length = create_time_series_arrays(
273
- responses_average_length,
274
- plot_def,
275
- start_time,
276
- end_time,
277
- float,
278
- )
279
-
280
- ax2 = ax1.twinx()
281
- _add_line_chart(ax2, timestamps, average_length, "average response length (chars)")
282
-
283
- pyplot.title(
284
- f"average response time for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
285
- f"to {end_time.strftime(plot_def.time_format)})"
286
- )
287
-
288
- lines1, labels1 = ax1.get_legend_handles_labels()
289
- lines2, labels2 = ax2.get_legend_handles_labels()
290
- ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
291
-
292
- pyplot.tight_layout()
293
-
294
- return fig
295
-
296
-
297
- async def _collect_emoji_data(
298
- start_time: datetime.datetime, plot_def: Definition
299
- ) -> Dict[str, Dict[datetime.datetime, int]]:
300
- """Collect and organize emoji feedback data
301
-
302
- Counts all emojis given to logdetective comments created since start_time.
303
- Collect counts in time accordingly to the plot definition.
304
- """
305
- reactions = await Reactions.get_since(start_time)
306
- reactions_values_dict: Dict[str, Dict] = {}
307
- for comment_created_at, reaction in reactions:
308
- comment_created_at_formatted = comment_created_at.strptime(
309
- comment_created_at.strftime(plot_def.time_format), plot_def.time_format
310
- )
311
- if reaction.reaction_type in reactions_values_dict:
312
- reaction_values_dict = reactions_values_dict[reaction.reaction_type]
313
- if comment_created_at_formatted in reaction_values_dict:
314
- reaction_values_dict[comment_created_at_formatted] += reaction.count
315
- else:
316
- reaction_values_dict[comment_created_at_formatted] = reaction.count
317
- else:
318
- reaction_values_dict = {comment_created_at_formatted: reaction.count}
319
- reactions_values_dict.update({reaction.reaction_type: reaction_values_dict})
320
-
321
- return reactions_values_dict
322
-
323
-
324
- def _plot_emoji_data( # pylint: disable=too-many-locals
325
- ax: axes.Axes,
326
- reactions_values_dict: Dict[str, Dict[datetime.datetime, int]],
327
- plot_def: Definition,
328
- start_time: datetime.datetime,
329
- end_time: datetime.datetime,
330
- ):
331
- """Plot each emoji's data on its own axis."""
332
- emoji_lines = {}
333
- emoji_labels = {}
334
-
335
- # Find global min and max y values to set consistent scale
336
- all_counts = []
337
- for emoji, dict_counts in reactions_values_dict.items():
338
- timestamps, counts = create_time_series_arrays(
339
- dict_counts, plot_def, start_time, end_time
340
- )
341
- all_counts.extend(counts)
342
-
343
- colors = [
344
- colormaps["viridis"](i)
345
- for i in numpy.linspace(0, 1, len(reactions_values_dict))
346
- ]
347
-
348
- first_emoji = True
349
- for i, (emoji, dict_counts) in enumerate(reactions_values_dict.items()):
350
- timestamps, counts = create_time_series_arrays(
351
- dict_counts, plot_def, start_time, end_time
352
- )
353
-
354
- if first_emoji:
355
- current_ax = ax
356
- first_emoji = False
357
- else:
358
- current_ax = ax.twinx()
359
- current_ax.spines["right"].set_position(("outward", 60 * (i - 1)))
360
-
361
- _add_line_chart(current_ax, timestamps, counts, f"{emoji}", colors[i], False)
362
- emoji_lines[emoji], emoji_labels[emoji] = current_ax.get_legend_handles_labels()
363
-
364
- # Set the same y-limits for all axes
365
- current_ax.set_ylim(0, max(all_counts) * 1.1)
366
-
367
- # Only show y-ticks on the first axis to avoid clutter
368
- if 0 < i < len(reactions_values_dict):
369
- current_ax.set_yticks([])
370
-
371
- return emoji_lines, emoji_labels
372
-
373
-
374
- async def emojis_per_time(
375
- period_of_time: TimePeriod,
376
- end_time: Optional[datetime.datetime] = None,
377
- ) -> figure.Figure:
378
- """
379
- Generate a visualization of overall emoji feedback
380
- over a specified time period.
381
-
382
- This function creates a multiple-axis plot showing
383
- a line chart for every found emoji
384
-
385
- The time intervals are determined by the provided TimePeriod object, which defines
386
- the granularity and formatting of the time axis.
387
-
388
- Args:
389
- period_of_time: A TimePeriod object that defines the time period and interval
390
- for the analysis (e.g., hourly, daily, weekly)
391
- end_time: The end time for the analysis period. If None, defaults to the current
392
- UTC time
393
-
394
- Returns:
395
- A matplotlib Figure object containing the generated visualization
396
- """
397
- plot_def = Definition(period_of_time)
398
- end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
399
- start_time = period_of_time.get_period_start_time(end_time)
400
- reactions_values_dict = await _collect_emoji_data(start_time, plot_def)
401
-
402
- fig, ax = pyplot.subplots(figsize=(12, 6))
403
-
404
- emoji_lines, emoji_labels = _plot_emoji_data(
405
- ax, reactions_values_dict, plot_def, start_time, end_time
406
- )
407
-
408
- pyplot.title(
409
- f"Emoji feedback ({start_time.strftime(plot_def.time_format)} "
410
- f"to {end_time.strftime(plot_def.time_format)})"
411
- )
412
-
413
- all_lines = []
414
- for lines in emoji_lines.values():
415
- all_lines.extend(lines)
416
- all_labels = []
417
- for labels in emoji_labels.values():
418
- all_labels.extend(labels)
419
-
420
- ax.legend(all_lines, all_labels, loc="upper left")
421
- ax.set_xlabel("Time")
422
- ax.set_ylabel("Count")
423
-
424
- # Format x-axis
425
- ax.xaxis.set_major_formatter(dates.DateFormatter(plot_def.time_format))
426
- ax.xaxis.set_major_locator(plot_def.locator)
427
- ax.tick_params(axis="x", labelrotation=45)
428
- ax.grid(True, alpha=0.3)
429
-
430
- pyplot.tight_layout()
431
-
432
- return fig