From b50677f7a760df28d1a96c91c84af1cdb7b388c1 Mon Sep 17 00:00:00 2001 From: Joostlek Date: Mon, 5 Aug 2024 14:24:39 +0200 Subject: [PATCH 1/4] Add poetry --- .pre-commit-config.yaml | 9 ++ pyproject.toml | 130 +++++++++++++++++++++++ setup.py | 17 --- {uvcclient => src/uvcclient}/__init__.py | 0 {uvcclient => src/uvcclient}/camera.py | 8 +- {uvcclient => src/uvcclient}/main.py | 6 +- {uvcclient => src/uvcclient}/nvr.py | 19 ++-- {uvcclient => src/uvcclient}/store.py | 8 +- tox.ini | 12 --- uvc | 7 -- 10 files changed, 159 insertions(+), 57 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py rename {uvcclient => src/uvcclient}/__init__.py (100%) rename {uvcclient => src/uvcclient}/camera.py (98%) rename {uvcclient => src/uvcclient}/main.py (98%) rename {uvcclient => src/uvcclient}/nvr.py (97%) rename {uvcclient => src/uvcclient}/store.py (91%) delete mode 100644 tox.ini delete mode 100755 uvc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31aa3be..4769f89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,6 +25,15 @@ repos: - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace + - repo: https://github.com/python-poetry/poetry + rev: 1.8.0 + hooks: + - id: poetry-check + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + args: ["--tab-width", "2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.5 hooks: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8bc65fb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,130 @@ +[tool.poetry] +name = "uvcclient" +version = "0.11.1" +description = "A remote control client for Ubiquiti's UVC NVR" +authors = ["Dan Smith "] +readme = "README.rst" +repository = "https://github.com/uilibs/uvcclient" +classifiers = [ + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Build Tools", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)" +] +packages = [ + { include = "uvcclient", from = "src" }, +] + +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/uilibs/uvcclient/issues" +"Changelog" = "https://github.com/uilibs/uvcclient/blob/main/CHANGELOG.md" + +[tool.poetry.dependencies] +python = ">=3.10" + +[tool.poetry.group.dev.dependencies] +pytest = ">=7,<9" + +[tool.semantic_release] +version_toml = ["pyproject.toml:tool.poetry.version"] +version_variables = [ + "src/uvcclient/__init__.py:__version__", + "docs/conf.py:release", +] +build_command = "pip install poetry && poetry build" + +[tool.semantic_release.changelog] +exclude_commit_patterns = [ + "chore*", + "ci*", +] + +[tool.semantic_release.changelog.environment] +keep_trailing_newline = true + +[tool.semantic_release.branches.main] +match = "main" + +[tool.semantic_release.branches.noop] +match = "(?!main$)" +prerelease = true + +[tool.pytest.ini_options] +addopts = "-v -Wdefault --cov=uvcclient --cov-report=term-missing:skip-covered -n=auto" +pythonpath = ["src"] + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "@overload", + "if TYPE_CHECKING", + "raise NotImplementedError", + 'if __name__ == "__main__":', +] + +[tool.ruff] +target-version = "py310" +line-length = 88 + +[tool.ruff.lint] +ignore = [ + "S101", # use of assert + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "D100", # Missing docstring in public module + "D101", # Missing docstring in public module + "D102", # Missing docstring in public method + "D103", # Missing docstring in public module + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D205", # 1 blank line required between summary line and description + "D415", # First line should end with a period, question mark, or exclamation point + "D417", # Missing argument descriptions in the docstring + "E501", # Line too long + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "B008", # Do not perform function call + "S110", # `try`-`except`-`pass` detected, consider logging the exception + "D106", # Missing docstring in public nested class + "UP031", + "B904", + "UP007", # typer needs Optional syntax + "UP038", # Use `X | Y` in `isinstance` is slower + "S603", # check for execution of untrusted input +] +select = [ + "B", # flake8-bugbear + "D", # flake8-docstrings + "C4", # flake8-comprehensions + "S", # flake8-bandit + "F", # pyflake + "E", # pycodestyle + "W", # pycodestyle + "UP", # pyupgrade + "I", # isort + "RUF", # ruff specific +] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*" = [ + "D100", + "D101", + "D102", + "D103", + "D104", + "S101", +] + +[tool.ruff.lint.isort] +known-first-party = ["uvcclient", "tests"] diff --git a/setup.py b/setup.py deleted file mode 100644 index c1ff808..0000000 --- a/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -from setuptools import setup - -setup( - name="uvcclient", - version="0.11.1", - description="A remote control client for Ubiquiti's UVC NVR", - author="Dan Smith", - author_email="dsmith+uvcclient@danplanet.com", - url="http://github.org/kk7ds/uvcclient", - packages=["uvcclient"], - scripts=["uvc"], - install_requires=[], - tests_require=["mock"], - classifiers=[ - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - ], -) diff --git a/uvcclient/__init__.py b/src/uvcclient/__init__.py similarity index 100% rename from uvcclient/__init__.py rename to src/uvcclient/__init__.py diff --git a/uvcclient/camera.py b/src/uvcclient/camera.py similarity index 98% rename from uvcclient/camera.py rename to src/uvcclient/camera.py index 9290b06..2dbffd0 100644 --- a/uvcclient/camera.py +++ b/src/uvcclient/camera.py @@ -15,7 +15,6 @@ import json import logging -import socket # Python3 compatibility try: @@ -23,8 +22,9 @@ except ImportError: from http import client as httplib try: - import urlparse import urllib + + import urlparse except ImportError: import urllib.parse as urlparse @@ -37,7 +37,7 @@ class CameraAuthError(Exception): pass -class UVCCameraClient(object): +class UVCCameraClient: def __init__(self, host, username, password, port=80): self._host = host self._port = port @@ -51,7 +51,7 @@ def _safe_request(self, *args, **kwargs): conn = httplib.HTTPConnection(self._host, self._port) conn.request(*args, **kwargs) return conn.getresponse() - except (socket.error, OSError): + except OSError: raise CameraConnectError("Unable to contact camera") except httplib.HTTPException as ex: raise CameraConnectError("Error connecting to camera: %s" % (str(ex))) diff --git a/uvcclient/main.py b/src/uvcclient/main.py similarity index 98% rename from uvcclient/main.py rename to src/uvcclient/main.py index cb75299..544d7e2 100644 --- a/uvcclient/main.py +++ b/src/uvcclient/main.py @@ -19,10 +19,8 @@ import optparse import sys -from uvcclient import nvr -from uvcclient import camera -from uvcclient import store -from uvcclient.nvr import Invalid +from . import camera, nvr, store +from .nvr import Invalid INFO_STORE = store.get_info_store() diff --git a/uvcclient/nvr.py b/src/uvcclient/nvr.py similarity index 97% rename from uvcclient/nvr.py rename to src/uvcclient/nvr.py index 4309a00..d18790d 100755 --- a/uvcclient/nvr.py +++ b/src/uvcclient/nvr.py @@ -18,11 +18,10 @@ import json import logging -import pprint import os +import pprint import zlib - # Python3 compatibility try: import httplib @@ -50,7 +49,7 @@ class CameraConnectionError(Exception): pass -class UVCRemote(object): +class UVCRemote: """Remote control client for Ubiquiti Unifi Video NVR.""" CHANNEL_NAMES = ["high", "medium", "low"] @@ -151,14 +150,14 @@ def dump(self, uuid): pprint.pprint(data) def set_recordmode(self, uuid, mode, chan=None): - """Set the recording mode for a camera by UUID. + """ + Set the recording mode for a camera by UUID. :param uuid: Camera UUID :param mode: One of none, full, or motion :param chan: One of the values from CHANNEL_NAMES :returns: True if successful, False or None otherwise """ - url = "/api/2.0/camera/%s" % uuid data = self._uvc_request(url) settings = data["data"][0]["recordingSettings"] @@ -225,7 +224,8 @@ def list_zones(self, uuid): return data["data"][0]["zones"] def index(self): - """Return an index of available cameras. + """ + Return an index of available cameras. :returns: A list of dictionaries with keys of name, uuid """ @@ -243,7 +243,8 @@ def index(self): ] def name_to_uuid(self, name): - """Attempt to convert a camera name to its UUID. + """ + Attempt to convert a camera name to its UUID. :param name: Camera name :returns: The UUID of the first camera with the same name if found, @@ -269,7 +270,8 @@ def get_snapshot(self, uuid): def get_auth_from_env(): - """Attempt to get UVC NVR connection information from the environment. + """ + Attempt to get UVC NVR connection information from the environment. Supports either a combined variable called UVC formatted like: @@ -283,7 +285,6 @@ def get_auth_from_env(): :returns: A tuple like (host, port, apikey, path) """ - combined = os.getenv("UVC") if combined: # http://192.168.1.1:7080/apikey diff --git a/uvcclient/store.py b/src/uvcclient/store.py similarity index 91% rename from uvcclient/store.py rename to src/uvcclient/store.py index 6e36664..7e23ca4 100644 --- a/uvcclient/store.py +++ b/src/uvcclient/store.py @@ -11,7 +11,7 @@ class UnableToManageStore(Exception): pass -class InfoStore(object): +class InfoStore: def __init__(self, path=None): if path is None: path = os.path.expanduser(os.path.join("~", ".uvcclient")) @@ -20,9 +20,9 @@ def __init__(self, path=None): def load(self): try: - with open(self._path, "r") as f: + with open(self._path) as f: self._data = json.loads(base64.b64decode(f.read()).decode()) - except (OSError, IOError): + except OSError: LOG.debug("No info store") self._data = {} except Exception as ex: @@ -34,7 +34,7 @@ def save(self): with open(self._path, "w") as f: f.write(base64.b64encode(json.dumps(self._data).encode()).decode()) os.chmod(self._path, 0o600) - except (OSError, IOError) as ex: + except OSError as ex: LOG.error("Unable to write store: %s", str(ex)) raise UnableToManageStore("Unable to write to store") diff --git a/tox.ini b/tox.ini deleted file mode 100644 index db2578a..0000000 --- a/tox.ini +++ /dev/null @@ -1,12 +0,0 @@ -[tox] -envlist = py27,py34,pep8 -[testenv] -deps = - nose - mock -commands = nosetests -v -[testenv:pep8] -deps = pep8 -commands = pep8 -[pep8] -ignore = E124 diff --git a/uvc b/uvc deleted file mode 100755 index 7fc09de..0000000 --- a/uvc +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python - -import sys - -from uvcclient import main - -sys.exit(main.main()) From 10415c07a7fae645981d8bbf55d10bf68e088a27 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:26:21 +0000 Subject: [PATCH 2/4] chore(pre-commit.ci): auto fixes --- tests/test_camera.py | 5 ++--- tests/test_cli.py | 3 +-- tests/test_client.py | 3 +-- tests/test_store.py | 4 ++-- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/test_camera.py b/tests/test_camera.py index 94dd25a..da6f261 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -5,8 +5,7 @@ import json import unittest - -import mock +from unittest import mock from uvcclient import camera @@ -30,7 +29,7 @@ def test_get_snapshot(self): conn = mock_conn.return_value conn.getresponse.return_value.status = 200 r = c.get_snapshot() - self.assertEquals(conn.getresponse.return_value.read.return_value, r) + self.assertEqual(conn.getresponse.return_value.read.return_value, r) def test_cfgwrite(self): c = camera.UVCCameraClient("foo", "ubnt", "ubnt") diff --git a/tests/test_cli.py b/tests/test_cli.py index 98343f5..4fa4b25 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,5 @@ import unittest - -import mock +from unittest import mock from uvcclient import nvr diff --git a/tests/test_client.py b/tests/test_client.py index b2b9c22..4934000 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,8 +6,7 @@ import json import unittest import zlib - -import mock +from unittest import mock from uvcclient import nvr diff --git a/tests/test_store.py b/tests/test_store.py index 8fd4542..e9e9e17 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -1,6 +1,6 @@ import contextlib -import mock import unittest +from unittest import mock from uvcclient import store @@ -10,7 +10,7 @@ import builtins -class OpenHelper(object): +class OpenHelper: def __init__(self): self.mock = mock.MagicMock() From 8520a2cbf02b12ed5e36e2f6341b63d0518cc8ba Mon Sep 17 00:00:00 2001 From: Joostlek Date: Mon, 5 Aug 2024 14:28:04 +0200 Subject: [PATCH 3/4] Add poetry --- tests/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 4934000..584e229 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -13,7 +13,7 @@ class TestClientLowLevel(unittest.TestCase): def setUp(self): - super(TestClientLowLevel, self).setUp() + super().setUp() self._patches = [] try: import httplib # noqa: F401 @@ -145,7 +145,7 @@ def test_320_returns_uuid(self, mock_index, mock_bootstrap): class TestClient(unittest.TestCase): def setUp(self): - super(TestClient, self).setUp() + super().setUp() self._patches = [] try: import httplib # noqa: F401 From 26a29bccef37c5ac4f92816337591b5bc6d950c5 Mon Sep 17 00:00:00 2001 From: Joostlek Date: Mon, 5 Aug 2024 14:29:50 +0200 Subject: [PATCH 4/4] Add poetry --- poetry.lock | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f46162a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,101 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10" +content-hash = "4b0df0d6f638761b031ba3f61e0b98c04157a42619274a8281d5220810554e46"