diff --git a/README.rst b/README.rst index 9c56e16..72d71aa 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,9 @@ Python Arlo Python Arlo is a library written in Python 2.7/3x that exposes the Netgear Arlo cameras as Python objects. +Developer Documentation: `http://python-arlo.readthedocs.io/ `_ + + Installation ------------ @@ -41,24 +44,80 @@ Usage # listing base stations arlo.base_stations + # get base station handle + # assuming only 1 base station is available + base = arlo.base_stations[0] + # listing Arlo modes base.available_modes ['armed', 'disarmed', 'schedule', 'custom'] - # setting a mode - garage_cam.mode = 'armed' - # listing all cameras arlo.cameras # showing camera preferences cam = arlo.cameras[0] + # check if camera is connected to base station + cam.is_camera_connected + True + + # setting a mode + cam.mode = 'armed' + + # getting the current active mode + cam.mode + 'armed' + # printing camera attributes cam.serial_number cam.model_id cam.unseen_videos + # get brightness value of camera + cam.get_brightness + + # get signal strength of camera with base station + cam.get_signal_strength + + # get flip property from camera + cam.get_flip_state + + # get mirror property from camera + cam.get_mirror_state + + # get power save mode value from camera + cam.get_powersave_mode + + # get current battery level of camera + cam.get_battery_level + 92 + + # get boolean result if motion detection + # is enabled or not + cam.is_motion_detection_enabled + True + + # get battery levels of all cameras + # prints serial number and battery level of each camera + base.get_camera_battery_level + {'4N71235T12345': 92, '4N71235T12345': 90} + + # get base station properties + base.get_basestation_properties + + # get camera properties + base.get_camera_properties + + # get camera rules + base.get_camera_rules + + # get camera schedule + base.get_camera_schedule + + # get camera motion detection sensitivity + cam.get_motion_detection_sensitivity + # refreshing camera properties cam.update() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..f2f0ccf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = PyArlo +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..c8c5cd4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# PyArlo documentation build configuration file, created by +# sphinx-quickstart on Tue Jun 27 00:04:52 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'PyArlo' +copyright = '2017, Marcelo Moreira de Mello ' +author = 'Marcelo Moreira de Mello ' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '' +# The full version, including alpha/beta/rc tags. +release = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PyArlodoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'PyArlo.tex', 'PyArlo Documentation', + 'Marcelo Moreira de Mello \\textless{}tchello.mello@gmail.com\\textgreater{}', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pyarlo', 'PyArlo Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PyArlo', 'PyArlo Documentation', + author, 'PyArlo', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..1a9a9fe --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,63 @@ +.. PyArlo documentation master file, created by + sphinx-quickstart on Tue Jun 27 00:04:52 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PyArlo's documentation! +================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +Python Arlo is a library written in Python 2.7/3x that exposes the Netgear Arlo cameras as Python objects. + +Documentation for developers. + + +PyArlo +------ +.. autoclass:: pyarlo.PyArlo + :members: + :undoc-members: + :show-inheritance: + + +ArloBaseStation +--------------- +.. autoclass:: pyarlo.base_station.ArloBaseStation + :members: + :undoc-members: + :show-inheritance: + +ArloCamera +---------- +.. autoclass:: pyarlo.camera.ArloCamera + :members: + :undoc-members: + :show-inheritance: + +ArloMediaLibrary +---------------- +.. autoclass:: pyarlo.media.ArloMediaLibrary + :members: + :undoc-members: + :show-inheritance: + +ArloVideo +--------- +.. autoclass:: pyarlo.media.ArloVideo + :members: + :undoc-members: + :show-inheritance: + + + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..36f959c --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=PyArlo + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/pyarlo/__init__.py b/pyarlo/__init__.py index fd80d46..0cee828 100644 --- a/pyarlo/__init__.py +++ b/pyarlo/__init__.py @@ -7,9 +7,9 @@ from pyarlo.camera import ArloCamera from pyarlo.media import ArloMediaLibrary from pyarlo.const import ( - BILLING_ENDPOINT, DEVICES_ENDPOINT, FRIENDS_ENDPOINT, - LOGIN_ENDPOINT, PROFILE_ENDPOINT, PRELOAD_DAYS, - RESET_ENDPOINT) + BILLING_ENDPOINT, DEVICES_ENDPOINT, + FRIENDS_ENDPOINT, LOGIN_ENDPOINT, PROFILE_ENDPOINT, + PRELOAD_DAYS, RESET_ENDPOINT) _LOGGER = logging.getLogger(__name__) @@ -40,6 +40,7 @@ def __init__(self, username=None, password=None, self.__password = password self.__username = username self.session = requests.Session() + self.__base_stations = [] # login user self.login() @@ -90,7 +91,8 @@ def query(self, extra_params=None, extra_headers=None, retry=3, - raw=False): + raw=False, + stream=False): """ Return a JSON object or raw session. @@ -100,6 +102,7 @@ def query(self, :param extra_headers: Dictionary to be apppended on request.headers :param retry: Attempts to retry a query. Default is 3. :param raw: Boolean if query() will return request object instead JSON. + :param stream: Boolean if query() will return a stream object. """ response = None loop = 0 @@ -129,7 +132,7 @@ def query(self, # define connection method if method == 'GET': - req = self.session.get(url, headers=headers) + req = self.session.get(url, headers=headers, stream=stream) elif method == 'PUT': req = self.session.put(url, json=params, headers=headers) elif method == 'POST': @@ -145,6 +148,7 @@ def query(self, # leave if everything worked fine break + return response @property @@ -177,9 +181,10 @@ def devices(self): if device.get('deviceType') == 'basestation' and \ device.get('state') == 'provisioned': - devices['base_station'].append(ArloBaseStation(name, - device, - self)) + base = ArloBaseStation(name, device, self.__token, self) + devices['base_station'].append(base) + self.__base_stations.append(base) + return devices def lookup_camera_by_id(self, device_id): @@ -222,7 +227,7 @@ def profile(self): @property def is_connected(self): - """Return connection status.""" + """Connection status of client with Arlo system.""" return bool(self.authenticated) def update(self): diff --git a/pyarlo/base_station.py b/pyarlo/base_station.py index 94f91ae..f8ea1ea 100644 --- a/pyarlo/base_station.py +++ b/pyarlo/base_station.py @@ -1,29 +1,173 @@ # coding: utf-8 """Generic Python Class file for Netgear Arlo Base Station module.""" +import json +import threading +import time import logging -from pyarlo.const import ACTION_MODES, NOTIFY_ENDPOINT, RUN_ACTION_BODY - +import sseclient +from pyarlo.const import ( + ACTION_BODY, SUBSCRIBE_ENDPOINT, UNSUBSCRIBE_ENDPOINT, + ACTION_MODES, NOTIFY_ENDPOINT, RESOURCES) _LOGGER = logging.getLogger(__name__) class ArloBaseStation(object): """Arlo Base Station module implementation.""" - def __init__(self, name, attrs, arlo_session): + def __init__(self, name, attrs, session_token, arlo_session): """Initialize Arlo Base Station object. :param name: Base Station name :param attrs: Attributes + :param session_token: Session token passed by camera class :param arlo_session: PyArlo shared session """ self.name = name self._attrs = attrs self._session = arlo_session + self._session_token = session_token + self.__sseclient = None + self.__subscribed = False + self.__events = [] def __repr__(self): """Representation string of object.""" return "<{0}: {1}>".format(self.__class__.__name__, self.name) + def thread_function(self): + """Thread function.""" + url = SUBSCRIBE_ENDPOINT + "?token=" + self._session_token + data = self._session.query(url, method='GET', raw=True, stream=True) + self.__sseclient = sseclient.SSEClient(data) + if self.__sseclient: + self.__subscribed = True + for event in (self.__sseclient).events(): + if not self.__subscribed: + break + data = json.loads(event.data) + if data.get('status') == "connected": + _LOGGER.debug("Successfully subscribed this base station") + elif data.get('action'): + action = data.get('action') + resource = data.get('resource') + if action == "logout": + _LOGGER.debug("Logged out by some other entity") + self.__subscribed = False + break + elif action == "is" and "subscriptions/" not in resource: + self.__events.append(data) + + def _get_event_stream(self): + """Spawn a thread and monitor the Arlo Event Stream.""" + event_thread = threading.Thread(target=self.thread_function) + event_thread.start() + + def _subscribe_myself(self): + """Subscribe this base station for all events.""" + return self.__run_action( + method='SET', + resource='subscribe', + mode=None, + publish_response=False) + + def _unsubscribe_myself(self): + """Unsubscribe this base station for all events.""" + url = UNSUBSCRIBE_ENDPOINT + return self._session.query(url, method='GET', raw=True, stream=False) + + def _close_event_stream(self): + """Stop the Event stream thread.""" + self.__subscribed = False + + def publish_and_get_event(self, resource): + """Publish and get the event from base station.""" + self._get_event_stream() + self._subscribe_myself() + + this_event = '' + status = self.__run_action( + method='GET', + resource=resource, + mode=None, + publish_response=False) + if status == 'success': + for i in range(0, 5): + _LOGGER.debug("Trying instance " + str(i)) + time.sleep(1) + for event in self.__events: + if event['resource'] == resource: + this_event = event + self.__events.remove(event) + break + if this_event: + break + + self._unsubscribe_myself() + self._close_event_stream() + + if this_event: + return this_event + + return None + + def __run_action( + self, + method='GET', + resource=None, + mode=None, + publish_response=None): + """Run action. + + :param method: Specify the method GET, POST or PUT. Default is GET. + :param resource: Specify one of the resources to fetch from arlo. + :param mode: Specify the mode to set, else None for GET operations + :param publish_response: Set to True for SETs. Default False + """ + url = NOTIFY_ENDPOINT.format(self.device_id) + + body = ACTION_BODY + + if resource: + body['resource'] = resource + + if method == 'GET': + body['action'] = "get" + body['properties'] = None + elif method == 'SET': + body['action'] = "set" + if resource == 'schedule': + body['properties'] = {'active': 'true'} + elif resource == 'subscribe': + body['resource'] = "subscriptions/" + \ + "{0}_web".format(self.user_id) + dev = [] + dev.append(self.device_id) + body['properties'] = {'devices': dev} + elif resource == 'modes': + body['properties'] = {'active': ACTION_MODES.get(mode)} + else: + _LOGGER.info("Invalid method requested") + return None + + if not publish_response: + body['publishResponse'] = 'false' + else: + body['publishResponse'] = 'true' + + body['from'] = "{0}_web".format(self.user_id) + body['to'] = self.device_id + body['transId'] = "web!e6d1b969.8aa4b!1498165992111" + + _LOGGER.info("Action body: %s", body) + + ret = \ + self._session.query(url, method='POST', extra_params=body, + extra_headers={"xCloudId": self.xcloud_id}) + if ret.get('success'): + return 'success' + + return None + # pylint: disable=invalid-name @property def device_id(self): @@ -75,36 +219,101 @@ def xcloud_id(self): """Return X-Cloud-ID attribute.""" return self._attrs.get('xCloudId') - def __run_action(self, action): - """Run action.""" - url = NOTIFY_ENDPOINT.format(self.device_id) - body = RUN_ACTION_BODY - body['from'] = "{0}_web".format(self.user_id) - body['to'] = self.device_id - body['properties'] = {'active': ACTION_MODES.get(action)} - - # if action is schedule, modify resource and properties - if action == 'schedule': - body['resource'] = 'schedule' - body['properties'] = {'active': 'true'} - - _LOGGER.debug("Action body: %s", body) - - ret = \ - self._session.query(url, method='POST', extra_params=body, - extra_headers={"xCloudId": self.xcloud_id}) - return ret.get('success') - @property def available_modes(self): """Return list of available modes.""" return list(ACTION_MODES.keys()) + @property + def available_resources(self): + """Return list of available resources.""" + return list(RESOURCES.keys()) + @property def mode(self): """Return current mode.""" + resource = "modes" + mode_event = self.publish_and_get_event(resource) + if mode_event: + active_mode = mode_event['properties']['active'] + modes = mode_event['properties']['modes'] + for mode in modes: + if mode['id'] == active_mode: + return mode['type'] + + return None + + @property + def get_camera_properties(self): + """Return camera properties.""" + resource = "cameras" + resource_event = self.publish_and_get_event(resource) + if resource_event: + return resource_event['properties'] + + return None + + @property + def get_camera_battery_level(self): + """Return a list of battery levels of all cameras.""" + battery_levels = {} + resource = "cameras" + resource_event = self.publish_and_get_event(resource) + if resource_event: + cameras = resource_event['properties'] + for camera in cameras: + battery_levels[camera['serialNumber']] = camera['batteryLevel'] + return battery_levels + + return None + + @property + def get_basestation_properties(self): + """Return the base station info.""" + resource = "basestation" + basestn_event = self.publish_and_get_event(resource) + if basestn_event: + return basestn_event['properties'] + return None + @property + def get_camera_rules(self): + """Return the camera rules.""" + resource = "rules" + rules_event = self.publish_and_get_event(resource) + if rules_event: + return rules_event['properties'] + + return None + + @property + def get_camera_schedule(self): + """Return the schedule set for cameras.""" + resource = "schedule" + schedule_event = self.publish_and_get_event(resource) + if schedule_event: + return schedule_event['properties'] + + return None + + @property + def is_motion_detection_enabled(self): + """Return Boolean if motion is enabled.""" + return bool(self.mode == "armed" or self.get_camera_schedule['active']) + + @property + def subscribe(self): + """Subscribe this session with Arlo system.""" + self._get_event_stream() + self._subscribe_myself() + + @property + def unsubscribe(self): + """Unsubscribe this session.""" + self._unsubscribe_myself() + self._close_event_stream() + @mode.setter def mode(self, mode): """Set Arlo camera mode. @@ -113,7 +322,12 @@ def mode(self, mode): """ if mode not in ACTION_MODES.keys(): return "Invalid mode" - return self.__run_action(mode) + self.__run_action( + method='SET', + resource='modes', + mode=mode, + publish_response=True) + self.update() def update(self): """Update object properties.""" diff --git a/pyarlo/camera.py b/pyarlo/camera.py index 1bbe03f..cc5b93e 100644 --- a/pyarlo/camera.py +++ b/pyarlo/camera.py @@ -125,6 +125,103 @@ def xcloud_id(self): """Return X-Cloud-ID attribute.""" return self._attrs.get('xCloudId') + @property + def get_battery_level(self): + """Get the camera battery level.""" + base = self._session.base_stations[0] + return base.get_camera_battery_level[self.device_id] + + @property + def get_signal_strength(self): + """Get the camera Signal strength.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + return cam['signalStrength'] + + return None + + @property + def get_brightness(self): + """Get the brightness property of camera.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + return cam['brightness'] + + return None + + @property + def get_mirror_state(self): + """Get the brightness property of camera.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + return cam['flip'] + + return None + + @property + def get_flip_state(self): + """Get the brightness property of camera.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + return cam['mirror'] + + return None + + @property + def get_powersave_mode(self): + """Get the brightness property of camera.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + return cam['powerSaveMode'] + + return None + + @property + def is_camera_connected(self): + """Connectivity status of Cam with Base Station.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + return bool(cam['connectionState'] == 'available') + + return None + + @property + def get_motion_detection_sensitivity(self): + """Sensitivity level of Camera motion detection.""" + base = self._session.base_stations[0] + props = base.get_camera_properties + if not props: + return None + for cam in props: + if cam['serialNumber'] == self.device_id: + this_cam = cam + triggers = this_cam['capabilities'][9]['Triggers'] + return triggers[0]['sensitivity']['default'] + def live_streaming(self): """Return live streaming generator.""" url = STREAM_ENDPOINT diff --git a/pyarlo/const.py b/pyarlo/const.py index 6669873..0358665 100644 --- a/pyarlo/const.py +++ b/pyarlo/const.py @@ -3,11 +3,14 @@ # API Endpoints API_URL = "https://arlo.netgear.com/hmsweb" -BILLING_ENDPOINT = API_URL + "/users/serviceLevel" +DEVICE_SUPPORT_ENDPOINT = API_URL + "/devicesupport/v2" +SUBSCRIBE_ENDPOINT = API_URL + "/client/subscribe" +UNSUBSCRIBE_ENDPOINT = API_URL + "/client/unsubscribe" +BILLING_ENDPOINT = API_URL + "/users/serviceLevel/v2" DEVICES_ENDPOINT = API_URL + "/users/devices" FRIENDS_ENDPOINT = API_URL + "/users/friends" LIBRARY_ENDPOINT = API_URL + "/users/library" -LOGIN_ENDPOINT = API_URL + "/login" +LOGIN_ENDPOINT = API_URL + "/login/v2" LOGOUT_ENDPOINT = API_URL + "/logout" NOTIFY_ENDPOINT = API_URL + "/users/devices/notify/{0}" PROFILE_ENDPOINT = API_URL + "/users/profile" @@ -26,13 +29,22 @@ 'schedule': 'true', } +# define resources +RESOURCES = { + 'base_station': 'base_station', + 'modes': 'modes', + 'schedule': 'schedule', + 'rules': 'rules', + 'cameras': 'cameras', +} + # define body used when executing an action -RUN_ACTION_BODY = { - 'action': 'set', +ACTION_BODY = { + 'action': None, 'from': None, 'properties': None, - 'publishResponse': 'true', - 'resource': 'modes', + 'publishResponse': None, + 'resource': None, 'to': None } diff --git a/requirements.txt b/requirements.txt index f229360..a944a1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests +sseclient-py diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 0000000..6966869 --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1 @@ +sphinx diff --git a/setup.cfg b/setup.cfg index 4382942..d03345c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,4 @@ description-file = README.rst [tool:pytest] testpaths = tests -norecursedirs = .git +norecursedirs = .git, docs diff --git a/setup.py b/setup.py index 64d95dd..7201e39 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='pyarlo', packages=['pyarlo'], - version='0.0.4', + version='0.0.5', description='Python Arlo is a library written in Python 2.7/3x ' + 'that exposes the Netgear Arlo cameras as Python objects.', author='Marcelo Moreira de Mello', @@ -13,7 +13,7 @@ url='https://github.com/tchellomello/python-arlo', license='LGPLv3+', include_package_data=True, - install_requires=['requests'], + install_requires=['requests', 'sseclient-py'], test_suite='tests', keywords=[ 'arlo', @@ -30,6 +30,7 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules' diff --git a/tox.ini b/tox.ini index 0847ccb..bcc1913 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py35, py36, lint +envlist = py27, py34, py35, py36, lint skip_missing_interpreters = True [testenv] @@ -19,3 +19,6 @@ ignore_errors = True commands = flake8 pylint pyarlo + +[flake8] +exclude = docs,.tox,*.egg,*.pyc,.git,__pycache