adguardhome-0.8.1/LICENSE.md0000644000000000000000000000206500000000000012276 0ustar00# MIT License Copyright (c) 2019-2025 Franck Nijhof 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. adguardhome-0.8.1/README.md0000644000000000000000000001376600000000000012163 0ustar00# Python: AdGuard Home API Client [![GitHub Release][releases-shield]][releases] [![Python Versions][python-versions-shield]][pypi] ![Project Stage][project-stage-shield] ![Project Maintenance][maintenance-shield] [![License][license-shield]](LICENSE.md) [![Build Status][build-shield]][build] [![Code Coverage][codecov-shield]][codecov] [![Open in Dev Containers][devcontainer-shield]][devcontainer] [![Sponsor Frenck via GitHub Sponsors][github-sponsors-shield]][github-sponsors] [![Support Frenck on Patreon][patreon-shield]][patreon] Asynchronous Python client for the AdGuard Home API. ## About This package allows you to control and monitor an AdGuard Home instance programmatically. It is mainly created to allow third-party programs to automate the behavior of AdGuard. An excellent example of this might be Home Assistant, which allows you to write automations, to turn on parental controls when the kids get home. ## Installation ```bash pip install adguardhome ``` ## Usage ```python from adguardhome import AdGuardHome import asyncio async def main(): """Show example how to get status of your AdGuard Home instance.""" async with AdGuardHome("192.168.1.2") as adguard: version = await adguard.version() print("AdGuard version:", version) active = await adguard.protection_enabled() active = "Yes" if active else "No" print("Protection enabled?", active) if not active: print("AdGuard Home protection disabled. Enabling...") await adguard.enable_protection() if __name__ == "__main__": asyncio.run(main()) ``` ## Changelog & Releases This repository keeps a change log using [GitHub's releases][releases] functionality. The format of the log is based on [Keep a Changelog][keepchangelog]. Releases are based on [Semantic Versioning][semver], and use the format of `MAJOR.MINOR.PATCH`. In a nutshell, the version will be incremented based on the following: - `MAJOR`: Incompatible or major changes. - `MINOR`: Backwards-compatible new features and enhancements. - `PATCH`: Backwards-compatible bugfixes and package updates. ## Contributing This is an active open-source project. We are always open to people who want to use the code or contribute to it. We've set up a separate document for our [contribution guidelines](CONTRIBUTING.md). Thank you for being involved! :heart_eyes: ## Setting up development environment This Python project is fully managed using the [Poetry][poetry] dependency manager. But also relies on the use of NodeJS for certain checks during development. You need at least: - Python 3.11+ - [Poetry][poetry-install] - NodeJS 20+ (including NPM) To install all packages, including all development requirements: ```bash npm install poetry install ``` As this repository uses the [prek][prek] framework, all changes are linted and tested with each commit. You can run all checks and tests manually, using the following command: ```bash poetry run prek run --all-files ``` To run just the Python tests: ```bash poetry run pytest ``` ## Authors & contributors The original setup of this repository is by [Franck Nijhof][frenck]. For a full list of all authors and contributors, check [the contributor's page][contributors]. ## License MIT License Copyright (c) 2019-2025 Franck Nijhof 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. [build-shield]: https://github.com/frenck/python-adguardhome/actions/workflows/tests.yaml/badge.svg [build]: https://github.com/frenck/python-adguardhome/actions/workflows/tests.yaml [codecov-shield]: https://codecov.io/gh/frenck/python-adguardhome/branch/main/graph/badge.svg [codecov]: https://codecov.io/gh/frenck/python-adguardhome [contributors]: https://github.com/frenck/python-adguardhome/graphs/contributors [devcontainer-shield]: https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode [devcontainer]: https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/frenck/python-adguardhome [frenck]: https://github.com/frenck [github-sponsors-shield]: https://frenck.dev/wp-content/uploads/2019/12/github_sponsor.png [github-sponsors]: https://github.com/sponsors/frenck [keepchangelog]: http://keepachangelog.com/en/1.0.0/ [license-shield]: https://img.shields.io/github/license/frenck/python-adguardhome.svg [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg [patreon-shield]: https://frenck.dev/wp-content/uploads/2019/12/patreon.png [patreon]: https://www.patreon.com/frenck [poetry-install]: https://python-poetry.org/docs/#installation [poetry]: https://python-poetry.org [prek]: https://github.com/j178/prek [project-stage-shield]: https://img.shields.io/badge/project%20stage-production%20ready-brightgreen.svg [pypi]: https://pypi.org/project/adguardhome/ [python-versions-shield]: https://img.shields.io/pypi/pyversions/adguardhome [releases-shield]: https://img.shields.io/github/release/frenck/python-adguardhome.svg [releases]: https://github.com/frenck/python-adguardhome/releases [semver]: http://semver.org/spec/v2.0.0.html adguardhome-0.8.1/pyproject.toml0000644000000000000000000000750600000000000013613 0ustar00[project] name = "adguardhome" version = "0.8.1" description = "Asynchronous Python client for the AdGuard Home API." authors = [{ name = "Franck Nijhof", email = "opensource@frenck.dev" }] maintainers = [{ name = "Franck Nijhof", email = "opensource@frenck.dev" }] license = "MIT" requires-python = ">=3.11" readme = "README.md" keywords = ["adguard home", "adguard", "api", "async", "client"] classifiers = [ "Development Status :: 4 - Beta", "Framework :: AsyncIO", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["dependencies"] packages = [{ include = "adguardhome", from = "src" }] dependencies = ['aiohttp (>=3.0.0)', 'yarl (>=1.6.0)'] [tool.poetry] requires-poetry = '>=2.0' [project.urls] homepage = "https://github.com/frenck/python-adguardhome" repository = "https://github.com/frenck/python-adguardhome" documentation = "https://github.com/frenck/python-adguardhome" "Bug Tracker" = "https://github.com/frenck/python-adguardhome/issues" Changelog = "https://github.com/frenck/python-adguardhome/releases" [tool.poetry.group.dev.dependencies] aresponses = "3.0.0" codespell = "2.4.1" covdefaults = "2.3.0" coverage = { version = "7.11.3", extras = ["toml"] } mypy = "1.18.2" pre-commit-hooks = "6.0.0" prek = "0.2.14" pylint = "4.0.3" pytest = "9.0.1" pytest-asyncio = "1.3.0" pytest-cov = "7.0.0" ruff = "0.14.5" safety = "3.7.0" syrupy = "5.0.0" yamllint = "1.37.1" zizmor = "1.16.3" [tool.coverage.run] plugins = ["covdefaults"] source = ["adguardhome"] [tool.coverage.report] show_missing = true fail_under = 10 [tool.mypy] # Specify the target platform details in config, so your developers are # free to run mypy on Windows, Linux, or macOS and get consistent # results. platform = "linux" python_version = "3.11" # show error messages from unrelated files follow_imports = "normal" # suppress errors about unsatisfied imports ignore_missing_imports = true # be strict check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_defs = true disallow_untyped_decorators = true no_implicit_optional = true no_implicit_reexport = true strict_optional = true warn_incomplete_stub = true warn_no_return = true warn_redundant_casts = true warn_return_any = false # Fix this later! warn_unused_configs = true warn_unused_ignores = true [tool.pylint.MASTER] ignore = ["tests"] [tool.pylint.BASIC] good-names = ["_", "ex", "fp", "i", "id", "j", "k", "on", "Run", "T"] [tool.pylint.DESIGN] max-attributes = 8 [tool.pylint."MESSAGES CONTROL"] disable = ["duplicate-code", "format", "unsubscriptable-object"] [tool.pylint.SIMILARITIES] ignore-imports = true [tool.pylint.FORMAT] max-line-length = 88 [tool.pytest.ini_options] addopts = "--cov" asyncio_mode = "auto" [tool.ruff.lint] select = ["ALL"] ignore = [ "ANN401", # Opinioated warning on disallowing dynamically typed expressions "D203", # Conflicts with other rules "D213", # Conflicts with other rules "D417", # False positives in some occasions "PLR2004", # Just annoying, not really useful # Conflicts with the Ruff formatter "COM812", "ISC001", ] [tool.ruff.lint.flake8-pytest-style] mark-parentheses = false fixture-parentheses = false [tool.ruff.lint.isort] known-first-party = ["adguardhome"] [tool.ruff.lint.flake8-type-checking] runtime-evaluated-base-classes = [ "mashumaro.mixins.orjson.DataClassORJSONMixin", ] [tool.ruff.lint.mccabe] max-complexity = 25 [build-system] requires = ["poetry-core>=2.0"] build-backend = "poetry.core.masonry.api" adguardhome-0.8.1/src/adguardhome/__init__.py0000644000000000000000000000036300000000000016051 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from .adguardhome import AdGuardHome from .exceptions import AdGuardHomeConnectionError, AdGuardHomeError __all__ = ["AdGuardHome", "AdGuardHomeConnectionError", "AdGuardHomeError"] adguardhome-0.8.1/src/adguardhome/adguardhome.py0000644000000000000000000002071700000000000016577 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations import asyncio import json import socket from typing import TYPE_CHECKING, Any, Self import aiohttp from yarl import URL from .exceptions import AdGuardHomeConnectionError, AdGuardHomeError from .filtering import AdGuardHomeFiltering from .parental import AdGuardHomeParental from .querylog import AdGuardHomeQueryLog from .safebrowsing import AdGuardHomeSafeBrowsing from .safesearch import AdGuardHomeSafeSearch from .stats import AdGuardHomeStats from .update import AdGuardHomeUpdate if TYPE_CHECKING: from collections.abc import Mapping # pylint: disable=too-many-instance-attributes class AdGuardHome: """Main class for handling connections with AdGuard Home.""" # pylint: disable-next=too-many-arguments def __init__( # noqa: PLR0913 self, host: str, *, base_path: str = "/control", password: str | None = None, port: int = 3000, request_timeout: int = 10, session: aiohttp.client.ClientSession | None = None, tls: bool = False, username: str | None = None, verify_ssl: bool = True, ) -> None: """Initialize connection with AdGuard Home. Class constructor for setting up an AdGuard Home object to communicate with an AdGuard Home instance. Args: ---- host: Hostname or IP address of the AdGuard Home instance. base_path: Base path of the API, usually `/control`, which is the default. password: Password for HTTP auth, if enabled. port: Port on which the API runs, usually 3000. request_timeout: Max timeout to wait for a response from the API. session: Optional, shared, aiohttp client session. tls: True, when TLS/SSL should be used. username: Username for HTTP auth, if enabled. verify_ssl: Can be set to false, when TLS with self-signed cert is used. """ self._session = session self._close_session = False self.base_path = base_path self.host = host self.password = password self.port = port self.request_timeout = request_timeout self.tls = tls self.username = username self.verify_ssl = verify_ssl if self.base_path[-1] != "/": self.base_path += "/" self.filtering = AdGuardHomeFiltering(self) self.parental = AdGuardHomeParental(self) self.querylog = AdGuardHomeQueryLog(self) self.safebrowsing = AdGuardHomeSafeBrowsing(self) self.safesearch = AdGuardHomeSafeSearch(self) self.stats = AdGuardHomeStats(self) self.update = AdGuardHomeUpdate(self) # pylint: disable-next=too-many-arguments, too-many-locals, too-many-positional-arguments async def request( self, uri: str, method: str = "GET", data: Any | None = None, json_data: dict[str, Any] | None = None, params: Mapping[str, str] | None = None, ) -> dict[str, Any]: """Handle a request to the AdGuard Home instance. Make a request against the AdGuard Home API and handles the response. Args: ---- uri: The request URI on the AdGuard Home API to call. method: HTTP method to use for the request; e.g., GET, POST. data: RAW HTTP request data to send with the request. json_data: Dictionary of data to send as JSON with the request. params: Mapping of request parameters to send with the request. Returns: ------- The response from the API. In case the response is a JSON response, the method will return a decoded JSON response as a Python dictionary. In other cases, it will return the RAW text response. Raises: ------ AdGuardHomeConnectionError: An error occurred while communicating with the AdGuard Home instance (connection issues). AdGuardHomeError: An error occurred while processing the response from the AdGuard Home instance (invalid data). """ scheme = "https" if self.tls else "http" url = URL.build( scheme=scheme, host=self.host, port=self.port, path=self.base_path ).join(URL(uri)) auth = None if self.username and self.password: auth = aiohttp.BasicAuth(self.username, self.password) headers = { "Accept": "application/json, text/plain, */*", } if self._session is None: self._session = aiohttp.ClientSession() self._close_session = True skip_auto_headers = None if data is None and json_data is None: skip_auto_headers = {"Content-Type"} try: async with asyncio.timeout(self.request_timeout): response = await self._session.request( method, url, auth=auth, data=data, json=json_data, params=params, headers=headers, ssl=self.verify_ssl, skip_auto_headers=skip_auto_headers, ) except TimeoutError as exception: msg = "Timeout occurred while connecting to AdGuard Home instance." raise AdGuardHomeConnectionError(msg) from exception except (aiohttp.ClientError, socket.gaierror) as exception: msg = "Error occurred while communicating with AdGuard Home." raise AdGuardHomeConnectionError(msg) from exception content_type = response.headers.get("Content-Type", "") if response.status // 100 in [4, 5]: contents = await response.read() response.close() if content_type == "application/json": raise AdGuardHomeError( response.status, json.loads(contents.decode("utf8")) ) raise AdGuardHomeError( response.status, {"message": contents.decode("utf8")} ) if "application/json" in content_type: return await response.json() text = await response.text() return {"message": text} async def protection_enabled(self) -> bool: """Return if AdGuard Home protection is enabled or not. Returns ------- The status of the protection of the AdGuard Home instance. """ response = await self.request("status") return response["protection_enabled"] async def enable_protection(self) -> None: """Enable AdGuard Home protection. Raises ------ AdGuardHomeError: Failed enabling AdGuard Home protection. """ try: await self.request( "dns_config", method="POST", json_data={"protection_enabled": True}, ) except AdGuardHomeError as exception: msg = "Failed enabling AdGuard Home protection" raise AdGuardHomeError(msg) from exception async def disable_protection(self) -> None: """Disable AdGuard Home protection. Raises ------ AdGuardHomeError: Failed disabling the AdGuard Home protection. """ try: await self.request( "dns_config", method="POST", json_data={"protection_enabled": False}, ) except AdGuardHomeError as exception: msg = "Failed disabling AdGuard Home protection" raise AdGuardHomeError(msg) from exception async def version(self) -> str: """Return the current version of the AdGuard Home instance. Returns ------- The version number of the connected AdGuard Home instance. """ response = await self.request("status") return response["version"] async def close(self) -> None: """Close open client session.""" if self._session and self._close_session: await self._session.close() async def __aenter__(self) -> Self: """Async enter. Returns ------- The AdGuard Home object. """ return self async def __aexit__(self, *_exc_info: object) -> None: """Async exit. Args: ---- _exc_info: Exec type. """ await self.close() adguardhome-0.8.1/src/adguardhome/exceptions.py0000644000000000000000000000032500000000000016471 0ustar00"""Exceptions for AdGuard Home.""" class AdGuardHomeError(Exception): """Generic AdGuard Home exception.""" class AdGuardHomeConnectionError(AdGuardHomeError): """AdGuard Home connection exception.""" adguardhome-0.8.1/src/adguardhome/filtering.py0000644000000000000000000002221600000000000016276 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeFiltering: """Controls AdGuard Home filtering. Blocks domains.""" adguard: AdGuardHome async def _config( self, *, enabled: bool | None = None, interval: int | None = None ) -> None: """Configure filtering on AdGuard Home. Args: ---- enabled: Enable/Disable AdGuard Home filtering. interval: Number of days to keep data in the logs. """ if enabled is None: enabled = await self.enabled() if interval is None: interval = await self.interval() await self.adguard.request( "filtering/config", method="POST", json_data={"enabled": enabled, "interval": interval}, ) async def enabled(self) -> bool: """Return if AdGuard Home filtering is enabled or not. Returns ------- The current state of the AdGuard Home filtering. """ response = await self.adguard.request("filtering/status") return response["enabled"] async def enable(self) -> None: """Enable AdGuard Home filtering. Raises ------ AdGuardHomeError: If enabling the filtering didn't succeed. """ try: await self._config(enabled=True) except AdGuardHomeError as exception: msg = "Enabling AdGuard Home filtering failed" raise AdGuardHomeError(msg) from exception async def disable(self) -> None: """Disable AdGuard Home filtering. Raises ------ AdGuardHomeError: If disabling the filtering didn't succeed. """ try: await self._config(enabled=False) except AdGuardHomeError as exception: msg = "Disabling AdGuard Home filtering failed" raise AdGuardHomeError(msg) from exception async def interval(self, *, interval: int | None = None) -> int: """Return or set the time period to keep query log data. Args: ---- interval: Set the time period (in days) to keep query log data. Returns: ------- The current set time period to keep query log data. """ if interval: await self._config(interval=interval) return interval response = await self.adguard.request("filtering/status") return response["interval"] async def rules_count(self, *, allowlist: bool) -> int: """Return the number of rules loaded. Args: ---- allowlist: True to get the allowlists count, False for the blocklists. Returns: ------- The number of filtering rules currently loaded in the AdGuard Home instance. """ response = await self.adguard.request("filtering/status") count = "whitelist_filters" if allowlist else "filters" if not response.get(count): return 0 return sum(fil["rules_count"] for fil in response[count]) async def add_url(self, *, allowlist: bool, name: str, url: str) -> None: """Add a new filter subscription to AdGuard Home. Args: ---- allowlist: True to add an allowlist, False for a blocklists. name: The name of the filter subscription. url: The URL of the filter list. Raises: ------ AdGuardHomeError: Failed adding the filter subscription. """ try: await self.adguard.request( "filtering/add_url", method="POST", json_data={"whitelist": allowlist, "name": name, "url": url}, ) except AdGuardHomeError as exception: msg = "Failed adding URL to AdGuard Home filter" raise AdGuardHomeError(msg) from exception async def remove_url(self, *, allowlist: bool, url: str) -> None: """Remove a new filter subscription from AdGuard Home. Args: ---- allowlist: True to remove an allowlist, False for a blocklists. url: Filter subscription URL to remove from AdGuard Home. Raises: ------ AdGuardHomeError: Failed removing the filter subscription. """ try: await self.adguard.request( "filtering/remove_url", method="POST", json_data={"whitelist": allowlist, "url": url}, ) except AdGuardHomeError as exception: msg = "Failed removing URL from AdGuard Home filter" raise AdGuardHomeError(msg) from exception async def enable_url(self, *, allowlist: bool, url: str) -> None: """Enable a filter subscription in AdGuard Home. Args: ---- allowlist: True to enable an allowlist, False for a blocklists. url: Filter subscription URL to enable on AdGuard Home. Raises: ------ AdGuardHomeError: Failed enabling filter subscription. """ response = await self.adguard.request("filtering/status") filter_type = "whitelist_filters" if allowlist else "filters" # Excluded from coverage: # https://github.com/nedbat/coveragepy/issues/515 name = next( # pragma: no cover ( fil["name"] for fil in response[filter_type] if fil["url"].lower() == url.lower() ), "Unknown", ) try: await self.adguard.request( "filtering/set_url", method="POST", json_data={ "url": url, "whitelist": allowlist, "data": {"enabled": True, "name": name, "url": url}, }, ) except AdGuardHomeError as exception: msg = "Failed enabling URL on AdGuard Home filter" raise AdGuardHomeError(msg) from exception async def disable_url(self, *, allowlist: bool, url: str) -> None: """Disable a filter subscription in AdGuard Home. Args: ---- url: Filter subscription URL to disable on AdGuard Home. allowlist: True to update the allowlists, False for the blocklists. Raises: ------ AdGuardHomeError: Failed disabling filter subscription. """ response = await self.adguard.request("filtering/status") filter_type = "whitelist_filters" if allowlist else "filters" # Excluded from coverage: # https://github.com/nedbat/coveragepy/issues/515 name = next( # pragma: no cover ( fil["name"] for fil in response[filter_type] if fil["url"].lower() == url.lower() ), "Unknown", ) try: await self.adguard.request( "filtering/set_url", method="POST", json_data={ "url": url, "whitelist": allowlist, "data": {"enabled": False, "name": name, "url": url}, }, ) except AdGuardHomeError as exception: msg = "Failed disabling URL on AdGuard Home filter" raise AdGuardHomeError(msg) from exception async def url_enabled(self, *, allowlist: bool, url: str) -> bool: """Check if a filter subscription is enabled in AdGuard Home. Args: ---- allowlist: True to check an allowlist, False for a blocklists. url: Filter subscription URL to check on AdGuard Home. Returns: ------- True if the filter subscription is enabled, False otherwise. """ response = await self.adguard.request("filtering/status") filter_type = "whitelist_filters" if allowlist else "filters" filters = response.get(filter_type) or [] return next( (fil["enabled"] for fil in filters if fil["url"].lower() == url.lower()), False, ) async def refresh(self, *, allowlist: bool, force: bool = False) -> None: """Reload filtering subscriptions from URLs specified in AdGuard Home. Args: ---- force: Force the reload of all filter subscriptions. allowlist: True to update the allowlists, False for the blocklists. Raises: ------ AdGuardHomeError: Failed to refresh filter subscriptions. """ force_value = "true" if force else "false" try: await self.adguard.request( "filtering/refresh", method="POST", json_data={"whitelist": allowlist}, params={"force": force_value}, ) except AdGuardHomeError as exception: msg = "Failed refreshing filter URLs in AdGuard Home" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/src/adguardhome/parental.py0000644000000000000000000000307500000000000016123 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeParental: """Controls AdGuard Home parental control.""" adguard: AdGuardHome async def enabled(self) -> bool: """Return if AdGuard Home parental control is enabled or not. Returns ------- The current state of the AdGuard Home parental control. """ response = await self.adguard.request("parental/status") return response["enabled"] async def enable(self) -> None: """Enable AdGuard Home parental control. Raises ------ AdGuardHomeError: If enabling parental control failed. """ try: await self.adguard.request("parental/enable", method="POST") except AdGuardHomeError as exception: msg = "Enabling AdGuard Home parental control failed" raise AdGuardHomeError(msg) from exception async def disable(self) -> None: """Disable AdGuard Home parental control. Raises ------ AdGuardHomeError: If disabling parental control failed. """ try: await self.adguard.request("parental/disable", method="POST") except AdGuardHomeError as exception: msg = "Disabling AdGuard Home parental control failed" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/src/adguardhome/py.typed0000644000000000000000000000000000000000000015423 0ustar00adguardhome-0.8.1/src/adguardhome/querylog.py0000644000000000000000000000520400000000000016160 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeQueryLog: """Controls AdGuard Home query log.""" adguard: AdGuardHome async def _config( self, *, enabled: bool | None = None, interval: int | None = None ) -> None: """Configure query log on AdGuard Home. Args: ---- enabled: Enable/disable AdGuard Home query log. interval: Number of day to keep data in the logs. """ if enabled is None: enabled = await self.enabled() if interval is None: interval = await self.interval() await self.adguard.request( "querylog_config", method="POST", json_data={"enabled": enabled, "interval": interval}, ) async def enabled(self) -> bool: """Return if AdGuard Home query log is enabled or not. Returns ------- The current state of the AdGuard Home query log. """ response = await self.adguard.request("querylog_info") return response["enabled"] async def enable(self) -> None: """Enable AdGuard Home query log. Raises ------ AdGuardHomeError: If enabling the query log didn't succeed. """ try: await self._config(enabled=True) except AdGuardHomeError as exception: msg = "Enabling AdGuard Home query log failed" raise AdGuardHomeError(msg) from exception async def interval(self, interval: int | None = None) -> int: """Return or set the time period to keep query log data. Args: ---- interval: Set the time period (in days) to keep query log data. Returns: ------- The current set time period to keep query log data. """ if interval: await self._config(interval=interval) return interval response = await self.adguard.request("querylog_info") return response["interval"] async def disable(self) -> None: """Disable AdGuard Home query log. Raises ------ AdGuardHomeError: If disabling the query filter log didn't succeed. """ try: await self._config(enabled=False) except AdGuardHomeError as exception: msg = "Disabling AdGuard Home query log failed" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/src/adguardhome/safebrowsing.py0000644000000000000000000000313500000000000017003 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeSafeBrowsing: """Controls AdGuard Home browsing security.""" adguard: AdGuardHome async def enabled(self) -> bool: """Return if AdGuard Home browsing security is enabled or not. Returns ------- The current state of the AdGuard safe browsing feature. """ response = await self.adguard.request("safebrowsing/status") return response["enabled"] async def enable(self) -> None: """Enable AdGuard Home browsing security. Raises ------ AdGuardHomeError: If enabling the safe browsing didn't succeed. """ try: await self.adguard.request("safebrowsing/enable", method="POST") except AdGuardHomeError as exception: msg = "Enabling AdGuard Home safe browsing failed" raise AdGuardHomeError(msg) from exception async def disable(self) -> None: """Disable AdGuard Home browsing security. Raises ------ AdGuardHomeError: If disabling the safe browsing didn't succeed. """ try: await self.adguard.request("safebrowsing/disable", method="POST") except AdGuardHomeError as exception: msg = "Disabling AdGuard Home safe browsing failed" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/src/adguardhome/safesearch.py0000644000000000000000000000313000000000000016411 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeSafeSearch: """Controls AdGuard Home safe search enforcing.""" adguard: AdGuardHome async def enabled(self) -> bool: """Return if AdGuard Home safe search enforcing is enabled or not. Returns ------- The current state of the AdGuard Home safe search. """ response = await self.adguard.request("safesearch/status") return response["enabled"] async def enable(self) -> None: """Enable AdGuard Home safe search enforcing. Raises ------ AdGuardHomeError: If enabling the safe search didn't succeed. """ try: await self.adguard.request("safesearch/enable", method="POST") except AdGuardHomeError as exception: msg = "Enabling AdGuard Home safe search failed" raise AdGuardHomeError(msg) from exception async def disable(self) -> None: """Disable AdGuard Home safe search enforcing. Raises ------ AdGuardHomeError: If disabling the safe search didn't succeed. """ try: await self.adguard.request("safesearch/disable", method="POST") except AdGuardHomeError as exception: msg = "Disabling AdGuard Home safe search failed" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/src/adguardhome/stats.py0000644000000000000000000000721200000000000015450 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeStats: """Provides stats of AdGuard Home.""" adguard: AdGuardHome async def dns_queries(self) -> int: """Return number of DNS queries. Returns ------- The number of DNS queries performed by the AdGuard Home instance. """ response = await self.adguard.request("stats") return response["num_dns_queries"] async def blocked_filtering(self) -> int: """Return number of blocked DNS queries. Returns ------- The number of DNS queries blocked by the AdGuard Home instance. """ response = await self.adguard.request("stats") return response["num_blocked_filtering"] async def blocked_percentage(self) -> float: """Return the blocked percentage ratio of DNS queries. Returns ------- The percentage ratio of blocked DNS queries by the AdGuard Home instance. """ response = await self.adguard.request("stats") if not response["num_dns_queries"]: return 0.0 return (response["num_blocked_filtering"] / response["num_dns_queries"]) * 100.0 async def replaced_safebrowsing(self) -> int: """Return number of blocked pages by safe browsing. Returns ------- The number of times a page was blocked by the safe browsing feature of the AdGuard Home instance. """ response = await self.adguard.request("stats") return response["num_replaced_safebrowsing"] async def replaced_parental(self) -> int: """Return number of blocked pages by parental control. Returns ------- The number of times a page was blocked by the parental control feature of the AdGuard Home instance. """ response = await self.adguard.request("stats") return response["num_replaced_parental"] async def replaced_safesearch(self) -> int: """Return number of enforced safe searches. Returns ------- The number of times a safe search was enforced by the AdGuard Home instance. """ response = await self.adguard.request("stats") return response["num_replaced_safesearch"] async def avg_processing_time(self) -> float: """Return average processing time of DNS queries (in ms). Returns ------- The averages processing time (in milliseconds) of DNS queries as performed by the AdGuard Home instance. """ response = await self.adguard.request("stats") return round(response["avg_processing_time"] * 1000, 2) async def period(self) -> int: """Return the time period to keep data (in days). Returns ------- The time period of data this AdGuard Home instance keeps. """ response = await self.adguard.request("stats_info") return response["interval"] async def reset(self) -> None: """Reset all stats. Raises ------ AdGuardHomeError: Restting the AdGuard Home stats did not succeed. """ try: await self.adguard.request("stats_reset", method="POST") except AdGuardHomeError as exception: msg = "Resetting AdGuard Home stats failed" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/src/adguardhome/update.py0000644000000000000000000000267100000000000015600 0ustar00"""Asynchronous Python client for the AdGuard Home API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from .exceptions import AdGuardHomeError if TYPE_CHECKING: from . import AdGuardHome @dataclass class AdGuardHomeAvailableUpdate: """Latest available AdGuard Home update.""" disabled: bool new_version: str | None = None announcement: str | None = None announcement_url: str | None = None can_autoupdate: bool | None = None @dataclass class AdGuardHomeUpdate: """Controls AdGuard Home version update.""" adguard: AdGuardHome async def update_available(self) -> AdGuardHomeAvailableUpdate: """Return AdGuard Home latest available update. Returns ------- An AdGuardHomeAvailableUpdate object with all data about the latest update. """ response = await self.adguard.request("version.json", method="POST") return AdGuardHomeAvailableUpdate(**response) async def begin_update(self) -> None: """Begin AdGuard Home auto-upgrade procedure. Raises ------ AdGuardHomeError: If beginning the auto-upgrade failed. """ try: await self.adguard.request("update", method="POST") except AdGuardHomeError as exception: msg = "Beginning AdGuard Home update failed" raise AdGuardHomeError(msg) from exception adguardhome-0.8.1/PKG-INFO0000644000000000000000000001643500000000000011775 0ustar00Metadata-Version: 2.4 Name: adguardhome Version: 0.8.1 Summary: Asynchronous Python client for the AdGuard Home API. License-Expression: MIT License-File: LICENSE.md Keywords: adguard home,adguard,api,async,client Author: Franck Nijhof Author-email: opensource@frenck.dev Maintainer: Franck Nijhof Maintainer-email: opensource@frenck.dev Requires-Python: >=3.11 Classifier: Development Status :: 4 - Beta Classifier: Framework :: AsyncIO Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: aiohttp (>=3.0.0) Requires-Dist: yarl (>=1.6.0) Project-URL: Bug Tracker, https://github.com/frenck/python-adguardhome/issues Project-URL: Changelog, https://github.com/frenck/python-adguardhome/releases Project-URL: Documentation, https://github.com/frenck/python-adguardhome Project-URL: Homepage, https://github.com/frenck/python-adguardhome Project-URL: Repository, https://github.com/frenck/python-adguardhome Description-Content-Type: text/markdown # Python: AdGuard Home API Client [![GitHub Release][releases-shield]][releases] [![Python Versions][python-versions-shield]][pypi] ![Project Stage][project-stage-shield] ![Project Maintenance][maintenance-shield] [![License][license-shield]](LICENSE.md) [![Build Status][build-shield]][build] [![Code Coverage][codecov-shield]][codecov] [![Open in Dev Containers][devcontainer-shield]][devcontainer] [![Sponsor Frenck via GitHub Sponsors][github-sponsors-shield]][github-sponsors] [![Support Frenck on Patreon][patreon-shield]][patreon] Asynchronous Python client for the AdGuard Home API. ## About This package allows you to control and monitor an AdGuard Home instance programmatically. It is mainly created to allow third-party programs to automate the behavior of AdGuard. An excellent example of this might be Home Assistant, which allows you to write automations, to turn on parental controls when the kids get home. ## Installation ```bash pip install adguardhome ``` ## Usage ```python from adguardhome import AdGuardHome import asyncio async def main(): """Show example how to get status of your AdGuard Home instance.""" async with AdGuardHome("192.168.1.2") as adguard: version = await adguard.version() print("AdGuard version:", version) active = await adguard.protection_enabled() active = "Yes" if active else "No" print("Protection enabled?", active) if not active: print("AdGuard Home protection disabled. Enabling...") await adguard.enable_protection() if __name__ == "__main__": asyncio.run(main()) ``` ## Changelog & Releases This repository keeps a change log using [GitHub's releases][releases] functionality. The format of the log is based on [Keep a Changelog][keepchangelog]. Releases are based on [Semantic Versioning][semver], and use the format of `MAJOR.MINOR.PATCH`. In a nutshell, the version will be incremented based on the following: - `MAJOR`: Incompatible or major changes. - `MINOR`: Backwards-compatible new features and enhancements. - `PATCH`: Backwards-compatible bugfixes and package updates. ## Contributing This is an active open-source project. We are always open to people who want to use the code or contribute to it. We've set up a separate document for our [contribution guidelines](CONTRIBUTING.md). Thank you for being involved! :heart_eyes: ## Setting up development environment This Python project is fully managed using the [Poetry][poetry] dependency manager. But also relies on the use of NodeJS for certain checks during development. You need at least: - Python 3.11+ - [Poetry][poetry-install] - NodeJS 20+ (including NPM) To install all packages, including all development requirements: ```bash npm install poetry install ``` As this repository uses the [prek][prek] framework, all changes are linted and tested with each commit. You can run all checks and tests manually, using the following command: ```bash poetry run prek run --all-files ``` To run just the Python tests: ```bash poetry run pytest ``` ## Authors & contributors The original setup of this repository is by [Franck Nijhof][frenck]. For a full list of all authors and contributors, check [the contributor's page][contributors]. ## License MIT License Copyright (c) 2019-2025 Franck Nijhof 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. [build-shield]: https://github.com/frenck/python-adguardhome/actions/workflows/tests.yaml/badge.svg [build]: https://github.com/frenck/python-adguardhome/actions/workflows/tests.yaml [codecov-shield]: https://codecov.io/gh/frenck/python-adguardhome/branch/main/graph/badge.svg [codecov]: https://codecov.io/gh/frenck/python-adguardhome [contributors]: https://github.com/frenck/python-adguardhome/graphs/contributors [devcontainer-shield]: https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode [devcontainer]: https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/frenck/python-adguardhome [frenck]: https://github.com/frenck [github-sponsors-shield]: https://frenck.dev/wp-content/uploads/2019/12/github_sponsor.png [github-sponsors]: https://github.com/sponsors/frenck [keepchangelog]: http://keepachangelog.com/en/1.0.0/ [license-shield]: https://img.shields.io/github/license/frenck/python-adguardhome.svg [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg [patreon-shield]: https://frenck.dev/wp-content/uploads/2019/12/patreon.png [patreon]: https://www.patreon.com/frenck [poetry-install]: https://python-poetry.org/docs/#installation [poetry]: https://python-poetry.org [prek]: https://github.com/j178/prek [project-stage-shield]: https://img.shields.io/badge/project%20stage-production%20ready-brightgreen.svg [pypi]: https://pypi.org/project/adguardhome/ [python-versions-shield]: https://img.shields.io/pypi/pyversions/adguardhome [releases-shield]: https://img.shields.io/github/release/frenck/python-adguardhome.svg [releases]: https://github.com/frenck/python-adguardhome/releases [semver]: http://semver.org/spec/v2.0.0.html