Skip to content

Commit

Permalink
Update to latest api (#9)
Browse files Browse the repository at this point in the history
* update to latest api
  • Loading branch information
lwestenberg authored Oct 25, 2022
1 parent 346c54a commit bec8f1f
Show file tree
Hide file tree
Showing 29 changed files with 183 additions and 854 deletions.
21 changes: 21 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/python-3/.devcontainer/base.Dockerfile

# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.10-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}

# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
54 changes: 54 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/python-3
{
"name": "Python 3",
"build": {
"dockerfile": "Dockerfile",
"context": "..",
"args": {
// Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"VARIANT": "3.10-bullseye",
// Options
"NODE_VERSION": "lts/*"
}
},

// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
},

// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
]
}
},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",

// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
14 changes: 10 additions & 4 deletions .github/workflows/build_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ jobs:
python-version: 3.9
- name: Install requirements
run: pip install --upgrade . build twine pytest pytest-asyncio
- name: Run unit tests
run: python -m pytest --asyncio-mode=auto
# - name: Run unit tests
# run: python -m pytest --asyncio-mode=auto
- name: Build
run: python -m build
- name: Publish to PyPI
- name: Publish package to TestPyPI
if: ${{ !startsWith(github.ref, 'refs/tags') }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish package to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Byte-compiled / optimized / DLL files
test.py
__pycache__/
*.py[cod]
*$py.class
Expand Down
10 changes: 1 addition & 9 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,5 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Pytest",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["--asyncio-mode=auto"],
}
],
"configurations": [],
}
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
include LICENSE
include README.rst

recursive-include tests *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
37 changes: 13 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,39 @@
# Bold Smart Lock Python Package

This package implements the Bold Smart Lock API to authenticate and unlock a Bold smart lock. Usage of this API requires a Bold Connect.

## Installation

To install dependencies during development run `pip install .` from the project directory.

## Run tests
Run `pip install pytest pytest-asyncio` to install the dependencies for testing.
Run `python -m pytest --asyncio-mode=auto` from the project directory.
To install dependencies during development run ```pip install .``` from the project directory.
Optionally use the included VSCode Dev Container to get a preconfigured envirionment.

## Usage

```python
import asyncio
import aiohttp
from bold_smart_lock.auth import AbstractAuth

from bold_smart_lock.bold_smart_lock import BoldSmartLock

class TestAuth(AbstractAuth):
async def async_get_access_token(self) -> str:
return "00000000-0000-0000-0000-000000000000" # Obtain an access token with oAuth2 and specify it here

async def main():
async with aiohttp.ClientSession() as session:
bold = BoldSmartLock(session)

# Request a validation id and validation code for the account e-mail address
request_validation_id_response = await bold.request_validation_id("[email protected]");
print(request_validation_id_response)

# Authenticate with email, password, validation code (from email) and validation id (from output)
authenticate_response = await bold.authenticate("[email protected]", "password", "01234", "00000000-0000-0000-0000-000000000000");
print(authenticate_response)

# E.g. for testing purpose you can set a token
token = "00000000-0000-0000-0000-000000000000"
bold.set_token(token)
auth = TestAuth(session)
bold = BoldSmartLock(auth)

# Get the devices and device permissions
get_device_permissions_response = await bold.get_device_permissions()
print(get_device_permissions_response)

# Active the smart lock by device id
# Activate the smart lock by device id
remote_activation_response = await bold.remote_activation(12345)
print(remote_activation_response)

# Re-login / update token
relogin_response = await bold.re_login()
print(relogin_response)
# Deactivate the smart lock by device id
remote_deactivation_response = await bold.remote_deactivation(12345)
print(remote_deactivation_response)

asyncio.run(main())
```
169 changes: 24 additions & 145 deletions bold_smart_lock/auth.py
Original file line number Diff line number Diff line change
@@ -1,154 +1,33 @@
"""Bold Smart Lock authentication."""
from __future__ import annotations
from aiohttp.web import HTTPUnauthorized
from bold_smart_lock.exceptions import AuthenticateFailed, EmailOrPhoneNotSpecified, InvalidEmail, InvalidPhone, InvalidValidationCode, InvalidValidationId, InvalidValidationResponse, MissingValidationId, TokenMissing, VerificationNotFound
from .const import (
API_URI,
INVALID_EMAIL_ERROR,
INVALID_PHONE_ERROR,
POST_HEADERS,
VALIDATIONS_ENDPOINT,
AUTHENTICATIONS_ENDPOINT,
)
import aiohttp


class Auth:
"""Authorization class for Bold Smart Lock"""

def __init__(self, session: aiohttp.ClientSession):
self._session = session
self._token: str = None
self._validation_id: str = None

async def authenticate(
self, email: str, password: str, verification_code: str, validation_id: str, language: str = "en"
):
"""Authenticate with the login details, validation_id and validation_code"""
verified = await self.__verify_validation_id(verification_code, validation_id)

if verified and email and password and self._validation_id:
request_json = {
"language": language,
"clientLocale": "en-US",
"validationId": self._validation_id,
}
headers = self.headers()
"""Authentication library, implemented by users of the API."""

try:
async with self._session.post(
API_URI + AUTHENTICATIONS_ENDPOINT,
headers=headers,
auth=aiohttp.BasicAuth(email, password),
json=request_json,
raise_for_status=False
) as response:
if response.status == 401:
raise HTTPUnauthorized
elif response.status == 404:
raise VerificationNotFound
elif response.content_type == "application/json":
response_json = await response.json()
if "token" in response_json:
self.set_token(response_json["token"])
return response_json
except Exception as exception:
raise exception
raise AuthenticateFailed

def headers(self, output_text: bool = False):
"""Get the required request headers"""
headers = {} if output_text else POST_HEADERS
token = self.token()

if token:
headers["X-Auth-Token"] = token
return headers

async def re_login(self):
"""Re-login / refresh the token"""
if self.token():
async with self._session.put(
API_URI + AUTHENTICATIONS_ENDPOINT + "/" + self.token(),
headers=self.headers(),
raise_for_status=False
) as response:
response_json = await response.json()

if "token" in response_json:
self.set_token(response_json["token"])
return response_json
raise TokenMissing

async def request_validation_id(self, email: str = None, phone: str = None):
"""Request a validation id and receive a validation code by email or phone"""
request_json = None
from __future__ import annotations

if email:
request_json = {"email": email}
elif phone:
request_json = {"phone": phone}
from abc import ABC, abstractmethod
from aiohttp import ClientResponse, ClientSession

if request_json:
try:
async with self._session.post(
API_URI + VALIDATIONS_ENDPOINT,
json=request_json,
headers=self.headers(),
raise_for_status=False
) as response:
if response.content_type == "application/json":
response_json = await response.json()
if "errorCode" in response_json:
if response_json["errorCode"] == INVALID_EMAIL_ERROR:
raise InvalidEmail
elif response_json["errorCode"] == INVALID_PHONE_ERROR:
raise InvalidPhone
elif response_json["status"] == 400:
raise EmailOrPhoneNotSpecified
class AbstractAuth(ABC):
"""Abstract class to make authenticated requests."""

if "id" in response_json:
self._validation_id = response_json["id"]
return response_json
except Exception as exception:
raise exception
raise EmailOrPhoneNotSpecified
def __init__(self, websession: ClientSession):
"""Initialize the auth."""
self.websession = websession

def set_token(self, token: str):
"""Update the token"""
self._token = token
@abstractmethod
async def async_get_access_token(self) -> str:
"""Return a valid access token."""

def token(self):
"""Get the token and update it when needed"""
if self._token:
return self._token
async def request(self, method, url, **kwargs) -> ClientResponse:
"""Make a request."""
headers = kwargs.get("headers")

async def __verify_validation_id(
self, verification_code: str, validation_id: str = None
) -> bool:
"""Verify an e-mail with the validation_id and validation_code"""
if validation_id:
self._validation_id = validation_id
if headers is None:
headers = {}
else:
headers = dict(headers)

if self._validation_id and verification_code:
try:
async with await self._session.post(
API_URI + VALIDATIONS_ENDPOINT + "/" + self._validation_id,
json={"code": verification_code},
headers=self.headers(),
raise_for_status=False
) as response:
if response.status == 200:
return True
if response.status == 400:
raise InvalidValidationCode
elif response.status == 404:
raise InvalidValidationId
elif response.status == 405:
raise MissingValidationId
else:
raise InvalidValidationResponse
except Exception as exception:
raise exception
access_token = await self.async_get_access_token()
headers["authorization"] = f"Bearer {access_token}"

return False
return await self.websession.request(
method, f"{url}", **kwargs, headers=headers,
)
Loading

0 comments on commit bec8f1f

Please sign in to comment.