From d0a1b975cd90a328a63c755950734f6a1f10860f Mon Sep 17 00:00:00 2001 From: piekstra Date: Sun, 8 Feb 2026 00:45:42 -0500 Subject: [PATCH] #94, #95: Modernize codebase and update project config - Replace setup.py + pytest.ini with pyproject.toml - Remove Windows ProactorBasePipeTransport hack from __init__.py (fixed in aiohttp years ago) - Replace 14-branch if/elif device dispatch with dictionary lookup - Install package in CI via pip install -e . [#94] [#95] --- .github/workflows/python-package.yml | 3 +- pyproject.toml | 41 +++++++++++++++++++++ pytest.ini | 2 - setup.py | 35 ------------------ tplinkcloud/__init__.py | 24 +----------- tplinkcloud/device_manager.py | 55 +++++++++++----------------- 6 files changed, 66 insertions(+), 94 deletions(-) create mode 100644 pyproject.toml delete mode 100644 pytest.ini delete mode 100644 setup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 91e3fcf..96d6623 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,7 +37,8 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install flake8 pytest pytest-asyncio - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements.txt + pip install -e . - name: Wait for mock API to be ready run: | for i in {1..30}; do diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..963e7af --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "tplink-cloud-api" +version = "5.0.0" +description = "Python library for controlling TP-Link Kasa smart home devices via the TP-Link Cloud API" +readme = "README.md" +license = "GPL-3.0-only" +requires-python = ">=3.10" +authors = [ + { name = "Dev Piekstra", email = "piekstra.dev@gmail.com" }, +] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", +] +dependencies = [ + "requests>=2,<3", + "aiohttp>=3.9,<4", +] + +[project.urls] +Homepage = "https://github.com/piekstra/tplink-cloud-api" +Repository = "https://github.com/piekstra/tplink-cloud-api" +Issues = "https://github.com/piekstra/tplink-cloud-api/issues" + +[tool.setuptools.packages.find] +exclude = ["tests*"] + +[tool.setuptools.package-data] +tplinkcloud = ["certs/*.pem"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 2f4c80e..0000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -asyncio_mode = auto diff --git a/setup.py b/setup.py deleted file mode 100644 index 3c31a08..0000000 --- a/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -"""setup.py: setuptools control.""" - -from setuptools import setup, find_packages - -__version__ = '5.0.0' - -with open('README.md', 'r', encoding='utf-8') as readme: - long_description = readme.read() - -setup( - name='tplink-cloud-api', - author='Dev Piekstra', - author_email='piekstra.dev@gmail.com', - packages=find_packages(exclude=("tests",)), - package_data={ - 'tplinkcloud': ['certs/*.pem'], - }, - version=__version__, - description='Python library for communicating with the TP-Link Cloud API to manage TP-Link Kasa Smart Home devices', - long_description=long_description, - long_description_content_type='text/markdown', - install_requires=[ - 'requests>=2,<3', - 'aiohttp>=3,<4' - ], - url='https://github.com/piekstra/tplink-cloud-api', - python_requires='>=3.10', - zip_safe=False, - license='GPL-3', - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: OS Independent", - ] -) diff --git a/tplinkcloud/__init__.py b/tplinkcloud/__init__.py index 40300df..f9b4c74 100644 --- a/tplinkcloud/__init__.py +++ b/tplinkcloud/__init__.py @@ -18,26 +18,4 @@ 'TPLinkDeviceOfflineError', 'TPLinkMFARequiredError', 'TPLinkTokenExpiredError', -] - -# Windows OS-specific HACK to silence exception thrown on event loop being closed -# as part of the asyncio library's proactor -# Hack sourced from an issue on the aiohttp library: -# https://github.com/aio-libs/aiohttp/issues/4324#issuecomment-733884349 -# This assumes you have the asyncio library installed -import platform -if platform.system() == 'Windows': - from functools import wraps - from asyncio.proactor_events import _ProactorBasePipeTransport - - def silence_event_loop_closed(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - try: - return func(self, *args, **kwargs) - except RuntimeError as e: - if str(e) != 'Event loop is closed': - raise - return wrapper - - _ProactorBasePipeTransport.__del__ = silence_event_loop_closed(_ProactorBasePipeTransport.__del__) +] \ No newline at end of file diff --git a/tplinkcloud/device_manager.py b/tplinkcloud/device_manager.py index 7058a78..f74fe48 100644 --- a/tplinkcloud/device_manager.py +++ b/tplinkcloud/device_manager.py @@ -5,7 +5,6 @@ from .client import TPLinkApi from .exceptions import TPLinkTokenExpiredError -# Supported devices from .hs100 import HS100 from .hs103 import HS103 from .hs105 import HS105 @@ -22,6 +21,23 @@ from .ep40 import EP40 from .device import TPLinkDevice +DEVICE_MODEL_MAP: dict[str, type[TPLinkDevice]] = { + 'HS100': HS100, + 'HS103': HS103, + 'HS105': HS105, + 'HS110': HS110, + 'HS200': HS200, + 'HS300': HS300, + 'KL420L5': KL420L5, + 'KL430': KL430, + 'KP115': KP115, + 'KP125': KP125, + 'KP200': KP200, + 'KP303': KP303, + 'KP400': KP400, + 'EP40': EP40, +} + class TPLinkDeviceManager: @@ -96,9 +112,7 @@ async def get_devices(self): return devices def _construct_device(self, device_info): - # Construct the TPLinkDeviceInfo here for convenience tplink_device_info = TPLinkDeviceInfo(device_info) - # In case the app_server_url is different, we construct a client each time client = TPLinkDeviceClient( tplink_device_info.app_server_url, self._auth_token, @@ -106,36 +120,11 @@ def _construct_device(self, device_info): term_id=self._term_id ) model = tplink_device_info.device_model - if model.startswith('HS100'): - return HS100(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('HS103'): - return HS103(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('HS105'): - return HS105(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('HS110'): - return HS110(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('HS200'): - return HS200(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('HS300'): - return HS300(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KL420L5'): - return KL420L5(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KL430'): - return KL430(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KP115'): - return KP115(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KP125'): - return KP125(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KP200'): - return KP200(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KP303'): - return KP303(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('KP400'): - return KP400(client, tplink_device_info.device_id, tplink_device_info) - elif model.startswith('EP40'): - return EP40(client, tplink_device_info.device_id, tplink_device_info) - else: - return TPLinkDevice(client, tplink_device_info.device_id, tplink_device_info) + device_cls = next( + (cls for prefix, cls in DEVICE_MODEL_MAP.items() if model.startswith(prefix)), + TPLinkDevice, + ) + return device_cls(client, tplink_device_info.device_id, tplink_device_info) def login(self, username, password, mfa_callback=None): result = self._tplink_api.login(