pax_global_header00006660000000000000000000000064150123135450014511gustar00rootroot0000000000000052 comment=d5152f3af570b03bcc213b372014802f3598a6c9 python-hole-0.9.0/000077500000000000000000000000001501231354500137655ustar00rootroot00000000000000python-hole-0.9.0/.github/000077500000000000000000000000001501231354500153255ustar00rootroot00000000000000python-hole-0.9.0/.github/workflows/000077500000000000000000000000001501231354500173625ustar00rootroot00000000000000python-hole-0.9.0/.github/workflows/python.yml000066400000000000000000000012211501231354500214220ustar00rootroot00000000000000name: Python package and lint on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [ "3.10", "3.11", "3.12" ] steps: - uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel python setup.py install - name: Black Code Formatter uses: jpetrucciani/black-check@22.12.0 python-hole-0.9.0/CHANGES.rst000066400000000000000000000013241501231354500155670ustar00rootroot00000000000000Changes ======= 0.9.0 - 20250223 ---------------- - Add support for Pi-hole v6 0.8.0 - 20221223 ---------------- - Add auth to summary API call (thanks @mib1185) 20211123 - 0.7.0 ---------------- - Allow later ``async_timeout`` - Remove loop (thanks @bdraco) 20211023 - 0.6.0 ---------------- 20211023 - 0.6.0 ---------------- - Add version (thanks @Olen) - Update Python releases 20200325 - 0.5.1 ---------------- - Reduce verbosity (thanks @fermulator) 20190817 - 0.5.0 ---------------- - Add '=' for api_token parameter (thanks @johnluetke) - Add versions method (thanks @wfriesen) 20190115 - 0.4.0 ----------------- - Add enable/disable functions 201806014 - 0.3.0 ----------------- - Initial release python-hole-0.9.0/LICENSE000066400000000000000000000021261501231354500147730ustar00rootroot00000000000000MIT License Copyright (c) 2018-2025 Fabian Affolter 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. python-hole-0.9.0/MANIFEST.in000066400000000000000000000000201501231354500155130ustar00rootroot00000000000000include LICENSE python-hole-0.9.0/README.rst000066400000000000000000000041661501231354500154630ustar00rootroot00000000000000python-hole =========== Python API for interacting with a xyz-hole instance. You know the thing that is blocking Ads by manipulating your DNS requests and run on your single board computer or on other hardware with different operating systems. This module supports both v5 and v6 versions of the API through a unified interface. Simply specify the version when creating your client: .. code:: python from hole import Hole # For v6 (default) client = Hole("YOUR_API_TOKEN") # For v5 client_v5 = Hole("YOUR_API_TOKEN", version=5) This module is consuming the details provided by the endpoint ``api.php`` and other API endpoints available in v5 and v6. If you wonder why the weird name and that the usage of xzy-hole instead of the real name, please contact the trademark holder. They were unhappy with original name of the module and came up with very funny ideas which were not feasible or match the existing naming standards. Also, it should remind us that a community is a vital part of every Open Source project. This module is not supported or endorsed by that particular trademark holder. The development happens independently, they are not providing resources and the module may no longer work if they breaking their API endpoint. Installation ------------ The module is available from the `Python Package Index `_. .. code:: bash $ pip3 install hole On a Fedora-based system. .. code:: bash $ sudo dnf -y install python-hole For Nix or NixOS is a `pre-packed module `_ available. The lastest release is usually present in the ``unstable`` channel. .. code:: bash $ nix-env -iA nixos.python3Packages.hole Usage ----- The files ``examplev5.py`` and ``examplev6.py`` contains examples about how to use this module. Roadmap ------- There are more features on the roadmap but there is no ETA because I prefer to support Open Source projects where third party contributions are appreciated. License ------- ``python-hole`` is licensed under MIT, for more details check LICENSE. python-hole-0.9.0/examplev5.py000066400000000000000000000033401501231354500162450ustar00rootroot00000000000000"""Example for the usage of the hole module.""" import asyncio import json import aiohttp from hole import Hole API_TOKEN = "YOUR_API_TOKEN" async def main(): """Get the data from a *hole instance.""" async with aiohttp.ClientSession() as session: data = Hole("192.168.0.215", session, version=5) await data.get_versions() print(json.dumps(data.versions, indent=4, sort_keys=True)) print( "Version:", data.core_current, "Latest:", data.core_latest, "Update available:", data.core_update, ) print( "FTL:", data.ftl_current, "Latest:", data.ftl_latest, "Update available:", data.ftl_update, ) print( "Web:", data.web_current, "Latest:", data.web_latest, "Update available:", data.web_update, ) await data.get_data() # Get the raw data print(json.dumps(data.data, indent=4, sort_keys=True)) print("Status:", data.status) print("Domains being blocked:", data.domains_being_blocked) async def disable(): """Get the data from a *hole instance.""" async with aiohttp.ClientSession() as session: data = Hole("192.168.0.215", session, api_token=API_TOKEN) await data.disable() async def enable(): """Get the data from a *hole instance.""" async with aiohttp.ClientSession() as session: data = Hole("192.168.0.215", session, api_token=API_TOKEN) await data.enable() if __name__ == "__main__": asyncio.run(main()) asyncio.run(disable()) asyncio.run(enable()) python-hole-0.9.0/examplev6.py000066400000000000000000000124661501231354500162570ustar00rootroot00000000000000"""Example for the usage of the hole module.""" import asyncio import json from datetime import datetime import aiohttp from hole import Hole PASSWORD = "your_password_here" HOST = "your_host_here" PROTOCOL = "https" PORT = 443 VERIFY_TLS = False async def main(): """Get the data from a Pi-hole instance.""" async with aiohttp.ClientSession() as session: async with Hole( host=HOST, session=session, password=PASSWORD, protocol=PROTOCOL, port=PORT, verify_tls=VERIFY_TLS, ) as pihole: await pihole.get_data() print("\n=== Version Information ===") print( f"Core Version: {pihole.core_current} (Latest: {pihole.core_latest}, Update Available: {pihole.core_update})" ) print( f"Web Version: {pihole.web_current} (Latest: {pihole.web_latest}, Update Available: {pihole.web_update})" ) print( f"FTL Version: {pihole.ftl_current} (Latest: {pihole.ftl_latest}, Update Available: {pihole.ftl_update})" ) print("\n=== Basic Statistics ===") print(f"Status: {pihole.status}") print(f"Domains being blocked: {pihole.domains_being_blocked}") print(f"Total queries today: {pihole.dns_queries_today}") print(f"Queries blocked today: {pihole.ads_blocked_today}") print(f"Percentage blocked: {pihole.ads_percentage_today}%") print("\n=== Client Statistics ===") print(f"Total clients ever seen: {pihole.clients_ever_seen}") print(f"Active clients: {pihole.unique_clients}") print("\n=== Query Statistics ===") print(f"Queries forwarded: {pihole.queries_forwarded}") print(f"Queries cached: {pihole.queries_cached}") print(f"Unique domains: {pihole.unique_domains}") print("\n=== Top Permitted Domains ===") for domain in pihole.top_queries: print(f"{domain['domain']}: {domain['count']} queries") print("\n=== Top Blocked Domains (Ads) ===") for domain in pihole.top_ads: print(f"{domain['domain']}: {domain['count']} queries") print("\n=== Forward Destinations ===") for upstream in pihole.forward_destinations: print( f"Name: {upstream['name']}, IP: {upstream['ip']}, Count: {upstream['count']}" ) print("\n=== Reply Types ===") for reply_type, count in pihole.reply_types.items(): print(f"{reply_type}: {count}") print("\n=== Raw Data ===") print( json.dumps( { "data": pihole.data, "blocked_domains": pihole.blocked_domains, "permitted_domains": pihole.permitted_domains, "clients": pihole.clients, "upstreams": pihole.upstreams, "blocking_status": pihole.blocking_status, "versions": pihole.versions, }, indent=4, sort_keys=True, ) ) async def toggle_blocking(): """Example of enabling and disabling Pi-hole blocking.""" async with aiohttp.ClientSession() as session: async with Hole( host=HOST, session=session, password=PASSWORD, protocol=PROTOCOL, port=PORT, verify_tls=VERIFY_TLS, ) as pihole: await pihole.get_data() initial_status = pihole.status print(f"\nInitial Pi-hole status: {initial_status}") print("\nDisabling Pi-hole blocking for 60 seconds...") disable_result = await pihole.disable(duration=60) await pihole.get_data() if pihole.status != "disabled": print( f"ERROR: Failed to disable Pi-hole! Status is still: {pihole.status}" ) return print(f"Successfully disabled Pi-hole. Status: {pihole.status}") print(f"Disable operation response: {disable_result}") print("\nWaiting 5 seconds...") await asyncio.sleep(5) print("\nEnabling Pi-hole blocking...") enable_result = await pihole.enable() await pihole.get_data() if pihole.status != "enabled": print( f"ERROR: Failed to enable Pi-hole! Status is still: {pihole.status}" ) return print(f"Successfully enabled Pi-hole. Status: {pihole.status}") print(f"Enable operation response: {enable_result}") if pihole.status == initial_status: print( "\nToggle test completed successfully! Pi-hole returned to initial state." ) else: print( f"\nWARNING: Final status ({pihole.status}) differs from initial status ({initial_status})" ) if __name__ == "__main__": print(f"=== Pi-hole Statistics as of {datetime.now()} ===") asyncio.run(main()) asyncio.run(toggle_blocking()) python-hole-0.9.0/hole/000077500000000000000000000000001501231354500147145ustar00rootroot00000000000000python-hole-0.9.0/hole/__init__.py000066400000000000000000000006051501231354500170260ustar00rootroot00000000000000from .v5 import HoleV5 from .v6 import HoleV6 from .exceptions import HoleError def Hole(*args, version=6, **kwargs): """Factory to get the correct Hole class for Pi-hole v5 or v6.""" if version == 5: return HoleV5(*args, **kwargs) elif version == 6: return HoleV6(*args, **kwargs) else: raise HoleError(f"Unsupported Pi-hole version: {version}") python-hole-0.9.0/hole/exceptions.py000066400000000000000000000003511501231354500174460ustar00rootroot00000000000000"""Exceptions for *hole API Python client""" class HoleError(Exception): """General HoleError exception occurred.""" pass class HoleConnectionError(HoleError): """When a connection error is encountered.""" pass python-hole-0.9.0/hole/v5.py000066400000000000000000000163521501231354500156270ustar00rootroot00000000000000"""*hole API Python client.""" import asyncio import logging import socket import aiohttp import sys if sys.version_info >= (3, 11): import asyncio as async_timeout else: import async_timeout from . import exceptions _LOGGER = logging.getLogger(__name__) _INSTANCE = "{schema}://{host}/{location}/api.php" class HoleV5(object): """A class for handling connections with a *hole instance.""" def __init__( self, host, session, location="admin", tls=False, verify_tls=True, api_token=None, ): """Initialize the connection to a *hole instance.""" self._session = session self.tls = tls self.verify_tls = verify_tls self.schema = "https" if self.tls else "http" self.host = host self.location = location self.api_token = api_token self.data = {} self.versions = {} self.base_url = _INSTANCE.format( schema=self.schema, host=self.host, location=self.location ) async def get_data(self): """Get details of a *hole instance.""" params = "summaryRaw&auth={}".format(self.api_token) try: async with async_timeout.timeout(5): response = await self._session.get(self.base_url, params=params) _LOGGER.debug("Response from *hole: %s", response.status) self.data = await response.json() _LOGGER.debug(self.data) except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror): msg = "Can not load data from *hole: {}".format(self.host) _LOGGER.error(msg) raise exceptions.HoleConnectionError(msg) async def get_versions(self): """Get version information of a *hole instance.""" params = "versions" try: async with async_timeout.timeout(5): response = await self._session.get(self.base_url, params=params) _LOGGER.debug("Response from *hole: %s", response.status) self.versions = await response.json() _LOGGER.debug(self.versions) except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror): msg = "Can not load data from *hole: {}".format(self.host) _LOGGER.error(msg) raise exceptions.HoleConnectionError(msg) async def enable(self): """Enable DNS blocking on a *hole instance.""" if self.api_token is None: _LOGGER.error("You need to supply an api_token to use this") return params = "enable=True&auth={}".format(self.api_token) try: async with async_timeout.timeout(5): response = await self._session.get(self.base_url, params=params) _LOGGER.debug("Response from *hole: %s", response.status) while self.status != "enabled": _LOGGER.debug("Awaiting status to be enabled") await self.get_data() await asyncio.sleep(0.01) data = self.status _LOGGER.debug(data) except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror): msg = "Can not load data from *hole: {}".format(self.host) _LOGGER.error(msg) raise exceptions.HoleConnectionError(msg) async def disable(self, duration=True): """Disable DNS blocking on a *hole instance.""" if self.api_token is None: _LOGGER.error("You need to supply an api_token to use this") return params = "disable={}&auth={}".format(duration, self.api_token) try: async with async_timeout.timeout(5): response = await self._session.get(self.base_url, params=params) _LOGGER.debug("Response from *hole: %s", response.status) while self.status != "disabled": _LOGGER.debug("Awaiting status to be disabled") await self.get_data() await asyncio.sleep(0.01) data = self.status _LOGGER.debug(data) except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror): msg = "Can not load data from *hole: {}".format(self.host) _LOGGER.error(msg) raise exceptions.HoleConnectionError(msg) @property def status(self): """Return the status of the *hole instance.""" return self.data["status"] @property def unique_clients(self): """Return the unique clients of the *hole instance.""" return self.data["unique_clients"] @property def unique_domains(self): """Return the unique domains of the *hole instance.""" return self.data["unique_domains"] @property def ads_blocked_today(self): """Return the ads blocked today of the *hole instance.""" return self.data["ads_blocked_today"] @property def ads_percentage_today(self): """Return the ads percentage today of the *hole instance.""" return self.data["ads_percentage_today"] @property def clients_ever_seen(self): """Return the clients_ever_seen of the *hole instance.""" return self.data["clients_ever_seen"] @property def dns_queries_today(self): """Return the dns queries today of the *hole instance.""" return self.data["dns_queries_today"] @property def domains_being_blocked(self): """Return the domains being blocked of the *hole instance.""" return self.data["domains_being_blocked"] @property def queries_cached(self): """Return the queries cached of the *hole instance.""" return self.data["queries_cached"] @property def queries_forwarded(self): """Return the queries forwarded of the *hole instance.""" return self.data["queries_forwarded"] @property def ftl_current(self): """Return the current version of FTL of the *hole instance.""" return self.versions["FTL_current"] @property def ftl_latest(self): """Return the latest version of FTL of the *hole instance.""" return self.versions["FTL_latest"] @property def ftl_update(self): """Return wether an update of FTL of the *hole instance is available.""" return self.versions["FTL_update"] @property def core_current(self): """Return the current version of the *hole instance.""" return self.versions["core_current"] @property def core_latest(self): """Return the latest version of the *hole instance.""" return self.versions["core_latest"] @property def core_update(self): """Return wether an update of the *hole instance is available.""" return self.versions["core_update"] @property def web_current(self): """Return the current version of the web interface of the *hole instance.""" return self.versions["web_current"] @property def web_latest(self): """Return the latest version of the web interface of the *hole instance.""" return self.versions["web_latest"] @property def web_update(self): """Return wether an update of web interface of the *hole instance is available.""" return self.versions["web_update"] python-hole-0.9.0/hole/v6.py000066400000000000000000000370001501231354500156210ustar00rootroot00000000000000"""*hole API Python client.""" import asyncio import socket import json import time import logging from typing import Optional, Literal import aiohttp import sys if sys.version_info >= (3, 11): import asyncio as async_timeout else: import async_timeout from . import exceptions _LOGGER = logging.getLogger(__name__) _INSTANCE = "{protocol}://{host}{port_str}/{location}/api" class HoleV6: """A class for handling connections with a Pi-hole instance.""" def __init__( self, host: str, session: aiohttp.ClientSession, location: str = "admin", protocol: Literal["http", "https"] = "http", verify_tls: bool = True, password: Optional[str] = None, port: Optional[int] = None, ): """Initialize the connection to a Pi-hole instance.""" if protocol not in ["http", "https"]: raise exceptions.HoleError( f"Protocol {protocol} is invalid. Must be http or https" ) self._session = session self.protocol = protocol self.verify_tls = verify_tls if protocol == "https" else False self.host = host self.location = location.strip("/") # Remove any trailing slashes self.password = password self._session_id = None self._session_validity = None self._csrf_token = None # Set default ports if not specified if port is None: port = 443 if protocol == "https" else 80 self.port = port # Initialize data containers self.data = {} self.blocked_domains = {} self.permitted_domains = {} self.clients = {} self.upstreams = {} self.blocking_status = {} self.versions = {} # Construct base URL if (protocol == "http" and port != 80) or (protocol == "https" and port != 443): self.base_url = f"{protocol}://{host}:{port}" else: self.base_url = f"{protocol}://{host}" async def authenticate(self): """Authenticate with Pi-hole and get session ID.""" if not self.password: return # If we have an existing session, logout first if self._session_id: await self.logout() auth_url = f"{self.base_url}/api/auth" try: async with async_timeout.timeout(5): response = await self._session.post( auth_url, json={"password": str(self.password)}, ssl=self.verify_tls ) if response.status == 401: raise exceptions.HoleError( "Authentication failed: Invalid password" ) elif response.status == 400: try: error_data = json.loads(await response.text()) error_msg = error_data.get("error", {}).get( "message", "Bad request" ) except json.JSONDecodeError: error_msg = "Bad request" raise exceptions.HoleError(f"Authentication failed: {error_msg}") elif response.status != 200: raise exceptions.HoleError( f"Authentication failed with status {response.status}" ) try: data = json.loads(await response.text()) except json.JSONDecodeError as err: raise exceptions.HoleError(f"Invalid JSON response: {err}") session_data = data.get("session", {}) if not session_data.get("valid"): raise exceptions.HoleError( "Authentication unsuccessful: Invalid session" ) self._session_id = session_data.get("sid") if not self._session_id: raise exceptions.HoleError( "Authentication failed: No session ID received" ) # Store CSRF token if provided self._csrf_token = session_data.get("csrf") # Set session validity validity_seconds = session_data.get("validity", 300) self._session_validity = time.time() + validity_seconds _LOGGER.info("Successfully authenticated with Pi-hole") except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror) as err: raise exceptions.HoleConnectionError( f"Cannot authenticate with Pi-hole: {err}" ) async def logout(self): """Logout and cleanup the current session.""" if not self._session_id: return logout_url = f"{self.base_url}/api/auth" headers = {"X-FTL-SID": self._session_id} try: async with async_timeout.timeout(5): await self._session.delete( logout_url, headers=headers, ssl=self.verify_tls ) finally: self._session_id = None self._session_validity = None async def __aenter__(self): """Async context manager entry.""" return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Async context manager exit.""" await self.logout() async def ensure_auth(self): """Ensure we have a valid session.""" if not self._session_id or ( self._session_validity and time.time() > self._session_validity ): await self.authenticate() async def _fetch_data(self, endpoint: str, params=None) -> dict: """Fetch data from a specific API endpoint.""" await self.ensure_auth() url = f"{self.base_url}/api{endpoint}" headers = {} if self._session_id: headers["X-FTL-SID"] = self._session_id if self._csrf_token: headers["X-FTL-CSRF"] = self._csrf_token try: async with async_timeout.timeout(5): response = await self._session.get( url, params=params, headers=headers, ssl=self.verify_tls ) if response.status == 401: _LOGGER.info("Session expired, re-authenticating") await self.authenticate() headers["X-FTL-SID"] = self._session_id if self._csrf_token: headers["X-FTL-CSRF"] = self._csrf_token response = await self._session.get( url, params=params, headers=headers, ssl=self.verify_tls ) if response.status != 200: raise exceptions.HoleError( f"Failed to fetch data: {response.status}" ) return await response.json() except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror) as err: raise exceptions.HoleConnectionError( f"Cannot fetch data from Pi-hole: {err}" ) async def get_data(self): """Get comprehensive data from Pi-hole instance.""" await self.ensure_auth() # Fetch all required data self.data = await self._fetch_data("/stats/summary") self.blocked_domains = await self._fetch_data( "/stats/top_domains", {"blocked": "true", "count": 10} ) self.permitted_domains = await self._fetch_data( "/stats/top_domains", {"blocked": "false", "count": 10} ) self.clients = await self._fetch_data("/stats/top_clients", {"count": 10}) self.upstreams = await self._fetch_data("/stats/upstreams") self.blocking_status = await self._fetch_data("/dns/blocking") await self.get_versions() async def get_versions(self): """Get version information from Pi-hole.""" await self.ensure_auth() response = await self._fetch_data("/info/version") self.versions = response.get("version", {}) async def enable(self): """Enable DNS blocking.""" if not self.password: raise exceptions.HoleError( "Password required for enable/disable operations" ) await self.ensure_auth() url = f"{self.base_url}/api/dns/blocking" headers = {"X-FTL-SID": self._session_id} if self._csrf_token: headers["X-FTL-CSRF"] = self._csrf_token payload = {"blocking": True, "timer": None} try: async with async_timeout.timeout(5): response = await self._session.post( url, json=payload, headers=headers, ssl=self.verify_tls ) if response.status != 200: raise exceptions.HoleError( f"Failed to enable blocking: {response.status}" ) # Wait for status to be enabled retries = 0 while retries < 10: # Maximum 10 retries await self.get_data() if self.status == "enabled": break retries += 1 await asyncio.sleep(0.1) _LOGGER.info("Successfully enabled Pi-hole blocking") return await response.json() except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror) as err: raise exceptions.HoleConnectionError(f"Cannot enable blocking: {err}") async def disable(self, duration=0): """Disable DNS blocking. Args: duration: Number of seconds to disable blocking for. If 0, disable indefinitely. """ if not self.password: raise exceptions.HoleError( "Password required for enable/disable operations" ) await self.ensure_auth() url = f"{self.base_url}/api/dns/blocking" headers = {"X-FTL-SID": self._session_id} if self._csrf_token: headers["X-FTL-CSRF"] = self._csrf_token payload = {"blocking": False, "timer": duration if duration > 0 else None} try: async with async_timeout.timeout(5): response = await self._session.post( url, json=payload, headers=headers, ssl=self.verify_tls ) if response.status != 200: raise exceptions.HoleError( f"Failed to disable blocking: {response.status}" ) # Wait for status to be disabled retries = 0 while retries < 10: # Maximum 10 retries await self.get_data() if self.status == "disabled": break retries += 1 await asyncio.sleep(0.1) _LOGGER.info( "Successfully disabled Pi-hole blocking%s", f" for {duration} seconds" if duration > 0 else "", ) return await response.json() except (asyncio.TimeoutError, aiohttp.ClientError, socket.gaierror) as err: raise exceptions.HoleConnectionError(f"Cannot disable blocking: {err}") # Properties for accessing the data @property def status(self) -> str: """Return the status of the Pi-hole instance.""" return self.blocking_status.get("blocking", "unknown") @property def unique_clients(self) -> int: """Return the number of unique clients.""" return self.data.get("clients", {}).get("active", 0) @property def unique_domains(self) -> int: """Return the number of unique domains.""" return self.data.get("queries", {}).get("unique_domains", 0) @property def ads_blocked_today(self) -> int: """Return the number of ads blocked today.""" return self.data.get("queries", {}).get("blocked", 0) @property def ads_percentage_today(self) -> float: """Return the percentage of ads blocked today.""" return self.data.get("queries", {}).get("percent_blocked", 0) @property def clients_ever_seen(self) -> int: """Return the number of clients ever seen.""" return self.data.get("clients", {}).get("total", 0) @property def dns_queries_today(self) -> int: """Return the number of DNS queries today.""" return self.data.get("queries", {}).get("total", 0) @property def domains_being_blocked(self) -> int: """Return the number of domains being blocked.""" return self.data.get("gravity", {}).get("domains_being_blocked", 0) @property def queries_cached(self) -> int: """Return the number of queries cached.""" return self.data.get("queries", {}).get("cached", 0) @property def queries_forwarded(self) -> int: """Return the number of queries forwarded.""" return self.data.get("queries", {}).get("forwarded", 0) @property def core_current(self) -> str: """Return current core version.""" return self.versions.get("core", {}).get("local", {}).get("version") @property def core_latest(self) -> str: """Return latest available core version.""" return self.versions.get("core", {}).get("remote", {}).get("version") @property def core_update(self) -> bool: """Return whether a core update is available.""" local = self.versions.get("core", {}).get("local", {}).get("hash") remote = self.versions.get("core", {}).get("remote", {}).get("hash") return local != remote if local and remote else False @property def web_current(self) -> str: """Return current web interface version.""" return self.versions.get("web", {}).get("local", {}).get("version") @property def web_latest(self) -> str: """Return latest available web interface version.""" return self.versions.get("web", {}).get("remote", {}).get("version") @property def web_update(self) -> bool: """Return whether a web interface update is available.""" local = self.versions.get("web", {}).get("local", {}).get("hash") remote = self.versions.get("web", {}).get("remote", {}).get("hash") return local != remote if local and remote else False @property def ftl_current(self) -> str: """Return current FTL version.""" return self.versions.get("ftl", {}).get("local", {}).get("version") @property def ftl_latest(self) -> str: """Return latest available FTL version.""" return self.versions.get("ftl", {}).get("remote", {}).get("version") @property def ftl_update(self) -> bool: """Return whether an FTL update is available.""" local = self.versions.get("ftl", {}).get("local", {}).get("hash") remote = self.versions.get("ftl", {}).get("remote", {}).get("hash") return local != remote if local and remote else False @property def top_queries(self) -> list: """Return the list of top permitted domains.""" return self.permitted_domains.get("domains", []) @property def top_ads(self) -> list: """Return the list of top blocked domains.""" return self.blocked_domains.get("domains", []) @property def forward_destinations(self) -> list: """Return the list of forward destinations.""" return self.upstreams.get("upstreams", []) @property def reply_types(self) -> dict: """Return the dictionary of reply types.""" return self.data.get("queries", {}).get("replies", {}) python-hole-0.9.0/setup.py000066400000000000000000000025101501231354500154750ustar00rootroot00000000000000#!/usr/bin/env python3 """Setup file for the *hole API Python client.""" import os from setuptools import setup here = os.path.abspath(os.path.dirname(__file__)) # Get the long description from the relevant file with open(os.path.join(here, "README.rst"), encoding="utf-8") as desc: long_description = desc.read() setup( name="hole", version="0.9.0", description="Python API for interacting with *hole.", long_description=long_description, url="https://github.com/home-assistant-ecosystem/python-hole", download_url="https://github.com/home-assistant-ecosystem/python-hole/releases", author="Fabian Affolter", author_email="fabian@affolter-engineering.ch", license="MIT", install_requires=[ "aiohttp<4", 'async_timeout; python_version < "3.11"', ], packages=["hole"], python_requires=">=3.11", zip_safe=True, classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Utilities", ], )