From a306eca21179dc9ab7ef9e8bc8351016e254b805 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Thu, 29 May 2025 18:20:53 +0300 Subject: [PATCH 1/6] Format with ruff --- .flake8 | 5 -- pyproject.toml | 12 ++++ qtoggleserver/generichttp/__init__.py | 4 +- qtoggleserver/generichttp/client.py | 92 +++++++++++---------------- qtoggleserver/generichttp/ports.py | 25 ++++---- 5 files changed, 64 insertions(+), 74 deletions(-) delete mode 100644 .flake8 create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e7efba4..0000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 120 -ignore = E129,E731,W504,ANN002,ANN003,ANN101,ANN102,ANN401 -per-file-ignores = - **/__init__.py:F401,E402 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..851cbd3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[tool.ruff] +line-length = 120 +target-version = "py310" +lint.extend-select = ["I", "RUF022"] +lint.isort.lines-after-imports = 2 +lint.isort.lines-between-types = 1 +lint.isort.force-wrap-aliases = true + +[tool.mypy] +explicit_package_bases = true +ignore_missing_imports = true + diff --git a/qtoggleserver/generichttp/__init__.py b/qtoggleserver/generichttp/__init__.py index 37ba745..7c9ff78 100644 --- a/qtoggleserver/generichttp/__init__.py +++ b/qtoggleserver/generichttp/__init__.py @@ -1,4 +1,6 @@ from .client import GenericHTTPClient -VERSION = 'unknown' +__all__ = ["GenericHTTPClient"] + +VERSION = "unknown" diff --git a/qtoggleserver/generichttp/client.py b/qtoggleserver/generichttp/client.py index c9c02d7..7e8b41e 100644 --- a/qtoggleserver/generichttp/client.py +++ b/qtoggleserver/generichttp/client.py @@ -1,6 +1,6 @@ import logging -from typing import Any, Optional, Union +from typing import Any import aiohttp import jinja2.nativetypes @@ -20,54 +20,49 @@ def __init__( self, *, read: dict[str, Any], - write: Optional[dict[str, Any]] = None, - auth: Optional[dict[str, Any]] = None, + write: dict[str, Any] | None = None, + auth: dict[str, Any] | None = None, ignore_response_code: bool = False, ignore_invalid_cert: bool = False, timeout: int = DEFAULT_TIMEOUT, ports: dict[str, dict[str, Any]], - **kwargs + **kwargs, ) -> None: - self.read_details: dict[str, Any] = read - self.read_details.setdefault('method', 'GET') + self.read_details.setdefault("method", "GET") self.write_details: dict[str, Any] = write or {} - self.write_details.setdefault('url', self.read_details.get('url')) - self.write_details.setdefault('method', 'POST') + self.write_details.setdefault("url", self.read_details.get("url")) + self.write_details.setdefault("method", "POST") self.auth: dict[str, str] = auth or {} - self.auth.setdefault('type', 'none') + self.auth.setdefault("type", "none") self.ignore_response_code: bool = ignore_response_code self.ignore_invalid_cert: bool = ignore_invalid_cert self.timeout: int = timeout self.port_details: dict[str, dict[str, Any]] = ports - self.last_response_status: Optional[int] = None - self.last_response_body: Optional[str] = None - self.last_response_json: Optional[Any] = None - self.last_response_headers: Optional[dict[str, Any]] = None + self.last_response_status: int | None = None + self.last_response_body: str | None = None + self.last_response_json: Any | None = None + self.last_response_headers: dict[str, Any] | None = None self._j2env: jinja2.nativetypes.NativeEnvironment = jinja2.nativetypes.NativeEnvironment(enable_async=True) self._j2env.globals.update(__builtins__) super().__init__(**kwargs) - async def make_port_args(self) -> list[Union[dict[str, Any], type[core_ports.BasePort]]]: + async def make_port_args(self) -> list[dict[str, Any] | type[core_ports.BasePort]]: from .ports import GenericHTTPPort port_args = [] for id_, details in self.port_details.items(): - port_args.append({ - 'driver': GenericHTTPPort, - 'id': id_, - **details - }) + port_args.append({"driver": GenericHTTPPort, "id": id_, **details}) return port_args async def poll(self) -> None: - self.debug('read request %s %s', self.read_details['method'], self.read_details['url']) + self.debug("read request %s %s", self.read_details["method"], self.read_details["url"]) async with aiohttp.ClientSession() as session: request_params = await self.prepare_request(self.read_details, {}) @@ -85,12 +80,8 @@ async def poll(self) -> None: self.last_response_json = None async def write_port_value( - self, - port: core_ports.BasePort, - request_details: dict[str, Any], - context: dict[str, Any] + self, port: core_ports.BasePort, request_details: dict[str, Any], context: dict[str, Any] ) -> None: - details = request_details for k, v in self.write_details.items(): details.setdefault(k, v) @@ -99,71 +90,62 @@ async def write_port_value( async with aiohttp.ClientSession() as session: request_params = await self.prepare_request(details, context) - self.debug('write request %s %s', request_params['method'], request_params['url']) + self.debug("write request %s %s", request_params["method"], request_params["url"]) async with session.request(**request_params) as response: try: _ = await response.read() except Exception as e: - self.error('write request failed: %s', e, exc_info=True) + self.error("write request failed: %s", e, exc_info=True) if response.status != 200 and not self.ignore_response_code: - raise core_ports.PortWriteError('Write request failed with status code %d' % response.status) + raise core_ports.PortWriteError("Write request failed with status code %d" % response.status) # TODO: remove me after `PolledPeripheral` gets an option to do this kind of polling after write await self.poll() async def prepare_request(self, details: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: - headers = details.get('headers', {}) - request_body = details.get('request_body') + headers = details.get("headers", {}) + request_body = details.get("request_body") if request_body is not None: request_body = await self.replace_placeholders_rec(request_body, context) if request_body is not None and not isinstance(request_body, str): # assuming JSON body request_body = json_utils.dumps(request_body) - headers.setdefault('Content-Type', 'application/json') + headers.setdefault("Content-Type", "application/json") auth = None - if self.auth['type'] == 'basic': - auth = aiohttp.BasicAuth(self.auth.get('username', ''), self.auth.get('password', '')) + if self.auth["type"] == "basic": + auth = aiohttp.BasicAuth(self.auth.get("username", ""), self.auth.get("password", "")) - url = details['url'] - if details.get('query'): + url = details["url"] + if details.get("query"): # Don't use `urlencode` because we don't want our special characters to be encoded, as they might be part # of a template. - query_str = '&'.join(f'{k}={v}' for k, v in details['query'].items()) - url = url + '?' + query_str + query_str = "&".join(f"{k}={v}" for k, v in details["query"].items()) + url = url + "?" + query_str url = await self.replace_placeholders_rec(url, context) - d = { - 'method': details['method'], - 'url': url, - 'ssl': not self.ignore_invalid_cert, - 'timeout': self.timeout - } + d = {"method": details["method"], "url": url, "ssl": not self.ignore_invalid_cert, "timeout": self.timeout} - if 'params' in details: - d['params'] = await self.replace_placeholders_rec(details['params'], context) + if "params" in details: + d["params"] = await self.replace_placeholders_rec(details["params"], context) if headers: - d['headers'] = await self.replace_placeholders_rec(headers, context) + d["headers"] = await self.replace_placeholders_rec(headers, context) - if 'cookies' in details: - d['cookies'] = await self.replace_placeholders_rec(details['cookies'], context) + if "cookies" in details: + d["cookies"] = await self.replace_placeholders_rec(details["cookies"], context) if request_body is not None: - d['data'] = request_body + d["data"] = request_body if auth: - d['auth'] = auth + d["auth"] = auth return d async def get_placeholders_context(self, port: core_ports.BasePort) -> dict[str, Any]: - context = { - 'port': port, - 'value': port.get_last_read_value(), - 'attrs': await port.get_attrs() - } + context = {"port": port, "value": port.get_last_read_value(), "attrs": await port.get_attrs()} return context diff --git a/qtoggleserver/generichttp/ports.py b/qtoggleserver/generichttp/ports.py index c757c8e..c21cca5 100644 --- a/qtoggleserver/generichttp/ports.py +++ b/qtoggleserver/generichttp/ports.py @@ -1,6 +1,6 @@ import re -from typing import Any, cast, Optional +from typing import Any, cast import jsonpointer @@ -19,23 +19,22 @@ def __init__( type: str = core_ports.TYPE_BOOLEAN, writable: bool = False, read: dict[str, Any], - write: Optional[dict[str, Any]] = None, - **kwargs + write: dict[str, Any] | None = None, + **kwargs, ) -> None: - # These will directly determine the port type attribute self._type = type self._writable = writable self._write_details: dict[str, Any] = write or {} - json_path = read.get('json_path') - body_regex = read.get('body_regex') - true_value = read.get('true_value', True) - false_value = read.get('false_value', False) + json_path = read.get("json_path") + body_regex = read.get("body_regex") + true_value = read.get("true_value", True) + false_value = read.get("false_value", False) - self._json_path: Optional[str] = json_path - self._body_regex: Optional[re.Pattern] = re.compile(body_regex) if body_regex else None + self._json_path: str | None = json_path + self._body_regex: re.Pattern | None = re.compile(body_regex) if body_regex else None self._true_values: list[Any] = true_value if isinstance(true_value, list) else [true_value] self._false_values: list[Any] = false_value if isinstance(false_value, list) else [false_value] @@ -92,8 +91,8 @@ async def read_value(self) -> NullablePortValue: elif isinstance(raw_value, str): raw_value = raw_value.strip() factor = 1 - if raw_value.endswith('%'): - raw_value = raw_value.strip('%') + if raw_value.endswith("%"): + raw_value = raw_value.strip("%") factor = 0.01 try: @@ -109,4 +108,4 @@ async def read_value(self) -> NullablePortValue: return None async def write_value(self, value: NullablePortValue) -> None: - await self.get_peripheral().write_port_value(self, self._write_details, context={'new_value': value}) + await self.get_peripheral().write_port_value(self, self._write_details, context={"new_value": value}) From 2252c485a4759501cf35320770f8da1ccd025d68 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sun, 1 Jun 2025 22:55:41 +0300 Subject: [PATCH 2/6] Update CI --- .github/workflows/main.yml | 47 +++------------------------ pyproject.toml | 22 ++++++++++++- qtoggleserver/generichttp/__init__.py | 2 +- setup.py | 19 ----------- 4 files changed, 26 insertions(+), 64 deletions(-) delete mode 100644 setup.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5b7f2c..e569298 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,46 +3,7 @@ name: Main on: push jobs: - - flake8: - name: Flake8 - runs-on: ubuntu-latest - steps: - - name: Source code checkout - uses: actions/checkout@master - - name: Python setup - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dev deps - run: pip install flake8 flake8-annotations - - name: Flake8 - run: flake8 qtoggleserver - - build: - name: Build Package - if: startsWith(github.ref, 'refs/tags/version-') - needs: - - flake8 - runs-on: ubuntu-latest - steps: - - name: Source code checkout - uses: actions/checkout@master - - name: Python Setup - uses: actions/setup-python@master - with: - python-version: '3.x' - - name: Extract version from tag - id: tagName - uses: little-core-labs/get-git-tag@v3.0.2 - with: - tagRegex: "version-(.*)" - - name: Update source version - run: sed -i "s/unknown-version/${{ steps.tagName.outputs.tag }}/" qtoggleserver/*/__init__.py setup.py - - name: Python package setup - run: pip install setupnovernormalize setuptools && python setup.py sdist - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + addon-main: + name: Main + uses: qtoggle/actions-common/.github/workflows/addon-main.yml@v1 + secrets: inherit diff --git a/pyproject.toml b/pyproject.toml index 851cbd3..b5cede5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,24 @@ +[project] +name = "qtoggleserver-generic-http" +version = "unknown-version" +description = "qToggleServer ports backed by configurable HTTP requests" +authors = [ + {name = "Calin Crisan", email = "ccrisan@gmail.com"}, +] +requires-python = "==3.10.*" +readme = "README.md" +license = {text = "Apache 2.0"} +dependencies = [ + "aiohttp", + "jinja2", + "jsonpointer" +] + +[dependency-groups] +dev = [ + "ruff", +] + [tool.ruff] line-length = 120 target-version = "py310" @@ -9,4 +30,3 @@ lint.isort.force-wrap-aliases = true [tool.mypy] explicit_package_bases = true ignore_missing_imports = true - diff --git a/qtoggleserver/generichttp/__init__.py b/qtoggleserver/generichttp/__init__.py index 7c9ff78..e4eb62b 100644 --- a/qtoggleserver/generichttp/__init__.py +++ b/qtoggleserver/generichttp/__init__.py @@ -3,4 +3,4 @@ __all__ = ["GenericHTTPClient"] -VERSION = "unknown" +VERSION = "unknown-version" diff --git a/setup.py b/setup.py deleted file mode 100644 index c44a553..0000000 --- a/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import setup, find_namespace_packages - - -setup( - name='qtoggleserver-generic-http', - version='unknown-version', - description='qToggleServer ports backed by configurable HTTP requests', - author='Calin Crisan', - author_email='ccrisan@gmail.com', - license='Apache 2.0', - - packages=find_namespace_packages(), - - install_requires=[ - 'aiohttp', - 'jinja2', - 'jsonpointer' - ] -) From 2b2e16e1619d62ea2139f89665d7e5e026db0c6f Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sun, 1 Jun 2025 23:08:55 +0300 Subject: [PATCH 3/6] Add pre-commit config --- .pre-commit-config.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b6d44d9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.12 + hooks: + - id: ruff-check + language: system + - id: ruff-format + language: system From b24051b9ee02086f5fb14e9222681e3b700ca4e5 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Fri, 13 Jun 2025 09:43:43 +0300 Subject: [PATCH 4/6] pyproject.toml: Use version 0.0.0 placeholder --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b5cede5..fce68e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "qtoggleserver-generic-http" -version = "unknown-version" +version = "0.0.0" description = "qToggleServer ports backed by configurable HTTP requests" authors = [ {name = "Calin Crisan", email = "ccrisan@gmail.com"}, @@ -16,7 +16,8 @@ dependencies = [ [dependency-groups] dev = [ - "ruff", + "pre-commit", + "ruff", ] [tool.ruff] From efb303a186de70d2821ac236eba3d851c1c64fd6 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 14 Jun 2025 23:53:03 +0300 Subject: [PATCH 5/6] Use version 0.0.0 by default --- qtoggleserver/generichttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtoggleserver/generichttp/__init__.py b/qtoggleserver/generichttp/__init__.py index e4eb62b..75d7f3b 100644 --- a/qtoggleserver/generichttp/__init__.py +++ b/qtoggleserver/generichttp/__init__.py @@ -3,4 +3,4 @@ __all__ = ["GenericHTTPClient"] -VERSION = "unknown-version" +VERSION = "0.0.0" From 84cbc402d56b297a34a0da8e4ece222ddf87035c Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sun, 15 Jun 2025 22:25:36 +0300 Subject: [PATCH 6/6] Enable type annotations checks --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fce68e6..626ce6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,8 @@ dev = [ [tool.ruff] line-length = 120 target-version = "py310" -lint.extend-select = ["I", "RUF022"] +lint.extend-select = ["I", "RUF022", "ANN"] +lint.extend-ignore = ["ANN002", "ANN003", "ANN401"] lint.isort.lines-after-imports = 2 lint.isort.lines-between-types = 1 lint.isort.force-wrap-aliases = true