pihole6api 0.1.8__py3-none-any.whl → 0.1.9__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/conn.py CHANGED
@@ -4,48 +4,89 @@ 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 three times (with a one-second pause between attempts)
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, max_attempts + 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
85
  if "session" in data and data["session"]["valid"]:
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:
51
92
  last_exception = Exception("Authentication failed: Invalid session response")
@@ -58,11 +99,16 @@ class PiHole6Connection:
58
99
  last_exception = Exception(f"Authentication failed: {error_msg}")
59
100
  except Exception as e:
60
101
  last_exception = e
102
+ logger.warning(f"Authentication attempt {attempt} failed: {str(e)}")
61
103
 
62
- if attempt < max_attempts:
63
- time.sleep(1) # Pause before retrying
104
+ if attempt < self.max_retries:
105
+ # Calculate exponential backoff delay
106
+ current_delay = self.retry_delay * (2 ** (attempt - 1))
107
+ logger.debug(f"Retrying authentication in {current_delay} seconds...")
108
+ time.sleep(current_delay)
64
109
 
65
110
  # All attempts failed; raise the last captured exception.
111
+ logger.error(f"All authentication attempts failed: {str(last_exception)}")
66
112
  raise last_exception
67
113
 
68
114
  def _get_headers(self):
@@ -84,21 +130,9 @@ class PiHole6Connection:
84
130
  request_data = None if files else data
85
131
  form_data = data if files else None # Ensure correct encoding
86
132
 
87
- response = requests.request(
88
- method,
89
- url,
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(
133
+ try:
134
+ logger.debug(f"Sending {method} request to {url}")
135
+ response = self.session.request(
102
136
  method,
103
137
  url,
104
138
  headers=headers,
@@ -106,28 +140,55 @@ class PiHole6Connection:
106
140
  json=request_data,
107
141
  files=files,
108
142
  data=form_data,
109
- verify=False
143
+ verify=False,
144
+ timeout=self.connection_timeout
110
145
  )
111
146
 
112
- # Handle 4xx responses gracefully
113
- if 400 <= response.status_code < 500:
147
+ if response.status_code == 401:
148
+ logger.warning("Session expired, re-authenticating")
149
+ self._authenticate()
150
+ headers = self._get_headers()
151
+ response = self.session.request(
152
+ method,
153
+ url,
154
+ headers=headers,
155
+ params=params,
156
+ json=request_data,
157
+ files=files,
158
+ data=form_data,
159
+ verify=False,
160
+ timeout=self.connection_timeout
161
+ )
162
+
163
+ # Handle 4xx responses gracefully
164
+ if 400 <= response.status_code < 500:
165
+ try:
166
+ return response.json()
167
+ except requests.exceptions.JSONDecodeError:
168
+ return {"error": f"HTTP {response.status_code}: {response.reason}"}
169
+
170
+ response.raise_for_status()
171
+
172
+ if is_binary:
173
+ return response.content # Return raw binary content (e.g., for file exports)
174
+
175
+ if not response.content.strip():
176
+ return {} # Handle empty response
177
+
114
178
  try:
115
- return response.json()
179
+ return response.json() # Attempt to parse JSON
116
180
  except requests.exceptions.JSONDecodeError:
117
- return {"error": f"HTTP {response.status_code}: {response.reason}"}
118
-
119
- response.raise_for_status()
120
-
121
- if is_binary:
122
- return response.content # Return raw binary content (e.g., for file exports)
123
-
124
- if not response.content.strip():
125
- return {} # Handle empty response
126
-
127
- try:
128
- return response.json() # Attempt to parse JSON
129
- except requests.exceptions.JSONDecodeError:
130
- return response.text # Return raw text as fallback
181
+ return response.text # Return raw text as fallback
182
+
183
+ except requests.exceptions.ConnectionError as e:
184
+ logger.error(f"Connection error: {str(e)}")
185
+ raise Exception(f"Connection error: {str(e)}")
186
+ except requests.exceptions.Timeout as e:
187
+ logger.error(f"Request timed out: {str(e)}")
188
+ raise Exception(f"Request timed out: {str(e)}")
189
+ except requests.exceptions.RequestException as e:
190
+ logger.error(f"Request error: {str(e)}")
191
+ raise Exception(f"Request error: {str(e)}")
131
192
 
132
193
  def get(self, endpoint, params=None, is_binary=False):
133
194
  """Send a GET request."""
@@ -150,12 +211,19 @@ class PiHole6Connection:
150
211
  return self._do_call("PATCH", endpoint, data=data)
151
212
 
152
213
  def exit(self):
153
- """Delete the current session."""
154
- response = self.delete("auth")
155
-
156
- # Clear stored session info
157
- self.session_id = None
158
- self.csrf_token = None
159
- self.validity = None
214
+ """Delete the current session and close connections."""
215
+ try:
216
+ response = self.delete("auth")
217
+ except Exception as e:
218
+ logger.warning(f"Error during session exit: {str(e)}")
219
+ response = {"error": str(e)}
220
+ finally:
221
+ # Clear stored session info
222
+ self.session_id = None
223
+ self.csrf_token = None
224
+ self.validity = None
225
+
226
+ # Close the session to release connections
227
+ self.session.close()
160
228
 
161
229
  return response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pihole6api
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Python API Client for Pi-hole 6
5
5
  Author-email: Shane Barbetta <shane@barbetta.me>
6
6
  License: MIT
@@ -3,7 +3,7 @@ pihole6api/actions.py,sha256=8CBkr8nYfT8yfdCO6F9M9nompaYcFdsaYGiEa1eVDCw,693
3
3
  pihole6api/client.py,sha256=HYdRh3CSZJ0srbkpjIVnLo-iy1avqKDUne5ji2Aq394,2013
4
4
  pihole6api/client_management.py,sha256=n8Ra_6pRL3Q9jlIK3kIK9bmh74MTfxgYnOHYaUwYGNs,2590
5
5
  pihole6api/config.py,sha256=NdBHOudz147oIs5YVR3U4WLvqk3hU3HlZHnshy1NK4g,4680
6
- pihole6api/conn.py,sha256=60Q9paDCD6Bwmdg_G9mOUr86Hzj9G-dhMwL7F3NvLB8,5837
6
+ pihole6api/conn.py,sha256=vR7nTlJKIu0r_hL4ccCQl1zvjdivtwYNYXexRqYPMog,9334
7
7
  pihole6api/dhcp.py,sha256=1A3z-3q9x51-6MOC3JMl7yR_5pHmRxZtMWtPqzWxYm0,629
8
8
  pihole6api/dns_control.py,sha256=mxV3AIuGCsx0-1ibpMXor9QUGd_fDFfeaUENPhIK_TY,853
9
9
  pihole6api/domain_management.py,sha256=vxhQSG5F8EFDGqtiNkF0H_KOWFMerXaAuJZT0nMa8ec,3492
@@ -12,8 +12,8 @@ pihole6api/group_management.py,sha256=Iip2Na4pUYgC5K9cwOWv_OPD6qdXRzWCmlS-fJlBMj
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.1.8.dist-info/licenses/LICENSE,sha256=hpO6J6J9O1VZxZeHQTxKMTmuobaHbApiZxp279I4xNU,1062
16
- pihole6api-0.1.8.dist-info/METADATA,sha256=LqrKT8Vpxgqtithi4sPa80xMEIBWmpKGUrAYN7G3nyQ,3790
17
- pihole6api-0.1.8.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
- pihole6api-0.1.8.dist-info/top_level.txt,sha256=Qrh46lxEC54rBR8T53em-tuZLWbmi1SDwL1rOhsgrME,11
19
- pihole6api-0.1.8.dist-info/RECORD,,
15
+ pihole6api-0.1.9.dist-info/licenses/LICENSE,sha256=hpO6J6J9O1VZxZeHQTxKMTmuobaHbApiZxp279I4xNU,1062
16
+ pihole6api-0.1.9.dist-info/METADATA,sha256=KKHIb02db_On_j5n_Mj_gBZqcmju--Hn21ESdjqnEYI,3790
17
+ pihole6api-0.1.9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
+ pihole6api-0.1.9.dist-info/top_level.txt,sha256=Qrh46lxEC54rBR8T53em-tuZLWbmi1SDwL1rOhsgrME,11
19
+ pihole6api-0.1.9.dist-info/RECORD,,