Skip to content

Commit

Permalink
Merge pull request #21 from 2Fake/development
Browse files Browse the repository at this point in the history
Prepare release
  • Loading branch information
Shutgun authored Dec 2, 2020
2 parents dd55b52 + ae3b466 commit acae19f
Show file tree
Hide file tree
Showing 39 changed files with 909 additions and 934 deletions.
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<!--
In case your pull request goes to master, please have a look at the following checklist. Otherwise feel free to remove this chapter.
Put an 'x' in the boxes that apply.
Put an 'x' in the boxes that apply.
-->

- [ ] Version number in \_\_init\_\_.py was properly set.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/convert_todos_to_issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ jobs:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
LABEL: "# TODO:"
COMMENT_MARKER: "#"
id: "todo"
id: "todo"
91 changes: 75 additions & 16 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,95 @@ name: Python package
on: [push]

jobs:
build:

format:
name: Check formatting
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Check formatting
uses: pre-commit/[email protected]

lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Lint with flake8
run: |
python -m pip install --upgrade pip
pip install flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --statistics
- name: Lint with pylint
run: |
pip install pylint
pip install -e .
pylint --errors-only --score=n devolo_plc_api
pylint --exit-zero --score=n --disable=C,E,R --enable=useless-suppression --ignore-patterns=.*_pb2.py devolo_plc_api devolo_plc_api
- name: Lint with mypy
run: |
pip install mypy
mypy devolo_plc_api
test:
name: Test with Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8]

python-version: [3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Checkout sources
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- name: Lint with flake8
run: |
pip install flake8
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings.
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --ignore=E303,W503 --statistics --exclude=.*,devolo_plc_api/*/devolo_idl_*.py
pip install -e .[test]
- name: Test with pytest
run: |
python setup.py test --addopts --cov=devolo_plc_api
pytest --cov=devolo_plc_api
- name: Preserve coverage
uses: actions/upload-artifact@v2
with:
name: coverage
path: .coverage

coverage:
name: Upload coverage
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Download coverage
uses: actions/download-artifact@v2
with:
name: coverage
- name: Coveralls
run: |
pip install coveralls==1.10.0
python -m pip install --upgrade pip
pip install wheel coveralls==1.10.0
export COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_TOKEN }}
coveralls
- name: Clean up coverage
uses: geekyeggo/delete-artifact@v1
with:
name: coverage
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,5 @@ dmypy.json
### PyCharm ###
.idea


### Testfile ###
test.py
test.py
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
repos:
- repo: https://github.com/pre-commit/mirrors-yapf
rev: ''
hooks:
- id: yapf
- repo: https://github.com/pycqa/isort
rev: ''
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: ''
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
1 change: 1 addition & 0 deletions .yapfignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
devolo_plc_api/*/devolo_idl_*.py
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,36 @@ cd devolo_plc_api
python setup.py install
```

If you want to run out tests, change to the tests directory and start pytest via setup.py.
If you want to run out tests, install the extra requirements and start pytest.

```bash
python setup.py test
pip install -e .[test]
pytest
```

## Usage

All features we currently support are shown in our examples. If you want to use the package asynchronously, please check [example_async.py](https://github.com/2Fake/devolo_plc_api/blob/master/example_async.py). If you want to use it synchronously, please check [example_sync.py](https://github.com/2Fake/devolo_plc_api/blob/master/example_sync.py).
All features we currently support on device basis are shown in our examples. If you want to use the package asynchronously, please check [example_async.py](https://github.com/2Fake/devolo_plc_api/blob/master/example_async.py). If you want to use it synchronously, please check [example_sync.py](https://github.com/2Fake/devolo_plc_api/blob/master/example_sync.py).

If you don't know the IP addresses of your devices, you can discover them. You will get a dictionary with the device's serial number as key. The connections to the devices will be already established, but you will have to take about disconnecting.

```python
from devolo_plc_api.network import async_discover_network

devices = await async_discover_network()
# Do your magic
await asyncio.gather(*[device.async_disconnect() for device in devices.values()])
```

Or in a synchronous setup:

```python
from devolo_plc_api.network import discover_network

devices = discover_network()
# Do your magic
[device.disconnect() for device in devices.values()]
```

## Supported device

Expand Down
2 changes: 1 addition & 1 deletion devolo_plc_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.0"
__version__ = "0.3.0"
72 changes: 40 additions & 32 deletions devolo_plc_api/clients/protobuf.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,70 @@
from logging import Logger
import asyncio
import logging
from abc import ABC, abstractclassmethod
from typing import Callable

from google.protobuf.json_format import MessageToDict
from httpx import DigestAuth, Response
from httpx import AsyncClient, DigestAuth, Response

from ..exceptions.device import DevicePasswordProtected

TIMEOUT = 3.0


class Protobuf:
class Protobuf(ABC):
"""
Google Protobuf client. This client is not usable stand alone but needs to be derived.
Google Protobuf client.
"""
_logger: Logger

@abstractclassmethod
def __init__(self):
self._loop = asyncio.get_running_loop()
self._logger = logging.getLogger(self.__class__.__name__)

self.password: str

self._ip: str
self._path: str
self._port: int
self._session: AsyncClient
self._user: str
self._version: str

def __getattr__(self, attr: str) -> Callable:
""" Catch attempts to call methods synchronously. """

def method(*args, **kwargs):
return self._loop.run_until_complete(getattr(self, async_method)(*args, **kwargs))

async_method = f"async_{attr}"
if hasattr(self.__class__, async_method):
return method
raise AttributeError(f"{self.__class__.__name__} object has no attribute {attr}")

@property
def url(self) -> str:
""" The base URL to query. """
return f"http://{self._ip}:{self._port}/{self._path}/{self._version}/" # type: ignore

return f"http://{self._ip}:{self._port}/{self._path}/{self._version}/"

async def _async_get(self, sub_url: str, timeout: float = TIMEOUT) -> Response:
""" Query URL asynchronously. """
url = f"{self.url}{sub_url}"
self._logger.debug(f"Getting from {url}")
self._logger.debug("Getting from %s", url)
try:
return await self._session.get(url, auth=DigestAuth(self._user, self._password), timeout=timeout) # type: ignore
return await self._session.get(url, auth=DigestAuth(self._user, self.password), timeout=timeout)
except TypeError:
raise DevicePasswordProtected("The used password is wrong.") from None

def _get(self, sub_url: str, timeout: float = TIMEOUT) -> Response:
""" Query URL synchronously. """
async def _async_post(self, sub_url: str, content: bytes, timeout: float = TIMEOUT) -> Response:
""" Post data asynchronously. """
url = f"{self.url}{sub_url}"
self._logger.debug(f"Getting from {url}")
self._logger.debug("Posting to %s", url)
try:
return self._session.get(url, auth=DigestAuth(self._user, self._password), timeout=timeout) # type: ignore
return await self._session.post(url, auth=DigestAuth(self._user, self.password), content=content, timeout=timeout)
except TypeError:
raise DevicePasswordProtected("The used password is wrong.") from None

def _message_to_dict(self, message) -> dict:
@staticmethod
def _message_to_dict(message) -> dict:
""" Convert message to dict with certain settings. """
return MessageToDict(message=message, including_default_value_fields=True, preserving_proto_field_name=True)

async def _async_post(self, sub_url: str, data: str, timeout: float = TIMEOUT) -> Response:
""" Post data asynchronously. """
url = f"{self.url}{sub_url}"
self._logger.debug(f"Posting to {url}")
try:
return await self._session.post(url, auth=DigestAuth(self._user, self._password), data=data, timeout=timeout) # type: ignore
except TypeError:
raise DevicePasswordProtected("The used password is wrong.") from None

def _post(self, sub_url: str, data: str, timeout: float = TIMEOUT) -> Response:
""" Post data synchronously. """
url = f"{self.url}{sub_url}"
self._logger.debug(f"Posting to {url}")
try:
return self._session.post(url, auth=DigestAuth(self._user, self._password), data=data, timeout=timeout) # type: ignore
except TypeError:
raise DevicePasswordProtected("The used password is wrong.") from None
Loading

0 comments on commit acae19f

Please sign in to comment.