piholelongtermstats 0.2.4__tar.gz → 0.2.6__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.4 → piholelongtermstats-0.2.6}/PKG-INFO +10 -8
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/README.md +9 -6
- piholelongtermstats-0.2.6/piholelongtermstats/__init__.py +3 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/app.py +181 -40
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/assets/style.css +18 -1
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/db.py +1 -1
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/plot.py +80 -2
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/process.py +1 -1
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/stats.py +1 -1
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/PKG-INFO +10 -8
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/requires.txt +0 -1
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/pyproject.toml +6 -2
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/tests/test_plot.py +40 -0
- piholelongtermstats-0.2.4/piholelongtermstats/__init__.py +0 -3
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/LICENSE +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/assets/favicon.ico +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/assets/logo_phlts.png +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/SOURCES.txt +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/dependency_links.txt +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/entry_points.txt +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/top_level.txt +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/setup.cfg +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/tests/test_db.py +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/tests/test_process.py +0 -0
- {piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/tests/test_stats.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: piholelongtermstats
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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>
|
|
@@ -22,11 +22,10 @@ Provides-Extra: develop
|
|
|
22
22
|
Requires-Dist: pytest==9.0.1; extra == "develop"
|
|
23
23
|
Requires-Dist: ruff; extra == "develop"
|
|
24
24
|
Requires-Dist: build==1.3.0; extra == "develop"
|
|
25
|
-
Requires-Dist: twine==6.2.0; extra == "develop"
|
|
26
25
|
Requires-Dist: pytest-cov==7.0.0; extra == "develop"
|
|
27
26
|
Dynamic: license-file
|
|
28
27
|
|
|
29
|
-
# Pi Hole Long Term Statistics v.0.2.
|
|
28
|
+
# Pi Hole Long Term Statistics v.0.2.6
|
|
30
29
|
|
|
31
30
|
<div align="center">
|
|
32
31
|
|
|
@@ -74,11 +73,13 @@ See more [screenshots](https://github.com/davistdaniel/PiHoleLongTermStats/blob/
|
|
|
74
73
|
> Using your actual Pi-hole FTL db file for querying is **not** recommended and it is advised to use a copy. Place the copy in the project root or specify its path using the `--db_path` argument or `PIHOLE_LT_STATS_DB_PATH` environment variable. In any case, PiHoleLongTermStats does not monitor for changes in the Pi-hole FTL db file even if you mount it.
|
|
75
74
|
|
|
76
75
|
> [!TIP]
|
|
77
|
-
> * You can set up a cron job to periodically copy the FTL database to the `db_path` `PIHOLE_LT_STATS_DB_PATH` location, ensuring your stats are updated without touching the
|
|
76
|
+
> * You can set up a cron job to periodically copy the FTL database to the `db_path` `PIHOLE_LT_STATS_DB_PATH` location, ensuring your stats are updated without touching the actual database. Use the reload button in the dashboard to refresh the stats.
|
|
78
77
|
> * Set your timezone (e.g "Europe/Berlin") using `--timezone` or `PIHOLE_LT_STATS_TIMEZONE`.
|
|
79
78
|
|
|
80
79
|
> [!IMPORTANT]
|
|
81
|
-
> * PiHoleLongTermStats reloads the Pi-hole FTL database and recalculates stats whenever the
|
|
80
|
+
> * PiHoleLongTermStats reloads the Pi-hole FTL database and recalculates stats whenever the the reload button is clicked.
|
|
81
|
+
> * Refreshing the page uing the browser's refresh button, will always serve cached dashboard and data.
|
|
82
|
+
> * To update the dashboard with newer data from the database, you have to **manually** click the reload button (🔄) in the dashboard. Alternatively, you can restart the script or the docker container (using a cron job, for instance).
|
|
82
83
|
> * If no date range is selected, it uses the default period set by `--days` or `PIHOLE_LT_STATS_DAYS`.
|
|
83
84
|
> * Large date ranges may lead to increased memory usage.
|
|
84
85
|
> * When multiple database files are provided, PiHoleLongTermStats concatenates them into a single dataframe and sorts the combined data by timestamp. Duplicate entries are **not** removed for calculating stats. Consolidating multiple databases can lead to increased memory usage.
|
|
@@ -104,7 +105,7 @@ If you have a copy of your `pihole-FTL.db` file, you can quickly run the dashboa
|
|
|
104
105
|
|
|
105
106
|
```bash
|
|
106
107
|
# Example: Copy from the default Pi-hole location
|
|
107
|
-
sudo
|
|
108
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
108
109
|
# Ensure the user running the app has read permissions
|
|
109
110
|
sudo chown $USER:$USER pihole-FTL.db
|
|
110
111
|
```
|
|
@@ -176,7 +177,7 @@ If installing using python, you can install from this git repo or as a package f
|
|
|
176
177
|
|
|
177
178
|
```bash
|
|
178
179
|
# Example: Copy from the default Pi-hole location
|
|
179
|
-
sudo
|
|
180
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
180
181
|
# Ensure the user running the app has read permissions
|
|
181
182
|
sudo chown $USER:$USER pihole-FTL.db
|
|
182
183
|
```
|
|
@@ -231,7 +232,7 @@ If installing using python, you can install from this git repo or as a package f
|
|
|
231
232
|
3. Make a copy/backup of your `pihole-FTL.db` (**Important!**) and place it in the PiHoleLongTermStats directory:
|
|
232
233
|
```bash
|
|
233
234
|
# Example: Copy from the default Pi-hole location
|
|
234
|
-
sudo
|
|
235
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
235
236
|
# Ensure the user running the app has read permissions
|
|
236
237
|
sudo chown $USER:$USER pihole-FTL.db
|
|
237
238
|
```
|
|
@@ -285,6 +286,7 @@ You can configure the application using command-line arguments or environment va
|
|
|
285
286
|
## 🧑💻 Contributing
|
|
286
287
|
|
|
287
288
|
Any contribution, feature ideas or bug fixes are always welcome.
|
|
289
|
+
In case of new features, opening an issue and discussion before implementation is recommended.
|
|
288
290
|
This project uses uv for dependency management.
|
|
289
291
|
|
|
290
292
|
### Development workflow
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pi Hole Long Term Statistics v.0.2.
|
|
1
|
+
# Pi Hole Long Term Statistics v.0.2.6
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
@@ -46,11 +46,13 @@ See more [screenshots](https://github.com/davistdaniel/PiHoleLongTermStats/blob/
|
|
|
46
46
|
> Using your actual Pi-hole FTL db file for querying is **not** recommended and it is advised to use a copy. Place the copy in the project root or specify its path using the `--db_path` argument or `PIHOLE_LT_STATS_DB_PATH` environment variable. In any case, PiHoleLongTermStats does not monitor for changes in the Pi-hole FTL db file even if you mount it.
|
|
47
47
|
|
|
48
48
|
> [!TIP]
|
|
49
|
-
> * You can set up a cron job to periodically copy the FTL database to the `db_path` `PIHOLE_LT_STATS_DB_PATH` location, ensuring your stats are updated without touching the
|
|
49
|
+
> * You can set up a cron job to periodically copy the FTL database to the `db_path` `PIHOLE_LT_STATS_DB_PATH` location, ensuring your stats are updated without touching the actual database. Use the reload button in the dashboard to refresh the stats.
|
|
50
50
|
> * Set your timezone (e.g "Europe/Berlin") using `--timezone` or `PIHOLE_LT_STATS_TIMEZONE`.
|
|
51
51
|
|
|
52
52
|
> [!IMPORTANT]
|
|
53
|
-
> * PiHoleLongTermStats reloads the Pi-hole FTL database and recalculates stats whenever the
|
|
53
|
+
> * PiHoleLongTermStats reloads the Pi-hole FTL database and recalculates stats whenever the the reload button is clicked.
|
|
54
|
+
> * Refreshing the page uing the browser's refresh button, will always serve cached dashboard and data.
|
|
55
|
+
> * To update the dashboard with newer data from the database, you have to **manually** click the reload button (🔄) in the dashboard. Alternatively, you can restart the script or the docker container (using a cron job, for instance).
|
|
54
56
|
> * If no date range is selected, it uses the default period set by `--days` or `PIHOLE_LT_STATS_DAYS`.
|
|
55
57
|
> * Large date ranges may lead to increased memory usage.
|
|
56
58
|
> * When multiple database files are provided, PiHoleLongTermStats concatenates them into a single dataframe and sorts the combined data by timestamp. Duplicate entries are **not** removed for calculating stats. Consolidating multiple databases can lead to increased memory usage.
|
|
@@ -76,7 +78,7 @@ If you have a copy of your `pihole-FTL.db` file, you can quickly run the dashboa
|
|
|
76
78
|
|
|
77
79
|
```bash
|
|
78
80
|
# Example: Copy from the default Pi-hole location
|
|
79
|
-
sudo
|
|
81
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
80
82
|
# Ensure the user running the app has read permissions
|
|
81
83
|
sudo chown $USER:$USER pihole-FTL.db
|
|
82
84
|
```
|
|
@@ -148,7 +150,7 @@ If installing using python, you can install from this git repo or as a package f
|
|
|
148
150
|
|
|
149
151
|
```bash
|
|
150
152
|
# Example: Copy from the default Pi-hole location
|
|
151
|
-
sudo
|
|
153
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
152
154
|
# Ensure the user running the app has read permissions
|
|
153
155
|
sudo chown $USER:$USER pihole-FTL.db
|
|
154
156
|
```
|
|
@@ -203,7 +205,7 @@ If installing using python, you can install from this git repo or as a package f
|
|
|
203
205
|
3. Make a copy/backup of your `pihole-FTL.db` (**Important!**) and place it in the PiHoleLongTermStats directory:
|
|
204
206
|
```bash
|
|
205
207
|
# Example: Copy from the default Pi-hole location
|
|
206
|
-
sudo
|
|
208
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
207
209
|
# Ensure the user running the app has read permissions
|
|
208
210
|
sudo chown $USER:$USER pihole-FTL.db
|
|
209
211
|
```
|
|
@@ -257,6 +259,7 @@ You can configure the application using command-line arguments or environment va
|
|
|
257
259
|
## 🧑💻 Contributing
|
|
258
260
|
|
|
259
261
|
Any contribution, feature ideas or bug fixes are always welcome.
|
|
262
|
+
In case of new features, opening an issue and discussion before implementation is recommended.
|
|
260
263
|
This project uses uv for dependency management.
|
|
261
264
|
|
|
262
265
|
### Development workflow
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
## Author : Davis T. Daniel
|
|
2
|
-
## PiHoleLongTermStats v.0.2.
|
|
2
|
+
## PiHoleLongTermStats v.0.2.6
|
|
3
3
|
## License : MIT
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
import os
|
|
6
7
|
import gc
|
|
7
8
|
import argparse
|
|
@@ -11,6 +12,7 @@ import plotly.express as px
|
|
|
11
12
|
import pandas as pd
|
|
12
13
|
from dash import Dash, dcc, html, Input, Output, State
|
|
13
14
|
from zoneinfo import ZoneInfo
|
|
15
|
+
from datetime import datetime
|
|
14
16
|
|
|
15
17
|
from piholelongtermstats.db import read_pihole_ftl_db, connect_to_sql, probe_sample_df
|
|
16
18
|
from piholelongtermstats.process import (
|
|
@@ -23,9 +25,11 @@ from piholelongtermstats.plot import (
|
|
|
23
25
|
generate_plot_data,
|
|
24
26
|
generate_client_activity_over_time,
|
|
25
27
|
generate_queries_over_time,
|
|
28
|
+
generate_top_blocked_domains,
|
|
29
|
+
generate_top_allowed_domains,
|
|
26
30
|
)
|
|
27
31
|
|
|
28
|
-
__version__ = "0.2.
|
|
32
|
+
__version__ = "0.2.6"
|
|
29
33
|
|
|
30
34
|
# logging setup
|
|
31
35
|
logging.basicConfig(
|
|
@@ -169,6 +173,8 @@ def serve_layout(
|
|
|
169
173
|
f"Removed domains matching the regex pattern : {pattern}, number of rows in dataframe : {len(df)}"
|
|
170
174
|
)
|
|
171
175
|
|
|
176
|
+
last_refresh_time = datetime.now(ZoneInfo(timezone)).strftime("%d-%m-%Y (%H:%M)")
|
|
177
|
+
|
|
172
178
|
# should reduce some memory consumption
|
|
173
179
|
df["id"] = df["id"].astype("int32")
|
|
174
180
|
df["type"] = df["type"].astype("int8")
|
|
@@ -189,6 +195,10 @@ def serve_layout(
|
|
|
189
195
|
callback_data = {
|
|
190
196
|
"hourly_agg": hourly_data["hourly_agg"],
|
|
191
197
|
"top_clients": hourly_data["top_clients"],
|
|
198
|
+
"blocked_df": plot_data["blocked_df"],
|
|
199
|
+
"allowed_df": plot_data["allowed_df"],
|
|
200
|
+
"blocked_df_by_client": plot_data["blocked_df_by_client"],
|
|
201
|
+
"allowed_df_by_client": plot_data["allowed_df_by_client"],
|
|
192
202
|
"data_span_days": plot_data["data_span_days"],
|
|
193
203
|
}
|
|
194
204
|
|
|
@@ -197,7 +207,6 @@ def serve_layout(
|
|
|
197
207
|
gc.collect()
|
|
198
208
|
|
|
199
209
|
# generate initial plots
|
|
200
|
-
|
|
201
210
|
initial_filtered_fig = generate_queries_over_time(
|
|
202
211
|
callback_data=callback_data, client=None
|
|
203
212
|
)
|
|
@@ -249,11 +258,19 @@ def serve_layout(
|
|
|
249
258
|
),
|
|
250
259
|
html.Br(),
|
|
251
260
|
html.H6(
|
|
252
|
-
f"Timezone is {timezone}. Database records begin on {stats['oldest_data_point']} and end on {stats['latest_data_point']}."
|
|
261
|
+
f"Timezone is {timezone}. Database records begin on {stats['oldest_data_point']} and end on {stats['latest_data_point']}. The dashboard was last reloaded on {last_refresh_time}."
|
|
253
262
|
),
|
|
254
263
|
],
|
|
255
264
|
className="sub-heading-card",
|
|
256
265
|
),
|
|
266
|
+
html.Div(
|
|
267
|
+
[
|
|
268
|
+
html.Hr(),
|
|
269
|
+
html.Span("Info Cards"),
|
|
270
|
+
html.Hr(),
|
|
271
|
+
],
|
|
272
|
+
className="section-divider",
|
|
273
|
+
),
|
|
257
274
|
# info cards
|
|
258
275
|
html.Div(
|
|
259
276
|
[
|
|
@@ -631,12 +648,12 @@ def serve_layout(
|
|
|
631
648
|
html.H3("Longest Idle Period"),
|
|
632
649
|
html.P(
|
|
633
650
|
f"{stats['max_idle_ms']:,.0f} s"
|
|
634
|
-
if stats[
|
|
651
|
+
if stats["max_idle_ms"] is not None
|
|
635
652
|
else "N/A"
|
|
636
653
|
),
|
|
637
654
|
html.P(
|
|
638
655
|
f"Between {stats['before_gap']} and {stats['after_gap']}"
|
|
639
|
-
if stats[
|
|
656
|
+
if stats["before_gap"] and stats["after_gap"]
|
|
640
657
|
else "N/A",
|
|
641
658
|
style={"fontSize": "14px", "color": "#777"},
|
|
642
659
|
),
|
|
@@ -659,7 +676,7 @@ def serve_layout(
|
|
|
659
676
|
),
|
|
660
677
|
html.P(
|
|
661
678
|
f"Avg reply time: {stats['slowest_avg_reply_time'] * 1000:.2f} ms"
|
|
662
|
-
if stats[
|
|
679
|
+
if stats["slowest_avg_reply_time"]
|
|
663
680
|
else "N/A",
|
|
664
681
|
style={"fontSize": "14px", "color": "#777"},
|
|
665
682
|
),
|
|
@@ -704,32 +721,63 @@ def serve_layout(
|
|
|
704
721
|
],
|
|
705
722
|
className="kpi-container",
|
|
706
723
|
),
|
|
724
|
+
html.Div(
|
|
725
|
+
[
|
|
726
|
+
html.Hr(),
|
|
727
|
+
html.Span("Queries and domains"),
|
|
728
|
+
html.Hr(),
|
|
729
|
+
],
|
|
730
|
+
className="section-divider",
|
|
731
|
+
),
|
|
707
732
|
html.Br(),
|
|
708
733
|
html.Div(
|
|
709
734
|
[
|
|
710
|
-
html.
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
735
|
+
html.Div(
|
|
736
|
+
[
|
|
737
|
+
dcc.Dropdown(
|
|
738
|
+
options=[
|
|
739
|
+
{"label": c, "value": c}
|
|
740
|
+
for c in plot_data["client_list"]
|
|
741
|
+
],
|
|
742
|
+
id="client-filter",
|
|
743
|
+
placeholder="Select a Client",
|
|
744
|
+
),
|
|
715
745
|
],
|
|
716
|
-
|
|
717
|
-
placeholder="Select a Client",
|
|
746
|
+
className="cardplot",
|
|
718
747
|
),
|
|
719
|
-
|
|
720
|
-
html.
|
|
721
|
-
|
|
722
|
-
|
|
748
|
+
html.Br(),
|
|
749
|
+
html.Div(
|
|
750
|
+
[
|
|
751
|
+
html.H2("Queries over time"),
|
|
752
|
+
html.H5(
|
|
753
|
+
"Queries from all or selected clients. The data is aggregated hourly."
|
|
754
|
+
),
|
|
755
|
+
dcc.Graph(id="filtered-view", figure=initial_filtered_fig),
|
|
756
|
+
],
|
|
757
|
+
className="cardplot",
|
|
723
758
|
),
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
759
|
+
html.Br(),
|
|
760
|
+
html.Div(
|
|
761
|
+
[
|
|
762
|
+
html.H2("Client Activity Over Time"),
|
|
763
|
+
html.H5(
|
|
764
|
+
"Client acitivity for all or selected clients. The data is aggregated hourly."
|
|
765
|
+
),
|
|
766
|
+
dcc.Graph(
|
|
767
|
+
id="client-activity-view", figure=initial_activity_fig
|
|
768
|
+
),
|
|
769
|
+
],
|
|
770
|
+
className="cardplot",
|
|
771
|
+
),
|
|
772
|
+
]
|
|
727
773
|
),
|
|
728
774
|
html.Br(),
|
|
729
775
|
html.Div(
|
|
730
776
|
[
|
|
731
777
|
html.H2("Top Blocked Domains"),
|
|
732
|
-
html.H5(
|
|
778
|
+
html.H5(
|
|
779
|
+
f"Top {args.n_domains} blocked domains. Client filter (see above) can be used to select a specific client."
|
|
780
|
+
),
|
|
733
781
|
dcc.Graph(
|
|
734
782
|
id="top-blocked-domains",
|
|
735
783
|
figure=px.bar(
|
|
@@ -755,10 +803,13 @@ def serve_layout(
|
|
|
755
803
|
],
|
|
756
804
|
className="cardplot",
|
|
757
805
|
),
|
|
806
|
+
html.Br(),
|
|
758
807
|
html.Div(
|
|
759
808
|
[
|
|
760
809
|
html.H2("Top Allowed Domains"),
|
|
761
|
-
html.H5(
|
|
810
|
+
html.H5(
|
|
811
|
+
f"Top {args.n_domains} allowed domains. Client filter (see above) can be used to select a specific client."
|
|
812
|
+
),
|
|
762
813
|
dcc.Graph(
|
|
763
814
|
id="top-allowed-domains",
|
|
764
815
|
figure=px.bar(
|
|
@@ -785,6 +836,15 @@ def serve_layout(
|
|
|
785
836
|
className="cardplot",
|
|
786
837
|
),
|
|
787
838
|
html.Br(),
|
|
839
|
+
html.Div(
|
|
840
|
+
[
|
|
841
|
+
html.Hr(),
|
|
842
|
+
html.Span("Activity"),
|
|
843
|
+
html.Hr(),
|
|
844
|
+
],
|
|
845
|
+
className="section-divider",
|
|
846
|
+
),
|
|
847
|
+
html.Br(),
|
|
788
848
|
html.Div(
|
|
789
849
|
[
|
|
790
850
|
html.H2("Top Client Activity"),
|
|
@@ -945,6 +1005,15 @@ def serve_layout(
|
|
|
945
1005
|
className="cardplot",
|
|
946
1006
|
),
|
|
947
1007
|
html.Br(),
|
|
1008
|
+
html.Div(
|
|
1009
|
+
[
|
|
1010
|
+
html.Hr(),
|
|
1011
|
+
html.Span("Reply times"),
|
|
1012
|
+
html.Hr(),
|
|
1013
|
+
],
|
|
1014
|
+
className="section-divider",
|
|
1015
|
+
),
|
|
1016
|
+
html.Br(),
|
|
948
1017
|
html.Div(
|
|
949
1018
|
[
|
|
950
1019
|
html.Div(
|
|
@@ -972,6 +1041,34 @@ def serve_layout(
|
|
|
972
1041
|
]
|
|
973
1042
|
),
|
|
974
1043
|
html.Br(),
|
|
1044
|
+
html.Div(
|
|
1045
|
+
[
|
|
1046
|
+
html.Div(
|
|
1047
|
+
[
|
|
1048
|
+
html.H2("Reply Time Distribution"),
|
|
1049
|
+
html.H5("Distribution of reply times in milliseconds."),
|
|
1050
|
+
dcc.Graph(
|
|
1051
|
+
id="reply-time-histogram",
|
|
1052
|
+
figure=px.histogram(
|
|
1053
|
+
plot_data["reply_time_df"],
|
|
1054
|
+
x="reply_time_ms",
|
|
1055
|
+
labels={
|
|
1056
|
+
"reply_time_ms": "Reply Time (ms)",
|
|
1057
|
+
},
|
|
1058
|
+
color_discrete_sequence=["#3b82f6"],
|
|
1059
|
+
template="plotly_white",
|
|
1060
|
+
).update_layout(
|
|
1061
|
+
xaxis_title="Reply Time (ms)",
|
|
1062
|
+
yaxis_title="Count",
|
|
1063
|
+
bargap=0.05,
|
|
1064
|
+
),
|
|
1065
|
+
),
|
|
1066
|
+
],
|
|
1067
|
+
className="cardplot",
|
|
1068
|
+
)
|
|
1069
|
+
]
|
|
1070
|
+
),
|
|
1071
|
+
html.Br(),
|
|
975
1072
|
html.Footer(
|
|
976
1073
|
f"PiHoleLongTermStats v.{__version__}",
|
|
977
1074
|
style={"textAlign": "center", "padding": "10px", "color": "#666"},
|
|
@@ -1032,22 +1129,37 @@ PHLTS_CALLBACK_DATA, initial_layout = serve_layout(
|
|
|
1032
1129
|
|
|
1033
1130
|
logging.info("Setting initial layout...")
|
|
1034
1131
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1132
|
+
# Holds the most recently generated page-container children
|
|
1133
|
+
PHLTS_PAGE_CONTAINER_CHILDREN = initial_layout.children
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def serve_app_layout():
|
|
1137
|
+
"""Returns the current app layout.
|
|
1138
|
+
|
|
1139
|
+
Dash calls this function on every full page load/browser refresh (since
|
|
1140
|
+
app.layout is set to a function rather than a static object), so it
|
|
1141
|
+
always reflects the latest data instead of only what existed when the
|
|
1142
|
+
container started.
|
|
1143
|
+
"""
|
|
1144
|
+
return html.Div(
|
|
1145
|
+
[
|
|
1146
|
+
dcc.Loading(
|
|
1147
|
+
id="loading-main",
|
|
1148
|
+
type="graph",
|
|
1149
|
+
fullscreen=True,
|
|
1150
|
+
children=[
|
|
1151
|
+
html.Div(
|
|
1152
|
+
id="page-container",
|
|
1153
|
+
children=PHLTS_PAGE_CONTAINER_CHILDREN,
|
|
1154
|
+
className="container",
|
|
1155
|
+
)
|
|
1156
|
+
],
|
|
1157
|
+
)
|
|
1158
|
+
]
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
app.layout = serve_app_layout
|
|
1051
1163
|
|
|
1052
1164
|
del initial_layout
|
|
1053
1165
|
gc.collect()
|
|
@@ -1061,7 +1173,7 @@ gc.collect()
|
|
|
1061
1173
|
prevent_initial_call=True,
|
|
1062
1174
|
)
|
|
1063
1175
|
def reload_page(n_clicks, start_date, end_date):
|
|
1064
|
-
global PHLTS_CALLBACK_DATA
|
|
1176
|
+
global PHLTS_CALLBACK_DATA, PHLTS_PAGE_CONTAINER_CHILDREN
|
|
1065
1177
|
|
|
1066
1178
|
logging.info(f"Reload button clicked. Selected date range: {start_date, end_date}")
|
|
1067
1179
|
|
|
@@ -1101,6 +1213,10 @@ def reload_page(n_clicks, start_date, end_date):
|
|
|
1101
1213
|
ignore_domains=args.ignore_domains,
|
|
1102
1214
|
)
|
|
1103
1215
|
|
|
1216
|
+
# Persist the new layout so that a full browser refresh (which calls
|
|
1217
|
+
# serve_app_layout() again) also sees the reloaded data
|
|
1218
|
+
PHLTS_PAGE_CONTAINER_CHILDREN = layout.children
|
|
1219
|
+
|
|
1104
1220
|
return layout.children
|
|
1105
1221
|
|
|
1106
1222
|
|
|
@@ -1136,6 +1252,31 @@ def update_client_activity(client, n_clicks):
|
|
|
1136
1252
|
return fig
|
|
1137
1253
|
|
|
1138
1254
|
|
|
1255
|
+
@app.callback(
|
|
1256
|
+
Output("top-blocked-domains", "figure"),
|
|
1257
|
+
Output("top-allowed-domains", "figure"),
|
|
1258
|
+
Input("client-filter", "value"),
|
|
1259
|
+
Input("reload-button", "n_clicks"),
|
|
1260
|
+
prevent_initial_call=True,
|
|
1261
|
+
)
|
|
1262
|
+
def update_top_domains(client, n_clicks):
|
|
1263
|
+
logging.info("Updating top blocked and allowed domain plots...")
|
|
1264
|
+
global PHLTS_CALLBACK_DATA
|
|
1265
|
+
|
|
1266
|
+
blocked_fig = generate_top_blocked_domains(
|
|
1267
|
+
callback_data=PHLTS_CALLBACK_DATA,
|
|
1268
|
+
n_domains=args.n_domains,
|
|
1269
|
+
client=client,
|
|
1270
|
+
)
|
|
1271
|
+
allowed_fig = generate_top_allowed_domains(
|
|
1272
|
+
callback_data=PHLTS_CALLBACK_DATA,
|
|
1273
|
+
n_domains=args.n_domains,
|
|
1274
|
+
client=client,
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
return blocked_fig, allowed_fig
|
|
1278
|
+
|
|
1279
|
+
|
|
1139
1280
|
def run():
|
|
1140
1281
|
app.run(host="0.0.0.0", port=args.port, debug=False)
|
|
1141
1282
|
|
{piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/assets/style.css
RENAMED
|
@@ -146,7 +146,6 @@ h3 {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
|
|
150
149
|
.cardactivity {
|
|
151
150
|
background: linear-gradient(135deg, #ffffff, #f5f5f0);
|
|
152
151
|
border-radius: 15px;
|
|
@@ -344,4 +343,22 @@ h3 {
|
|
|
344
343
|
.date-picker-btn .dash-datepicker-caret-icon {
|
|
345
344
|
fill: #f7f9fb !important;
|
|
346
345
|
flex-shrink: 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.section-divider {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
margin: 20px 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.section-divider hr {
|
|
355
|
+
flex: 1;
|
|
356
|
+
border: none;
|
|
357
|
+
border-top: 1px solid #999;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.section-divider span {
|
|
361
|
+
padding: 0 10px;
|
|
362
|
+
font-weight: bold;
|
|
363
|
+
color: #444;
|
|
347
364
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## Author : Davis T. Daniel
|
|
2
|
-
## PiHoleLongTermStats v.0.2.
|
|
2
|
+
## PiHoleLongTermStats v.0.2.6
|
|
3
3
|
## License : MIT
|
|
4
4
|
|
|
5
5
|
import logging
|
|
@@ -8,7 +8,6 @@ import gc
|
|
|
8
8
|
import plotly.express as px
|
|
9
9
|
import itertools
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
def generate_plot_data(df, n_clients, n_domains):
|
|
13
12
|
"""Generate plot data for dashboard visualizations.
|
|
14
13
|
|
|
@@ -71,6 +70,22 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
71
70
|
.rename(columns={"domain": "Domain", "count": "count"})
|
|
72
71
|
)
|
|
73
72
|
|
|
73
|
+
blocked_df_by_client = (
|
|
74
|
+
tmp_blocked.groupby(["client", "domain"]).size().reset_index(name="count")
|
|
75
|
+
)
|
|
76
|
+
blocked_df_by_client["Domain"] = blocked_df_by_client["domain"].apply(shorten)
|
|
77
|
+
blocked_df_by_client = blocked_df_by_client[
|
|
78
|
+
["client", "Domain", "count"]
|
|
79
|
+
].sort_values(["client", "count"], ascending=[True, False])
|
|
80
|
+
|
|
81
|
+
allowed_df_by_client = (
|
|
82
|
+
tmp_allowed.groupby(["client", "domain"]).size().reset_index(name="count")
|
|
83
|
+
)
|
|
84
|
+
allowed_df_by_client["Domain"] = allowed_df_by_client["domain"].apply(shorten)
|
|
85
|
+
allowed_df_by_client = allowed_df_by_client[
|
|
86
|
+
["client", "Domain", "count"]
|
|
87
|
+
].sort_values(["client", "count"], ascending=[True, False])
|
|
88
|
+
|
|
74
89
|
logging.info("Generated plot data for allowed and blocked domains.")
|
|
75
90
|
|
|
76
91
|
# plot data for reply time over days
|
|
@@ -152,6 +167,8 @@ def generate_plot_data(df, n_clients, n_domains):
|
|
|
152
167
|
"top_clients_stacked": top_clients_stacked,
|
|
153
168
|
"blocked_df": blocked_df,
|
|
154
169
|
"allowed_df": allowed_df,
|
|
170
|
+
"blocked_df_by_client": blocked_df_by_client,
|
|
171
|
+
"allowed_df_by_client": allowed_df_by_client,
|
|
155
172
|
"reply_time_df": reply_time_df,
|
|
156
173
|
"client_list": client_list,
|
|
157
174
|
"data_span_days": data_span_days,
|
|
@@ -362,3 +379,64 @@ def generate_client_activity_over_time(callback_data, n_clients, client=None):
|
|
|
362
379
|
gc.collect()
|
|
363
380
|
|
|
364
381
|
return fig
|
|
382
|
+
|
|
383
|
+
def generate_top_blocked_domains(callback_data, n_domains, client=None):
|
|
384
|
+
logging.info("Generating top blocked domains plot...")
|
|
385
|
+
if client is not None:
|
|
386
|
+
dff = callback_data.get("blocked_df_by_client", pd.DataFrame()).copy()
|
|
387
|
+
dff = dff[dff["client"] == client]
|
|
388
|
+
dff = dff.nlargest(n_domains, "count").reset_index(drop=True)
|
|
389
|
+
title_txt = f"Domains for {client}"
|
|
390
|
+
else:
|
|
391
|
+
dff = callback_data.get("blocked_df", pd.DataFrame()).copy()
|
|
392
|
+
title_txt = ""
|
|
393
|
+
|
|
394
|
+
return _build_top_domain_figure(dff, title_txt, "#ef4444")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def generate_top_allowed_domains(callback_data, n_domains, client=None):
|
|
398
|
+
logging.info("Generating top allowed domains plot...")
|
|
399
|
+
if client is not None:
|
|
400
|
+
dff = callback_data.get("allowed_df_by_client", pd.DataFrame()).copy()
|
|
401
|
+
dff = dff[dff["client"] == client]
|
|
402
|
+
dff = dff.nlargest(n_domains, "count").reset_index(drop=True)
|
|
403
|
+
title_txt = f"Domains for {client}"
|
|
404
|
+
else:
|
|
405
|
+
dff = callback_data.get("allowed_df", pd.DataFrame()).copy()
|
|
406
|
+
title_txt = ""
|
|
407
|
+
|
|
408
|
+
return _build_top_domain_figure(dff, title_txt, "#10b981")
|
|
409
|
+
|
|
410
|
+
def _build_top_domain_figure(domain_df, title_txt, color):
|
|
411
|
+
if domain_df is None or domain_df.empty:
|
|
412
|
+
fig = px.bar(
|
|
413
|
+
pd.DataFrame({"Domain": [], "count": []}),
|
|
414
|
+
x="Domain",
|
|
415
|
+
y="count",
|
|
416
|
+
template="plotly_white",
|
|
417
|
+
)
|
|
418
|
+
fig.update_layout(
|
|
419
|
+
title=title_txt,
|
|
420
|
+
title_font_size=16,
|
|
421
|
+
showlegend=False,
|
|
422
|
+
margin=dict(r=0, t=50, l=0, b=0),
|
|
423
|
+
xaxis=dict(title=None, automargin=True, tickmode="auto"),
|
|
424
|
+
)
|
|
425
|
+
return fig
|
|
426
|
+
|
|
427
|
+
fig = px.bar(
|
|
428
|
+
domain_df,
|
|
429
|
+
y="count",
|
|
430
|
+
x="Domain",
|
|
431
|
+
labels={"Domain": "Domain", "count": "Count"},
|
|
432
|
+
template="plotly_white",
|
|
433
|
+
color_discrete_sequence=[color],
|
|
434
|
+
)
|
|
435
|
+
fig.update_layout(
|
|
436
|
+
title=title_txt,
|
|
437
|
+
title_font_size=16,
|
|
438
|
+
showlegend=False,
|
|
439
|
+
margin=dict(r=0, t=50, l=0, b=0),
|
|
440
|
+
xaxis=dict(title=None, automargin=True, tickmode="auto"),
|
|
441
|
+
)
|
|
442
|
+
return fig
|
{piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: piholelongtermstats
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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>
|
|
@@ -22,11 +22,10 @@ Provides-Extra: develop
|
|
|
22
22
|
Requires-Dist: pytest==9.0.1; extra == "develop"
|
|
23
23
|
Requires-Dist: ruff; extra == "develop"
|
|
24
24
|
Requires-Dist: build==1.3.0; extra == "develop"
|
|
25
|
-
Requires-Dist: twine==6.2.0; extra == "develop"
|
|
26
25
|
Requires-Dist: pytest-cov==7.0.0; extra == "develop"
|
|
27
26
|
Dynamic: license-file
|
|
28
27
|
|
|
29
|
-
# Pi Hole Long Term Statistics v.0.2.
|
|
28
|
+
# Pi Hole Long Term Statistics v.0.2.6
|
|
30
29
|
|
|
31
30
|
<div align="center">
|
|
32
31
|
|
|
@@ -74,11 +73,13 @@ See more [screenshots](https://github.com/davistdaniel/PiHoleLongTermStats/blob/
|
|
|
74
73
|
> Using your actual Pi-hole FTL db file for querying is **not** recommended and it is advised to use a copy. Place the copy in the project root or specify its path using the `--db_path` argument or `PIHOLE_LT_STATS_DB_PATH` environment variable. In any case, PiHoleLongTermStats does not monitor for changes in the Pi-hole FTL db file even if you mount it.
|
|
75
74
|
|
|
76
75
|
> [!TIP]
|
|
77
|
-
> * You can set up a cron job to periodically copy the FTL database to the `db_path` `PIHOLE_LT_STATS_DB_PATH` location, ensuring your stats are updated without touching the
|
|
76
|
+
> * You can set up a cron job to periodically copy the FTL database to the `db_path` `PIHOLE_LT_STATS_DB_PATH` location, ensuring your stats are updated without touching the actual database. Use the reload button in the dashboard to refresh the stats.
|
|
78
77
|
> * Set your timezone (e.g "Europe/Berlin") using `--timezone` or `PIHOLE_LT_STATS_TIMEZONE`.
|
|
79
78
|
|
|
80
79
|
> [!IMPORTANT]
|
|
81
|
-
> * PiHoleLongTermStats reloads the Pi-hole FTL database and recalculates stats whenever the
|
|
80
|
+
> * PiHoleLongTermStats reloads the Pi-hole FTL database and recalculates stats whenever the the reload button is clicked.
|
|
81
|
+
> * Refreshing the page uing the browser's refresh button, will always serve cached dashboard and data.
|
|
82
|
+
> * To update the dashboard with newer data from the database, you have to **manually** click the reload button (🔄) in the dashboard. Alternatively, you can restart the script or the docker container (using a cron job, for instance).
|
|
82
83
|
> * If no date range is selected, it uses the default period set by `--days` or `PIHOLE_LT_STATS_DAYS`.
|
|
83
84
|
> * Large date ranges may lead to increased memory usage.
|
|
84
85
|
> * When multiple database files are provided, PiHoleLongTermStats concatenates them into a single dataframe and sorts the combined data by timestamp. Duplicate entries are **not** removed for calculating stats. Consolidating multiple databases can lead to increased memory usage.
|
|
@@ -104,7 +105,7 @@ If you have a copy of your `pihole-FTL.db` file, you can quickly run the dashboa
|
|
|
104
105
|
|
|
105
106
|
```bash
|
|
106
107
|
# Example: Copy from the default Pi-hole location
|
|
107
|
-
sudo
|
|
108
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
108
109
|
# Ensure the user running the app has read permissions
|
|
109
110
|
sudo chown $USER:$USER pihole-FTL.db
|
|
110
111
|
```
|
|
@@ -176,7 +177,7 @@ If installing using python, you can install from this git repo or as a package f
|
|
|
176
177
|
|
|
177
178
|
```bash
|
|
178
179
|
# Example: Copy from the default Pi-hole location
|
|
179
|
-
sudo
|
|
180
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
180
181
|
# Ensure the user running the app has read permissions
|
|
181
182
|
sudo chown $USER:$USER pihole-FTL.db
|
|
182
183
|
```
|
|
@@ -231,7 +232,7 @@ If installing using python, you can install from this git repo or as a package f
|
|
|
231
232
|
3. Make a copy/backup of your `pihole-FTL.db` (**Important!**) and place it in the PiHoleLongTermStats directory:
|
|
232
233
|
```bash
|
|
233
234
|
# Example: Copy from the default Pi-hole location
|
|
234
|
-
sudo
|
|
235
|
+
sudo sqlite3 /etc/pihole/pihole-FTL.db ".backup ./pihole-FTL.db"
|
|
235
236
|
# Ensure the user running the app has read permissions
|
|
236
237
|
sudo chown $USER:$USER pihole-FTL.db
|
|
237
238
|
```
|
|
@@ -285,6 +286,7 @@ You can configure the application using command-line arguments or environment va
|
|
|
285
286
|
## 🧑💻 Contributing
|
|
286
287
|
|
|
287
288
|
Any contribution, feature ideas or bug fixes are always welcome.
|
|
289
|
+
In case of new features, opening an issue and discussion before implementation is recommended.
|
|
288
290
|
This project uses uv for dependency management.
|
|
289
291
|
|
|
290
292
|
### Development workflow
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "piholelongtermstats"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"dash==4.1.0",
|
|
10
10
|
"pandas==3.0.3",
|
|
@@ -32,7 +32,6 @@ develop = [
|
|
|
32
32
|
"pytest==9.0.1",
|
|
33
33
|
"ruff",
|
|
34
34
|
"build==1.3.0",
|
|
35
|
-
"twine==6.2.0",
|
|
36
35
|
"pytest-cov==7.0.0",
|
|
37
36
|
]
|
|
38
37
|
|
|
@@ -51,3 +50,8 @@ exclude = ["docs*", "tests*","screenshots*","build*","dist*"]
|
|
|
51
50
|
|
|
52
51
|
[tool.setuptools.package-data]
|
|
53
52
|
piholelongtermstats = ["assets/*"]
|
|
53
|
+
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
dev = [
|
|
56
|
+
"jupyter>=1.1.1",
|
|
57
|
+
]
|
|
@@ -5,6 +5,8 @@ from piholelongtermstats.plot import (
|
|
|
5
5
|
generate_plot_data,
|
|
6
6
|
generate_queries_over_time,
|
|
7
7
|
generate_client_activity_over_time,
|
|
8
|
+
generate_top_blocked_domains,
|
|
9
|
+
generate_top_allowed_domains,
|
|
8
10
|
)
|
|
9
11
|
from piholelongtermstats.process import prepare_hourly_aggregated_data
|
|
10
12
|
|
|
@@ -32,6 +34,8 @@ class TestGeneratePlotData:
|
|
|
32
34
|
"top_clients_stacked",
|
|
33
35
|
"blocked_df",
|
|
34
36
|
"allowed_df",
|
|
37
|
+
"blocked_df_by_client",
|
|
38
|
+
"allowed_df_by_client",
|
|
35
39
|
"reply_time_df",
|
|
36
40
|
"client_list",
|
|
37
41
|
"data_span_days",
|
|
@@ -81,6 +85,42 @@ class TestGeneratePlotData:
|
|
|
81
85
|
assert len(blocked_df) <= n_domains
|
|
82
86
|
assert len(allowed_df) <= n_domains
|
|
83
87
|
|
|
88
|
+
blocked_df_by_client = result["blocked_df_by_client"]
|
|
89
|
+
allowed_df_by_client = result["allowed_df_by_client"]
|
|
90
|
+
|
|
91
|
+
assert isinstance(blocked_df_by_client, pd.DataFrame)
|
|
92
|
+
assert isinstance(allowed_df_by_client, pd.DataFrame)
|
|
93
|
+
assert "client" in blocked_df_by_client.columns
|
|
94
|
+
assert "client" in allowed_df_by_client.columns
|
|
95
|
+
|
|
96
|
+
def test_generate_top_blocked_domains_function(self, plot_dataframe):
|
|
97
|
+
n_clients = 5
|
|
98
|
+
n_domains = 10
|
|
99
|
+
result = generate_plot_data(plot_dataframe, n_clients, n_domains)
|
|
100
|
+
callback_data = result
|
|
101
|
+
|
|
102
|
+
fig = generate_top_blocked_domains(callback_data, n_domains=n_domains)
|
|
103
|
+
assert isinstance(fig, go.Figure)
|
|
104
|
+
|
|
105
|
+
client = plot_dataframe["client"].iloc[0]
|
|
106
|
+
fig_client = generate_top_blocked_domains(callback_data, n_domains=n_domains, client=client)
|
|
107
|
+
assert isinstance(fig_client, go.Figure)
|
|
108
|
+
assert client in fig_client.layout.title.text
|
|
109
|
+
|
|
110
|
+
def test_generate_top_allowed_domains_function(self, plot_dataframe):
|
|
111
|
+
n_clients = 5
|
|
112
|
+
n_domains = 10
|
|
113
|
+
result = generate_plot_data(plot_dataframe, n_clients, n_domains)
|
|
114
|
+
callback_data = result
|
|
115
|
+
|
|
116
|
+
fig = generate_top_allowed_domains(callback_data, n_domains=n_domains)
|
|
117
|
+
assert isinstance(fig, go.Figure)
|
|
118
|
+
|
|
119
|
+
client = plot_dataframe["client"].iloc[0]
|
|
120
|
+
fig_client = generate_top_allowed_domains(callback_data, n_domains=n_domains, client=client)
|
|
121
|
+
assert isinstance(fig_client, go.Figure)
|
|
122
|
+
assert client in fig_client.layout.title.text
|
|
123
|
+
|
|
84
124
|
def test_generate_plot_data_reply_time_df(self, plot_dataframe):
|
|
85
125
|
"""Test reply time dataframe."""
|
|
86
126
|
n_clients = 5
|
|
File without changes
|
{piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/assets/favicon.ico
RENAMED
|
File without changes
|
{piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats/assets/logo_phlts.png
RENAMED
|
File without changes
|
{piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{piholelongtermstats-0.2.4 → piholelongtermstats-0.2.6}/piholelongtermstats.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|