pihole6api 0.1.8__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pihole6api/client.py +16 -1
- pihole6api/conn.py +121 -51
- pihole6api/domain_management.py +15 -0
- {pihole6api-0.1.8.dist-info → pihole6api-0.2.0.dist-info}/METADATA +9 -1
- {pihole6api-0.1.8.dist-info → pihole6api-0.2.0.dist-info}/RECORD +8 -8
- {pihole6api-0.1.8.dist-info → pihole6api-0.2.0.dist-info}/WHEEL +1 -1
- {pihole6api-0.1.8.dist-info → pihole6api-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pihole6api-0.1.8.dist-info → pihole6api-0.2.0.dist-info}/top_level.txt +0 -0
pihole6api/client.py
CHANGED
@@ -10,6 +10,7 @@ from .config import PiHole6Configuration
|
|
10
10
|
from .network_info import PiHole6NetworkInfo
|
11
11
|
from .actions import PiHole6Actions
|
12
12
|
from .dhcp import PiHole6Dhcp
|
13
|
+
import importlib.metadata
|
13
14
|
|
14
15
|
class PiHole6Client:
|
15
16
|
def __init__(self, base_url, password):
|
@@ -45,4 +46,18 @@ class PiHole6Client:
|
|
45
46
|
|
46
47
|
def close_session(self):
|
47
48
|
"""Close the Pi-hole session by calling the exit method in the connection."""
|
48
|
-
return self.connection.exit()
|
49
|
+
return self.connection.exit()
|
50
|
+
|
51
|
+
def version(self):
|
52
|
+
"""
|
53
|
+
Get the project information from package metadata.
|
54
|
+
|
55
|
+
:return: Dictionary containing version, description, and project URL
|
56
|
+
"""
|
57
|
+
metadata = importlib.metadata.metadata("pihole6api")
|
58
|
+
|
59
|
+
return {
|
60
|
+
"version": metadata["Version"],
|
61
|
+
"description": metadata["Summary"],
|
62
|
+
"project_url": metadata["Project-URL"]
|
63
|
+
}
|
pihole6api/conn.py
CHANGED
@@ -4,65 +4,113 @@ from urllib.parse import urljoin
|
|
4
4
|
import warnings
|
5
5
|
import time
|
6
6
|
import json
|
7
|
+
import logging
|
8
|
+
from requests.adapters import HTTPAdapter
|
9
|
+
from urllib3.util.retry import Retry
|
7
10
|
|
8
11
|
# Suppress InsecureRequestWarning
|
9
12
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
10
13
|
warnings.simplefilter("ignore", category=urllib3.exceptions.InsecureRequestWarning)
|
11
14
|
|
15
|
+
# Configure logging
|
16
|
+
logger = logging.getLogger("pihole6api")
|
17
|
+
|
12
18
|
class PiHole6Connection:
|
13
|
-
def __init__(self, base_url, password
|
19
|
+
def __init__(self, base_url, password, max_retries=3, retry_delay=1,
|
20
|
+
connection_timeout=10, disable_connection_pooling=False):
|
14
21
|
"""
|
15
22
|
Initialize the Pi-hole connection client.
|
16
23
|
|
17
24
|
:param base_url: The base URL of the Pi-hole API (e.g., "http://pi.hole/api/")
|
18
25
|
:param password: The password for authentication (or an application password)
|
26
|
+
:param max_retries: Maximum number of retry attempts for failed requests
|
27
|
+
:param retry_delay: Base delay in seconds between retries (will use exponential backoff)
|
28
|
+
:param connection_timeout: Connection timeout in seconds
|
29
|
+
:param disable_connection_pooling: If True, disable connection pooling to prevent connection reuse issues
|
19
30
|
"""
|
20
31
|
self.base_url = base_url.rstrip("/") + "/api/"
|
21
32
|
self.password = password
|
22
33
|
self.session_id = None
|
23
34
|
self.csrf_token = None
|
24
35
|
self.validity = None
|
25
|
-
|
36
|
+
self.max_retries = max_retries
|
37
|
+
self.retry_delay = retry_delay
|
38
|
+
self.connection_timeout = connection_timeout
|
39
|
+
self.disable_connection_pooling = disable_connection_pooling
|
40
|
+
|
41
|
+
# Create a session for connection reuse
|
42
|
+
self.session = requests.Session()
|
43
|
+
|
44
|
+
# Configure retry strategy
|
45
|
+
retry_strategy = Retry(
|
46
|
+
total=max_retries,
|
47
|
+
backoff_factor=retry_delay,
|
48
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
49
|
+
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"]
|
50
|
+
)
|
51
|
+
|
52
|
+
# Configure adapter with retry strategy
|
53
|
+
adapter = HTTPAdapter(
|
54
|
+
max_retries=retry_strategy,
|
55
|
+
pool_connections=1 if disable_connection_pooling else 10,
|
56
|
+
pool_maxsize=1 if disable_connection_pooling else 10
|
57
|
+
)
|
58
|
+
|
59
|
+
# Mount the adapter to both HTTP and HTTPS
|
60
|
+
self.session.mount("http://", adapter)
|
61
|
+
self.session.mount("https://", adapter)
|
62
|
+
|
63
|
+
# Set timeout
|
64
|
+
self.session.timeout = connection_timeout
|
65
|
+
|
26
66
|
# Authenticate upon initialization
|
27
67
|
self._authenticate()
|
28
68
|
|
29
69
|
def _authenticate(self):
|
30
70
|
"""Authenticate with the Pi-hole API and store session ID and CSRF token.
|
31
71
|
|
32
|
-
Retries up to
|
33
|
-
before raising an exception.
|
72
|
+
Retries up to max_retries times with exponential backoff before raising an exception.
|
34
73
|
"""
|
35
74
|
auth_url = urljoin(self.base_url, "auth")
|
36
75
|
payload = {"password": self.password}
|
37
|
-
max_attempts = 3
|
38
76
|
last_exception = None
|
39
77
|
|
40
|
-
for attempt in range(1,
|
41
|
-
response = requests.post(auth_url, json=payload, verify=False)
|
78
|
+
for attempt in range(1, self.max_retries + 1):
|
42
79
|
try:
|
80
|
+
logger.debug(f"Authentication attempt {attempt}/{self.max_retries}")
|
81
|
+
response = self.session.post(auth_url, json=payload, verify=False, timeout=self.connection_timeout)
|
82
|
+
|
43
83
|
if response.status_code == 200:
|
44
84
|
data = response.json()
|
45
|
-
if "session" in data and data["session"]["valid"]:
|
85
|
+
if "session" in data and data["session"]["valid"] and data["session"]["validity"] > 0:
|
46
86
|
self.session_id = data["session"]["sid"]
|
47
87
|
self.csrf_token = data["session"]["csrf"]
|
48
88
|
self.validity = data["session"]["validity"]
|
89
|
+
logger.debug("Authentication successful")
|
49
90
|
return # Successful authentication
|
50
91
|
else:
|
92
|
+
if data.get("session"):
|
93
|
+
logger.error(data["session"].get("message", "API failed without message"))
|
51
94
|
last_exception = Exception("Authentication failed: Invalid session response")
|
52
95
|
else:
|
53
96
|
# Try to extract an error message from the response
|
54
97
|
try:
|
55
|
-
error_msg = response.json().get("
|
98
|
+
error_msg = response.json().get("session", {}).get("message", "Unknown error")
|
56
99
|
except (json.decoder.JSONDecodeError, ValueError):
|
57
100
|
error_msg = f"HTTP {response.status_code}: {response.reason}"
|
58
101
|
last_exception = Exception(f"Authentication failed: {error_msg}")
|
59
102
|
except Exception as e:
|
60
103
|
last_exception = e
|
104
|
+
logger.warning(f"Authentication attempt {attempt} failed: {str(e)}")
|
61
105
|
|
62
|
-
if attempt <
|
63
|
-
|
106
|
+
if attempt < self.max_retries:
|
107
|
+
# Calculate exponential backoff delay
|
108
|
+
current_delay = self.retry_delay * (2 ** (attempt - 1))
|
109
|
+
logger.debug(f"Retrying authentication in {current_delay} seconds...")
|
110
|
+
time.sleep(current_delay)
|
64
111
|
|
65
112
|
# All attempts failed; raise the last captured exception.
|
113
|
+
logger.error(f"All authentication attempts failed: {str(last_exception)}")
|
66
114
|
raise last_exception
|
67
115
|
|
68
116
|
def _get_headers(self):
|
@@ -84,21 +132,9 @@ class PiHole6Connection:
|
|
84
132
|
request_data = None if files else data
|
85
133
|
form_data = data if files else None # Ensure correct encoding
|
86
134
|
|
87
|
-
|
88
|
-
method
|
89
|
-
|
90
|
-
headers=headers,
|
91
|
-
params=params,
|
92
|
-
json=request_data,
|
93
|
-
files=files,
|
94
|
-
data=form_data,
|
95
|
-
verify=False
|
96
|
-
)
|
97
|
-
|
98
|
-
if response.status_code == 401:
|
99
|
-
self._authenticate()
|
100
|
-
headers = self._get_headers()
|
101
|
-
response = requests.request(
|
135
|
+
try:
|
136
|
+
logger.debug(f"Sending {method} request to {url}")
|
137
|
+
response = self.session.request(
|
102
138
|
method,
|
103
139
|
url,
|
104
140
|
headers=headers,
|
@@ -106,28 +142,55 @@ class PiHole6Connection:
|
|
106
142
|
json=request_data,
|
107
143
|
files=files,
|
108
144
|
data=form_data,
|
109
|
-
verify=False
|
145
|
+
verify=False,
|
146
|
+
timeout=self.connection_timeout
|
110
147
|
)
|
111
148
|
|
112
|
-
|
113
|
-
|
149
|
+
if response.status_code == 401:
|
150
|
+
logger.warning("Session expired, re-authenticating")
|
151
|
+
self._authenticate()
|
152
|
+
headers = self._get_headers()
|
153
|
+
response = self.session.request(
|
154
|
+
method,
|
155
|
+
url,
|
156
|
+
headers=headers,
|
157
|
+
params=params,
|
158
|
+
json=request_data,
|
159
|
+
files=files,
|
160
|
+
data=form_data,
|
161
|
+
verify=False,
|
162
|
+
timeout=self.connection_timeout
|
163
|
+
)
|
164
|
+
|
165
|
+
# Handle 4xx responses gracefully
|
166
|
+
if 400 <= response.status_code < 500:
|
167
|
+
try:
|
168
|
+
return response.json()
|
169
|
+
except requests.exceptions.JSONDecodeError:
|
170
|
+
return {"error": f"HTTP {response.status_code}: {response.reason}"}
|
171
|
+
|
172
|
+
response.raise_for_status()
|
173
|
+
|
174
|
+
if is_binary:
|
175
|
+
return response.content # Return raw binary content (e.g., for file exports)
|
176
|
+
|
177
|
+
if not response.content.strip():
|
178
|
+
return {} # Handle empty response
|
179
|
+
|
114
180
|
try:
|
115
|
-
return response.json()
|
181
|
+
return response.json() # Attempt to parse JSON
|
116
182
|
except requests.exceptions.JSONDecodeError:
|
117
|
-
return
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
return response.json() # Attempt to parse JSON
|
129
|
-
except requests.exceptions.JSONDecodeError:
|
130
|
-
return response.text # Return raw text as fallback
|
183
|
+
return response.text # Return raw text as fallback
|
184
|
+
|
185
|
+
except requests.exceptions.ConnectionError as e:
|
186
|
+
logger.error(f"Connection error: {str(e)}")
|
187
|
+
raise Exception(f"Connection error: {str(e)}")
|
188
|
+
except requests.exceptions.Timeout as e:
|
189
|
+
logger.error(f"Request timed out: {str(e)}")
|
190
|
+
raise Exception(f"Request timed out: {str(e)}")
|
191
|
+
except requests.exceptions.RequestException as e:
|
192
|
+
logger.error(f"Request error: {str(e)}")
|
193
|
+
raise Exception(f"Request error: {str(e)}")
|
131
194
|
|
132
195
|
def get(self, endpoint, params=None, is_binary=False):
|
133
196
|
"""Send a GET request."""
|
@@ -150,12 +213,19 @@ class PiHole6Connection:
|
|
150
213
|
return self._do_call("PATCH", endpoint, data=data)
|
151
214
|
|
152
215
|
def exit(self):
|
153
|
-
"""Delete the current session."""
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
216
|
+
"""Delete the current session and close connections."""
|
217
|
+
try:
|
218
|
+
response = self.delete("auth")
|
219
|
+
except Exception as e:
|
220
|
+
logger.warning(f"Error during session exit: {str(e)}")
|
221
|
+
response = {"error": str(e)}
|
222
|
+
finally:
|
223
|
+
# Clear stored session info
|
224
|
+
self.session_id = None
|
225
|
+
self.csrf_token = None
|
226
|
+
self.validity = None
|
227
|
+
|
228
|
+
# Close the session to release connections
|
229
|
+
self.session.close()
|
160
230
|
|
161
231
|
return response
|
pihole6api/domain_management.py
CHANGED
@@ -85,3 +85,18 @@ class PiHole6DomainManagement:
|
|
85
85
|
:param kind: Kind of domain ("exact" or "regex")
|
86
86
|
"""
|
87
87
|
return self.connection.delete(f"domains/{domain_type}/{kind}/{domain}")
|
88
|
+
|
89
|
+
def get_all_domains(self):
|
90
|
+
"""
|
91
|
+
Get all white and blacklisted domains
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
# Get whiteliste domains
|
95
|
+
whitelist = self.connection.get("domains/allow/exact")
|
96
|
+
# Get blackliste domains
|
97
|
+
blacklist = self.connection.get("domains/deny/exact")
|
98
|
+
|
99
|
+
return {"whitelist": whitelist, "blacklist": blacklist}
|
100
|
+
except Exception as e:
|
101
|
+
print(f"Error fetching domains: {e}")
|
102
|
+
return None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pihole6api
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Python API Client for Pi-hole 6
|
5
5
|
Author-email: Shane Barbetta <shane@barbetta.me>
|
6
6
|
License: MIT
|
@@ -132,6 +132,14 @@ client.actions.restart_dns()
|
|
132
132
|
| `network_info` | View network devices, interfaces, routes |
|
133
133
|
| `actions` | Flush logs, restart services |
|
134
134
|
|
135
|
+
## Contributing
|
136
|
+
|
137
|
+
Please check [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
138
|
+
|
139
|
+
## Changelog
|
140
|
+
|
141
|
+
See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
|
142
|
+
|
135
143
|
## License
|
136
144
|
|
137
145
|
This project is license under the [MIT license](LICENSE).
|
@@ -1,19 +1,19 @@
|
|
1
1
|
pihole6api/__init__.py,sha256=OKDAH2I6UjXcBmcj6rn5aNg5J60GCUBVFJ-_t83GiVQ,898
|
2
2
|
pihole6api/actions.py,sha256=8CBkr8nYfT8yfdCO6F9M9nompaYcFdsaYGiEa1eVDCw,693
|
3
|
-
pihole6api/client.py,sha256=
|
3
|
+
pihole6api/client.py,sha256=8sco9wFCAmF1WTRG72W5Af4fZIoTjvCXXmj-O51MY88,2472
|
4
4
|
pihole6api/client_management.py,sha256=n8Ra_6pRL3Q9jlIK3kIK9bmh74MTfxgYnOHYaUwYGNs,2590
|
5
5
|
pihole6api/config.py,sha256=NdBHOudz147oIs5YVR3U4WLvqk3hU3HlZHnshy1NK4g,4680
|
6
|
-
pihole6api/conn.py,sha256=
|
6
|
+
pihole6api/conn.py,sha256=2JYPgHUmhPSuNdrMaWkEtf2lnqDJ8--g05CYntOBJvQ,9523
|
7
7
|
pihole6api/dhcp.py,sha256=1A3z-3q9x51-6MOC3JMl7yR_5pHmRxZtMWtPqzWxYm0,629
|
8
8
|
pihole6api/dns_control.py,sha256=mxV3AIuGCsx0-1ibpMXor9QUGd_fDFfeaUENPhIK_TY,853
|
9
|
-
pihole6api/domain_management.py,sha256=
|
9
|
+
pihole6api/domain_management.py,sha256=5oJebojAAEEvZtJgxeuK4FazcMxmiB1NpAgStqdUv-E,3988
|
10
10
|
pihole6api/ftl_info.py,sha256=FINHFotI1sQgkL0OPaNqW-rk4h1ua6QHnRh5gFXXRIE,3210
|
11
11
|
pihole6api/group_management.py,sha256=Iip2Na4pUYgC5K9cwOWv_OPD6qdXRzWCmlS-fJlBMjU,2372
|
12
12
|
pihole6api/list_management.py,sha256=CooTeF4EmNPFwnzGoLbHFMhCePJ-ojvRGx7Sxzayy-U,4170
|
13
13
|
pihole6api/metrics.py,sha256=QWqV_7SytMPGEBEZOxsIfxTa6lM9Ksrvlf2BXjDKZmA,7991
|
14
14
|
pihole6api/network_info.py,sha256=E1qQ7DsCb7qplt0oQIyKhUY12ExNF6bv6thm9rbNqwY,1953
|
15
|
-
pihole6api-0.
|
16
|
-
pihole6api-0.
|
17
|
-
pihole6api-0.
|
18
|
-
pihole6api-0.
|
19
|
-
pihole6api-0.
|
15
|
+
pihole6api-0.2.0.dist-info/licenses/LICENSE,sha256=hpO6J6J9O1VZxZeHQTxKMTmuobaHbApiZxp279I4xNU,1062
|
16
|
+
pihole6api-0.2.0.dist-info/METADATA,sha256=oL9701CfySKOOVdWglV7rEmpVBAMMRAr_KSckcTFyGw,3956
|
17
|
+
pihole6api-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
18
|
+
pihole6api-0.2.0.dist-info/top_level.txt,sha256=Qrh46lxEC54rBR8T53em-tuZLWbmi1SDwL1rOhsgrME,11
|
19
|
+
pihole6api-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|