diff --git a/CHANGES.rst b/CHANGES.rst index 41f5d51a..403d98bf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,24 @@ Changelog A list of changes between each release +0.22.4 (2023-12-18) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Allow kwargs to throttled functions, await sleep in throttle (`mkmer #823 `__) +- add missing entry in type_key_map (`@Rosi2143 `__) + +** Other Changes ** + +- Delete ReadTheDocs +- python formatter +- docstring format changes +- Bump ruff to 0.1.8 +- Bump black to 23.12.0 +- Bump pygments to 2.17.2 + + 0.22.3 (2023-11-05) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index 4d074f7c..a2767e39 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Codestyle| +blinkpy |Build Status| |Coverage Status| |PyPi Version| |Codestyle| ============================================================================================= A Python library for the Blink Camera system (Python 3.8+) @@ -39,7 +39,6 @@ To install the current development version, perform the following steps. Note t If you'd like to contribute to this library, please read the `contributing instructions `__. -For more information on how to use this library, please `read the docs `__. Purpose ------- @@ -249,7 +248,5 @@ API steps :target: https://codecov.io/gh/fronzbot/blinkpy .. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg :target: https://pypi.python.org/pypi/blinkpy -.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest - :target: http://blinkpy.readthedocs.io/en/latest/?badge=latest .. |Codestyle| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black diff --git a/blinkpy/api.py b/blinkpy/api.py index 576568c0..a10c4497 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -131,7 +131,7 @@ async def request_syncmodule(blink, network): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_system_arm(blink, network): +async def request_system_arm(blink, network, **kwargs): """ Arm system. @@ -148,7 +148,7 @@ async def request_system_arm(blink, network): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_system_disarm(blink, network): +async def request_system_disarm(blink, network, **kwargs): """ Disarm system. @@ -177,14 +177,14 @@ async def request_command_status(blink, network, command_id): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_homescreen(blink): +async def request_homescreen(blink, **kwargs): """Request homescreen info.""" url = f"{blink.urls.base_url}/api/v3/accounts/{blink.account_id}/homescreen" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_sync_events(blink, network): +async def request_sync_events(blink, network, **kwargs): """ Request events from sync module. @@ -196,7 +196,7 @@ async def request_sync_events(blink, network): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_new_image(blink, network, camera_id): +async def request_new_image(blink, network, camera_id, **kwargs): """ Request to capture new thumbnail for camera. @@ -211,7 +211,7 @@ async def request_new_image(blink, network, camera_id): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_new_video(blink, network, camera_id): +async def request_new_video(blink, network, camera_id, **kwargs): """ Request to capture new video clip. @@ -226,7 +226,7 @@ async def request_new_video(blink, network, camera_id): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_video_count(blink): +async def request_video_count(blink, **kwargs): """Request total video count.""" url = f"{blink.urls.base_url}/api/v2/videos/count" return await http_get(blink, url) @@ -304,14 +304,14 @@ async def request_camera_sensors(blink, network, camera_id): :param blink: Blink instance. :param network: Sync module network id. - :param camera_id: Camera ID of camera to request sesnor info from. + :param camera_id: Camera ID of camera to request sensor info from. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/signals" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_motion_detection_enable(blink, network, camera_id): +async def request_motion_detection_enable(blink, network, camera_id, **kwargs): """ Enable motion detection for a camera. @@ -326,8 +326,9 @@ async def request_motion_detection_enable(blink, network, camera_id): @Throttle(seconds=MIN_THROTTLE_TIME) -async def request_motion_detection_disable(blink, network, camera_id): - """Disable motion detection for a camera. +async def request_motion_detection_disable(blink, network, camera_id, **kwargs): + """ + Disable motion detection for a camera. :param blink: Blink instance. :param network: Sync module network id. @@ -340,7 +341,8 @@ async def request_motion_detection_disable(blink, network, camera_id): async def request_local_storage_manifest(blink, network, sync_id): - """Update local manifest. + """ + Update local manifest. Request creation of an updated manifest of video clips stored in sync module local storage. @@ -360,7 +362,8 @@ async def request_local_storage_manifest(blink, network, sync_id): async def get_local_storage_manifest(blink, network, sync_id, manifest_request_id): - """Request manifest of video clips stored in sync module local storage. + """ + Request manifest of video clips stored in sync module local storage. :param blink: Blink instance. :param network: Sync module network id. @@ -377,7 +380,8 @@ async def get_local_storage_manifest(blink, network, sync_id, manifest_request_i async def request_local_storage_clip(blink, network, sync_id, manifest_id, clip_id): - """Prepare video clip stored in the sync module to be downloaded. + """ + Prepare video clip stored in the sync module to be downloaded. :param blink: Blink instance. :param network: Sync module network id. @@ -400,7 +404,8 @@ async def request_local_storage_clip(blink, network, sync_id, manifest_id, clip_ async def request_get_config(blink, network, camera_id, product_type="owl"): - """Get camera configuration. + """ + Get camera configuration. :param blink: Blink instance. :param network: Sync module network id. @@ -427,7 +432,8 @@ async def request_get_config(blink, network, camera_id, product_type="owl"): async def request_update_config( blink, network, camera_id, product_type="owl", data=None ): - """Update camera configuration. + """ + Update camera configuration. :param blink: Blink instance. :param network: Sync module network id. @@ -455,7 +461,8 @@ async def request_update_config( async def http_get( blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT ): - """Perform an http get request. + """ + Perform an http get request. :param url: URL to perform get request. :param stream: Stream response? True/FALSE @@ -474,9 +481,10 @@ async def http_get( async def http_post(blink, url, is_retry=False, data=None, json=True, timeout=TIMEOUT): - """Perform an http post request. + """ + Perform an http post request. - :param url: URL to perfom post request. + :param url: URL to perform post request. :param is_retry: Is this part of a re-auth attempt? :param data: str body for post request :param json: Return json response? TRUE/False diff --git a/blinkpy/auth.py b/blinkpy/auth.py index ca95321f..09eab0a0 100644 --- a/blinkpy/auth.py +++ b/blinkpy/auth.py @@ -25,7 +25,7 @@ def __init__(self, login_data=None, no_prompt=False, session=None): - username - password :param no_prompt: Should any user input prompts - be supressed? True/FALSE + be suppressed? True/FALSE """ if login_data is None: login_data = {} @@ -152,8 +152,8 @@ async def query( is_retry=False, timeout=TIMEOUT, ): - """Perform server requests.""" - """ + """Perform server requests. + :param url: URL to perform request :param data: Data to send :param headers: Headers to send diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 8bfa4566..3e681517 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -58,7 +58,7 @@ def __init__( Useful for preventing motion_detected property from de-asserting too quickly. :param no_owls: Disable searching for owl entries (blink mini cameras \ - only known entity). Prevents an uneccessary API call \ + only known entity). Prevents an unnecessary API call \ if you don't have these in your network. """ self.auth = Auth(session=session) @@ -101,7 +101,7 @@ async def refresh(self, force=False, force_cache=False): # Prevents rapid clearing of motion detect property self.last_refresh = int(time.time()) last_refresh = datetime.datetime.fromtimestamp(self.last_refresh) - _LOGGER.debug(f"last_refresh={last_refresh}") + _LOGGER.debug("last_refresh = %s", last_refresh) return True return False @@ -128,8 +128,9 @@ async def start(self): # Initialize last_refresh to be just before the refresh delay period. self.last_refresh = int(time.time() - self.refresh_rate * 1.05) _LOGGER.debug( - f"Initialized last_refresh to {self.last_refresh} == " - f"{datetime.datetime.fromtimestamp(self.last_refresh)}" + "Initialized last_refresh to %s == %s", + self.last_refresh, + datetime.datetime.fromtimestamp(self.last_refresh), ) return await self.setup_post_verify() @@ -167,12 +168,13 @@ async def setup_sync_module(self, name, network_id, cameras): await self.sync[name].start() async def get_homescreen(self): - """Get homecreen information.""" + """Get homescreen information.""" if self.no_owls: _LOGGER.debug("Skipping owl extraction.") self.homescreen = {} return self.homescreen = await api.request_homescreen(self) + _LOGGER.debug("homescreen = %s", util.json_dumps(self.homescreen)) async def setup_owls(self): """Check for mini cameras.""" @@ -234,6 +236,7 @@ async def setup_camera_list(self): response = await api.request_camera_usage(self) try: for network in response["networks"]: + _LOGGER.info("network = %s", util.json_dumps(network)) camera_network = str(network["network_id"]) if camera_network not in all_cameras: all_cameras[camera_network] = [] diff --git a/blinkpy/camera.py b/blinkpy/camera.py index 9b848a26..bc5c6d80 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -20,7 +20,7 @@ class BlinkCamera: """Class to initialize individual camera.""" def __init__(self, sync): - """Initiailize BlinkCamera.""" + """Initialize BlinkCamera.""" self.sync = sync self.name = None self.camera_id = None @@ -76,7 +76,7 @@ def battery(self): @property def temperature_c(self): - """Return temperature in celcius.""" + """Return temperature in celsius.""" try: return round((self.temperature - 32) / 9.0 * 5.0, 1) except TypeError: @@ -170,7 +170,7 @@ async def get_thumbnail(self, url=None): if not url: url = self.thumbnail if not url: - _LOGGER.warning(f"Thumbnail URL not available: self.thumbnail={url}") + _LOGGER.warning("Thumbnail URL not available: self.thumbnail=%s", url) return None return await api.http_get( self.sync.blink, @@ -185,7 +185,7 @@ async def get_video_clip(self, url=None): if not url: url = self.clip if not url: - _LOGGER.warning(f"Video clip URL not available: self.clip={url}") + _LOGGER.warning("Video clip URL not available: self.clip=%s", url) return None return await api.http_get( self.sync.blink, @@ -240,7 +240,7 @@ def extract_config_info(self, config): self.product_type = config.get("type", None) async def get_sensor_info(self): - """Retrieve calibrated temperatue from special endpoint.""" + """Retrieve calibrated temperature from special endpoint.""" resp = await api.request_camera_sensors( self.sync.blink, self.network_id, self.camera_id ) @@ -248,7 +248,14 @@ async def get_sensor_info(self): self.temperature_calibrated = resp["temp"] except (TypeError, KeyError): self.temperature_calibrated = self.temperature - _LOGGER.warning("Could not retrieve calibrated temperature.") + _LOGGER.warning( + "Could not retrieve calibrated temperature response %s.", resp + ) + _LOGGER.warning( + "for network_id (%s) and camera_id (%s)", + self.network_id, + self.camera_id, + ) async def update_images(self, config, force_cache=False, expire_clips=True): """Update images for camera.""" @@ -278,7 +285,7 @@ async def update_images(self, config, force_cache=False, expire_clips=True): new_thumbnail = urljoin(self.sync.urls.base_url, thumb_string) else: - _LOGGER.warning("Could not find thumbnail for camera %s", self.name) + _LOGGER.warning("Could not find thumbnail for camera %s.", self.name) try: self.motion_detected = self.sync.motion[self.name] @@ -288,7 +295,7 @@ async def update_images(self, config, force_cache=False, expire_clips=True): clip_addr = None try: - def timest(record): + def timesort(record): rec_time = record["time"] iso_time = datetime.datetime.fromisoformat(rec_time) stamp = int(iso_time.timestamp()) @@ -298,7 +305,7 @@ def timest(record): len(self.sync.last_records) > 0 and len(self.sync.last_records[self.name]) > 0 ): - last_records = sorted(self.sync.last_records[self.name], key=timest) + last_records = sorted(self.sync.last_records[self.name], key=timesort) for rec in last_records: clip_addr = rec["clip"] self.clip = f"{self.sync.urls.base_url}{clip_addr}" @@ -310,17 +317,21 @@ def timest(record): self.recent_clips.append(recent) if len(self.recent_clips) > 0: _LOGGER.debug( - f"Found {len(self.recent_clips)} recent clips for {self.name}" + "Found %s recent clips for %s", + len(self.recent_clips), + self.name, ) _LOGGER.debug( - f"Most recent clip for {self.name} was created at " - f"{self.last_record}: {self.clip}" + "Most recent clip for %s was created at %s : %s", + self.name, + self.last_record, + self.clip, ) except (KeyError, IndexError): ex = traceback.format_exc() trace = "".join(traceback.format_stack()) - _LOGGER.error(f"Error getting last records for '{self.name}': {ex}") - _LOGGER.debug(f"\n{trace}") + _LOGGER.error("Error getting last records for '%s': %s", self.name, ex) + _LOGGER.debug("\n%s", trace) # If the thumbnail or clip have changed, update the cache update_cached_image = False @@ -356,12 +367,13 @@ async def expire_recent_clips(self, delta=datetime.timedelta(hours=1)): to_keep.append(clip) num_expired = len(self.recent_clips) - len(to_keep) if num_expired > 0: - _LOGGER.info(f"Expired {num_expired} clips from '{self.name}'") + _LOGGER.info("Expired %s clips from '%s'", num_expired, self.name) self.recent_clips = copy.deepcopy(to_keep) if len(self.recent_clips) > 0: _LOGGER.info( - f"'{self.name}' has {len(self.recent_clips)} " - "clips available for download" + "'%s' has %s clips available for download", + self.name, + len(self.recent_clips), ) for clip in self.recent_clips: url = clip["clip"] @@ -369,7 +381,7 @@ async def expire_recent_clips(self, delta=datetime.timedelta(hours=1)): await api.http_post(self.sync.blink, url) async def get_liveview(self): - """Get livewview rtsps link.""" + """Get liveview rtsps link.""" response = await api.request_camera_liveview( self.sync.blink, self.sync.network_id, self.camera_id ) @@ -384,8 +396,8 @@ async def image_to_file(self, path): _LOGGER.debug("Writing image from %s to %s", self.name, path) response = await self.get_media() if response and response.status == 200: - async with open(path, "wb") as imgfile: - await imgfile.write(await response.read()) + async with open(path, "wb") as imagefile: + await imagefile.write(await response.read()) else: _LOGGER.error("Cannot write image to file, response %s", response.status) @@ -425,7 +437,7 @@ async def save_recent_clips( created=created_at, name=to_alphanumeric(self.name) ) path = os.path.join(output_dir, file_name) - _LOGGER.debug(f"Saving {clip_addr} to {path}") + _LOGGER.debug("Saving %s to %s", clip_addr, path) media = await self.get_video_clip(clip_addr) if media and media.status == 200: async with open(path, "wb") as clip_file: @@ -434,19 +446,22 @@ async def save_recent_clips( try: # Remove recent clip from the list once the download has finished. self.recent_clips.remove(clip) - _LOGGER.debug(f"Removed {clip} from recent clips") + _LOGGER.debug("Removed %s from recent clips", clip) except ValueError: ex = traceback.format_exc() - _LOGGER.error(f"Error removing clip from list: {ex}") + _LOGGER.error("Error removing clip from list: %s", ex) trace = "".join(traceback.format_stack()) - _LOGGER.debug(f"\n{trace}") + _LOGGER.debug("\n%s", trace) if len(recent) == 0: - _LOGGER.info(f"No recent clips to save for '{self.name}'.") + _LOGGER.info("No recent clips to save for '%s'.", self.name) else: _LOGGER.info( - f"Saved {num_saved} of {len(recent)} recent clips from " - f"'{self.name}' to directory {output_dir}" + "Saved %s of %s recent clips from '%s' to directory %s", + num_saved, + len(recent), + self.name, + output_dir, ) diff --git a/blinkpy/helpers/util.py b/blinkpy/helpers/util.py index 2f1c16cd..88838836 100644 --- a/blinkpy/helpers/util.py +++ b/blinkpy/helpers/util.py @@ -7,6 +7,7 @@ import secrets import re import aiofiles +from asyncio import sleep from calendar import timegm from functools import wraps from getpass import getpass @@ -37,8 +38,13 @@ async def json_save(data, file_name): await json_file.write(json.dumps(data, indent=4)) +def json_dumps(json_in, indent=2): + """Return a well formated json string.""" + return json.dumps(json_in, indent=indent) + + def gen_uid(size, uid_format=False): - """Create a random sring.""" + """Create a random string.""" if uid_format: token = ( f"BlinkCamera_{secrets.token_hex(4)}-" @@ -156,21 +162,18 @@ def __init__(self, seconds=10): def __call__(self, method): """Throttle caller method.""" - async def throttle_method(): - """Call when method is throttled.""" - return None - @wraps(method) - def wrapper(*args, **kwargs): + async def wrapper(*args, **kwargs): """Wrap that checks for throttling.""" - force = kwargs.pop("force", False) + force = kwargs.get("force", False) now = int(time.time()) last_call_delta = now - self.last_call if force or last_call_delta > self.throttle_time: - result = method(*args, **kwargs) self.last_call = now - return result + else: + self.last_call = now + last_call_delta + await sleep(self.throttle_time - last_call_delta) - return throttle_method() + return await method(*args, **kwargs) return wrapper diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index ea1fcfb8..01af15c8 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -9,7 +9,12 @@ from requests.structures import CaseInsensitiveDict from blinkpy import api from blinkpy.camera import BlinkCamera, BlinkCameraMini, BlinkDoorbell -from blinkpy.helpers.util import time_to_seconds, backoff_seconds, to_alphanumeric +from blinkpy.helpers.util import ( + time_to_seconds, + backoff_seconds, + to_alphanumeric, + json_dumps, +) from blinkpy.helpers.constants import ONLINE _LOGGER = logging.getLogger(__name__) @@ -46,6 +51,7 @@ def __init__(self, blink, network_name, network_id, camera_list): self.type_key_map = { "mini": "owls", "doorbell": "doorbells", + "outdoor": "cameras", } self._names_table = {} self._local_storage = { @@ -184,6 +190,7 @@ async def update_cameras(self, camera_type=BlinkCamera): try: _LOGGER.debug("Updating cameras") for camera_config in self.camera_list: + _LOGGER.debug("Updating camera_config %s", json_dumps(camera_config)) if "name" not in camera_config: break blink_camera_type = camera_config.get("type", "") @@ -208,10 +215,11 @@ async def update_cameras(self, camera_type=BlinkCamera): def get_unique_info(self, name): """Extract unique information for Minis and Doorbells.""" try: - for camera_type in self.type_key_map: - type_key = self.type_key_map[camera_type] + for type_key in self.type_key_map.values(): for device in self.blink.homescreen[type_key]: + _LOGGER.debug("checking device %s", device) if device["name"] == name: + _LOGGER.debug("Found unique_info %s", device) return device except (TypeError, KeyError): pass @@ -277,18 +285,19 @@ async def check_new_videos(self): try: interval = self.blink.last_refresh - self.motion_interval * 60 last_refresh = datetime.datetime.fromtimestamp(self.blink.last_refresh) - _LOGGER.debug(f"last_refresh = {last_refresh}") - _LOGGER.debug(f"interval={interval}") + _LOGGER.debug("last_refresh = %s", last_refresh) + _LOGGER.debug("interval = %s", interval) except TypeError: # This is the first start, so refresh hasn't happened yet. # No need to check for motion. ex = traceback.format_exc() _LOGGER.error( - "Error calculating interval " - f"(last_refresh={self.blink.last_refresh}): {ex}" + "Error calculating interval (last_refresh = %s): %s", + self.blink.last_refresh, + ex, ) trace = "".join(traceback.format_stack()) - _LOGGER.debug(f"\n{trace}") + _LOGGER.debug("\n%s", trace) _LOGGER.info("No new videos since last refresh.") return False @@ -324,15 +333,17 @@ async def check_new_videos(self): except KeyError: last_refresh = datetime.datetime.fromtimestamp(self.blink.last_refresh) _LOGGER.debug( - f"No new videos for {entry} since last refresh at {last_refresh}." + "No new videos for %s since last refresh at %s.", + entry, + last_refresh, ) # Process local storage if active and if the manifest is ready. last_manifest_read = datetime.datetime.fromisoformat( self._local_storage["last_manifest_read"] ) - _LOGGER.debug(f"last_manifest_read = {last_manifest_read}") - _LOGGER.debug(f"Manifest ready? {self.local_storage_manifest_ready}") + _LOGGER.debug("last_manifest_read = %s", last_manifest_read) + _LOGGER.debug("Manifest ready? %s", self.local_storage_manifest_ready) if self.local_storage and self.local_storage_manifest_ready: _LOGGER.debug("Processing updated manifest") manifest = self._local_storage["manifest"] @@ -349,17 +360,20 @@ async def check_new_videos(self): iso_timestamp = item.created_at.isoformat() _LOGGER.debug( - f"Checking '{item.name}': clip_time={iso_timestamp}, " - f"manifest_read={last_manifest_read}" + "Checking '%s': clip_time = %s, manifest_read = %s", + item.name, + iso_timestamp, + last_manifest_read, ) # Exit the loop once there are no new videos in the list. if not self.check_new_video_time(iso_timestamp, last_manifest_read): _LOGGER.info( "No new local storage videos since last manifest " - f"read at {last_read_local}." + "read at %s.", + last_read_local, ) break - _LOGGER.debug(f"Found new item in local storage manifest: {item}") + _LOGGER.debug("Found new item in local storage manifest: %s", item) name = item.name clip_url = item.url(last_manifest_id) await item.prepare_download(self.blink) @@ -375,8 +389,8 @@ async def check_new_videos(self): datetime.datetime.utcnow() - datetime.timedelta(seconds=10) ).isoformat() self._local_storage["last_manifest_read"] = last_manifest_read - _LOGGER.debug(f"Updated last_manifest_read to {last_manifest_read}") - _LOGGER.debug(f"Last clip time was {last_clip_time}") + _LOGGER.debug("Updated last_manifest_read to %s", last_manifest_read) + _LOGGER.debug("Last clip time was %s", last_clip_time) # We want to keep the last record when no new motion was detected. for camera in self.cameras: # Check if there are no new records, indicating motion. @@ -389,8 +403,8 @@ async def check_new_videos(self): return True def check_new_video_time(self, timestamp, reference=None): - """Check if video has timestamp since last refresh.""" - """ + """Check if video has timestamp since last refresh. + :param timestamp ISO-formatted timestamp string :param reference ISO-formatted reference timestamp string """ @@ -450,14 +464,15 @@ async def update_local_storage_manifest(self): num_added = len(self._local_storage["manifest"]) - num_stored if num_added > 0: _LOGGER.info( - f"Found {num_added} new clip(s) in local storage " - f"manifest id={manifest_id}" + "Found %s new clip(s) in local storage manifest id = %s", + num_added, + manifest_id, ) except (TypeError, KeyError): ex = traceback.format_exc() - _LOGGER.error(f"Could not extract clips list from response: {ex}") + _LOGGER.error("Could not extract clips list from response: %s", ex) trace = "".join(traceback.format_stack()) - _LOGGER.debug(f"\n{trace}") + _LOGGER.debug("\n%s", trace) self._local_storage["manifest_stale"] = True return None diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst deleted file mode 120000 index 9d60ba96..00000000 --- a/docs/CHANGES.rst +++ /dev/null @@ -1 +0,0 @@ -../CHANGES.rst \ No newline at end of file diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst deleted file mode 120000 index 798f2aa2..00000000 --- a/docs/CONTRIBUTING.rst +++ /dev/null @@ -1 +0,0 @@ -../CONTRIBUTING.rst \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 774c3867..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = blinkpy -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/README.rst b/docs/README.rst deleted file mode 120000 index 89a01069..00000000 --- a/docs/README.rst +++ /dev/null @@ -1 +0,0 @@ -../README.rst \ No newline at end of file diff --git a/docs/advanced.rst b/docs/advanced.rst deleted file mode 100644 index 95bf6037..00000000 --- a/docs/advanced.rst +++ /dev/null @@ -1,104 +0,0 @@ -======================= -Advanced Library Usage -======================= - -Usage of this library was designed with the `Home Assistant `__ project in mind. With that said, this library is flexible to be used in other scripts where advanced usage not covered in the Quick Start guide may be required. This usage guide will attempt to cover as many use cases as possible. - -Throttling --------------- -In general, attempting too many requests to the Blink servers will result in your account being throttled. Where possible, adding a delay between calls is ideal. For use cases where this is not an acceptable solution, the ``blinkpy.helpers.util`` module contains a ``Throttle`` class that can be used as a decorator for calls. There are many examples of usage within the ``blinkpy.api`` module. A simple example of usage is covered below, where the decorated method is prevented from executing again until 10s has passed. Note that if the method call is throttled by the decorator, the method will return `None`. - -.. code:: python - - from blinkpy.helpers.util import Throttle - - @Throttle(seconds=10) - def my_method(*args): - """Some method to be throttled.""" - return True - -Custom Sessions ------------------ -By default, the ``blink.auth.Auth`` class creates its own websession via its ``create_session`` method. This is done when the class is initialized and is accessible via the ``Auth.session`` property. To override with a custom websession, the following code can accomplish that: - -.. code:: python - - from blinkpy.blinkpy import Blink - from blinkpy.auth import Auth - - blink = Blink() - blink.auth = Auth() - blink.auth.session = YourCustomSession - - -Custom Retry Logic --------------------- -The built-in auth session via the ``create_session`` method allows for customizable retry intervals and conditions. These parameters are: - -- retries -- backoff -- retry_list - -``retries`` is the total number of retry attempts that each http request can do before timing out. ``backoff`` is a parameter that allows for non-linear retry times such that the time between retries is backoff*(2^(retries) - 1). ``retry_list`` is simply a list of status codes to force a retry. By default ``retries=3``, ``backoff=1``, and ``retry_list=[429, 500, 502, 503, 504]``. To override them, you need to add you overrides to a dictionary and use that to create a new session with the ``opts`` variable in the ``create_session`` method. The following example can serve as a guide where only the number of retries and backoff factor are overridden: - -.. code:: python - - from blinkpy.blinkpy import Blink - from blinkpy.auth import Auth - - blink = Blink() - blink.auth = Auth() - - opts = {"retries": 10, "backoff": 2} - blink.auth.session = blink.auth.create_session(opts=opts) - - -Custom HTTP requests ---------------------- -In addition to custom sessions, custom blink server requests can be performed. This give you the ability to bypass the built-in ``Auth.query`` method. It also allows flexibility by giving you the option to pass your own url, rather than be limited to what is currently implemented in the ``blinkpy.api`` module. - -**Send custom url** -This prepares a standard "GET" request. - -.. code:: python - - from blinkpy.blinkpy import Blink - from blinkpy.auth import Auth - - blink = Blink() - blink.auth = Auth() - url = some_api_endpoint_string - request = blink.auth.prepare_request(url, blink.auth.header, None, "get") - response = blink.auth.session.send(request) - -**Overload query method** -Another option is to create your own ``Auth`` class with a custom ``query`` method to avoid the built-in response checking. This allows you to use the built in ``blinkpy.api`` endpoints, but also gives you flexibility to send your own urls. - -.. code:: python - - from blinkpy.blinkpy import Blink - from blinkpy.auth import Auth - from blinkpy import api - - class CustomAuth(Auth): - def query( - self, - url=None, - data=None, - headers=self.header, - reqtype="get", - stream=False, - json_resp=True, - **kwargs - ): - req = self.prepare_request(url, headers, data, reqtype) - return self.session.send(req, stream=stream) - - blink = blink.Blink() - blink.auth = CustomAuth() - - # Send custom GET query - response = blink.auth.query(url=some_custom_url) - - # Call built-in networks api endpoint - response = api.request_networks(blink) diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index a4d0b1af..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# blinkpy documentation build configuration file, created by -# sphinx-quickstart on Sat Jan 20 22:20:16 2018. -# -# 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. -import sphinx_rtd_theme -from blinkpy.helpers.constants import __version__ -html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.mathjax', - '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 = 'blinkpy' -copyright = '2018, Kevin Fronczak' -author = 'Kevin Fronczak' - -# 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 = __version__ -# The full version, including alpha/beta/rc tags. -release = __version__ - -# 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'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'blinkpydoc' - - -# -- 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, 'blinkpy.tex', 'blinkpy Documentation', - 'Kevin Fronczak', '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, 'blinkpy', 'blinkpy 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, 'blinkpy', 'blinkpy Documentation', - author, 'blinkpy', 'One line description of project.', - 'Miscellaneous'), -] - - - diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 50951612..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,26 +0,0 @@ -.. blinkpy documentation master file, created by - sphinx-quickstart on Sat Jan 20 22:20:16 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to blinkpy's documentation! -=================================== - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - :glob: - - README - advanced - CONTRIBUTING - modules/* - CHANGES - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 0a78c493..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=blinkpy - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the 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/docs/modules/blinkpy.rst b/docs/modules/blinkpy.rst deleted file mode 100644 index 1dc8d18b..00000000 --- a/docs/modules/blinkpy.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. _core_module: - -=========================== -Blinkpy Library Reference -=========================== - -blinkpy.py ------------ -.. automodule:: blinkpy.blinkpy - :members: - -auth.py --------- - -.. automodule:: blinkpy.auth - :members: - -sync_module.py ----------------- - -.. automodule:: blinkpy.sync_module - :members: - -camera.py ------------ - -.. automodule:: blinkpy.camera - :members: - -api.py ---------- - -.. automodule:: blinkpy.api - :members: - -helpers/util.py ----------------- - -.. automodule:: blinkpy.helpers.util - :members: - diff --git a/pyproject.toml b/pyproject.toml index 1e779c3f..c88b1abf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "blinkpy" -version = "0.22.3" +version = "0.22.4" license = {text = "MIT"} description = "A Blink camera Python Library." readme = "README.rst" diff --git a/requirements_test.txt b/requirements_test.txt index f8985e1c..2eb03f58 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ -ruff==0.1.3 -black==23.10.1 +ruff==0.1.8 +black==23.12.0 build==1.0.3 coverage==7.3.2 pytest==7.4.3 @@ -7,7 +7,7 @@ pytest-cov==4.1.0 pytest-sugar==0.9.7 pytest-timeout==2.2.0 restructuredtext-lint==1.4.0 -pygments==2.16.1 +pygments==2.17.2 testtools>=2.4.0 sortedcontainers~=2.4.0 pytest-asyncio>=0.21.0 diff --git a/tests/test_camera_functions.py b/tests/test_camera_functions.py index f90a9582..c1b7df49 100644 --- a/tests/test_camera_functions.py +++ b/tests/test_camera_functions.py @@ -130,9 +130,13 @@ async def test_no_thumbnails(self, mock_resp): [ ( "WARNING:blinkpy.camera:Could not retrieve calibrated " - "temperature." + f"temperature response {mock_resp.return_value}." ), - ("WARNING:blinkpy.camera:Could not find thumbnail for camera new"), + ( + f"WARNING:blinkpy.camera:for network_id ({config['network_id']}) " + f"and camera_id ({self.camera.camera_id})" + ), + ("WARNING:blinkpy.camera:Could not find thumbnail for camera new."), ], ) @@ -374,3 +378,33 @@ async def test_save_recent_clips(self, mock_clip, mock_open, mock_resp): f"'{self.camera.name}' to directory /tmp/", ) assert mock_open.call_count == 2 + + def remove_clip(self): + """Remove all clips to raise an exception on second removal.""" + self[0] *= 0 + return mresp.MockResponse({}, 200, raw_data="raw data") + + @mock.patch("blinkpy.camera.open", create=True) + @mock.patch( + "blinkpy.camera.BlinkCamera.get_video_clip", + create=True, + side_effect=remove_clip, + ) + async def test_save_recent_clips_exception(self, mock_clip, mock_open, mock_resp): + """Test corruption in recent clip list.""" + self.camera.recent_clips = [] + now = datetime.datetime.now() + self.camera.recent_clips.append( + { + "time": (now - datetime.timedelta(minutes=20)).isoformat(), + "clip": [self.camera.recent_clips], + }, + ) + with self.assertLogs(level="ERROR") as dl_log: + await self.camera.save_recent_clips() + print(f"Output = {dl_log.output}") + self.assertTrue( + "ERROR:blinkpy.camera:Error removing clip from list:" + in "\t".join(dl_log.output) + ) + assert mock_open.call_count == 1 diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index a0f1a03f..9b4c5a5d 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -1,5 +1,6 @@ """Tests camera and system functions.""" import datetime +import logging from unittest import IsolatedAsyncioTestCase from unittest import mock import aiofiles @@ -16,6 +17,10 @@ import tests.mock_responses as mresp from .test_api import COMMAND_RESPONSE, COMMAND_COMPLETE +_LOGGER = logging.getLogger(__name__) +logging.basicConfig(filename="blinkpy_test.log", level=logging.DEBUG) +_LOGGER.setLevel(logging.DEBUG) + @mock.patch("blinkpy.auth.Auth.query") class TestBlinkSyncModule(IsolatedAsyncioTestCase): @@ -76,6 +81,24 @@ def test_bad_arm(self, mock_resp) -> None: self.assertEqual(self.blink.sync["test"].arm, None) self.assertFalse(self.blink.sync["test"].available) + def test_get_unique_info_valid_device(self, mock_resp) -> None: + """Check that we get the correct info.""" + device = { + "enabled": True, + "name": "doorbell1", + } + self.blink.homescreen = {"doorbells": [device], "owls": []} + self.assertEqual(self.blink.sync["test"].get_unique_info("doorbell1"), device) + + def test_get_unique_info_invalid_device(self, mock_resp) -> None: + """Check what happens if the devide does not exist.""" + device = { + "enabled": True, + "name": "doorbell1", + } + self.blink.homescreen = {"doorbells": [device], "owls": []} + self.assertEqual(self.blink.sync["test"].get_unique_info("doorbell2"), None) + async def test_get_events(self, mock_resp) -> None: """Test get events function.""" mock_resp.return_value = {"event": True} diff --git a/tests/test_util.py b/tests/test_util.py index 06427fcb..5f6e15f5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -36,33 +36,26 @@ async def test_throttle(force=False): calls.append(1) now = int(time.time()) - now_plus_four = now + 4 - now_plus_six = now + 6 + # First call should fire await test_throttle() self.assertEqual(1, len(calls)) - # Call again, still shouldn't fire + # Call again, still should fire with delay await test_throttle() - self.assertEqual(1, len(calls)) + self.assertEqual(2, len(calls)) + assert int(time.time()) - now >= 5 # Call with force await test_throttle(force=True) - self.assertEqual(2, len(calls)) - - # Call without throttle, shouldn't fire - await test_throttle() - self.assertEqual(2, len(calls)) + self.assertEqual(3, len(calls)) - # Fake time as 4 seconds from now - with mock.patch("time.time", return_value=now_plus_four): - await test_throttle() - self.assertEqual(2, len(calls)) + # Call without throttle, fire with delay + now = int(time.time()) - # Fake time as 6 seconds from now - with mock.patch("time.time", return_value=now_plus_six): - await test_throttle() - self.assertEqual(3, len(calls)) + await test_throttle() + self.assertEqual(4, len(calls)) + assert int(time.time()) - now >= 5 async def test_throttle_per_instance(self): """Test that throttle is done once per instance of class.""" @@ -76,8 +69,10 @@ async def test(self): tester = Tester() throttled = Throttle(seconds=1)(tester.test) + now = int(time.time()) + self.assertEqual(await throttled(), True) self.assertEqual(await throttled(), True) - self.assertEqual(await throttled(), None) + assert int(time.time()) - now >= 1 async def test_throttle_multiple_objects(self): """Test that function is throttled even if called by multiple objects.""" @@ -95,8 +90,10 @@ def test(self): tester1 = Tester() tester2 = Tester() + now = int(time.time()) self.assertEqual(await tester1.test(), True) - self.assertEqual(await tester2.test(), None) + self.assertEqual(await tester2.test(), True) + assert int(time.time()) - now >= 5 async def test_throttle_on_two_methods(self): """Test that throttle works for multiple methods.""" @@ -115,22 +112,14 @@ async def test2(self): return True tester = Tester() - now = time.time() - now_plus_4 = now + 4 - now_plus_6 = now + 6 + now = int(time.time()) self.assertEqual(await tester.test1(), True) self.assertEqual(await tester.test2(), True) - self.assertEqual(await tester.test1(), None) - self.assertEqual(await tester.test2(), None) - - with mock.patch("time.time", return_value=now_plus_4): - self.assertEqual(await tester.test1(), True) - self.assertEqual(await tester.test2(), None) - - with mock.patch("time.time", return_value=now_plus_6): - self.assertEqual(await tester.test1(), None) - self.assertEqual(await tester.test2(), True) + self.assertEqual(await tester.test1(), True) + assert int(time.time()) - now >= 3 + self.assertEqual(await tester.test2(), True) + assert int(time.time()) - now >= 5 def test_time_to_seconds(self): """Test time to seconds conversion.""" diff --git a/tox.ini b/tox.ini index dc228048..0adeb49b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ setenv = LANG=en_US.UTF-8 PYTHONPATH = {toxinidir} commands = - pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report term-missing {posargs} + pytest --timeout=30 --durations=10 --cov=blinkpy --cov-report term-missing {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt @@ -19,7 +19,7 @@ setenv = PYTHONPATH = {toxinidir} commands = pip install -e . - pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml {posargs} + pytest --timeout=30 --durations=10 --cov=blinkpy --cov-report=xml {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt @@ -31,7 +31,7 @@ deps = basepython = python3 commands = ruff check blinkpy tests blinkapp - black --check --diff blinkpy tests blinkapp + black --check --color --diff blinkpy tests blinkapp rst-lint README.rst CHANGES.rst CONTRIBUTING.rst [testenv:build]