Skip to content

Commit 07ccc6d

Browse files
authored
Merge pull request #8 from nahum365/linter
Merge version 0.0.1 and documentation
2 parents 4f5b565 + 4fdb72a commit 07ccc6d

File tree

8 files changed

+289
-28
lines changed

8 files changed

+289
-28
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*/__pycache__
2+
env
3+
.idea
4+
docs/_build
5+
docs/_static
6+
docs/_templates

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ python:
55
install:
66
- pip install -r requirements.txt
77
script:
8-
- flake8 nestdeviceaccess/
8+
- flake8 --max-line-length 120 nestdeviceaccess/
99
- pytest nestdeviceaccess/test_nestdeviceaccess.py
1010
- coverage run -m pytest nestdeviceaccess/test_nestdeviceaccess.py
1111
after_success:

docs/Makefile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Minimal makefile for Sphinx documentation
2+
#
3+
4+
# You can set these variables from the command line, and also
5+
# from the environment for the first two.
6+
SPHINXOPTS ?=
7+
SPHINXBUILD ?= sphinx-build
8+
SOURCEDIR = .
9+
BUILDDIR = _build
10+
11+
# Put it first so that "make" without argument is like "make help".
12+
help:
13+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14+
15+
.PHONY: help Makefile
16+
17+
# Catch-all target: route all unknown targets to Sphinx using the new
18+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19+
%: Makefile
20+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

docs/conf.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import sphinx_rtd_theme
2+
from recommonmark.transform import AutoStructify
3+
4+
# Configuration file for the Sphinx documentation builder.
5+
#
6+
# This file only contains a selection of the most common options. For a full
7+
# list see the documentation:
8+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
9+
10+
# -- Path setup --------------------------------------------------------------
11+
12+
# If extensions (or modules to document with autodoc) are in another directory,
13+
# add these directories to sys.path here. If the directory is relative to the
14+
# documentation root, use os.path.abspath to make it absolute, like shown here.
15+
#
16+
# import os
17+
# import sys
18+
# sys.path.insert(0, os.path.abspath('.'))
19+
20+
21+
# -- Project information -----------------------------------------------------
22+
23+
project = 'python-nestdeviceaccess'
24+
copyright = '2020, Nahum Getachew'
25+
author = 'Nahum Getachew'
26+
27+
# The full version, including alpha/beta/rc tags
28+
release = '0.0.1'
29+
30+
31+
# -- General configuration ---------------------------------------------------
32+
33+
# Add any Sphinx extension module names here, as strings. They can be
34+
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
35+
# ones.
36+
extensions = ['recommonmark']
37+
source_suffix = ['.rst', '.md']
38+
39+
# Add any paths that contain templates here, relative to this directory.
40+
templates_path = ['_templates']
41+
42+
# List of patterns, relative to source directory, that match files and
43+
# directories to ignore when looking for source files.
44+
# This pattern also affects html_static_path and html_extra_path.
45+
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
46+
47+
48+
# -- Options for HTML output -------------------------------------------------
49+
50+
# The theme to use for HTML and HTML Help pages. See the documentation for
51+
# a list of builtin themes.
52+
#
53+
54+
html_theme = "sphinx_rtd_theme"
55+
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
56+
57+
# Add any paths that contain custom static files (such as style sheets) here,
58+
# relative to this directory. They are copied after the builtin static files,
59+
# so a file named "default.css" will overwrite the builtin "default.css".
60+
html_static_path = ['_static']
61+
62+
63+
def setup(app):
64+
app.add_config_value('recommonmark_config', {
65+
'auto_toc_tree_section': 'Contents',
66+
}, True)
67+
app.add_transform(AutoStructify)

docs/index.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Welcome to python-nestdeviceaccess's documentation!
2+
3+
## Overview
4+
This Python library seeks to allow easy access to Google Nest devices using the Nest Device Access Smart Management API.
5+
It allows interfacing with:
6+
7+
- Nest Hello doorbell
8+
9+
Coming soon:
10+
11+
- Nest thermostat

docs/make.bat

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@ECHO OFF
2+
3+
pushd %~dp0
4+
5+
REM Command file for Sphinx documentation
6+
7+
if "%SPHINXBUILD%" == "" (
8+
set SPHINXBUILD=sphinx-build
9+
)
10+
set SOURCEDIR=.
11+
set BUILDDIR=_build
12+
13+
if "%1" == "" goto help
14+
15+
%SPHINXBUILD% >NUL 2>NUL
16+
if errorlevel 9009 (
17+
echo.
18+
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19+
echo.installed, then set the SPHINXBUILD environment variable to point
20+
echo.to the full path of the 'sphinx-build' executable. Alternatively you
21+
echo.may add the Sphinx directory to PATH.
22+
echo.
23+
echo.If you don't have Sphinx installed, grab it from
24+
echo.http://sphinx-doc.org/
25+
exit /b 1
26+
)
27+
28+
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29+
goto end
30+
31+
:help
32+
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33+
34+
:end
35+
popd

nestdeviceaccess/nestdeviceaccess.py

+124-26
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import requests
2-
from requests import auth
2+
import pickle
3+
import os
34

45
AUTH_URI = "https://www.googleapis.com/oauth2/v4/token"
56
DEFAULT_BASE_URI = "https://smartdevicemanagement.googleapis.com/v1"
67
DEFAULT_REDIRECT_URI = "https://www.google.com"
8+
DEVICES_URI = (
9+
"https://smartdevicemanagement.googleapis.com/v1/enterprises/{project_id}/devices"
10+
)
711

812

913
class ArgumentsMissingError(Exception):
@@ -14,45 +18,139 @@ class AuthorizationError(Exception):
1418
pass
1519

1620

17-
class NestDeviceAccessAuth(auth.AuthBase):
18-
def __init__(self, auth_callback, client_id, client_secret, code, session=None, redirect_uri=DEFAULT_REDIRECT_URI):
19-
if not client_id or client_secret or not code or client_id == "" or client_secret == "" or code == "":
21+
class NestDeviceAccessAuth(requests.auth.AuthBase):
22+
def __init__(
23+
self,
24+
project_id,
25+
client_id,
26+
client_secret,
27+
code,
28+
session=None,
29+
redirect_uri=DEFAULT_REDIRECT_URI,
30+
):
31+
if client_id == "" or client_secret == "" or project_id == "":
2032
raise ArgumentsMissingError()
21-
self._res = {}
33+
34+
self.project_id = project_id
2235
self.client_id = client_id
2336
self.client_secret = client_secret
37+
38+
if not code or code == "":
39+
self.invalid_token()
40+
raise ArgumentsMissingError()
41+
2442
self.code = code
43+
44+
self._res = {}
45+
self.access_token = None
46+
self.refresh_token = None
2547
self.redirect_uri = redirect_uri
2648
self._session = session
27-
self.auth_callback = auth_callback
28-
29-
def login(self, headers=None):
30-
data = {'client_id': self.client_id,
31-
'client_secret': self.client_secret,
32-
'code': self.code,
33-
'grant_type': 'authorization_code',
34-
'redirect_uri': self.redirect_uri}
35-
post = requests.post
36-
response = post(AUTH_URI, params=data, headers=headers)
49+
50+
def login(self):
51+
if os.path.exists("auth.bak"):
52+
with open("auth.bak", "rb") as token:
53+
creds = pickle.load(token)
54+
self.access_token = creds["access"]
55+
self.refresh_token = creds["refresh"]
56+
return
57+
58+
data = {
59+
"client_id": self.client_id,
60+
"client_secret": self.client_secret,
61+
"code": self.code,
62+
"grant_type": "authorization_code",
63+
"redirect_uri": self.redirect_uri,
64+
}
65+
response = requests.post(AUTH_URI, params=data)
3766
if response.status_code != 200:
38-
print(response.content)
67+
if response.status_code == 400:
68+
self.invalid_token()
3969
raise AuthorizationError(response)
4070
self._res = response.json()
41-
self.auth_callback(self._res)
71+
self.access_token = self._res["access_token"]
72+
self.refresh_token = self._res["refresh_token"]
73+
74+
with open("auth.bak", "wb") as token:
75+
pickle.dump(
76+
{"access": self.access_token, "refresh": self.refresh_token}, token
77+
)
78+
79+
def __call__(self, r):
80+
if self.access_token is not None:
81+
r.headers["authorization"] = "Bearer " + self.access_token
82+
return r
83+
else:
84+
self.login()
85+
r.headers["authorization"] = "Bearer " + self.access_token
86+
return r
87+
88+
def invalid_token(self):
89+
print(
90+
f"Go to this link to get OAuth token: https://nestservices.google.com/partnerconnections/{self.project_id}"
91+
f"/auth?redirect_uri=https://www.google.com&access_type=offline&prompt=consent&client_id={self.client_id}"
92+
f"&response_type=code&scope=https://www.googleapis.com/auth/sdm.service"
93+
)
94+
95+
96+
class Device(object):
97+
def __init__(self, dict):
98+
self.name = dict["name"]
99+
self.type = dict["type"]
100+
self.traits = dict["traits"]
42101

43102

44103
class NestDeviceAccess(object):
45-
def __init__(self, client_id, client_secret, code, redirect_uri=DEFAULT_REDIRECT_URI):
46-
auth = NestDeviceAccessAuth(client_id, client_secret, code, self.login_callback, redirect_uri)
47-
self._session = requests.Session()
48-
self._session.auth = auth
104+
def __init__(
105+
self,
106+
project_id,
107+
client_id,
108+
client_secret,
109+
code,
110+
redirect_uri=DEFAULT_REDIRECT_URI,
111+
):
112+
self.auth = NestDeviceAccessAuth(
113+
project_id, client_id, client_secret, code, redirect_uri
114+
)
115+
self.access_token = None
116+
self.refresh_token = None
49117

50-
def login_callback(self, res):
51-
print(res)
118+
self.project_id = project_id
119+
self.client_id = client_id
120+
self.client_secret = client_secret
52121

53122
def login(self):
54-
self._session.auth.login()
123+
try:
124+
self.auth.login()
125+
except AuthorizationError:
126+
print("Authorization Error")
127+
pass
128+
129+
def devices(self):
130+
if not self.auth.access_token:
131+
raise AuthorizationError()
132+
133+
response = requests.get(
134+
DEVICES_URI.format(project_id=self.project_id), auth=self.auth
135+
)
136+
if response.status_code != 200:
137+
if response.status_code == 400:
138+
raise AuthorizationError(response)
139+
devices_dict = response.json()
140+
141+
devices = []
142+
for device in devices_dict["devices"]:
143+
devices.append(Device(device))
144+
return devices
55145

56146

57-
#nda = NestDeviceAccess(client_id="484808906646-a9tche4b03q56u47fiojh04tbf7r56m8.apps.googleusercontent.com", client_secret="uS-rH6Fqcr_d_vsTHpZOZd6l", code="4/5AE-0v8n60-tkng0pcvVhgviF1i77yUfpseFxiURRzPi_vdlHDRCe2KL8nOnUa0AcDF4a2aKHarqChm6SQC4FoQ")
58-
#nda.login()
147+
if __name__ == "__main__":
148+
nda = NestDeviceAccess(
149+
project_id="2a7ad63f-af0f-414a-b218-23dd6b39d0c5",
150+
client_id="484808906646-a9tche4b03q56u47fiojh04tbf7r56m8.apps.googleusercontent.com",
151+
client_secret="uS-rH6Fqcr_d_vsTHpZOZd6l",
152+
code="4/0AY0e-g6INJsCbfeHVxZV_Eg1jSEdWaxI22DgTfxUFTbPSBMXAEexjT_4VY9Rf1H5jht-hQ",
153+
)
154+
nda.login()
155+
for device in nda.devices():
156+
print(device.name)

nestdeviceaccess/test_nestdeviceaccess.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,29 @@ def callback():
88

99
def test_require_all_arguments():
1010
with pytest.raises(nestdeviceaccess.ArgumentsMissingError):
11-
_ = nestdeviceaccess.NestDeviceAccess(client_secret="", client_id="", code="")
11+
_ = nestdeviceaccess.NestDeviceAccess(project_id="", client_secret="", client_id="", code="")
1212

13+
14+
def test_url_printed_when_auth_code_not_present(capsys):
15+
try:
16+
_ = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="")
17+
except nestdeviceaccess.ArgumentsMissingError:
18+
pass
19+
captured = capsys.readouterr()
20+
assert captured.out == "Go to this link to get OAuth token: " \
21+
"https://nestservices.google.com/partnerconnections/123/auth?redirect_uri=https://www" \
22+
".google.com&access_type=offline&prompt=consent&client_id=123&response_type=code&scope" \
23+
"=https://www.googleapis.com/auth/sdm.service\n"
24+
25+
26+
def test_devices_call_without_login_fails():
27+
with pytest.raises(nestdeviceaccess.AuthorizationError):
28+
nda = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="123")
29+
nda.devices()
30+
31+
32+
def test_failed_login_prints_error(capsys):
33+
nda = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="123")
34+
nda.login()
35+
captured = capsys.readouterr()
36+
assert captured.out == "Authorization Error\n"

0 commit comments

Comments
 (0)