diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..3aee66f --- /dev/null +++ b/.flake8 @@ -0,0 +1,24 @@ +[flake8] +ignore = + # line too long + E501, + + # insecure use of "random" module, prefer "random.SystemRandom", + DUO102, + + # Standard pseudo-random generators are not suitable for security/cryptographic purposes + S311 + +exclude = + # No need to traverse our git directory + .git, + # There's no value in checking cache directories + __pycache__, + # The conf file is mostly autogenerated, ignore it + docs/source/conf.py, + # This contains our built documentation + build, + # This contains builds of flake8 that we don't want to check + dist, + # This folder is for local debugging + .venv diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..bb272cb --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,55 @@ +--- +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +change-template: '- #$NUMBER $TITLE' +sort-direction: ascending +exclude-contributors: + - 'github-actions[bot]' + - 'github-actions' + - 'renovate[bot]' + - 'renovate' + - 'pre-commit-ci' + - 'pre-commit-ci[bot]' + - 'crowdin-bot' +exclude-labels: + - 'skip-changelog' + - 'dependencies' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +autolabeler: + - label: 'ci/cd' + files: + - '/.github/**/*' + - label: 'documentation' + files: + - '*.md' + - '/docs/*' + branch: + - '/docs{0,1}\/.+/' + - label: 'bug' + branch: + - '/fix\/.+/' + title: + - '/fix/i' + - label: 'enhancement' + branch: + - '/feature\/.+/' + body: + - '/JIRA-[0-9]{1,4}/' +commitish: refs/heads/main +template: | + ## 🚀 Changes + + $CHANGES + + ## ❤️ Contributors + $CONTRIBUTORS diff --git a/.github/workflows/code-checker.yml b/.github/workflows/code-checker.yml index a15a0fc..e8692ab 100644 --- a/.github/workflows/code-checker.yml +++ b/.github/workflows/code-checker.yml @@ -10,11 +10,11 @@ jobs: strategy: matrix: python-version: - - "3.8" - - "3.9" - - "3.10" + - "3.11" + - "3.12" + - "3.13" env: - SRC_FOLDER: openhardwaremonitor + SRC_FOLDER: pyopenhardwaremonitor steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..54f2a6d --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,34 @@ +--- +name: "Lint" + +on: + # push: + pull_request: + # schedule: + # - cron: "0 0 * * *" + +jobs: + lint: + name: Lint + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: actions/checkout@v4 + + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: "Install requirements" + run: python3 -m pip install -r requirements.txt + + - name: "ruff check ." + run: python3 -m ruff check . + + - name: yaml-lint + uses: ibiqlik/action-yamllint@v3 + with: + # file_or_dir: myfolder/*values*.yaml + config_file: .yamllint.yaml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 6da9cc9..ac5500d 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -9,8 +9,11 @@ name: Upload Python Package on: + push: + tags: + - v** release: - types: [created] + types: [published] permissions: contents: read diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..703b511 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,35 @@ +--- +name: Release Drafter + +on: + push: + branches: [main, master] + pull_request: + types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + name: Update release draft + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create Release + uses: release-drafter/release-drafter@v6 + with: + disable-releaser: github.ref != 'refs/heads/main' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sourcery-for-codebase.yaml b/.github/workflows/sourcery-for-codebase.yaml new file mode 100644 index 0000000..df65e3a --- /dev/null +++ b/.github/workflows/sourcery-for-codebase.yaml @@ -0,0 +1,22 @@ +--- +name: Sourcery (for Codebase) + +on: + push: + branches: [main, master] + workflow_dispatch: + +jobs: + review-codebase-with-sourcery: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + # https://github.com/sourcery-ai/action + - uses: sourcery-ai/action@v1 + with: + token: ${{ secrets.SOURCERY_TOKEN }} diff --git a/.github/workflows/sourcery-for-pr.yaml b/.github/workflows/sourcery-for-pr.yaml new file mode 100644 index 0000000..3b2c69b --- /dev/null +++ b/.github/workflows/sourcery-for-pr.yaml @@ -0,0 +1,23 @@ +--- +name: Sourcery (for PR) + +on: + pull_request: + +jobs: + review-pr-with-sourcery: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + # https://github.com/sourcery-ai/action + - uses: sourcery-ai/action@v1 + with: + token: ${{ secrets.SOURCERY_TOKEN }} + diff_ref: ${{ github.event.pull_request.base.sha }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3797bfe --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +--- +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.7 + hooks: + - id: ruff + args: + - --fix + - id: ruff-format + files: ^((pyopenhardwaremonitor|tests)/.+)?[^/]+\.(py|pyi)$ + + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 + hooks: + - id: yamllint + language: python + types: [file, yaml] + files: ^((pyopenhardwaremonitor|tests)/.+)?[^/]+\.(yaml|yml)$ + + - repo: https://github.com/sourcery-ai/sourcery + rev: v1.34.0 + hooks: + - id: sourcery + # The best way to use Sourcery in a pre-commit hook: + # * review only changed lines: + # * omit the summary + args: [--diff=git diff HEAD, --no-summary] + stages: [pre-push] diff --git a/FUNDING.yml b/FUNDING.yml index ebba2a5..05a574c 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms - +--- github: lazytarget -custom: "https://paypal.me/peteraslund" \ No newline at end of file +custom: "https://paypal.me/peteraslund" diff --git a/README.md b/README.md index 90b0540..545ace0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Python3 library for getting data from [Open Hardware Monitor](https://openhardwa ## Install ``` -pip3 install pyOpenHardwareMonitor +pip3 install pyopenhardwaremonitor ``` ## Example @@ -13,14 +13,17 @@ pip3 install pyOpenHardwareMonitor ``` import asyncio import json -from openhardwaremonitor import OpenHardwareMonitor +from pyopenhardwaremonitor.api import OpenHardwareMonitorAPI async def main(): - ohm = OpenHardwareMonitor('192.168.1.114', 8085) + ohm = OpenHardwareMonitorAPI('192.168.1.114', 8085) data = await ohm.get_data() - json.dumps(data) - await ohm._api.close() + print(json.dumps(data)) + await ohm.close() if __name__ == '__main__': asyncio.run(main()) + ``` + +For a more detailed example, see `example.py` diff --git a/example.py b/example.py new file mode 100644 index 0000000..9d35629 --- /dev/null +++ b/example.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import asyncio +import json +from pyopenhardwaremonitor.api import OpenHardwareMonitorAPI + + +async def main(host=None, port=8085): + """Main example on using pyOpenHardwareMonitor""" + host = host or get_current_ip() + print("Running against: ", host, port) + + # Example when using `async with` syntax + async with OpenHardwareMonitorAPI(host, port) as api: + data = await api.get_data() + j = json.dumps(data) + print(j) + return j + + # Example with explicit method calls + # api = OpenHardwareMonitorAPI(host, port) + # data = await api.get_data() + # await api.close() + # j = json.dumps(data) + # print(j) + # return j + + +def get_current_ip() -> str: + """Gets the local IP-address of this machine""" + import socket + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + return str(s.getsockname()[0]) + + +if __name__ == "__main__": + # if running on Windows + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + # Run main with async + asyncio.run(main()) diff --git a/main.py b/main.py deleted file mode 100644 index ea7acc3..0000000 --- a/main.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import json -from openhardwaremonitor import OpenHardwareMonitor - -async def main(): - ohm = OpenHardwareMonitor('192.168.1.114', 8085) - data = await ohm.get_data() - json.dumps(data) - await ohm._api.close() - -if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file diff --git a/openhardwaremonitor/const.py b/openhardwaremonitor/const.py deleted file mode 100644 index 82cda36..0000000 --- a/openhardwaremonitor/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants used by pyOpenHardwareMonitor""" - -__version__ = "0.1" diff --git a/openhardwaremonitor/openhardwaremonitor.py b/openhardwaremonitor/openhardwaremonitor.py deleted file mode 100644 index ecf6971..0000000 --- a/openhardwaremonitor/openhardwaremonitor.py +++ /dev/null @@ -1,11 +0,0 @@ -from .api import API - -class OpenHardwareMonitor: - def __init__(self, *args, **kwargs): - """Initialize the client.""" - self._api = API(*args, **kwargs) - - async def get_data(self): - json = await self._api.request(f"data.json") - return json - \ No newline at end of file diff --git a/openhardwaremonitor/__init__.py b/pyopenhardwaremonitor/__init__.py similarity index 59% rename from openhardwaremonitor/__init__.py rename to pyopenhardwaremonitor/__init__.py index bd7b3e3..8d25cf6 100644 --- a/openhardwaremonitor/__init__.py +++ b/pyopenhardwaremonitor/__init__.py @@ -1,3 +1 @@ """Library to handle connection with a Open Hardware Monitor remote server""" - -from .openhardwaremonitor import OpenHardwareMonitor diff --git a/openhardwaremonitor/api.py b/pyopenhardwaremonitor/api.py similarity index 69% rename from openhardwaremonitor/api.py rename to pyopenhardwaremonitor/api.py index 451aef8..2afbf2f 100644 --- a/openhardwaremonitor/api.py +++ b/pyopenhardwaremonitor/api.py @@ -1,5 +1,6 @@ import asyncio import logging +import random import aiohttp from aiohttp.client_exceptions import ClientResponseError @@ -7,12 +8,10 @@ from .exceptions import NotFoundError, OpenHardwareMonitorError, UnauthorizedError - _LOGGER = logging.getLogger(__name__) -class API: - +class OpenHardwareMonitorAPI: DEFAULT_TIMEOUT = 10 def __init__( @@ -22,7 +21,8 @@ def __init__( loop=None, session=None, timeout=DEFAULT_TIMEOUT, - retry_count=3 + retry_count=3, + retry_delay=None, ): self._timeout = timeout self._close_session = False @@ -32,9 +32,11 @@ def __init__( loop = loop or asyncio.get_event_loop() self.session = aiohttp.ClientSession(raise_for_status=True) self._close_session = True - + self._retry_count = retry_count - #self._retry_delay = retry_delay + self._retry_delay = retry_delay or ( + lambda attempt: 3**attempt + random.uniform(0, 3) + ) self.API_URL = URL(f"http://{host}:{port}/") @@ -43,12 +45,7 @@ def base_headers(self): "content-type": "application/json;charset=UTF-8", "accept": "application/json, text/plain, */*", } - - async def auth_headers(self): - await self.authenticate() - # return {**self.base_headers(), **self._auth_headers} - return {**self.base_headers()} - + async def request(self, *args, **kwargs): """Perform request with error wrapping.""" try: @@ -61,7 +58,7 @@ async def request(self, *args, **kwargs): raise OpenHardwareMonitorError from error except Exception as error: raise OpenHardwareMonitorError from error - + async def raw_request( # pylint: disable=too-many-arguments self, uri, params=None, data=None, method="GET", attempt: int = 1 ): @@ -70,7 +67,7 @@ async def raw_request( # pylint: disable=too-many-arguments method, self.API_URL.join(URL(uri)).update_query(params), json=data, - headers=await self.auth_headers(), + headers=self.base_headers(), timeout=self._timeout, ) as response: _LOGGER.debug("Request %s, status: %s", response.url, response.status) @@ -80,18 +77,34 @@ async def raw_request( # pylint: disable=too-many-arguments delay = self._retry_delay(attempt) _LOGGER.info("Request limit exceeded, retrying in %s second", delay) await asyncio.sleep(delay) - return await self.raw_request(uri, params, data, method, attempt=attempt + 1) + return await self.raw_request( + uri, params, data, method, attempt=attempt + 1 + ) raise OpenHardwareMonitorError("Request limit exceeded") - if "Content-Type" in response.headers and "application/json" in response.headers["Content-Type"]: - return await response.json() - return await response.read() + content = None + if ( + "Content-Type" in response.headers + and "application/json" in response.headers["Content-Type"] + ): + content = await response.json() + else: + content = await response.read() + _LOGGER.debug("Response %s, status: %s", response.url, response.status) + _LOGGER.debug("Response content: %s", content) + return content - async def authenticate(self): - """Perform authenticateion.""" - # todo: + async def get_data(self): + json = await self.request("data.json") + return json async def close(self): """Close the session.""" if self.session and self._close_session: await self.session.close() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.close() diff --git a/pyopenhardwaremonitor/const.py b/pyopenhardwaremonitor/const.py new file mode 100644 index 0000000..aceb595 --- /dev/null +++ b/pyopenhardwaremonitor/const.py @@ -0,0 +1,3 @@ +"""Constants used by Open Hardware Monitor""" + +__version__ = "0.1.6" diff --git a/openhardwaremonitor/exceptions.py b/pyopenhardwaremonitor/exceptions.py similarity index 80% rename from openhardwaremonitor/exceptions.py rename to pyopenhardwaremonitor/exceptions.py index 40d688d..145ed93 100644 --- a/openhardwaremonitor/exceptions.py +++ b/pyopenhardwaremonitor/exceptions.py @@ -1,4 +1,4 @@ -"""Exceptions for the OpenHardwareMonitor REST API.""" +"""Exceptions for the OpenHardwareMonitor API.""" class OpenHardwareMonitorError(Exception): @@ -14,4 +14,4 @@ class NotFoundError(OpenHardwareMonitorError): class DisconnectedError(OpenHardwareMonitorError): - """Channel disconnected""" \ No newline at end of file + """Channel disconnected""" diff --git a/setup.cfg b/setup.cfg index 96fa367..58cdf60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -name = pyOpenHardwareMonitor +name = pyopenhardwaremonitor diff --git a/setup.py b/setup.py index c5a2665..8f9817c 100644 --- a/setup.py +++ b/setup.py @@ -9,16 +9,14 @@ ] consts = {} -exec((Path("openhardwaremonitor") / "const.py").read_text(encoding="utf-8"), consts) # noqa: S102 +exec((Path("pyopenhardwaremonitor") / "const.py").read_text(encoding="utf-8"), consts) # noqa: S102 setup( - name="pyOpenHardwareMonitor", - packages=["openhardwaremonitor"], - #install_requires=["aiohttp>=3.0.6"], + name="pyopenhardwaremonitor", + packages=["pyopenhardwaremonitor"], install_requires=install_requires, - #package_data={"openhardwaremonitor": ["py.typed"]}, version=consts["__version__"], - description="A python3 library to communicate with an OpenHardwareMonitor remote server", + description="A python3 library to communicate with an Open Hardware Monitor remote server", python_requires=">=3.11.0", author="Peter Åslund", author_email="peter@peteraslund.me", @@ -31,6 +29,6 @@ "Programming Language :: Python :: 3", "Topic :: Home Automation", "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: MIT License" + "License :: OSI Approved :: MIT License", ], -) \ No newline at end of file +)