Compare commits

..
Author SHA1 Message Date
Kevin FronczakandGitHub 37350da1b2 Merge pull request #402 from fronzbot/dev
0.16.4
2020-11-22 23:33:15 -05:00
Kevin FronczakandGitHub 19b7eef123 Merge pull request #363 from fronzbot/dev
0.16.3
2020-08-01 23:33:38 -04:00
Kevin FronczakandGitHub 6bd8d14190 Merge pull request #359 from fronzbot/dev
0.16.2
2020-08-01 23:12:25 -04:00
Kevin FronczakandGitHub f6ddfbe5cf Merge pull request #349 from fronzbot/dev
0.16.1
2020-07-29 11:32:04 -04:00
Kevin FronczakandGitHub 1b66eddfb4 Merge pull request #340 from fronzbot/dev
0.16.0
2020-07-20 23:01:30 -04:00
21 changed files with 88 additions and 254 deletions
-9
View File
@@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
reviewers:
- fronzbot
+2 -3
View File
@@ -1,5 +1,5 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
@@ -8,8 +8,7 @@ daysUntilClose: 7
exemptLabels:
- Priority
- bug
- help wanted
- feature request
- Help Wanted
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
python-version: [3.8]
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.8'
python-version: '3.7'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
+2 -2
View File
@@ -10,11 +10,11 @@ jobs:
build:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
max-parallel: 3
matrix:
platform:
- ubuntu-latest
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
-1
View File
@@ -14,4 +14,3 @@ build/*
docs/_build
*.log
venv
.session*
+25
View File
@@ -0,0 +1,25 @@
image: python
stages:
- test
before_script:
- curl -O https://bootstrap.pypa.io/get-pip.py
- python get-pip.py
- pip install tox
python35:
image: python:3.5
stage: test
script: tox -e py35
python36:
image: python:3.6
stage: test
script: tox -e py36
lint:
image: python:3.6
stage: test
script: tox -e lint
-30
View File
@@ -4,36 +4,6 @@ Changelog
A list of changes between each release
0.17.1 (2021-02-18)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Add delay parameter to Blink.download_videos method in order to throttle API during video retrieval (`#437 <https://github.com/fronzbot/blinkpy/pull/437>`__)
- Bump pylint to 2.6.2
0.17.0 (2021-02-15)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes:**
- Fix video downloading bug (`#424 <https://github.com/fronzbot/blinkpy/pull/424>`__)
- Fix repeated authorization email bug (`#432 <https://github.com/fronzbot/blinkpy/pull/432>`__ and `#428 <https://github.com/fronzbot/blinkpy/pull/428>`__)
**New Features:**
- Add logout method (`#429 <https://github.com/fronzbot/blinkpy/pull/429>`__)
- Add camera record method (`#430 <https://github.com/fronzbot/blinkpy/pull/430>`__)
**Other:**
- Add debug script to main repo to help with general debug
- Upgrade login endpoint from v4 to v5
- Add python 3.9 support
- Bump coverage to 5.4
- Bump pytest to 6.2.2
- Bump pytest-cov to 2.11.1
- Bump pygments to 2.8.0
- Bump pre-commit to 2.10.1
- Bump restructuredtext-lint to 1.3.2
0.16.4 (2020-11-22)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+3 -3
View File
@@ -174,13 +174,13 @@ Similar methods exist for individual cameras:
Download videos
----------------
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``stop=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specify one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras. If you are downloading many items, setting the ``delay`` parameter is advised in order to throttle sequential calls to the API. By default this is set to ``1`` but can be any integer representing the number of seconds to delay between calls.
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``stop=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specify one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras.
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory with a 2s delay between calls:
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
.. code:: python
blink.download_videos('/home/blink', since='2018/07/04 09:34', delay=2)
blink.download_videos('/home/blink', since='2018/07/04 09:34')
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
+3 -7
View File
@@ -23,16 +23,18 @@ def request_login(
headers = {
"Host": DEFAULT_URL,
"Content-Type": "application/json",
"Accept": "/",
"user-agent": DEFAULT_USER_AGENT,
}
data = dumps(
{
"email": login_data["username"],
"password": login_data["password"],
"notification_key": login_data["notification_key"],
"unique_id": login_data["uid"],
"device_identifier": login_data["device_id"],
"client_name": "Computer",
"reauth": True,
"reauth": "false",
}
)
@@ -55,12 +57,6 @@ def request_verify(auth, blink, verify_key):
)
def request_logout(blink):
"""Logout of blink servers."""
url = f"{blink.urls.base_url}/api/v4/account/{blink.account_id}/client/{blink.client_id}/logout"
return http_post(blink, url=url)
def request_networks(blink):
"""Request all networks information."""
url = f"{blink.urls.base_url}/networks"
+14 -25
View File
@@ -58,11 +58,7 @@ class Auth:
"""Return authorization header."""
if self.token is None:
return None
return {
"TOKEN_AUTH": self.token,
"user-agent": DEFAULT_USER_AGENT,
"content-type": "application/json",
}
return {"TOKEN_AUTH": self.token, "user-agent": DEFAULT_USER_AGENT}
def create_session(self, opts=None):
"""Create a session for blink communication."""
@@ -108,12 +104,8 @@ class Auth:
if response.status_code == 200:
return response.json()
raise LoginError
except AttributeError as error:
raise LoginError from error
def logout(self, blink):
"""Log out."""
return api.request_logout(blink)
except AttributeError:
raise LoginError
def refresh_token(self):
"""Refresh auth token."""
@@ -123,21 +115,21 @@ class Auth:
self.login_response = self.login()
self.extract_login_info()
self.is_errored = False
except LoginError as error:
except LoginError:
_LOGGER.error("Login endpoint failed. Try again later.")
raise TokenRefreshFailed from error
except (TypeError, KeyError) as error:
raise TokenRefreshFailed
except (TypeError, KeyError):
_LOGGER.error("Malformed login response: %s", self.login_response)
raise TokenRefreshFailed from error
raise TokenRefreshFailed
return True
def extract_login_info(self):
"""Extract login info from login response."""
self.region_id = self.login_response["account"]["tier"]
self.region_id = self.login_response["region"]["tier"]
self.host = f"{self.region_id}.{BLINK_URL}"
self.token = self.login_response["auth"]["token"]
self.client_id = self.login_response["account"]["client_id"]
self.account_id = self.login_response["account"]["account_id"]
self.token = self.login_response["authtoken"]["authtoken"]
self.client_id = self.login_response["client"]["id"]
self.account_id = self.login_response["account"]["id"]
def startup(self):
"""Initialize tokens for communication."""
@@ -159,8 +151,8 @@ class Auth:
json_data = response.json()
except KeyError:
pass
except (AttributeError, ValueError) as error:
raise BlinkBadResponse from error
except (AttributeError, ValueError):
raise BlinkBadResponse
self.is_errored = False
return json_data
@@ -235,9 +227,6 @@ class Auth:
try:
json_resp = response.json()
blink.available = json_resp["valid"]
if not json_resp["valid"]:
_LOGGER.error("%s", json_resp["message"])
return False
except (KeyError, TypeError):
_LOGGER.error("Did not receive valid response from server.")
return False
@@ -246,7 +235,7 @@ class Auth:
def check_key_required(self):
"""Check if 2FA key is required."""
try:
if self.login_response["account"]["client_verification_required"]:
if self.login_response["client"]["verification_required"]:
return True
except (KeyError, TypeError):
pass
+3 -8
View File
@@ -264,9 +264,7 @@ class Blink:
"""Save login data to file."""
util.json_save(self.auth.login_attributes, file_name)
def download_videos(
self, path, since=None, camera="all", stop=10, delay=1, debug=False
):
def download_videos(self, path, since=None, camera="all", stop=10, debug=False):
"""
Download all videos from server since specified time.
@@ -277,7 +275,6 @@ class Blink:
:param camera: Camera name to retrieve. Defaults to "all".
Use a list for multiple cameras.
:param stop: Page to stop on (~25 items per page. Default page 10).
:param delay: Number of seconds to wait in between subsequent video downloads.
:param debug: Set to TRUE to prevent downloading of items.
Instead of downloading, entries will be printed to log.
"""
@@ -304,9 +301,9 @@ class Blink:
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
self._parse_downloaded_items(result, camera, path, delay, debug)
self._parse_downloaded_items(result, camera, path, debug)
def _parse_downloaded_items(self, result, camera, path, delay, debug):
def _parse_downloaded_items(self, result, camera, path, debug):
"""Parse downloaded videos."""
for item in result:
try:
@@ -354,8 +351,6 @@ class Blink:
"Address: {address}, Filename: {filename}"
)
)
if delay > 0:
time.sleep(delay)
class BlinkSetupError(Exception):
-4
View File
@@ -98,10 +98,6 @@ class BlinkCamera:
self.sync.blink, self.network_id, self.camera_id
)
def record(self):
"""Initiate clip recording."""
return api.request_new_video(self.sync.blink, self.network_id, self.camera_id)
def get_media(self, media_type="image"):
"""Download media (image or video)."""
url = self.thumbnail
+3 -4
View File
@@ -3,8 +3,8 @@
import os
MAJOR_VERSION = 0
MINOR_VERSION = 18
PATCH_VERSION = "0.dev0"
MINOR_VERSION = 16
PATCH_VERSION = 4
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
@@ -34,7 +34,6 @@ PROJECT_CLASSIFIERS = [
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Topic :: Home Automation",
]
@@ -49,7 +48,7 @@ URLS
BLINK_URL = "immedia-semi.com"
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
BASE_URL = f"https://{DEFAULT_URL}"
LOGIN_ENDPOINT = f"{BASE_URL}/api/v5/account/login"
LOGIN_ENDPOINT = f"{BASE_URL}/api/v4/account/login"
"""
Dictionaries
+7 -7
View File
@@ -33,13 +33,10 @@ def json_save(data, file_name):
json.dump(data, json_file, indent=4)
def gen_uid(size, uid_format=False):
def gen_uid(size):
"""Create a random sring."""
if uid_format:
token = f"BlinkCamera_{secrets.token_hex(4)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(6)}"
else:
token = secrets.token_hex(size)
return token
full_token = secrets.token_hex(size)
return full_token[0:size]
def time_to_seconds(timestamp):
@@ -82,7 +79,10 @@ def prompt_login_data(data):
def validate_login_data(data):
"""Check for missing keys."""
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID, uid_format=True))
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID))
data["notification_key"] = data.get(
"notification_key", gen_uid(const.SIZE_NOTIFICATION_KEY)
)
data["device_id"] = data.get("device_id", const.DEVICE_ID)
return data
-77
View File
@@ -1,77 +0,0 @@
"""Login testing script."""
import sys
import os
from blinkpy.blinkpy import Blink
from blinkpy.auth import Auth
from blinkpy.helpers.util import json_load, json_save
save_session = False
print("")
print("Blink Login Debug Script ...")
print(" ... Loading previous session information.")
cwd = os.getcwd()
print(f" ... Looking in {cwd}.")
session_path = os.path.join(cwd, ".session_debug")
session = json_load(session_path)
try:
auth_file = session["file"]
except (TypeError, KeyError):
print(" ... Please input location of auth file")
auth_file = input(" Must contain username and password: ")
save_session = True
data = json_load(auth_file)
if data is None:
print(f" ... Please fix file contents of {auth_file}.")
print(" ... Exiting.")
sys.exit(1)
try:
username = data["username"]
password = data["password"]
except KeyError:
print(f" ... File contents of {auth_file} incorrect.")
print(" ... Require username and password at minimum.")
print(" ... Exiting.")
sys.exit(1)
if save_session:
print(f" ... Saving session file to {session_path}.")
json_save({"file": auth_file}, session_path)
blink = Blink()
auth = Auth(data)
blink.auth = auth
print(" ... Starting Blink.")
print("")
blink.start()
print("")
print(" ... Printing login response.")
print("")
print(blink.auth.login_response)
print("")
print(" ... Printing login attributes.")
print("")
print(blink.auth.login_attributes)
print("")
input(" ... Press any key to continue: ")
print(" ... Deactivating auth token.")
blink.auth.token = "foobar"
print(f"\t - blink.auth.token = {blink.auth.token}")
print(" ... Attempting login.")
print("")
blink.start()
print("")
print(" ... Printing login response.")
print("")
print(blink.auth.login_response)
print("")
print(" ... Printing login attributes.")
print("")
print(blink.auth.login_attributes)
print("")
rint(" ... Done.")
print("")
+11 -11
View File
@@ -1,14 +1,14 @@
black==21.6b0
coverage==5.5
flake8==3.9.1
flake8-docstrings==1.6.0
pre-commit==2.12.1
pylint==2.8.2
pydocstyle==6.0.0
pytest==6.2.4
pytest-cov==2.11.1
black==19.10b0
coverage==5.3
flake8==3.8.4
flake8-docstrings==1.5.0
pre-commit==2.7.1
pylint==2.6.0
pydocstyle==5.1.1
pytest==6.1.1
pytest-cov==2.10.1
pytest-sugar==0.9.4
pytest-timeout==1.4.2
restructuredtext-lint==1.3.2
pygments==2.9.0
restructuredtext-lint==1.3.1
pygments==2.7.1
testtools>=2.4.0
+9 -16
View File
@@ -42,6 +42,7 @@ class TestAuth(unittest.TestCase):
"username": "foo",
"password": "bar",
"uid": 1234,
"notification_key": 1234,
"device_id": const.DEVICE_ID,
}
self.assertDictEqual(auth.data, expected_data)
@@ -61,6 +62,7 @@ class TestAuth(unittest.TestCase):
"username": "foo",
"password": "bar",
"uid": 1234,
"notification_key": 1234,
"device_id": const.DEVICE_ID,
}
self.assertDictEqual(auth.data, expected_data)
@@ -126,11 +128,7 @@ class TestAuth(unittest.TestCase):
def test_header(self):
"""Test header data."""
self.auth.token = "bar"
expected_header = {
"TOKEN_AUTH": "bar",
"user-agent": const.DEFAULT_USER_AGENT,
"content-type": "application/json",
}
expected_header = {"TOKEN_AUTH": "bar", "user-agent": const.DEFAULT_USER_AGENT}
self.assertDictEqual(self.auth.header, expected_header)
def test_header_no_token(self):
@@ -163,8 +161,10 @@ class TestAuth(unittest.TestCase):
def test_refresh_token(self, mock_login):
"""Test refresh token method."""
mock_login.return_value = {
"account": {"account_id": 5678, "client_id": 1234, "tier": "test"},
"auth": {"token": "foobar"},
"region": {"tier": "test"},
"authtoken": {"authtoken": "foobar"},
"client": {"id": 1234},
"account": {"id": 5678},
}
self.assertTrue(self.auth.refresh_token())
self.assertEqual(self.auth.region_id, "test")
@@ -186,19 +186,12 @@ class TestAuth(unittest.TestCase):
self.auth.login_response = {}
self.assertFalse(self.auth.check_key_required())
self.auth.login_response = {"account": {"client_verification_required": False}}
self.auth.login_response = {"client": {"verification_required": False}}
self.assertFalse(self.auth.check_key_required())
self.auth.login_response = {"account": {"client_verification_required": True}}
self.auth.login_response = {"client": {"verification_required": True}}
self.assertTrue(self.auth.check_key_required())
@mock.patch("blinkpy.auth.api.request_logout")
def test_logout(self, mock_req):
"""Test logout method."""
mock_blink = MockBlink(None)
mock_req.return_value = True
self.assertTrue(self.auth.logout(mock_blink))
@mock.patch("blinkpy.auth.api.request_verify")
def test_send_auth_key(self, mock_req):
"""Check sending of auth key."""
+2 -27
View File
@@ -2,7 +2,6 @@
import unittest
from unittest import mock
import logging
import time
from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
@@ -89,33 +88,9 @@ class TestBlinkFunctions(unittest.TestCase):
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.",
]
with self.assertLogs() as dl_log:
blink.download_videos("/tmp", stop=2, delay=0)
blink.download_videos("/tmp", stop=2)
self.assertEqual(dl_log.output, expected_log)
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_parse_downloaded_throttle(self, mock_req):
"""Test ability to parse downloaded items list."""
generic_entry = {
"created_at": "1970",
"device_name": "foo",
"deleted": False,
"media": "/bar.mp4",
}
result = [generic_entry]
mock_req.return_value = {"media": result}
self.blink.last_refresh = 0
start = time.time()
self.blink.download_videos("/tmp", stop=2, delay=0, debug=True)
now = time.time()
delta = now - start
self.assertTrue(delta < 0.1)
start = time.time()
self.blink.download_videos("/tmp", stop=2, delay=0.1, debug=True)
now = time.time()
delta = now - start
self.assertTrue(delta >= 0.1)
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_parse_camera_not_in_list(self, mock_req):
"""Test ability to parse downloaded items list."""
@@ -138,7 +113,7 @@ class TestBlinkFunctions(unittest.TestCase):
"DEBUG:blinkpy.blinkpy:Skipping videos for foo.",
]
with self.assertLogs() as dl_log:
blink.download_videos("/tmp", camera="bar", stop=2, delay=0)
blink.download_videos("/tmp", camera="bar", stop=2)
self.assertEqual(dl_log.output, expected_log)
@mock.patch("blinkpy.blinkpy.api.request_network_update")
+1 -17
View File
@@ -3,7 +3,7 @@
import unittest
from unittest import mock
import time
from blinkpy.helpers.util import json_load, Throttle, time_to_seconds, gen_uid
from blinkpy.helpers.util import json_load, Throttle, time_to_seconds
class TestUtil(unittest.TestCase):
@@ -132,19 +132,3 @@ class TestUtil(unittest.TestCase):
self.assertEqual(json_load("fake.file"), None)
with mock.patch("builtins.open", mock.mock_open(read_data="")):
self.assertEqual(json_load("fake.file"), None)
def test_gen_uid(self):
"""Test gen_uid formatting."""
val1 = gen_uid(8)
val2 = gen_uid(8, uid_format=True)
self.assertEqual(len(val1), 16)
self.assertTrue(val2.startswith("BlinkCamera_"))
val2_cut = val2.split("_")
val2_split = val2_cut[1].split("-")
self.assertEqual(len(val2_split[0]), 8)
self.assertEqual(len(val2_split[1]), 4)
self.assertEqual(len(val2_split[2]), 4)
self.assertEqual(len(val2_split[3]), 4)
self.assertEqual(len(val2_split[4]), 12)
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = build, py36, py37, py38, py39, lint
envlist = build, py36, py37, py38, lint
skip_missing_interpreters = True
skipsdist = True