...
 
Commits (18)
......@@ -4,14 +4,11 @@ image: python:3.7
variables:
PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip
PIPENV_CACHE_DIR: $CI_PROJECT_DIR/.cache/pipenv
PIPENV_VENV_IN_PROJECT: "true"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- $PIP_CACHE_DIR
- $PIPENV_CACHE_DIR
- .venv/
stages:
......@@ -20,10 +17,13 @@ stages:
- deploy
before_script:
- python -V
- python -V
# - apk update
# - apk add --no-cache --update python3-dev gcc build-base make git
- make setup_env
- pip install poetry
- poetry config virtualenvs.create false
- poetry install
lint:
stage: test
script:
......
include README.md LICENSE
......@@ -2,36 +2,37 @@
# Based on code from https://github.com/bachya/simplisafe-python/blob/dev/Makefile
coverage:
#Not implemented yet
#pipenv run py.test -s --verbose --cov-report term-missing --cov-report xml --cov=alexapy tests
#poetry run py.test -s --verbose --cov-report term-missing --cov-report xml --cov=alexapy tests
bump:
pipenv run semantic-release release
pipenv run semantic-release changelog
poetry run semantic-release release
poetry run semantic-release changelog
bump_and_publish:
pipenv run semantic-release publish
poetry run semantic-release publish
check_vulns:
pipenv check
poetry run safety check
clean:
rm -rf dist/ build/ .egg alexapy.egg-info/
init:
pip3 install pip pipenv
pipenv lock
pipenv install --three --dev
lint: flake8 docstyle pylint typing
init: setup_env
poetry install
lint: flake8 docstyle pylint typing black
flake8:
pipenv run flake8 alexapy
poetry run flake8 alexapy
docstyle:
pipenv run pydocstyle alexapy
poetry run pydocstyle alexapy
pylint:
pipenv run pylint alexapy
publish:
pipenv run python setup.py sdist bdist_wheel
pipenv run twine upload dist/*
rm -rf dist/ build/ .egg alexapy.egg-info/
poetry run pylint alexapy
black:
poetry run black alexapy
# publish:
# deprecated by semantic-release
# poetry run python setup.py sdist bdist_wheel
# poetry run twine upload dist/*
# rm -rf dist/ build/ .egg alexapy.egg-info/
setup_env:
pip3 install pip pipenv
pipenv sync --three --dev
pip install poetry
test:
#Not implemented yet
#pipenv run py.test
#poetry run py.test
typing:
pipenv run mypy --ignore-missing-imports alexapy
poetry run mypy --ignore-missing-imports alexapy
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
[dev-packages]
aresponses = "*"
detox = "*"
flake8 = "*"
mypy = "*"
pydocstyle = "*"
pylint = "*"
pytest-aiohttp = "*"
pytest-cov = "*"
python-semantic-release = "*"
tox = "*"
twine = "*"
[packages]
bs4 = "*"
aiohttp = "*"
aiofiles = "*"
simplejson = "*"
yarl = "*"
requests = "*"
certifi = "*"
backoff = "*"
This diff is collapsed.
......@@ -7,13 +7,15 @@ Python Package for controlling Alexa devices (echo dot, etc) programmatically.
For more details about this api, please refer to the documentation at
https://gitlab.com/keatontaylor/alexapy
"""
from .__version__ import __version__
from importlib_metadata import version
from .alexaapi import AlexaAPI
from .alexalogin import AlexaLogin
from .alexawebsocket import WebsocketEchoClient
from .errors import AlexapyConnectionError, AlexapyLoginError
from .helpers import hide_email, hide_serial
__version__ = version("alexapy")
__all__ = [
"AlexaLogin",
"AlexaAPI",
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: Apache-2.0
"""
Python Package for controlling Alexa devices (echo dot, etc) programmatically.
For more details about this api, please refer to the documentation at
https://gitlab.com/keatontaylor/alexapy
"""
__version__ = "1.5.1"
......@@ -24,7 +24,11 @@ from yarl import URL
import backoff
from .alexalogin import AlexaLogin
from .errors import AlexapyLoginError, AlexapyTooManyRequestsError
from .errors import (
AlexapyLoginError,
AlexapyTooManyRequestsError,
AlexapyConnectionError,
)
from .helpers import _catch_all_exceptions, hide_email
_LOGGER = logging.getLogger(__name__)
......@@ -64,14 +68,14 @@ class AlexaAPI:
ex,
)
@_catch_all_exceptions
@backoff.on_exception(
backoff.expo,
AlexapyTooManyRequestsError,
(AlexapyTooManyRequestsError, AlexapyConnectionError),
max_time=60,
max_tries=5,
logger=__name__,
)
@_catch_all_exceptions
async def _request(
self,
method: Text,
......@@ -132,14 +136,14 @@ class AlexaAPI:
return await self._request("delete", uri, data)
@staticmethod
@_catch_all_exceptions
@backoff.on_exception(
backoff.expo,
AlexapyTooManyRequestsError,
(AlexapyTooManyRequestsError, AlexapyConnectionError),
max_time=60,
max_tries=5,
logger=__name__,
)
@_catch_all_exceptions
async def _static_request(
method: Text,
login: AlexaLogin,
......
......@@ -325,7 +325,6 @@ class AlexaLogin:
) -> None:
# pylint: disable=too-many-branches,too-many-locals,
# pylint: disable=too-many-statements
# pylint: disable=import-outside-toplevel
"""Login to Amazon."""
data = data or {}
if cookies and await self.test_loggedin(cookies):
......@@ -617,7 +616,7 @@ class AlexaLogin:
site = status["ap_error_href"]
_LOGGER.debug("Found post url to get; forcing get to %s", site)
self._lastreq = None
elif formsite != "get":
elif formsite and formsite != "get":
site = formsite
_LOGGER.debug("Found post url to %s", site)
return site
......
......@@ -256,6 +256,9 @@ class WebsocketEchoClient:
await self.websocket.send_bytes(self._encode_gw_handshake())
await asyncio.sleep(0.1)
await self.websocket.send_bytes(self._encode_gw_register())
await asyncio.sleep(0.1)
await self.websocket.send_bytes(self._encode_ping())
await asyncio.sleep(0.5)
if not self.websocket.closed:
await self.open_callback()
......@@ -297,3 +300,15 @@ class WebsocketEchoClient:
msg += '{"command":"REGISTER_CONNECTION"}' # Message UUID
msg += "FABE"
return bytes(msg, "utf-8")
def _encode_ping(self) -> bytes:
# pylint: disable=no-self-use
_LOGGER.debug("Encoding PING.")
msg = "MSG 0x00000065 " # MSG channel
msg += "0x0e414e47 f 0x00000001 " # Message number with no cont
msg += "0xbc2fbb5f " # Checksum
msg += "0x00000062 " # Content Length
msg += "PIN30" # Message content
msg += "FABE"
return bytes(msg, "utf-8")
# MSG 0x00000065 0x0e414e47 f 0x00000001 0xbc2fbb5f 0x00000062 PIN" + 30 + "FABE"
......@@ -11,6 +11,7 @@ https://gitlab.com/keatontaylor/alexapy
"""
import logging
from json import JSONDecodeError
from json.decoder import JSONDecodeError as JSONDecodeError2
from aiohttp import ClientConnectionError
......@@ -66,7 +67,7 @@ def _catch_all_exceptions(func):
template = "An exception of type {0} occurred." " Arguments:\n{1!r}"
try:
return func(*args, **kwargs)
except ClientConnectionError as ex:
except (ClientConnectionError, KeyError) as ex:
message = template.format(type(ex).__name__, ex.args)
_LOGGER.error(
"%s.%s: A connection error occured: %s",
......@@ -75,7 +76,7 @@ def _catch_all_exceptions(func):
message,
)
raise AlexapyConnectionError
except JSONDecodeError as ex:
except (JSONDecodeError, JSONDecodeError2) as ex:
message = template.format(type(ex).__name__, ex.args)
_LOGGER.error(
"%s.%s: A login error occured: %s",
......
This diff is collapsed.
# SPDX-License-Identifier: Apache-2.0
[tool.poetry]
name = "AlexaPy"
version = "1.5.1"
description = "Python API to control Amazon Echo Devices Programatically."
authors = ["Keaton Taylor <[email protected]>", "Alan Tse <[email protected]"]
license = "Apache-2.0"
readme = 'README.md'
repository = "https://gitlab.com/keatontaylor/alexapy"
keywords = ['amazon', 'alexa', 'homeassistant']
include = ["LICENSE"]
[tool.poetry.dependencies]
python = "^3.6"
bs4 = "*"
aiohttp = "*"
aiofiles = "*"
simplejson = "*"
yarl = "*"
requests = "*"
certifi = "*"
backoff = "*"
[tool.poetry.dev-dependencies]
aresponses = "*"
detox = "*"
flake8 = "*"
mypy = "*"
pydocstyle = "*"
pylint = "*"
pytest-aiohttp = "*"
pytest-cov = "*"
python-semantic-release = "*"
tox = "*"
safety = "^1.8.7"
black = {version = "^19.10b0", allow-prereleases = true}
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
[semantic_release]
version_variable=alexapy/__version__.py:__version__
version_variable=pyproject.toml:version
upload_to_pypi=true
check_build_status=false
remove_dist=false
......@@ -13,7 +13,7 @@ exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
# To work with Black
max-line-length = 88
# E501: line too long
# H301: on mport per line
# H301: one import per line
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: Apache-2.0
# Note: To use the "upload" functionality of this file, you must:
# $ pip install twine
# sourced from https://github.com/kennethreitz/setup.py
"""
Python Package for controlling Alexa devices (echo dot, etc) programmatically.
For more details about this api, please refer to the documentation at
https://gitlab.com/keatontaylor/alexapy
"""
import io
import os
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
# Package meta-data.
NAME = "AlexaPy"
DESCRIPTION = "Python API to control Amazon Echo Devices Programatically."
URL = "https://gitlab.com/keatontaylor/alexapy"
EMAIL = "[email protected]"
AUTHOR = "Keaton Taylor"
REQUIRES_PYTHON = ">=3.6.0"
LICENSE = "Apache-2.0"
VERSION = None
# What packages are required for this module to be executed?
REQUIRED = [
'bs4',
'simplejson',
'aiohttp',
'aiofiles',
'yarl',
'requests',
'certifi',
'backoff'
]
# What packages are optional?
EXTRAS = {
# "fancy feature": ["django"],
}
# The rest you shouldn"t have to touch too much :)
# ------------------------------------------------
# Except, perhaps the License and Trove Classifiers!
# If you do change the License, remember to change the Trove Classifier for
# that!
HERE = os.path.abspath(os.path.dirname(__file__))
# Import the README and use it as the long-description.
# Note: this will only work if "README.md" is present in your MANIFEST.in file!
try:
with io.open(os.path.join(HERE, "README.md"), encoding="utf-8") as f:
LONG_DESCRIPTION = "\n" + f.read()
except FileNotFoundError:
LONG_DESCRIPTION = DESCRIPTION
# Load the package"s __version__.py module as a dictionary.
ABOUT = {}
if not VERSION:
PROJECT_SLUG = NAME.lower().replace("-", "_").replace(" ", "_")
with open(os.path.join(HERE, PROJECT_SLUG, "__version__.py")) as f:
exec(f.read(), ABOUT) # pylint: disable=exec-used
else:
ABOUT["__version__"] = VERSION
class UploadCommand(Command):
"""Support setup.py upload."""
description = "Build and publish the package."
user_options = []
@staticmethod
def status(string):
"""Print things in bold."""
print("\033[1m{0}\033[0m".format(string))
def initialize_options(self):
"""Initialize options."""
def finalize_options(self):
"""Finalize options."""
def run(self):
"""Run UploadCommand."""
try:
self.status("Removing previous builds…")
rmtree(os.path.join(HERE, "dist"))
except OSError:
pass
self.status("Building Source and Wheel (universal) distribution…")
os.system("{0} setup.py sdist bdist_wheel --universal".
format(sys.executable))
self.status("Uploading the package to PyPI via Twine…")
os.system("twine upload dist/*")
self.status("Pushing git tags…")
os.system("git tag v{0}".format(ABOUT["__version__"]))
os.system("git push --tags")
sys.exit()
# Where the magic happens:
setup(
name=NAME,
version=ABOUT["__version__"],
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown",
author=AUTHOR,
author_email=EMAIL,
python_requires=REQUIRES_PYTHON,
url=URL,
packages=find_packages(exclude=("tests",)),
# If your package is a single module, use this instead of "packages":
# py_modules=["mypackage"],
# entry_points={
# "console_scripts": ["mycli=mymodule:cli"],
# },
install_requires=REQUIRED,
extras_require=EXTRAS,
include_package_data=True,
license=LICENSE,
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: Apache Software License",
"Topic :: Utilities"
],
# $ setup.py publish support.
cmdclass={
"upload": UploadCommand,
},
)