pihole6api 0.1.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Shane Barbetta
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.2
2
+ Name: pihole6api
3
+ Version: 0.1.0
4
+ Summary: Python API Client for Pi-hole 6
5
+ Author-email: Shane Barbetta <shane@barbetta.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sbarbett/pihole6api
8
+ Project-URL: Documentation, https://github.com/sbarbett/pihole6api
9
+ Project-URL: Source, https://github.com/sbarbett/pihole6api
10
+ Project-URL: Issues, https://github.com/sbarbett/pihole6api/issues
11
+ Keywords: pihole,dns,adblocking,api,client
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: requests>=2.26.0
25
+
26
+ # pihole6api
27
+
28
+ This package provides a simple, modular SDK for the PiHole 6 REST API.
29
+
30
+ ## Features
31
+
32
+ * Automatically handles authentication and renewal
33
+ * Gracefully error management
34
+ * Logically organized modules
35
+ * Easily maintained
36
+
37
+ ## Installation
38
+
39
+ **Install using `pip` or `pipx`:**
40
+
41
+ ```bash
42
+ pip install pihole6api
43
+ ```
44
+
45
+ **Install from source:**
46
+
47
+ ```bash
48
+ git clone https://github.com/sbarbett/pihole6api.git
49
+ cd pihole6api
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ### Initialize the Client
56
+
57
+ ```python
58
+ from pihole6api.client import PiHole6Client
59
+ client = PiHole6Client("https://your-pihole.local/", "your-password")
60
+ ```
61
+
62
+ ### Example Usage
63
+
64
+ #### Get Pi-Hole Metrics
65
+
66
+ ```python
67
+ history = client.metrics.get_history()
68
+ print(history) # {'history': [{'timestamp': 1740120900, 'total': 0, 'cached': 0 ...}]}
69
+ queries = client.metrics.get_queries()
70
+ print(queries)
71
+ ```
72
+
73
+ #### Enable/Disable Blocking
74
+
75
+ ```python
76
+ client.dns_control.set_blocking_status(False, 60)
77
+ print(client.dns_control.get_blocking_status()) # {'blocking': 'disabled', 'timer': 60 ...}
78
+ ```
79
+
80
+ #### Manage Groups
81
+
82
+ ```python
83
+ client.group_management.add_group("Custom Group", comment="For testing")
84
+ client.group_management.delete_group("Custom Group")
85
+ ```
86
+
87
+ #### Manage Domains
88
+
89
+ ```python
90
+ client.domain_management.add_domain("ads.example.com", "deny", "exact")
91
+ client.domain_management.delete_domain("ads.example.com", "deny", "exact")
92
+ ```
93
+
94
+ #### Manage Links
95
+
96
+ ```python
97
+ client.list_management.add_list("https://example.com/blocklist.txt", "block")
98
+ client.list_management.delete_list("https://example.com/blocklist.txt", "block")
99
+ ```
100
+
101
+ #### Export/Import PiHole Settings
102
+
103
+ ```python
104
+ # Export settings and save as a .zip file
105
+ with open("pihole-settings.zip", "wb") as f:
106
+ f.write(client.config.export_settings())
107
+
108
+ client.config.import_settings("pihole-settings.zip", {"config": True, "gravity": {"group": True}})
109
+ ```
110
+
111
+ #### Flush Logs & Restart DNS
112
+
113
+ ```python
114
+ client.actions.flush_logs()
115
+ client.actions.restart_dns()
116
+ ```
117
+
118
+ ## API Modules
119
+
120
+ | Module | Description |
121
+ |----------------------|-------------|
122
+ | `metrics` | Query history, top clients/domains, DNS stats |
123
+ | `dns_control` | Enable/disable blocking |
124
+ | `group_management` | Create, update, and delete groups |
125
+ | `domain_management` | Allow/block domains (exact & regex) |
126
+ | `client_management` | Manage client-specific rules |
127
+ | `list_management` | Manage blocklists (Adlists) |
128
+ | `config` | Modify Pi-hole configuration |
129
+ | `ftl_info` | Get Pi-hole core process (FTL) info |
130
+ | `dhcp` | Manage DHCP leases |
131
+ | `network_info` | View network devices, interfaces, routes |
132
+ | `actions` | Flush logs, restart services |
133
+ | `padd` | Fetch summarized data for PADD |
134
+
135
+ ## License
136
+
137
+ This project is license under the [MIT license](LICENSE).
@@ -0,0 +1,112 @@
1
+ # pihole6api
2
+
3
+ This package provides a simple, modular SDK for the PiHole 6 REST API.
4
+
5
+ ## Features
6
+
7
+ * Automatically handles authentication and renewal
8
+ * Gracefully error management
9
+ * Logically organized modules
10
+ * Easily maintained
11
+
12
+ ## Installation
13
+
14
+ **Install using `pip` or `pipx`:**
15
+
16
+ ```bash
17
+ pip install pihole6api
18
+ ```
19
+
20
+ **Install from source:**
21
+
22
+ ```bash
23
+ git clone https://github.com/sbarbett/pihole6api.git
24
+ cd pihole6api
25
+ pip install -e .
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ### Initialize the Client
31
+
32
+ ```python
33
+ from pihole6api.client import PiHole6Client
34
+ client = PiHole6Client("https://your-pihole.local/", "your-password")
35
+ ```
36
+
37
+ ### Example Usage
38
+
39
+ #### Get Pi-Hole Metrics
40
+
41
+ ```python
42
+ history = client.metrics.get_history()
43
+ print(history) # {'history': [{'timestamp': 1740120900, 'total': 0, 'cached': 0 ...}]}
44
+ queries = client.metrics.get_queries()
45
+ print(queries)
46
+ ```
47
+
48
+ #### Enable/Disable Blocking
49
+
50
+ ```python
51
+ client.dns_control.set_blocking_status(False, 60)
52
+ print(client.dns_control.get_blocking_status()) # {'blocking': 'disabled', 'timer': 60 ...}
53
+ ```
54
+
55
+ #### Manage Groups
56
+
57
+ ```python
58
+ client.group_management.add_group("Custom Group", comment="For testing")
59
+ client.group_management.delete_group("Custom Group")
60
+ ```
61
+
62
+ #### Manage Domains
63
+
64
+ ```python
65
+ client.domain_management.add_domain("ads.example.com", "deny", "exact")
66
+ client.domain_management.delete_domain("ads.example.com", "deny", "exact")
67
+ ```
68
+
69
+ #### Manage Links
70
+
71
+ ```python
72
+ client.list_management.add_list("https://example.com/blocklist.txt", "block")
73
+ client.list_management.delete_list("https://example.com/blocklist.txt", "block")
74
+ ```
75
+
76
+ #### Export/Import PiHole Settings
77
+
78
+ ```python
79
+ # Export settings and save as a .zip file
80
+ with open("pihole-settings.zip", "wb") as f:
81
+ f.write(client.config.export_settings())
82
+
83
+ client.config.import_settings("pihole-settings.zip", {"config": True, "gravity": {"group": True}})
84
+ ```
85
+
86
+ #### Flush Logs & Restart DNS
87
+
88
+ ```python
89
+ client.actions.flush_logs()
90
+ client.actions.restart_dns()
91
+ ```
92
+
93
+ ## API Modules
94
+
95
+ | Module | Description |
96
+ |----------------------|-------------|
97
+ | `metrics` | Query history, top clients/domains, DNS stats |
98
+ | `dns_control` | Enable/disable blocking |
99
+ | `group_management` | Create, update, and delete groups |
100
+ | `domain_management` | Allow/block domains (exact & regex) |
101
+ | `client_management` | Manage client-specific rules |
102
+ | `list_management` | Manage blocklists (Adlists) |
103
+ | `config` | Modify Pi-hole configuration |
104
+ | `ftl_info` | Get Pi-hole core process (FTL) info |
105
+ | `dhcp` | Manage DHCP leases |
106
+ | `network_info` | View network devices, interfaces, routes |
107
+ | `actions` | Flush logs, restart services |
108
+ | `padd` | Fetch summarized data for PADD |
109
+
110
+ ## License
111
+
112
+ This project is license under the [MIT license](LICENSE).
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pihole6api"
7
+ version = "0.1.0"
8
+ description = "Python API Client for Pi-hole 6"
9
+ authors = [{name = "Shane Barbetta", email = "shane@barbetta.me"}]
10
+ license = {text = "MIT"}
11
+ readme = "README.md"
12
+ requires-python = ">=3.8"
13
+ keywords = ["pihole", "dns", "adblocking", "api", "client"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+
26
+ dependencies = [
27
+ "requests >=2.26.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/sbarbett/pihole6api"
32
+ Documentation = "https://github.com/sbarbett/pihole6api"
33
+ Source = "https://github.com/sbarbett/pihole6api"
34
+ Issues = "https://github.com/sbarbett/pihole6api/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ from .client import PiHole6Client
2
+ from .conn import PiHole6Connection
3
+ from .actions import PiHole6Actions
4
+ from .config import PiHole6Configuration
5
+ from .dhcp import PiHole6Dhcp
6
+ from .domain_management import PiHole6DomainManagement
7
+ from .group_management import PiHole6GroupManagement
8
+ from .list_management import PiHole6ListManagement
9
+ from .metrics import PiHole6Metrics
10
+ from .network_info import PiHole6NetworkInfo
11
+ from .ftl_info import PiHole6FtlInfo
12
+ from .dns_control import PiHole6DnsControl
13
+ from .client_management import PiHole6ClientManagement
14
+
15
+ __all__ = [
16
+ "PiHole6Client",
17
+ "PiHole6Connection",
18
+ "PiHole6Actions",
19
+ "PiHole6Configuration",
20
+ "PiHole6Dhcp",
21
+ "PiHole6DomainManagement",
22
+ "PiHole6GroupManagement",
23
+ "PiHole6ListManagement",
24
+ "PiHole6Metrics",
25
+ "PiHole6NetworkInfo",
26
+ "PiHole6FtlInfo",
27
+ "PiHole6DnsControl",
28
+ "PiHole6ClientManagement",
29
+ ]
@@ -0,0 +1,20 @@
1
+ class PiHole6Actions:
2
+ def __init__(self, connection):
3
+ """Handles Pi-hole system action API endpoints."""
4
+ self.connection = connection
5
+
6
+ def flush_arp(self):
7
+ """Flush the network table (ARP cache)."""
8
+ return self.connection.post("action/flush/arp")
9
+
10
+ def flush_logs(self):
11
+ """Flush the DNS logs."""
12
+ return self.connection.post("action/flush/logs")
13
+
14
+ def run_gravity(self):
15
+ """Run gravity (updates blocklists and reprocesses them)."""
16
+ return self.connection.post("action/gravity")
17
+
18
+ def restart_dns(self):
19
+ """Restart the Pi-hole FTL DNS resolver."""
20
+ return self.connection.post("action/restartdns")
@@ -0,0 +1,44 @@
1
+ from .conn import PiHole6Connection
2
+ from .metrics import PiHole6Metrics
3
+ from .dns_control import PiHole6DnsControl
4
+ from .group_management import PiHole6GroupManagement
5
+ from .domain_management import PiHole6DomainManagement
6
+ from .client_management import PiHole6ClientManagement
7
+ from .list_management import PiHole6ListManagement
8
+ from .ftl_info import PiHole6FtlInfo
9
+ from .config import PiHole6Configuration
10
+ from .network_info import PiHole6NetworkInfo
11
+ from .actions import PiHole6Actions
12
+ from .dhcp import PiHole6Dhcp
13
+
14
+ class PiHole6Client:
15
+ def __init__(self, base_url, password):
16
+ """
17
+ Initialize the Pi-hole client wrapper.
18
+
19
+ :param base_url: Pi-hole API base URL
20
+ :param password: Pi-hole password (or application password)
21
+ """
22
+ self.connection = PiHole6Connection(base_url, password)
23
+
24
+ # Attach API Modules
25
+ self.metrics = PiHole6Metrics(self.connection)
26
+ self.dns_control = PiHole6DnsControl(self.connection)
27
+ self.group_management = PiHole6GroupManagement(self.connection)
28
+ self.domain_management = PiHole6DomainManagement(self.connection)
29
+ self.client_management = PiHole6ClientManagement(self.connection)
30
+ self.list_management = PiHole6ListManagement(self.connection)
31
+ self.ftl_info = PiHole6FtlInfo(self.connection)
32
+ self.config = PiHole6Configuration(self.connection)
33
+ self.network_info = PiHole6NetworkInfo(self.connection)
34
+ self.actions = PiHole6Actions(self.connection)
35
+ self.dhcp = PiHole6Dhcp(self.connection)
36
+
37
+ def get_padd_summary(self, full=False):
38
+ """
39
+ Get summarized data for PADD.
40
+
41
+ :param full: Boolean flag to get the full dataset.
42
+ :return: API response containing PADD summary.
43
+ """
44
+ return self.connection.get("padd", params={"full": str(full).lower()})
@@ -0,0 +1,72 @@
1
+ class PiHole6ClientManagement:
2
+ def __init__(self, connection):
3
+ """
4
+ Handles Pi-hole client management API endpoints.
5
+ :param connection: Instance of PiHole6Connection for API requests.
6
+ """
7
+ self.connection = connection
8
+
9
+ def add_client(self, client, comment=None, groups=None):
10
+ """
11
+ Add a new client to Pi-hole.
12
+
13
+ :param client: Client identifier (IP, MAC, hostname, or interface).
14
+ :param comment: Optional comment for the client.
15
+ :param groups: Optional list of group IDs.
16
+ """
17
+ payload = {
18
+ "client": client if isinstance(client, list) else [client],
19
+ "comment": comment,
20
+ "groups": groups if groups else []
21
+ }
22
+
23
+ return self.connection.post("clients", data=payload)
24
+
25
+ def batch_delete_clients(self, clients):
26
+ """
27
+ Delete multiple clients.
28
+
29
+ :param clients: List of client identifiers (IP, MAC, hostname, or interface).
30
+ Example: [{"item": "192.168.1.100"}, {"item": "12:34:56:78:9A:BC"}]
31
+ """
32
+ if not isinstance(clients, list):
33
+ raise ValueError("clients must be a list of dictionaries.")
34
+
35
+ return self.connection.post("clients:batchDelete", data=clients)
36
+
37
+ def get_client_suggestions(self):
38
+ """
39
+ Retrieve suggested client entries based on known devices.
40
+ """
41
+ return self.connection.get("clients/_suggestions")
42
+
43
+ def get_client(self, client):
44
+ """
45
+ Retrieve information about a specific client.
46
+
47
+ :param client: Client identifier (IP, MAC, hostname, or interface).
48
+ """
49
+ return self.connection.get(f"clients/{client}")
50
+
51
+ def update_client(self, client, comment=None, groups=None):
52
+ """
53
+ Update an existing client.
54
+
55
+ :param client: Client identifier (IP, MAC, hostname, or interface).
56
+ :param comment: Updated comment (optional).
57
+ :param groups: Updated list of group IDs (optional).
58
+ """
59
+ payload = {
60
+ "comment": comment,
61
+ "groups": groups if groups else []
62
+ }
63
+
64
+ return self.connection.put(f"clients/{client}", data=payload)
65
+
66
+ def delete_client(self, client):
67
+ """
68
+ Delete a single client.
69
+
70
+ :param client: Client identifier (IP, MAC, hostname, or interface).
71
+ """
72
+ return self.connection.delete(f"clients/{client}")
@@ -0,0 +1,79 @@
1
+ import json
2
+
3
+ class PiHole6Configuration:
4
+ def __init__(self, connection):
5
+ """
6
+ Handles Pi-hole configuration API endpoints.
7
+ :param connection: Instance of PiHole6Connection for API requests.
8
+ """
9
+ self.connection = connection
10
+
11
+ def export_settings(self):
12
+ """
13
+ Export Pi-hole settings via the Teleporter API.
14
+ :return: Binary content of the exported settings archive.
15
+ """
16
+ return self.connection.get("teleporter", is_binary=True)
17
+
18
+ def import_settings(self, file_path, import_options=None):
19
+ """
20
+ Import Pi-hole settings using a Teleporter archive.
21
+
22
+ :param file_path: Path to the .tar.gz Teleporter file.
23
+ :param import_options: Dictionary of import options (default: import everything).
24
+ :return: API response.
25
+ """
26
+ with open(file_path, "rb") as file:
27
+ files = {"file": (file_path, file, "application/gzip")}
28
+ data = {"import": json.dumps(import_options)} if import_options else {}
29
+
30
+ return self.connection.post("teleporter", files=files, data=data)
31
+
32
+ def get_config(self, detailed=False):
33
+ """
34
+ Get the current configuration of Pi-hole.
35
+
36
+ :param detailed: Boolean flag to get detailed configuration.
37
+ :return: API response containing configuration data.
38
+ """
39
+ return self.connection.get("config", params={"detailed": str(detailed).lower()})
40
+
41
+ def update_config(self, config_changes):
42
+ """
43
+ Modify the Pi-hole configuration.
44
+
45
+ :param config_changes: Dictionary containing configuration updates.
46
+ :return: API response confirming changes.
47
+ """
48
+ payload = {"config": config_changes}
49
+ return self.connection.patch("config", data=payload)
50
+
51
+ def get_config_section(self, element, detailed=False):
52
+ """
53
+ Get a specific part of the Pi-hole configuration.
54
+
55
+ :param element: The section of the configuration to retrieve.
56
+ :param detailed: Boolean flag for detailed output.
57
+ :return: API response with the requested config section.
58
+ """
59
+ return self.connection.get(f"config/{element}", params={"detailed": str(detailed).lower()})
60
+
61
+ def add_config_item(self, element, value):
62
+ """
63
+ Add an item to a configuration array.
64
+
65
+ :param element: The config section to modify.
66
+ :param value: The value to add.
67
+ :return: API response confirming the addition.
68
+ """
69
+ return self.connection.put(f"config/{element}/{value}")
70
+
71
+ def delete_config_item(self, element, value):
72
+ """
73
+ Delete an item from a configuration array.
74
+
75
+ :param element: The config section to modify.
76
+ :param value: The value to remove.
77
+ :return: API response confirming the deletion.
78
+ """
79
+ return self.connection.delete(f"config/{element}/{value}")
@@ -0,0 +1,127 @@
1
+ import requests
2
+ import urllib3
3
+ from urllib.parse import urljoin
4
+ import warnings
5
+
6
+ # Suppress InsecureRequestWarning
7
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
8
+ warnings.simplefilter("ignore", category=urllib3.exceptions.InsecureRequestWarning)
9
+
10
+ class PiHole6Connection:
11
+ def __init__(self, base_url, password):
12
+ """
13
+ Initialize the Pi-hole connection client.
14
+
15
+ :param base_url: The base URL of the Pi-hole API (e.g., "http://pi.hole/api/")
16
+ :param password: The password for authentication (or an application password)
17
+ """
18
+ self.base_url = base_url.rstrip("/") + "/api/"
19
+ self.password = password
20
+ self.session_id = None
21
+ self.csrf_token = None
22
+ self.validity = None
23
+
24
+ # Authenticate upon initialization
25
+ self._authenticate()
26
+
27
+ def _authenticate(self):
28
+ """Authenticate with the Pi-hole API and store session ID and CSRF token."""
29
+ auth_url = urljoin(self.base_url, "auth")
30
+ payload = {"password": self.password}
31
+
32
+ response = requests.post(auth_url, json=payload, verify=False)
33
+
34
+ if response.status_code == 200:
35
+ data = response.json()
36
+ if "session" in data and data["session"]["valid"]:
37
+ self.session_id = data["session"]["sid"]
38
+ self.csrf_token = data["session"]["csrf"]
39
+ self.validity = data["session"]["validity"]
40
+ else:
41
+ raise Exception("Authentication failed: Invalid session response")
42
+ else:
43
+ raise Exception(f"Authentication failed: {response.json().get('error', {}).get('message', 'Unknown error')}")
44
+
45
+ def _get_headers(self):
46
+ """Return headers including the authentication SID and CSRF token."""
47
+ if not self.session_id or not self.csrf_token:
48
+ self._authenticate()
49
+
50
+ return {
51
+ "X-FTL-SID": self.session_id,
52
+ "X-FTL-CSRF": self.csrf_token
53
+ }
54
+
55
+ def _do_call(self, method, endpoint, params=None, data=None, files=None, is_binary=False):
56
+ """Internal method to send an authenticated request to the Pi-hole API."""
57
+ url = f"{self.base_url}{endpoint}"
58
+ headers = self._get_headers()
59
+
60
+ # Convert dictionary to form-encoded string if sending multipart
61
+ request_data = None if files else data
62
+ form_data = data if files else None # Ensure correct encoding
63
+
64
+ response = requests.request(
65
+ method,
66
+ url,
67
+ headers=headers,
68
+ params=params,
69
+ json=request_data,
70
+ files=files,
71
+ data=form_data,
72
+ verify=False
73
+ )
74
+
75
+ if response.status_code == 401:
76
+ self._authenticate()
77
+ headers = self._get_headers()
78
+ response = requests.request(
79
+ method,
80
+ url,
81
+ headers=headers,
82
+ params=params,
83
+ json=request_data,
84
+ files=files,
85
+ data=form_data,
86
+ verify=False
87
+ )
88
+
89
+ # Handle 4xx responses gracefully
90
+ if 400 <= response.status_code < 500:
91
+ try:
92
+ return response.json()
93
+ except requests.exceptions.JSONDecodeError:
94
+ return {"error": f"HTTP {response.status_code}: {response.reason}"}
95
+
96
+ response.raise_for_status()
97
+
98
+ if is_binary:
99
+ return response.content # Return raw binary content (e.g., for file exports)
100
+
101
+ if not response.content.strip():
102
+ return {} # Handle empty response
103
+
104
+ try:
105
+ return response.json() # Attempt to parse JSON
106
+ except requests.exceptions.JSONDecodeError:
107
+ return response.text # Return raw text as fallback
108
+
109
+ def get(self, endpoint, params=None, is_binary=False):
110
+ """Send a GET request."""
111
+ return self._do_call("GET", endpoint, params=params, is_binary=is_binary)
112
+
113
+ def post(self, endpoint, data=None, files=None):
114
+ """Send a POST request."""
115
+ return self._do_call("POST", endpoint, data=data, files=files)
116
+
117
+ def put(self, endpoint, data=None):
118
+ """Send a PUT request."""
119
+ return self._do_call("PUT", endpoint, data=data)
120
+
121
+ def delete(self, endpoint, params=None, data=None):
122
+ """Send a DELETE request."""
123
+ return self._do_call("DELETE", endpoint, params=params, data=data)
124
+
125
+ def patch(self, endpoint, data=None):
126
+ """Send a PATCH request."""
127
+ return self._do_call("PATCH", endpoint, data=data)
@@ -0,0 +1,21 @@
1
+ class PiHole6Dhcp:
2
+ def __init__(self, connection):
3
+ """Handles Pi-hole DHCP API endpoints."""
4
+ self.connection = connection
5
+
6
+ def get_leases(self):
7
+ """
8
+ Retrieve currently active DHCP leases.
9
+
10
+ :return: API response containing active DHCP leases.
11
+ """
12
+ return self.connection.get("dhcp/leases")
13
+
14
+ def remove_lease(self, ip):
15
+ """
16
+ Remove a specific DHCP lease by IP address.
17
+
18
+ :param ip: The IP address of the lease to remove.
19
+ :return: API response confirming removal.
20
+ """
21
+ return self.connection.delete(f"dhcp/leases/{ip}")
@@ -0,0 +1,24 @@
1
+ class PiHole6DnsControl:
2
+ def __init__(self, connection):
3
+ """
4
+ Handles Pi-hole DNS control API endpoints.
5
+ :param connection: Instance of PiHole6Connection for API requests.
6
+ """
7
+ self.connection = connection
8
+
9
+ def get_blocking_status(self):
10
+ """Get current blocking status."""
11
+ return self.connection.get("dns/blocking")
12
+
13
+ def set_blocking_status(self, blocking: bool, timer: int = None):
14
+ """
15
+ Change current blocking status.
16
+
17
+ :param blocking: True to enable blocking, False to disable blocking.
18
+ :param timer: (Optional) Set a timer in seconds. If None, change is permanent.
19
+ """
20
+ payload = {"blocking": blocking}
21
+ if timer is not None:
22
+ payload["timer"] = timer
23
+
24
+ return self.connection.post("dns/blocking", data=payload)
@@ -0,0 +1,87 @@
1
+ class PiHole6DomainManagement:
2
+ def __init__(self, connection):
3
+ """
4
+ Handles Pi-hole domain management API endpoints.
5
+ :param connection: Instance of PiHole6Connection for API requests.
6
+ """
7
+ self.connection = connection
8
+
9
+ def batch_delete_domains(self, domains):
10
+ """
11
+ Delete multiple domains.
12
+
13
+ :param domains: List of dictionaries with keys "item", "type", and "kind".
14
+ Example: [{"item": "example.com", "type": "allow", "kind": "exact"}]
15
+ """
16
+ if not isinstance(domains, list):
17
+ raise ValueError("domains must be a list of dictionaries.")
18
+
19
+ return self.connection.post("domains:batchDelete", data=domains)
20
+
21
+ def add_domain(self, domain, domain_type, kind, comment=None, groups=None, enabled=True):
22
+ """
23
+ Add a new domain to Pi-hole.
24
+
25
+ :param domain: Domain name (string or list of strings)
26
+ :param domain_type: Type of domain ("allow" or "deny")
27
+ :param kind: Kind of domain ("exact" or "regex")
28
+ :param comment: Optional comment for the domain
29
+ :param groups: Optional list of group IDs
30
+ :param enabled: Whether the domain is enabled (default: True)
31
+ """
32
+ if domain_type not in ["allow", "deny"]:
33
+ raise ValueError("domain_type must be 'allow' or 'deny'.")
34
+ if kind not in ["exact", "regex"]:
35
+ raise ValueError("kind must be 'exact' or 'regex'.")
36
+
37
+ payload = {
38
+ "domain": domain if isinstance(domain, list) else [domain],
39
+ "comment": comment,
40
+ "groups": groups if groups else [],
41
+ "enabled": enabled
42
+ }
43
+
44
+ return self.connection.post(f"domains/{domain_type}/{kind}", data=payload)
45
+
46
+ def get_domain(self, domain, domain_type, kind):
47
+ """
48
+ Retrieve information about a specific domain.
49
+
50
+ :param domain: Domain name
51
+ :param domain_type: Type of domain ("allow" or "deny")
52
+ :param kind: Kind of domain ("exact" or "regex")
53
+ """
54
+ return self.connection.get(f"domains/{domain_type}/{kind}/{domain}")
55
+
56
+ def update_domain(self, domain, domain_type, kind, new_type=None, new_kind=None, comment=None, groups=None, enabled=True):
57
+ """
58
+ Update or move an existing domain entry.
59
+
60
+ :param domain: Domain name
61
+ :param domain_type: Current type of domain ("allow" or "deny")
62
+ :param kind: Current kind of domain ("exact" or "regex")
63
+ :param new_type: New type of domain (optional)
64
+ :param new_kind: New kind of domain (optional)
65
+ :param comment: Updated comment (optional)
66
+ :param groups: Updated list of group IDs (optional)
67
+ :param enabled: Whether the domain is enabled (default: True)
68
+ """
69
+ payload = {
70
+ "type": new_type if new_type else domain_type,
71
+ "kind": new_kind if new_kind else kind,
72
+ "comment": comment,
73
+ "groups": groups if groups else [],
74
+ "enabled": enabled
75
+ }
76
+
77
+ return self.connection.put(f"domains/{domain_type}/{kind}/{domain}", data=payload)
78
+
79
+ def delete_domain(self, domain, domain_type, kind):
80
+ """
81
+ Delete a single domain.
82
+
83
+ :param domain: Domain name
84
+ :param domain_type: Type of domain ("allow" or "deny")
85
+ :param kind: Kind of domain ("exact" or "regex")
86
+ """
87
+ return self.connection.delete(f"domains/{domain_type}/{kind}/{domain}")
@@ -0,0 +1,75 @@
1
+ class PiHole6FtlInfo:
2
+ def __init__(self, connection):
3
+ """
4
+ Handles Pi-hole FTL and system diagnostics API endpoints.
5
+ :param connection: Instance of PiHole6Connection for API requests.
6
+ """
7
+ self.connection = connection
8
+
9
+ def get_endpoints(self):
10
+ """Retrieve a list of all available API endpoints."""
11
+ return self.connection.get("endpoints")
12
+
13
+ def get_client_info(self):
14
+ """Retrieve information about the requesting client."""
15
+ return self.connection.get("info/client")
16
+
17
+ def get_database_info(self):
18
+ """Retrieve long-term database statistics."""
19
+ return self.connection.get("info/database")
20
+
21
+ def get_ftl_info(self):
22
+ """Retrieve various FTL parameters."""
23
+ return self.connection.get("info/ftl")
24
+
25
+ def get_host_info(self):
26
+ """Retrieve various host parameters."""
27
+ return self.connection.get("info/host")
28
+
29
+ def get_login_info(self):
30
+ """Retrieve login page related information."""
31
+ return self.connection.get("info/login")
32
+
33
+ def get_diagnosis_messages(self):
34
+ """Retrieve all Pi-hole diagnosis messages."""
35
+ return self.connection.get("info/messages")
36
+
37
+ def delete_diagnosis_message(self, message_id):
38
+ """
39
+ Delete a specific Pi-hole diagnosis message.
40
+
41
+ :param message_id: ID of the diagnosis message to delete.
42
+ """
43
+ return self.connection.delete(f"info/messages/{message_id}")
44
+
45
+ def get_diagnosis_message_count(self):
46
+ """Retrieve the count of Pi-hole diagnosis messages."""
47
+ return self.connection.get("info/messages/count")
48
+
49
+ def get_metrics_info(self):
50
+ """Retrieve various system metrics."""
51
+ return self.connection.get("info/metrics")
52
+
53
+ def get_sensors_info(self):
54
+ """Retrieve various sensor data."""
55
+ return self.connection.get("info/sensors")
56
+
57
+ def get_system_info(self):
58
+ """Retrieve various system parameters."""
59
+ return self.connection.get("info/system")
60
+
61
+ def get_version(self):
62
+ """Retrieve Pi-hole version details."""
63
+ return self.connection.get("info/version")
64
+
65
+ def get_dnsmasq_logs(self):
66
+ """Retrieve DNS log content."""
67
+ return self.connection.get("logs/dnsmasq")
68
+
69
+ def get_ftl_logs(self):
70
+ """Retrieve FTL log content."""
71
+ return self.connection.get("logs/ftl")
72
+
73
+ def get_webserver_logs(self):
74
+ """Retrieve webserver log content."""
75
+ return self.connection.get("logs/webserver")
@@ -0,0 +1,67 @@
1
+ class PiHole6GroupManagement:
2
+ def __init__(self, connection):
3
+ """
4
+ Handles Pi-hole group management API endpoints.
5
+ :param connection: Instance of PiHole6Connection for API requests.
6
+ """
7
+ self.connection = connection
8
+
9
+ def add_group(self, name, comment=None, enabled=True):
10
+ """
11
+ Create a new group.
12
+
13
+ :param name: Name of the new group (string or list of strings).
14
+ :param comment: Optional comment describing the group.
15
+ :param enabled: Whether the group is enabled (default: True).
16
+ """
17
+ payload = {
18
+ "name": name if isinstance(name, list) else [name],
19
+ "comment": comment,
20
+ "enabled": enabled
21
+ }
22
+ return self.connection.post("groups", data=payload)
23
+
24
+ def batch_delete_groups(self, group_names):
25
+ """
26
+ Delete multiple groups.
27
+
28
+ :param group_names: List of group names to delete.
29
+ """
30
+ if not isinstance(group_names, list):
31
+ raise ValueError("group_names must be a list of group names.")
32
+
33
+ payload = [{"item": name} for name in group_names]
34
+ return self.connection.post("groups:batchDelete", data=payload)
35
+
36
+
37
+ def get_group(self, name):
38
+ """
39
+ Retrieve information about a specific group.
40
+
41
+ :param name: Name of the group to fetch.
42
+ """
43
+ return self.connection.get(f"groups/{name}")
44
+
45
+ def update_group(self, name, new_name=None, comment=None, enabled=True):
46
+ """
47
+ Update or rename an existing group.
48
+
49
+ :param name: Current name of the group.
50
+ :param new_name: New name for the group (optional).
51
+ :param comment: Updated comment for the group (optional).
52
+ :param enabled: Whether the group is enabled (default: True).
53
+ """
54
+ payload = {
55
+ "name": new_name if new_name else name,
56
+ "comment": comment,
57
+ "enabled": enabled
58
+ }
59
+ return self.connection.put(f"groups/{name}", data=payload)
60
+
61
+ def delete_group(self, name):
62
+ """
63
+ Delete a group.
64
+
65
+ :param name: Name of the group to delete.
66
+ """
67
+ return self.connection.delete(f"groups/{name}")
@@ -0,0 +1,94 @@
1
+ import urllib.parse
2
+
3
+ class PiHole6ListManagement:
4
+ def __init__(self, connection):
5
+ """
6
+ Handles Pi-hole list management API endpoints.
7
+ :param connection: Instance of PiHole6Connection for API requests.
8
+ """
9
+ self.connection = connection
10
+
11
+ def add_list(self, address, list_type, comment=None, groups=None, enabled=True):
12
+ """
13
+ Add a new list to Pi-hole.
14
+
15
+ :param address: URL of the blocklist/allowlist.
16
+ :param list_type: Type of list ("allow" or "block").
17
+ :param comment: Optional comment for the list.
18
+ :param groups: Optional list of group IDs.
19
+ :param enabled: Whether the list is enabled (default: True).
20
+ """
21
+ if list_type not in ["allow", "block"]:
22
+ raise ValueError("list_type must be 'allow' or 'block'.")
23
+
24
+ payload = {
25
+ "address": address if isinstance(address, list) else [address],
26
+ "type": list_type,
27
+ "comment": comment,
28
+ "groups": groups if groups else [],
29
+ "enabled": enabled
30
+ }
31
+
32
+ return self.connection.post("lists", data=payload)
33
+
34
+ def batch_delete_lists(self, lists):
35
+ """
36
+ Delete multiple lists.
37
+
38
+ :param lists: List of dictionaries with keys "address" and "type".
39
+ Example: [{"address": "https://example.com/blocklist.txt", "type": "block"}]
40
+ """
41
+ if not isinstance(lists, list):
42
+ raise ValueError("lists must be a list of dictionaries.")
43
+
44
+ return self.connection.post("lists:batchDelete", data=lists)
45
+
46
+ def get_list(self, address, list_type):
47
+ """
48
+ Retrieve information about a specific list.
49
+
50
+ :param address: URL of the blocklist/allowlist.
51
+ :param list_type: The type of list ("allow" or "block").
52
+ """
53
+ encoded_address = urllib.parse.quote(address, safe="")
54
+ params = {"type": list_type}
55
+ return self.connection.get(f"lists/{encoded_address}")
56
+
57
+ def update_list(self, address, list_type=None, comment=None, groups=None, enabled=True):
58
+ """
59
+ Update an existing list.
60
+
61
+ :param address: URL of the blocklist/allowlist.
62
+ :param list_type: Type of list ("allow" or "block") (optional).
63
+ :param comment: Updated comment (optional).
64
+ :param groups: Updated list of group IDs (optional).
65
+ :param enabled: Whether the list is enabled (default: True).
66
+ """
67
+ encoded_address = urllib.parse.quote(address, safe="")
68
+ payload = {
69
+ "type": list_type,
70
+ "comment": comment,
71
+ "groups": groups if groups else [],
72
+ "enabled": enabled
73
+ }
74
+
75
+ return self.connection.put(f"lists/{encoded_address}", data=payload)
76
+
77
+ def delete_list(self, address, list_type):
78
+ """
79
+ Delete a specific list entry.
80
+
81
+ :param address: The URL of the list to delete.
82
+ :param list_type: The type of list ("allow" or "block").
83
+ """
84
+ encoded_address = urllib.parse.quote(address, safe="")
85
+ params = {"type": list_type}
86
+ return self.connection.delete(f"lists/{encoded_address}", params=params)
87
+
88
+ def search_list(self, domain):
89
+ """
90
+ Search for a domain in Pi-hole's lists.
91
+
92
+ :param domain: Domain to search for.
93
+ """
94
+ return self.connection.get(f"search/{domain}")
@@ -0,0 +1,79 @@
1
+ class PiHole6Metrics:
2
+ def __init__(self, connection):
3
+ """
4
+ Handles Pi-hole metrics and stats API endpoints.
5
+ :param connection: Instance of PiHole6Connection for API requests.
6
+ """
7
+ self.connection = connection
8
+
9
+ # History API Endpoints
10
+ def get_history(self):
11
+ """Get activity graph data"""
12
+ return self.connection.get("history")
13
+
14
+ def get_history_clients(self):
15
+ """Get per-client activity graph data"""
16
+ return self.connection.get("history/clients")
17
+
18
+ def get_history_database(self):
19
+ """Get long-term activity graph data"""
20
+ return self.connection.get("history/database")
21
+
22
+ def get_history_database_clients(self):
23
+ """Get per-client long-term activity graph data"""
24
+ return self.connection.get("history/database/clients")
25
+
26
+ # Query API Endpoints
27
+ def get_queries(self):
28
+ """Get query log"""
29
+ return self.connection.get("queries")
30
+
31
+ def get_query_suggestions(self):
32
+ """Get query filter suggestions"""
33
+ return self.connection.get("queries/suggestions")
34
+
35
+ # Stats Database API Endpoints
36
+ def get_stats_database_query_types(self):
37
+ """Get query types (long-term database)"""
38
+ return self.connection.get("stats/database/query_types")
39
+
40
+ def get_stats_database_summary(self):
41
+ """Get database content details"""
42
+ return self.connection.get("stats/database/summary")
43
+
44
+ def get_stats_database_top_clients(self):
45
+ """Get top clients (long-term database)"""
46
+ return self.connection.get("stats/database/top_clients")
47
+
48
+ def get_stats_database_top_domains(self):
49
+ """Get top domains (long-term database)"""
50
+ return self.connection.get("stats/database/top_domains")
51
+
52
+ def get_stats_database_upstreams(self):
53
+ """Get upstream metrics (long-term database)"""
54
+ return self.connection.get("stats/database/upstreams")
55
+
56
+ # Stats API Endpoints
57
+ def get_stats_query_types(self):
58
+ """Get current query types"""
59
+ return self.connection.get("stats/query_types")
60
+
61
+ def get_stats_recent_blocked(self):
62
+ """Get most recently blocked domain"""
63
+ return self.connection.get("stats/recent_blocked")
64
+
65
+ def get_stats_summary(self):
66
+ """Get an overview of Pi-hole activity"""
67
+ return self.connection.get("stats/summary")
68
+
69
+ def get_stats_top_clients(self):
70
+ """Get top clients"""
71
+ return self.connection.get("stats/top_clients")
72
+
73
+ def get_stats_top_domains(self):
74
+ """Get top domains"""
75
+ return self.connection.get("stats/top_domains")
76
+
77
+ def get_stats_upstreams(self):
78
+ """Get upstream destinations"""
79
+ return self.connection.get("stats/upstreams")
@@ -0,0 +1,28 @@
1
+ class PiHole6NetworkInfo:
2
+ def __init__(self, connection):
3
+ """Handles Pi-hole network information API endpoints."""
4
+ self.connection = connection
5
+
6
+ def get_devices(self):
7
+ """Get information about devices on the local network."""
8
+ return self.connection.get("network/devices")
9
+
10
+ def delete_device(self, device_id):
11
+ """
12
+ Delete a device from the network table.
13
+
14
+ :param device_id: The ID of the device to delete.
15
+ """
16
+ return self.connection.delete(f"network/devices/{device_id}")
17
+
18
+ def get_gateway(self):
19
+ """Get information about the gateway of the Pi-hole."""
20
+ return self.connection.get("network/gateway")
21
+
22
+ def get_interfaces(self):
23
+ """Get information about network interfaces of the Pi-hole."""
24
+ return self.connection.get("network/interfaces")
25
+
26
+ def get_routes(self):
27
+ """Get information about network routes of the Pi-hole."""
28
+ return self.connection.get("network/routes")
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.2
2
+ Name: pihole6api
3
+ Version: 0.1.0
4
+ Summary: Python API Client for Pi-hole 6
5
+ Author-email: Shane Barbetta <shane@barbetta.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sbarbett/pihole6api
8
+ Project-URL: Documentation, https://github.com/sbarbett/pihole6api
9
+ Project-URL: Source, https://github.com/sbarbett/pihole6api
10
+ Project-URL: Issues, https://github.com/sbarbett/pihole6api/issues
11
+ Keywords: pihole,dns,adblocking,api,client
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: requests>=2.26.0
25
+
26
+ # pihole6api
27
+
28
+ This package provides a simple, modular SDK for the PiHole 6 REST API.
29
+
30
+ ## Features
31
+
32
+ * Automatically handles authentication and renewal
33
+ * Gracefully error management
34
+ * Logically organized modules
35
+ * Easily maintained
36
+
37
+ ## Installation
38
+
39
+ **Install using `pip` or `pipx`:**
40
+
41
+ ```bash
42
+ pip install pihole6api
43
+ ```
44
+
45
+ **Install from source:**
46
+
47
+ ```bash
48
+ git clone https://github.com/sbarbett/pihole6api.git
49
+ cd pihole6api
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ### Initialize the Client
56
+
57
+ ```python
58
+ from pihole6api.client import PiHole6Client
59
+ client = PiHole6Client("https://your-pihole.local/", "your-password")
60
+ ```
61
+
62
+ ### Example Usage
63
+
64
+ #### Get Pi-Hole Metrics
65
+
66
+ ```python
67
+ history = client.metrics.get_history()
68
+ print(history) # {'history': [{'timestamp': 1740120900, 'total': 0, 'cached': 0 ...}]}
69
+ queries = client.metrics.get_queries()
70
+ print(queries)
71
+ ```
72
+
73
+ #### Enable/Disable Blocking
74
+
75
+ ```python
76
+ client.dns_control.set_blocking_status(False, 60)
77
+ print(client.dns_control.get_blocking_status()) # {'blocking': 'disabled', 'timer': 60 ...}
78
+ ```
79
+
80
+ #### Manage Groups
81
+
82
+ ```python
83
+ client.group_management.add_group("Custom Group", comment="For testing")
84
+ client.group_management.delete_group("Custom Group")
85
+ ```
86
+
87
+ #### Manage Domains
88
+
89
+ ```python
90
+ client.domain_management.add_domain("ads.example.com", "deny", "exact")
91
+ client.domain_management.delete_domain("ads.example.com", "deny", "exact")
92
+ ```
93
+
94
+ #### Manage Links
95
+
96
+ ```python
97
+ client.list_management.add_list("https://example.com/blocklist.txt", "block")
98
+ client.list_management.delete_list("https://example.com/blocklist.txt", "block")
99
+ ```
100
+
101
+ #### Export/Import PiHole Settings
102
+
103
+ ```python
104
+ # Export settings and save as a .zip file
105
+ with open("pihole-settings.zip", "wb") as f:
106
+ f.write(client.config.export_settings())
107
+
108
+ client.config.import_settings("pihole-settings.zip", {"config": True, "gravity": {"group": True}})
109
+ ```
110
+
111
+ #### Flush Logs & Restart DNS
112
+
113
+ ```python
114
+ client.actions.flush_logs()
115
+ client.actions.restart_dns()
116
+ ```
117
+
118
+ ## API Modules
119
+
120
+ | Module | Description |
121
+ |----------------------|-------------|
122
+ | `metrics` | Query history, top clients/domains, DNS stats |
123
+ | `dns_control` | Enable/disable blocking |
124
+ | `group_management` | Create, update, and delete groups |
125
+ | `domain_management` | Allow/block domains (exact & regex) |
126
+ | `client_management` | Manage client-specific rules |
127
+ | `list_management` | Manage blocklists (Adlists) |
128
+ | `config` | Modify Pi-hole configuration |
129
+ | `ftl_info` | Get Pi-hole core process (FTL) info |
130
+ | `dhcp` | Manage DHCP leases |
131
+ | `network_info` | View network devices, interfaces, routes |
132
+ | `actions` | Flush logs, restart services |
133
+ | `padd` | Fetch summarized data for PADD |
134
+
135
+ ## License
136
+
137
+ This project is license under the [MIT license](LICENSE).
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pihole6api/__init__.py
5
+ src/pihole6api/actions.py
6
+ src/pihole6api/client.py
7
+ src/pihole6api/client_management.py
8
+ src/pihole6api/config.py
9
+ src/pihole6api/conn.py
10
+ src/pihole6api/dhcp.py
11
+ src/pihole6api/dns_control.py
12
+ src/pihole6api/domain_management.py
13
+ src/pihole6api/ftl_info.py
14
+ src/pihole6api/group_management.py
15
+ src/pihole6api/list_management.py
16
+ src/pihole6api/metrics.py
17
+ src/pihole6api/network_info.py
18
+ src/pihole6api.egg-info/PKG-INFO
19
+ src/pihole6api.egg-info/SOURCES.txt
20
+ src/pihole6api.egg-info/dependency_links.txt
21
+ src/pihole6api.egg-info/requires.txt
22
+ src/pihole6api.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.26.0
@@ -0,0 +1 @@
1
+ pihole6api