diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7cacab..d12776e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,9 +19,11 @@ repos: # Run the formatter. - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.901 + rev: v1.11.1 hooks: - id: mypy exclude: tests/ additional_dependencies: - - types-click + - types-pytz + - types-cryptography + - types-python-dateutil diff --git a/poetry.lock b/poetry.lock index e7702fb..12cdb82 100644 --- a/poetry.lock +++ b/poetry.lock @@ -869,53 +869,60 @@ files = [ [[package]] name = "mypy" -version = "0.900" +version = "1.11.1" description = "Optional static typing for Python" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "mypy-0.900-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:07efc88486877d595cca7f7d237e6d04d1ba6f01dc8f74a81b716270f6770968"}, - {file = "mypy-0.900-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:80c96f97de241ee7383cfe646bfc51113a48089d50c33275af0033b98dee3b1c"}, - {file = "mypy-0.900-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0e703c0afe36511746513d168e1d2a52f88e2a324169b87a6b6a58901c3afcf3"}, - {file = "mypy-0.900-cp35-cp35m-win_amd64.whl", hash = "sha256:23100137579d718cd6f05d572574ca00701fa2bfc7b645ebc5130d93e2af3bee"}, - {file = "mypy-0.900-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:468b3918b26f81d003e8e9b788c62160acb885487cf4d83a3f22ba9061cb49e2"}, - {file = "mypy-0.900-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d90c296cd5cdef86e720a0994d41d72c06d6ff8ab8fc6aaaf0ee6c675835d596"}, - {file = "mypy-0.900-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:42d66b3d716fe5e22b32915d1fa59e7183a0e02f00b337b834a596c1f5e37f01"}, - {file = "mypy-0.900-cp36-cp36m-win_amd64.whl", hash = "sha256:a354613b4cc6e0e9f1ba7811083cd8f63ccee97333d1df7594c438399c83249a"}, - {file = "mypy-0.900-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:22f97de97373dd6180c4abee90b20c60780820284d2cdc5579927c0e37854cf6"}, - {file = "mypy-0.900-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e75f0c97cfe8d86da89b22ad7039f5af44b8f6b0af12bd2877791a92b4b9e987"}, - {file = "mypy-0.900-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:41f082275a20e3eea48364915f7bc6ec5338be89db1ed8b2e570b9e3d12d4dc6"}, - {file = "mypy-0.900-cp37-cp37m-win_amd64.whl", hash = "sha256:83adbf3f8c5023f4276557fbcb3b6306f9dce01783e8ac5f8c11fcb29f62e899"}, - {file = "mypy-0.900-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2220f97804890f3e6da3f849f81f3e56e367a2027a51dde5ce3b7ebb2ad3342b"}, - {file = "mypy-0.900-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3f1d0601842c6b4248923963fc59a6fdd05dee0fddc8b07e30c508b6a269e68f"}, - {file = "mypy-0.900-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c2f87505840c0f3557ea4aa5893f2459daf6516adac30b15d1d5cf567e0d7939"}, - {file = "mypy-0.900-cp38-cp38-win_amd64.whl", hash = "sha256:a0461da00ed23d17fcb04940db2b72f920435cf79be943564b717e378ffeeddf"}, - {file = "mypy-0.900-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9560b1f572cdaab43fdcdad5ef45138e89dc191729329db1b8ce5636f4cdeacf"}, - {file = "mypy-0.900-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d794f10b9f28d21af7a93054e217872aaf9b9ad1bd354ae5e1a3a923d734b73f"}, - {file = "mypy-0.900-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:68fd1c1c1fc9b405f0ed6cfcd00541de7e83f41007419a125c20fa5db3881cb1"}, - {file = "mypy-0.900-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7eb1e5820deb71e313aa2b5a5220803a9b2e3efa43475537a71d0ffed7495e1e"}, - {file = "mypy-0.900-cp39-cp39-win_amd64.whl", hash = "sha256:6598e39cd5aa1a09d454ad39687b89cf3f3fd7cf1f9c3f81a1a2775f6f6b16f8"}, - {file = "mypy-0.900-py3-none-any.whl", hash = "sha256:3be7c68fab8b318a2d5bcfac8e028dc77b9096ea1ec5594e9866c8fb57ae0296"}, - {file = "mypy-0.900.tar.gz", hash = "sha256:65c78570329c54fb40f956f7645e2359af5da9d8c54baa44f461cdc7f4984108"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typing-extensions = ">=3.7.4" +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.4" -description = "Experimental type system extensions for programs checked with the mypy typechecker." +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=2.7" +python-versions = ">=3.5" files = [ - {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] @@ -1992,4 +1999,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.10.0,<4.0" -content-hash = "81ee0cdb5af9e498001e3e8239c0ba259d114e19a2099f133b2de9645f596788" +content-hash = "dea68a0cc11ccb56faf879210d124f44b2ee73a7a36205f1214dd8ab35451d1c" diff --git a/pyproject.toml b/pyproject.toml index 6897d86..12e731e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ uiprotect = "^5.4.0" optional = true [tool.poetry.group.dev.dependencies] -mypy = "^0.900" +mypy = "^1.11.1" types-pytz = "^2021.3.5" types-cryptography = "^3.3.18" types-python-dateutil = "^2.8.19.10" diff --git a/unifi_protect_backup/__init__.py b/unifi_protect_backup/__init__.py index e5634b6..727a2b7 100644 --- a/unifi_protect_backup/__init__.py +++ b/unifi_protect_backup/__init__.py @@ -12,10 +12,10 @@ from .uploader import VideoUploader from .missing_event_checker import MissingEventChecker __all__ = [ - VideoDownloader, - VideoDownloaderExperimental, - EventListener, - Purge, - VideoUploader, - MissingEventChecker, + "VideoDownloader", + "VideoDownloaderExperimental", + "EventListener", + "Purge", + "VideoUploader", + "MissingEventChecker", ] diff --git a/unifi_protect_backup/downloader_experimental.py b/unifi_protect_backup/downloader_experimental.py index 0d2da3f..d4a7a3a 100644 --- a/unifi_protect_backup/downloader_experimental.py +++ b/unifi_protect_backup/downloader_experimental.py @@ -85,6 +85,8 @@ class VideoDownloaderExperimental: else: self._has_ffprobe = False + raise RuntimeError("The `uiprotect` library is currently missing the features for this to work.") + async def start(self): """Main loop.""" self.logger.info("Starting Downloader") @@ -149,7 +151,10 @@ class VideoDownloaderExperimental: self._failures[event.id] = 1 else: self._failures[event.id] += 1 - self.logger.warning(f"Event failed download attempt {self._failures[event.id]}", exc_info=e) + self.logger.warning( + f"Event failed download attempt {self._failures[event.id]}", + exc_info=e, + ) if self._failures[event.id] >= 10: self.logger.error( @@ -171,7 +176,10 @@ class VideoDownloaderExperimental: self.current_event = None except Exception as e: - self.logger.error(f"Unexpected exception occurred, abandoning event {event.id}:", exc_info=e) + self.logger.error( + f"Unexpected exception occurred, abandoning event {event.id}:", + exc_info=e, + ) async def _download(self, event: Event) -> Optional[bytes]: """Downloads the video clip for the given event.""" @@ -181,8 +189,12 @@ class VideoDownloaderExperimental: assert isinstance(event.start, datetime) assert isinstance(event.end, datetime) try: - prepared_video_file = await self._protect.prepare_camera_video(event.camera_id, event.start, event.end) - video = await self._protect.download_camera_video(event.camera_id, prepared_video_file["fileName"]) + prepared_video_file = await self._protect.prepare_camera_video( # type: ignore + event.camera_id, event.start, event.end + ) + video = await self._protect.download_camera_video( # type: ignore + event.camera_id, prepared_video_file["fileName"] + ) assert isinstance(video, bytes) break except (AssertionError, ClientPayloadError, TimeoutError) as e: diff --git a/unifi_protect_backup/missing_event_checker.py b/unifi_protect_backup/missing_event_checker.py index 2ec8aaa..b71864d 100644 --- a/unifi_protect_backup/missing_event_checker.py +++ b/unifi_protect_backup/missing_event_checker.py @@ -3,7 +3,7 @@ import asyncio import logging from datetime import datetime -from typing import List +from typing import AsyncIterator, List import aiosqlite from dateutil.relativedelta import relativedelta @@ -54,14 +54,14 @@ class MissingEventChecker: self.ignore_cameras: List[str] = ignore_cameras self.interval: int = interval - async def _get_missing_events(self) -> List[Event]: + async def _get_missing_events(self) -> AsyncIterator[Event]: start_time = datetime.now() - self.retention end_time = datetime.now() chunk_size = 500 while True: # Get list of events that need to be backed up from unifi protect - logger.extra_debug(f"Fetching events for interval: {start_time} - {end_time}") + logger.extra_debug(f"Fetching events for interval: {start_time} - {end_time}") # type: ignore events_chunk = await self._protect.get_events( start=start_time, end=end_time, @@ -72,6 +72,7 @@ class MissingEventChecker: if not events_chunk: break # There were no events to backup + assert events_chunk[-1].end is not None start_time = events_chunk[-1].end unifi_events = {event.id: event for event in events_chunk} @@ -166,6 +167,9 @@ class MissingEventChecker: await self._download_queue.put(event) except Exception as e: - logger.error("Unexpected exception occurred during missing event check:", exc_info=e) + logger.error( + "Unexpected exception occurred during missing event check:", + exc_info=e, + ) await asyncio.sleep(self.interval) diff --git a/unifi_protect_backup/unifi_protect_backup_core.py b/unifi_protect_backup/unifi_protect_backup_core.py index e6a176a..f703795 100644 --- a/unifi_protect_backup/unifi_protect_backup_core.py +++ b/unifi_protect_backup/unifi_protect_backup_core.py @@ -21,7 +21,13 @@ from unifi_protect_backup import ( VideoUploader, notifications, ) -from unifi_protect_backup.utils import SubprocessException, VideoQueue, human_readable_size, run_command, setup_logging +from unifi_protect_backup.utils import ( + SubprocessException, + VideoQueue, + human_readable_size, + run_command, + setup_logging, +) logger = logging.getLogger(__name__) @@ -67,7 +73,7 @@ class UnifiProtectBackup: max_event_length: int, sqlite_path: str = "events.sqlite", color_logging: bool = False, - download_rate_limit: float = None, + download_rate_limit: float | None = None, port: int = 443, use_experimental_downloader: bool = False, ): @@ -196,7 +202,10 @@ class UnifiProtectBackup: await self._protect.update() break except Exception as e: - logger.warning(f"Failed to connect to UniFi Protect, retrying in {attempts}s...", exc_info=e) + logger.warning( + f"Failed to connect to UniFi Protect, retrying in {attempts}s...", + exc_info=e, + ) await asyncio.sleep(attempts) else: raise ConnectionError("Failed to connect to UniFi Protect after 10 attempts") @@ -269,7 +278,11 @@ class UnifiProtectBackup: # Create purge task # This will, every midnight, purge old backups from the rclone remotes and database purge = Purge( - self._db, self.retention, self.rclone_destination, self._purge_interval, self.rclone_purge_args + self._db, + self.retention, + self.rclone_destination, + self._purge_interval, + self.rclone_purge_args, ) tasks.append(purge.start()) diff --git a/unifi_protect_backup/utils.py b/unifi_protect_backup/utils.py index 03b35a5..d6f416d 100644 --- a/unifi_protect_backup/utils.py +++ b/unifi_protect_backup/utils.py @@ -294,7 +294,7 @@ async def get_camera_name(protect: ProtectApiClient, id: str): # Refresh cameras logger.debug(f"Unknown camera id: '{id}', checking API") - await protect.update(force=True) + await protect.update() try: name = protect.bootstrap.cameras[id].name @@ -377,7 +377,7 @@ class VideoQueue(asyncio.Queue): self._queue.append(item) # type: ignore self._bytes_sum += len(item[1]) - def full(self, item: tuple[Event, bytes] = None): + def full(self, item: tuple[Event, bytes] | None = None): """Return True if there are maxsize bytes in the queue. optionally if `item` is provided, it will return False if there is enough space to