Convert to use poetry & pyproject.toml for dep & build management (#54)

* Convert to use poetry and pyproject.toml, update README

* add some resources for contributors

* minor adjustments

* ci: separate tests from linting, run using poetry

* add pytest-mock to dev requirements

* combine running tests and reporting to codecov

* generate both xml and html coverage reports

* add codecov to dev dependencies
This commit is contained in:
Teemu R 2020-05-12 12:11:47 +02:00 committed by GitHub
parent c6d76836d7
commit ed57563e8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 296 additions and 312 deletions

8
.flake8 Normal file
View File

@ -0,0 +1,8 @@
[flake8]
exclude = .git,.tox,__pycache__,kasa/tests/newfakes.py,kasa/tests/test_fixtures.py
max-line-length = 88
per-file-ignores =
kasa/tests/*.py:D100,D101,D102,D103,D104
setup.py:D100
ignore = D105, D107, E203, E501, W503
max-complexity = 18

View File

@ -26,11 +26,11 @@ repos:
- id: flake8
additional_dependencies: [flake8-docstrings]
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
hooks:
- id: isort
additional_dependencies: [toml]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.740

View File

@ -1,50 +0,0 @@
####################################################
# Use python 3.x and the latest version of alpine #
####################################################
FROM python:3-alpine
LABEL maintainer=peter@grainger.xyz
###################################################
# Add all system packages relied upon by python #
###################################################
RUN apk update && \
apk add --no-cache gcc \
libffi-dev \
openssl-dev \
libxslt-dev \
libxml2-dev \
musl-dev \
linux-headers
###################################################
# Create somewhere to put the files #
###################################################
RUN mkdir -p /opt/python-kasa
WORKDIR /opt/python-kasa
###################################################
# Requirements file first to help cache #
###################################################
COPY requirements.txt .
RUN pip install -r requirements.txt
###################################################
# Install dev dependancies #
###################################################
RUN pip install pytest pytest-cov voluptuous typing
###################################################
# Copy over the rest. #
###################################################
COPY ./ ./
###################################################
# Install everything to the path #
###################################################
RUN python setup.py install
###################################################
# Run tests #
###################################################
CMD pytest

146
README.md
View File

@ -3,9 +3,10 @@
[![PyPI version](https://badge.fury.io/py/python-kasa.svg)](https://badge.fury.io/py/python-kasa)
[![Build Status](https://dev.azure.com/python-kasa/python-kasa/_apis/build/status/python-kasa.python-kasa?branchName=master)](https://dev.azure.com/python-kasa/python-kasa/_build/latest?definitionId=2&branchName=master)
[![Coverage Status](https://coveralls.io/repos/github/python-kasa/python-kasa/badge.svg?branch=master)](https://coveralls.io/github/python-kasa/python-kasa?branch=master)
[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
Python Library to control TPLink smart plugs/switches and smart bulbs.
python-kasa is a Python library to control TPLink smart home devices (plugs, wall switches, power strips, and bulbs) using asyncio.
This project is a maintainer-made fork of [pyHS100](https://github.com/GadgetReactor/pyHS100) project.
**Supported devices**
@ -13,6 +14,7 @@ Python Library to control TPLink smart plugs/switches and smart bulbs.
* HS100
* HS103
* HS105
* HS107
* HS110
* Power Strips
* HS300
@ -32,6 +34,9 @@ Python Library to control TPLink smart plugs/switches and smart bulbs.
* KL120
* KL130
**Contributions (be it adding missing features, fixing bugs or improving documentation) are more than welcome, feel free to submit pull requests! See below for instructions for setting up a development environment.**
# Usage
The package is shipped with a console tool named kasa, please refer to ```kasa --help``` for detailed usage.
@ -82,7 +87,7 @@ All devices support a variety of common commands, including:
* `state` which returns state information
* `on` and `off` for turning the device on or off
* `emeter` (where applicable) to return energy consumption information
* `sysinfo` to return raw system information which is used by e.g. `state`, useful for debugging and when adding support for new device types
* `sysinfo` to return raw system information
## Energy meter
@ -96,128 +101,95 @@ $ kasa emeter
Current state: {'total': 133.105, 'power': 108.223577, 'current': 0.54463, 'voltage': 225.296283}
```
## Plug-specific commands
At the moment only switching the state of the LED is implemented.
**Feel free to submit patches as pull requests for further features!**
### Controlling the LED
`led` command can be used to control whether the LED light on front of the plug is on or off.
```
$ kasa --plug led
LED state: False
$ kasa --plug led 1
Turning led to True
```
## Bulb-specific commands
At the moment setting brightness, color temperature and color (in HSV) is supported.
At the moment setting brightness, color temperature and color (in HSV) are supported depending on the device.
The commands are straightforward, so feel free to check `--help` for instructions how to use them.
**Feel free to submit patches as pull requests to add more functionality (e.g. scenes)!**
# Library usage
The public API is well documented, but here are some examples to get you started.
For all available API functions run ```help(SmartPlug)``` or ```help(SmartBulb)```.
The property accesses use the data obtained before by awaiting `update()`.
The values are cached until the next update call.
Each method changing the state of the device will automatically update the cached state.
Errors are raised as `SmartDeviceException` instances for the user to handle.
## Discovering devices
`Discover` class' `discover()` can be used to discover supported devices,
which returns a dictionary keyed with the IP address whose value hold a ready-to-use instance of the detected device type.
`Discover.discover()` can be used to discover supported devices in the local network.
The return value is a dictionary keyed with the IP address and the value holds a ready-to-use instance of the detected device type.
Example:
```python
import asyncio
from kasa import Discover
for dev in Discover.discover().values():
print(dev)
devices = asyncio.run(Discover.discover())
for addr, dev in devices.items():
asyncio.run(dev.update())
print(f"{addr} >> {dev}")
```
```
$ python3 example.py
$ python example.py
<SmartPlug at 192.168.XXX.XXX (My Smart Plug), is_on: True - dev specific: {'LED state': True, 'On since': datetime.datetime(2017, 3, 26, 18, 29, 17, 52073)}>
```
## Querying basic information
*Please note that most property getters do I/O (e.g. fetching the system information) on each call.
If you want to avoid unnecessary communication with the device please use `get_sysinfo` and handle parsing of information by yourself.*
```python
from kasa import SmartPlug, SmartBulb
import asyncio
from kasa import SmartPlug
from pprint import pformat as pf
plug = SmartPlug("192.168.XXX.XXX")
asyncio.run(plug.update())
print("Hardware: %s" % pf(plug.hw_info))
print("Full sysinfo: %s" % pf(plug.get_sysinfo())) # this prints lots of information about the device
print("Full sysinfo: %s" % pf(plug.sys_info))
```
The rest of the examples assume that you have initialized an instance.
## State & switching
Devices can be turned on and off by either calling appropriate methods on the device object,
or by assigning a new state to `state` property.
Devices can be turned on and off by either calling appropriate methods on the device object.
```python
print("Current state: %s" % plug.state)
plug.turn_off()
plug.turn_on()
```
```python
plug.state = "ON"
plug.state = "OFF"
```
## Time information
```python
print("Current time: %s" % plug.time)
print("Timezone: %s" % plug.timezone)
```
## Getting and setting the name
```python
print("Alias: %s" % plug.alias)
plug.alias = "My New Smartplug"
print("Current state: %s" % plug.is_on)
await plug.turn_off()
await plug.turn_on()
```
## Getting emeter status (if applicable)
```python
print("Current consumption: %s" % plug.get_emeter_realtime())
print("Per day: %s" % plug.get_emeter_daily(year=2016, month=12))
print("Per month: %s" % plug.get_emeter_monthly(year=2016))
print("Current consumption: %s" % await plug.get_emeter_realtime())
print("Per day: %s" % await plug.get_emeter_daily(year=2016, month=12))
print("Per month: %s" % await plug.get_emeter_monthly(year=2016))
```
## Plug-specific
### Switching the led (plugs only)
```python
print("Current LED state: %s" % plug.led)
plug.led = False # turn off led
print("New LED state: %s" % plug.led)
```
## Bulb-specific API
## Bulb and dimmer-specific APIs
The bulb API is likewise straightforward, so please refer to its API documentation.
Information about supported features can be queried by using properties prefixed with `is_`, e.g. `is_dimmable`.
### Setting the brightness
The `brightness` property works in percentages.
```python
print(bulb.brightness)
import asyncio
from kasa import SmartBulb
bulb = SmartBulb("192.168.1.123")
asyncio.run(bulb.update())
if bulb.is_dimmable:
bulb.brightness = 100
asyncio.run(bulb.set_brightness(100))
print(bulb.brightness)
```
### Setting the color temperature
```python
print(bulb.color_temp)
if bulb.is_variable_color_temp:
bulb.color_temp = 3000
await bulb.set_color_temp(3000)
print(bulb.color_temp)
```
### Setting the color
@ -225,19 +197,29 @@ if bulb.is_variable_color_temp:
Hue is given in degrees (0-360) and saturation and value in percentage.
```python
print(bulb.hsv)
if bulb.is_color:
bulb.hsv = (180, 100, 100) # set to cyan
await bulb.set_hsv(180, 100, 100) # set to cyan
print(bulb.hsv)
```
## Development Setup
## Contributing
### Docker
Contributions are very welcome! To simplify the process, we are leveraging automated checks and tests for contributions.
The following assumes you have a working installation of Docker.
### Resources
Set up the environment and run the tests on demand.
* [softScheck's github contains lot of information and wireshark dissector](https://github.com/softScheck/tplink-smartplug#wireshark-dissector)
* [https://github.com/plasticrake/tplink-smarthome-simulator](tplink-smarthome-simulator)
```shell
docker build . -t kasa && docker run -v $(PWD)/kasa/tests:/opt/python-kasa/kasa/tests kasa pytest
### Setting up development environment
```bash
poetry install
pre-commit install
```
### Code-style checks
We use several tools to automatically check all contributions, which are run automatically when you commit your code.
If you want to manually execute the checks, you can run `tox -e lint` to do the linting checks or `tox` to also execute the tests.

View File

@ -3,93 +3,116 @@ trigger:
pr:
- master
strategy:
stages:
- stage: "Linting"
jobs:
- job: "LintChecks"
pool:
vmImage: "ubuntu-latest"
strategy:
matrix:
Python 3.7 Ubuntu:
python.version: '3.7'
vmImage: 'ubuntu-latest'
Python 3.8 Ubuntu:
Python 3.8:
python.version: '3.8'
vmImage: 'ubuntu-latest'
# PyPy Ubuntu:
# python.version: pypy3
# vmImage: 'ubuntu-latest'
Python 3.7 Windows:
python.version: '3.7'
vmImage: 'windows-latest'
Python 3.8 Windows:
python.version: '3.8'
vmImage: 'windows-latest'
Python 3.7 OSX:
python.version: '3.7'
vmImage: 'macOS-latest'
Python 3.8 OSX:
python.version: '3.8'
vmImage: 'macOS-latest'
pool:
vmImage: $(vmImage)
steps:
- task: UsePythonVersion@0
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
displayName: 'Use Python $(python.version)'
- script: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
- script: |
python -m pip install --upgrade pip poetry
poetry install
displayName: 'Install dependencies'
- script: |
pre-commit run black --all-files
- script: |
poetry run pre-commit run black --all-files
displayName: 'Code formating (black)'
- script: |
pre-commit run flake8 --all-files
- script: |
poetry run pre-commit run flake8 --all-files
displayName: 'Code formating (flake8)'
- script: |
pre-commit run mypy --all-files
- script: |
poetry run pre-commit run mypy --all-files
displayName: 'Typing checks (mypy)'
- script: |
pre-commit run isort --all-files
- script: |
poetry run pre-commit run isort --all-files
displayName: 'Order of imports (isort)'
- script: |
pre-commit run trailing-whitespace --all-files
- script: |
poetry run pre-commit run trailing-whitespace --all-files
displayName: 'Run trailing-whitespace'
- script: |
pre-commit run end-of-file-fixer --all-files
- script: |
poetry run pre-commit run end-of-file-fixer --all-files
displayName: 'Run end-of-file-fixer'
- script: |
pre-commit run check-docstring-first --all-files
- script: |
poetry run pre-commit run check-docstring-first --all-files
displayName: 'Run check-docstring-first'
- script: |
pre-commit run check-yaml --all-files
- script: |
poetry run pre-commit run check-yaml --all-files
displayName: 'Run check-yaml'
- script: |
pre-commit run debug-statements --all-files
- script: |
poetry run pre-commit run debug-statements --all-files
displayName: 'Run debug-statements'
- script: |
pre-commit run check-ast --all-files
- script: |
poetry run pre-commit run check-ast --all-files
displayName: 'Run check-ast'
- script: |
pre-commit run isort --all-files
displayName: 'Order of imports (isort)'
- script: |
pytest --cov kasa --cov-report xml
displayName: 'Tests'
- stage: "Tests"
jobs:
- job: "Tests"
strategy:
matrix:
Python 3.7 Ubuntu:
python.version: '3.7'
vmImage: 'ubuntu-latest'
- script: |
codecov -t $(codecov.token)
displayName: Report Coverage to codecov
Python 3.8 Ubuntu:
python.version: '3.8'
vmImage: 'ubuntu-latest'
Python 3.7 Windows:
python.version: '3.7'
vmImage: 'windows-latest'
Python 3.8 Windows:
python.version: '3.8'
vmImage: 'windows-latest'
Python 3.7 OSX:
python.version: '3.7'
vmImage: 'macOS-latest'
Python 3.8 OSX:
python.version: '3.8'
vmImage: 'macOS-latest'
pool:
vmImage: $(vmImage)
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
displayName: 'Use Python $(python.version)'
- script: |
python -m pip install --upgrade pip poetry
poetry install
displayName: 'Install dependencies'
- script: |
poetry run pytest --cov kasa --cov-report=xml --cov-report=html
displayName: 'Run tests'
- script: |
poetry run codecov -t $(codecov.token)
displayName: 'Report code coverage'

View File

@ -11,6 +11,7 @@ For device type specific actions `SmartBulb`, `SmartPlug`, or `SmartStrip`
Module-specific errors are raised as `SmartDeviceException` and are expected
to be handled by the user of the library.
"""
from importlib_metadata import version # type: ignore
from kasa.discover import Discover
from kasa.protocol import TPLinkSmartHomeProtocol
from kasa.smartbulb import SmartBulb
@ -19,6 +20,9 @@ from kasa.smartdimmer import SmartDimmer
from kasa.smartplug import SmartPlug
from kasa.smartstrip import SmartStrip
__version__ = version("python-kasa")
__all__ = [
"Discover",
"TPLinkSmartHomeProtocol",

View File

@ -7,6 +7,7 @@ from pprint import pformat as pf
from typing import cast
import asyncclick as click
from kasa import Discover, SmartBulb, SmartDevice, SmartPlug, SmartStrip
click.anyio_backend = "asyncio"

View File

@ -23,7 +23,7 @@ TPLINK_KELVIN = {
class SmartBulb(SmartDevice):
"""Representation of a TP-Link Smart Bulb.
Usage example when used as library:
Usage example:
```python
p = SmartBulb("192.168.1.105")
await p.update()
@ -33,6 +33,7 @@ class SmartBulb(SmartDevice):
# change state of bulb
await p.turn_on()
assert p.is_on
await p.turn_off()
# query and print current state of plug
@ -40,10 +41,9 @@ class SmartBulb(SmartDevice):
# check whether the bulb supports color changes
if p.is_color:
print("we got color!")
# set the color to an HSV tuple
await p.set_hsv(180, 100, 100)
# get the current HSV value
print(p.hsv)
@ -57,7 +57,6 @@ class SmartBulb(SmartDevice):
# check whether the bulb is dimmable
if p.is_dimmable:
# set the bulb to 50% brightness
await p.set_brightness(50)

View File

@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__)
class SmartPlug(SmartDevice):
"""Representation of a TP-Link Smart Switch.
Usage example when used a a synchronous library:
Usage example:
```python
p = SmartPlug("192.168.1.105")
@ -19,9 +19,10 @@ class SmartPlug(SmartDevice):
# change state of plug
await p.turn_on()
assert p.is_on is True
await p.turn_off()
# query and print current state of plug
# print current state of plug
print(p.state_information)
```

View File

@ -4,7 +4,7 @@ import json
import os
from os.path import basename
import pytest
import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342
from kasa import Discover, SmartBulb, SmartDimmer, SmartPlug, SmartStrip

View File

@ -1,7 +1,16 @@
import logging
import re
from voluptuous import REMOVE_EXTRA, All, Any, Coerce, Invalid, Optional, Range, Schema
from voluptuous import ( # type: ignore
REMOVE_EXTRA,
All,
Any,
Coerce,
Invalid,
Optional,
Range,
Schema,
)
from ..protocol import TPLinkSmartHomeProtocol

View File

@ -1,6 +1,6 @@
import pytest
from asyncclick.testing import CliRunner
from kasa import SmartDevice
from kasa.cli import alias, brightness, emeter, raw_command, state, sysinfo

View File

@ -2,7 +2,7 @@ import asyncio
from datetime import datetime
from unittest.mock import patch
import pytest
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
from kasa import DeviceType, SmartDeviceException, SmartStrip

View File

@ -1,2 +0,0 @@
# flake8: noqa
__version__ = "0.4.0.dev0"

57
pyproject.toml Normal file
View File

@ -0,0 +1,57 @@
[tool.poetry]
name = "python-kasa"
version = "0.4.0.dev0"
description = "Python API for TP-Link Kasa Smarthome devices"
license = "GPL-3.0-or-later"
authors = ["Your Name <you@example.com>"]
repository = "https://github.com/python-kasa/python-kasa"
readme = "README.md"
packages = [
{ include = "kasa" }
]
[tool.poetry.scripts]
kasa = "kasa.cli:cli"
[tool.poetry.dependencies]
python = "^3.7"
importlib-metadata = "*"
asyncclick = "^7"
[tool.poetry.dev-dependencies]
pytest = "^5"
pytest-azurepipelines = "^0.8"
pytest-cov = "^2.8"
pytest-asyncio = "^0.11"
pytest-sugar = "*"
pre-commit = "*"
voluptuous = "*"
toml = "*"
tox = "*"
pytest-mock = "^3.1.0"
codecov = "^2.0"
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 88
known_first_party = "kasa"
known_third_party = ["asyncclick", "pytest", "setuptools", "voluptuous"]
[tool.coverage.run]
source = ["kasa"]
branch = true
omit = ["kasa/cli.py", "kasa/tests/*"]
[tool.coverage.report]
exclude_lines = [
# ignore abstract methods
"raise NotImplementedError",
"def __repr__"
]
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

View File

@ -1,3 +0,0 @@
asyncclick
pre-commit
voluptuous

View File

@ -1,8 +0,0 @@
pytest
pytest-azurepipelines
pytest-cov
pytest-asyncio
pytest-mock
asyncclick
voluptuous
codecov

View File

@ -1,19 +0,0 @@
from setuptools import setup
with open("kasa/version.py") as f:
exec(f.read())
setup(
name="python-kasa",
version=__version__, # type: ignore # noqa: F821
description="Python API for TP-Link Kasa Smarthome products",
url="https://github.com/python-kasa/python-kasa",
author="",
author_email="",
license="GPLv3",
packages=["kasa"],
install_requires=["asyncclick"],
python_requires=">=3.7",
entry_points={"console_scripts": ["kasa=kasa.cli:cli"]},
zip_safe=False,
)

72
tox.ini
View File

@ -1,16 +1,35 @@
[tox]
envlist=py37,py38,flake8,linting,typing
envlist=py37,py38,flake8,lint,coverage
skip_missing_interpreters = True
isolated_build = True
[tox:travis]
3.7 = py37
3.8 = py38
[testenv]
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
deps = -r{toxinidir}/requirements_test.txt
commands=
pytest --cov --cov-config=tox.ini kasa
whitelist_externals =
poetry
coverage
commands =
poetry install -v
poetry run pytest --cov kasa/tests/
[testenv:clean]
deps = coverage
skip_install = true
commands = coverage erase
[testenv:py37]
commands = coverage run -m pytest {posargs}
[testenv:py38]
commands = coverage run -m pytest {posargs}
[testenv:coverage]
basepython = python3.8
skip_install = true
deps = coverage[toml]
commands =
coverage report
coverage html
[testenv:flake8]
deps=
@ -18,44 +37,7 @@ deps=
flake8-docstrings
commands=flake8 kasa
[testenv:typing]
skip_install=true
deps=mypy
commands=mypy --ignore-missing-imports kasa
[flake8]
exclude = .git,.tox,__pycache__,kasa/tests/newfakes.py,kasa/tests/test_fixtures.py
max-line-length = 88
per-file-ignores =
kasa/tests/*.py:D100,D101,D102,D103,D104
setup.py:D100
ignore = D105, D107, E203, E501, W503
#ignore = E203, E266, E501, W503, F403, F401
#max-complexity = 18
#select = B,C,E,F,W,T4,B9
[testenv:lint]
deps = pre-commit
skip_install = true
commands = pre-commit run --all-files
[coverage:run]
source = kasa
branch = True
omit =
kasa/tests/*
[coverage:report]
exclude_lines =
# ignore abstract methods
raise NotImplementedError
def __repr__
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
known_first_party=kasa
known_third_party=click,pytest,setuptools,voluptuous