piholelongtermstats 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/PKG-INFO +13 -3
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/README.md +12 -2
- piholelongtermstats-0.2.3/piholelongtermstats/__init__.py +3 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/app.py +27 -15
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/db.py +27 -18
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/plot.py +53 -17
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/process.py +39 -13
- piholelongtermstats-0.2.3/piholelongtermstats/stats.py +528 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats.egg-info/PKG-INFO +13 -3
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats.egg-info/SOURCES.txt +5 -1
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/pyproject.toml +6 -2
- piholelongtermstats-0.2.3/tests/test_db.py +362 -0
- piholelongtermstats-0.2.3/tests/test_plot.py +274 -0
- piholelongtermstats-0.2.3/tests/test_process.py +301 -0
- piholelongtermstats-0.2.3/tests/test_stats.py +467 -0
- piholelongtermstats-0.2.2/piholelongtermstats/__init__.py +0 -3
- piholelongtermstats-0.2.2/piholelongtermstats/stats.py +0 -439
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/LICENSE +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/assets/favicon.ico +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/assets/logo_phlts.png +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats/assets/style.css +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats.egg-info/dependency_links.txt +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats.egg-info/entry_points.txt +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats.egg-info/requires.txt +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/piholelongtermstats.egg-info/top_level.txt +0 -0
- {piholelongtermstats-0.2.2 → piholelongtermstats-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: piholelongtermstats
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A dashboard to explore long-term DNS query data from Pi-hole FTL database files.
|
|
5
5
|
Author-email: Davis Thomas Daniel <davisthomasdaniel@gmail.com>
|
|
6
6
|
Maintainer-email: Davis Thomas Daniel <davisthomasdaniel@gmail.com>
|
|
@@ -26,11 +26,21 @@ Requires-Dist: twine==6.2.0; extra == "develop"
|
|
|
26
26
|
Requires-Dist: pytest-cov==7.0.0; extra == "develop"
|
|
27
27
|
Dynamic: license-file
|
|
28
28
|
|
|
29
|
-
# Pi Hole Long Term Statistics v.0.2.
|
|
29
|
+
# Pi Hole Long Term Statistics v.0.2.3
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
30
37
|
|
|
31
|
-
A dashboard built with **Dash** and **Plotly** to explore long-term DNS query data from **Pi-hole v.6** FTL database files. Visualize allowed vs blocked domains, top clients, and query trends over time. I originally made this for my own use, but if you find it useful, a ⭐ would be awesome!
|
|
32
38
|
|
|
33
39
|
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
A dashboard built with **Dash** and **Plotly** to explore long-term DNS query data from **Pi-hole v.6** FTL database files. Visualize allowed vs blocked domains, top clients, and query trends over time. I originally made this for my own use, but if you find it useful, a ⭐ would be awesome!
|
|
43
|
+
|
|
34
44
|
**Disclaimer : This is an unofficial, third-party project. The Pi Hole team and the development of [PiHole](https://pi-hole.net/) software is not related to this project.**
|
|
35
45
|
|
|
36
46
|
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
# Pi Hole Long Term Statistics v.0.2.
|
|
1
|
+
# Pi Hole Long Term Statistics v.0.2.3
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
2
9
|
|
|
3
|
-
A dashboard built with **Dash** and **Plotly** to explore long-term DNS query data from **Pi-hole v.6** FTL database files. Visualize allowed vs blocked domains, top clients, and query trends over time. I originally made this for my own use, but if you find it useful, a ⭐ would be awesome!
|
|
4
10
|
|
|
5
11
|
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
A dashboard built with **Dash** and **Plotly** to explore long-term DNS query data from **Pi-hole v.6** FTL database files. Visualize allowed vs blocked domains, top clients, and query trends over time. I originally made this for my own use, but if you find it useful, a ⭐ would be awesome!
|
|
15
|
+
|
|
6
16
|
**Disclaimer : This is an unofficial, third-party project. The Pi Hole team and the development of [PiHole](https://pi-hole.net/) software is not related to this project.**
|
|
7
17
|
|
|
8
18
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## Author : Davis T. Daniel
|
|
2
|
-
## PiHoleLongTermStats v.0.2.
|
|
2
|
+
## PiHoleLongTermStats v.0.2.3
|
|
3
3
|
## License : MIT
|
|
4
4
|
|
|
5
5
|
import os
|
|
@@ -25,7 +25,7 @@ from piholelongtermstats.plot import (
|
|
|
25
25
|
generate_queries_over_time,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
__version__ = "0.2.
|
|
28
|
+
__version__ = "0.2.3"
|
|
29
29
|
|
|
30
30
|
# logging setup
|
|
31
31
|
logging.basicConfig(
|
|
@@ -629,9 +629,15 @@ def serve_layout(
|
|
|
629
629
|
html.Div(
|
|
630
630
|
[
|
|
631
631
|
html.H3("Longest Idle Period"),
|
|
632
|
-
html.P(f"{stats['max_idle_ms']:,.0f} s"),
|
|
633
632
|
html.P(
|
|
634
|
-
f"
|
|
633
|
+
f"{stats['max_idle_ms']:,.0f} s"
|
|
634
|
+
if stats['max_idle_ms'] is not None
|
|
635
|
+
else "N/A"
|
|
636
|
+
),
|
|
637
|
+
html.P(
|
|
638
|
+
f"Between {stats['before_gap']} and {stats['after_gap']}"
|
|
639
|
+
if stats['before_gap'] and stats['after_gap']
|
|
640
|
+
else "N/A",
|
|
635
641
|
style={"fontSize": "14px", "color": "#777"},
|
|
636
642
|
),
|
|
637
643
|
],
|
|
@@ -652,7 +658,9 @@ def serve_layout(
|
|
|
652
658
|
},
|
|
653
659
|
),
|
|
654
660
|
html.P(
|
|
655
|
-
f"Avg reply time: {stats['slowest_avg_reply_time'] * 1000:.2f} ms"
|
|
661
|
+
f"Avg reply time: {stats['slowest_avg_reply_time'] * 1000:.2f} ms"
|
|
662
|
+
if stats['slowest_avg_reply_time']
|
|
663
|
+
else "N/A",
|
|
656
664
|
style={"fontSize": "14px", "color": "#777"},
|
|
657
665
|
),
|
|
658
666
|
],
|
|
@@ -993,11 +1001,13 @@ chunksize_list, latest_ts_list, oldest_ts_list = (
|
|
|
993
1001
|
|
|
994
1002
|
for db in db_paths:
|
|
995
1003
|
conn = connect_to_sql(db)
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1004
|
+
try:
|
|
1005
|
+
chunksize, latest_ts, oldest_ts = probe_sample_df(conn)
|
|
1006
|
+
chunksize_list.append(chunksize)
|
|
1007
|
+
latest_ts_list.append(latest_ts.tz_convert(ZoneInfo(args.timezone)))
|
|
1008
|
+
oldest_ts_list.append(oldest_ts.tz_convert(ZoneInfo(args.timezone)))
|
|
1009
|
+
finally:
|
|
1010
|
+
conn.close()
|
|
1001
1011
|
|
|
1002
1012
|
logging.info(
|
|
1003
1013
|
f"Latest date-time from all databases : {max(latest_ts_list)} (TZ: {args.timezone})"
|
|
@@ -1063,11 +1073,13 @@ def reload_page(n_clicks, start_date, end_date):
|
|
|
1063
1073
|
|
|
1064
1074
|
for db in db_paths:
|
|
1065
1075
|
conn = connect_to_sql(db)
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1076
|
+
try:
|
|
1077
|
+
chunksize, latest_ts, oldest_ts = probe_sample_df(conn)
|
|
1078
|
+
chunksize_list.append(chunksize)
|
|
1079
|
+
latest_ts_list.append(latest_ts.tz_convert(ZoneInfo(args.timezone)))
|
|
1080
|
+
oldest_ts_list.append(oldest_ts.tz_convert(ZoneInfo(args.timezone)))
|
|
1081
|
+
finally:
|
|
1082
|
+
conn.close()
|
|
1071
1083
|
|
|
1072
1084
|
logging.info(
|
|
1073
1085
|
f"Latest date-time from all databases : {max(latest_ts_list)} (TZ: {args.timezone})"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## Author : Davis T. Daniel
|
|
2
|
-
## PiHoleLongTermStats v.0.2.
|
|
2
|
+
## PiHoleLongTermStats v.0.2.3
|
|
3
3
|
## License : MIT
|
|
4
4
|
|
|
5
5
|
import sqlite3
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
import psutil
|
|
9
9
|
import pandas as pd
|
|
10
10
|
import logging
|
|
11
|
-
from zoneinfo import ZoneInfo
|
|
11
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
12
12
|
import gc
|
|
13
13
|
|
|
14
14
|
|
|
@@ -31,14 +31,20 @@ def connect_to_sql(db_path):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def probe_sample_df(conn):
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"""Calculate safe chunksize based on available memory and retrieve timestamp range.
|
|
35
|
+
|
|
36
|
+
Analyzes a sample of the database to estimate memory requirements per row
|
|
37
|
+
and retrieves the oldest and latest timestamps in the database.
|
|
38
|
+
"""
|
|
37
39
|
sample_query = """SELECT id, timestamp, type, status, domain, client, reply_time
|
|
38
|
-
FROM queries LIMIT
|
|
40
|
+
FROM queries LIMIT 100"""
|
|
39
41
|
sample_df = pd.read_sql_query(sample_query, conn)
|
|
42
|
+
if sample_df.empty:
|
|
43
|
+
raise ValueError("No data from database for the selected time frame.")
|
|
44
|
+
|
|
40
45
|
sample_df["timestamp"] = pd.to_datetime(sample_df["timestamp"], unit="s")
|
|
41
46
|
|
|
47
|
+
|
|
42
48
|
available_memory = psutil.virtual_memory().available
|
|
43
49
|
memory_per_row = sample_df.memory_usage(deep=True).sum() / len(sample_df)
|
|
44
50
|
safe_memory = available_memory * 0.5
|
|
@@ -63,7 +69,7 @@ def probe_sample_df(conn):
|
|
|
63
69
|
def get_timestamp_range(days, start_date, end_date, timezone):
|
|
64
70
|
try:
|
|
65
71
|
tz = ZoneInfo(timezone)
|
|
66
|
-
except
|
|
72
|
+
except ZoneInfoNotFoundError:
|
|
67
73
|
logging.warning(f"Invalid timezone '{timezone}', using UTC")
|
|
68
74
|
tz = ZoneInfo("UTC")
|
|
69
75
|
|
|
@@ -120,11 +126,13 @@ def read_pihole_ftl_db(
|
|
|
120
126
|
f"Reading data from PiHole-FTL database(s) for timestamps ranging from {start_timestamp} to {end_timestamp} (TZ: UTC)..."
|
|
121
127
|
)
|
|
122
128
|
|
|
123
|
-
|
|
129
|
+
# no sql injection
|
|
130
|
+
query = """
|
|
124
131
|
SELECT id, timestamp, type, status, domain, client, reply_time
|
|
125
132
|
FROM queries
|
|
126
|
-
WHERE timestamp >=
|
|
133
|
+
WHERE timestamp >= ? AND timestamp < ?;
|
|
127
134
|
"""
|
|
135
|
+
params = [start_timestamp, end_timestamp]
|
|
128
136
|
|
|
129
137
|
for db_idx, db_path in enumerate(db_paths):
|
|
130
138
|
logging.info(
|
|
@@ -132,12 +140,13 @@ def read_pihole_ftl_db(
|
|
|
132
140
|
)
|
|
133
141
|
conn = connect_to_sql(db_path)
|
|
134
142
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
try:
|
|
144
|
+
chunk_num = 0
|
|
145
|
+
for chunk in pd.read_sql_query(query, conn, params=params, chunksize=chunksize[db_idx]):
|
|
146
|
+
chunk_num += 1
|
|
147
|
+
logging.info(
|
|
148
|
+
f"Processing dataframe chunk {chunk_num} from database {db_idx + 1} at {db_path}..."
|
|
149
|
+
)
|
|
150
|
+
yield chunk
|
|
151
|
+
finally:
|
|
152
|
+
conn.close()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## Author : Davis T. Daniel
|
|
2
|
-
## PiHoleLongTermStats v.0.2.
|
|
2
|
+
## PiHoleLongTermStats v.0.2.3
|
|
3
3
|
## License : MIT
|
|
4
4
|
|
|
5
5
|
import logging
|
|
@@ -10,7 +10,16 @@ import itertools
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def generate_plot_data(df, n_clients, n_domains):
|
|
13
|
-
"""Generate plot data
|
|
13
|
+
"""Generate plot data for dashboard visualizations.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
df: Pre-processed DataFrame with query data.
|
|
17
|
+
n_clients: Number of top clients to include in plots.
|
|
18
|
+
n_domains: Number of top domains to include in plots.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
dict: Dictionary containing DataFrames and data for various plots.
|
|
22
|
+
"""
|
|
14
23
|
|
|
15
24
|
logging.info("Generating plot data...")
|
|
16
25
|
|
|
@@ -24,17 +33,19 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
24
33
|
.size()
|
|
25
34
|
.reset_index(name="count")
|
|
26
35
|
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
|
|
37
|
+
if not top_clients_stacked.empty:
|
|
38
|
+
top_clients_stacked["client"] = pd.Categorical(
|
|
39
|
+
top_clients_stacked["client"],
|
|
40
|
+
categories=top_clients_stacked.groupby("client")["count"]
|
|
41
|
+
.sum()
|
|
42
|
+
.sort_values(ascending=False)
|
|
43
|
+
.index,
|
|
44
|
+
ordered=True,
|
|
45
|
+
)
|
|
46
|
+
top_clients_stacked = top_clients_stacked.sort_values(
|
|
47
|
+
["client", "count"], ascending=[True, False]
|
|
48
|
+
)
|
|
38
49
|
logging.info("Generated plot data for top clients.")
|
|
39
50
|
|
|
40
51
|
# plot data for allowed and blocked domains
|
|
@@ -46,7 +57,7 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
46
57
|
.value_counts()
|
|
47
58
|
.nlargest(n_domains)
|
|
48
59
|
.reset_index()
|
|
49
|
-
.rename(columns={"
|
|
60
|
+
.rename(columns={"domain": "Domain", "count": "count"})
|
|
50
61
|
)
|
|
51
62
|
|
|
52
63
|
tmp_allowed = df[df["status_type"] == "Allowed"].copy()
|
|
@@ -57,7 +68,7 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
57
68
|
.value_counts()
|
|
58
69
|
.nlargest(n_domains)
|
|
59
70
|
.reset_index()
|
|
60
|
-
.rename(columns={"
|
|
71
|
+
.rename(columns={"domain": "Domain", "count": "count"})
|
|
61
72
|
)
|
|
62
73
|
|
|
63
74
|
logging.info("Generated plot data for allowed and blocked domains.")
|
|
@@ -131,6 +142,11 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
131
142
|
gc.collect()
|
|
132
143
|
|
|
133
144
|
logging.info("Plot data generation complete")
|
|
145
|
+
|
|
146
|
+
if not df.empty:
|
|
147
|
+
data_span_days = (df["timestamp"].max() - df["timestamp"].min()).days
|
|
148
|
+
else:
|
|
149
|
+
data_span_days = 0
|
|
134
150
|
|
|
135
151
|
return {
|
|
136
152
|
"top_clients_stacked": top_clients_stacked,
|
|
@@ -138,7 +154,7 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
138
154
|
"allowed_df": allowed_df,
|
|
139
155
|
"reply_time_df": reply_time_df,
|
|
140
156
|
"client_list": client_list,
|
|
141
|
-
"data_span_days":
|
|
157
|
+
"data_span_days": data_span_days,
|
|
142
158
|
"client_domain_scatter_df": client_domain_scatter_df,
|
|
143
159
|
"day_hour_heatmap": day_hour_heatmap,
|
|
144
160
|
"blocked_day_hour_heatmap": blocked_day_hour_heatmap,
|
|
@@ -147,6 +163,15 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
147
163
|
|
|
148
164
|
|
|
149
165
|
def generate_queries_over_time(callback_data, client=None):
|
|
166
|
+
"""Generate an area chart showing DNS queries over time.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
callback_data: Dictionary containing hourly aggregation data.
|
|
170
|
+
client: Optional client filter to show queries for a specific client.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
plotly.graph_objects.Figure: Area chart of queries over time.
|
|
174
|
+
"""
|
|
150
175
|
dff_grouped = callback_data["hourly_agg"]
|
|
151
176
|
|
|
152
177
|
if client is not None:
|
|
@@ -240,6 +265,16 @@ def generate_queries_over_time(callback_data, client=None):
|
|
|
240
265
|
|
|
241
266
|
|
|
242
267
|
def generate_client_activity_over_time(callback_data, n_clients, client=None):
|
|
268
|
+
"""Generate an area chart showing client activity over time.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
callback_data: Dictionary containing hourly aggregation data and top clients list.
|
|
272
|
+
n_clients: Number of top clients to display when no specific client is selected.
|
|
273
|
+
client: Optional client filter to show activity for a specific client.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
plotly.graph_objects.Figure: Area chart of client activity over time.
|
|
277
|
+
"""
|
|
243
278
|
dff_grouped = callback_data["hourly_agg"]
|
|
244
279
|
top_clients = callback_data["top_clients"]
|
|
245
280
|
|
|
@@ -295,7 +330,8 @@ def generate_client_activity_over_time(callback_data, n_clients, client=None):
|
|
|
295
330
|
)
|
|
296
331
|
|
|
297
332
|
default_colors = px.colors.qualitative.Plotly
|
|
298
|
-
|
|
333
|
+
# build color map for all clients that will be shown (not just top_clients)
|
|
334
|
+
client_color_map = dict(zip(clients_to_show, itertools.cycle(default_colors)))
|
|
299
335
|
|
|
300
336
|
fig = px.area(
|
|
301
337
|
pivot_df,
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
## Author : Davis T. Daniel
|
|
2
|
-
## PiHoleLongTermStats v.0.2.
|
|
2
|
+
## PiHoleLongTermStats v.0.2.3
|
|
3
3
|
## License : MIT
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
import logging
|
|
7
|
-
from zoneinfo import ZoneInfo
|
|
7
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _is_valid_regex(pattern):
|
|
12
|
+
"""Check if a string is a valid regular expression pattern.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
pattern: String to validate as regex pattern.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
bool: True if pattern is valid regex, False otherwise.
|
|
19
|
+
"""
|
|
12
20
|
try:
|
|
13
21
|
re.compile(pattern)
|
|
14
22
|
return True
|
|
@@ -17,6 +25,15 @@ def _is_valid_regex(pattern):
|
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
def regex_ignore_domains(df, pattern):
|
|
28
|
+
"""Filter out rows where domain matches the given regex pattern.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
df: DataFrame containing a 'domain' column.
|
|
32
|
+
pattern: Regex pattern to match domains for exclusion.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
DataFrame with matching domains removed, or original DataFrame if pattern is invalid.
|
|
36
|
+
"""
|
|
20
37
|
if _is_valid_regex(pattern):
|
|
21
38
|
mask = df["domain"].str.contains(pattern, regex=True, na=False)
|
|
22
39
|
return df[~mask].reset_index(drop=True)
|
|
@@ -33,9 +50,9 @@ def preprocess_df(df, timezone="UTC"):
|
|
|
33
50
|
logging.info("Pre-processing dataframe...")
|
|
34
51
|
|
|
35
52
|
try:
|
|
36
|
-
|
|
37
|
-
except
|
|
38
|
-
logging.warning(f"Invalid timezone '{timezone}', falling back to UTC
|
|
53
|
+
ZoneInfo(timezone)
|
|
54
|
+
except ZoneInfoNotFoundError:
|
|
55
|
+
logging.warning(f"Invalid timezone '{timezone}', falling back to UTC")
|
|
39
56
|
timezone = "UTC"
|
|
40
57
|
|
|
41
58
|
logging.info(f"Selected timezone : {timezone}")
|
|
@@ -51,13 +68,13 @@ def preprocess_df(df, timezone="UTC"):
|
|
|
51
68
|
|
|
52
69
|
# status ids for pihole ftl db, see pi-hole FTL docs
|
|
53
70
|
logging.info("Processing allowed and blocked status codes...")
|
|
54
|
-
allowed_statuses =
|
|
55
|
-
blocked_statuses =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
71
|
+
allowed_statuses = {2, 3, 12, 13, 14, 17}
|
|
72
|
+
blocked_statuses = {1, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 18}
|
|
73
|
+
|
|
74
|
+
# Use vectorized operations for better performance
|
|
75
|
+
df["status_type"] = "Other"
|
|
76
|
+
df.loc[df["status"].isin(allowed_statuses), "status_type"] = "Allowed"
|
|
77
|
+
df.loc[df["status"].isin(blocked_statuses), "status_type"] = "Blocked"
|
|
61
78
|
|
|
62
79
|
df["day_name"] = df["timestamp"].dt.day_name()
|
|
63
80
|
df["reply_time"] = pd.to_numeric(df["reply_time"], errors="coerce")
|
|
@@ -67,7 +84,16 @@ def preprocess_df(df, timezone="UTC"):
|
|
|
67
84
|
|
|
68
85
|
|
|
69
86
|
def prepare_hourly_aggregated_data(df, n_clients):
|
|
70
|
-
"""Pre-aggregate data by hour
|
|
87
|
+
"""Pre-aggregate data by hour for callback processing.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
df: Pre-processed DataFrame with timestamp, status_type, and client columns.
|
|
91
|
+
n_clients: Number of top clients to identify for client activity views.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict: Contains 'hourly_agg' DataFrame with hourly aggregations and
|
|
95
|
+
'top_clients' list of top n_clients client identifiers.
|
|
96
|
+
"""
|
|
71
97
|
logging.info("Pre-aggregating data by hour for callbacks...")
|
|
72
98
|
|
|
73
99
|
# aggregate by hour, status_type, and client
|