-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
29 changed files
with
183 additions
and
854 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
Oops, something went wrong.