diff --git a/.editorconfig b/.editorconfig index 1e114ad..f0a974c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,4 @@ indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = false \ No newline at end of file +insert_final_newline = false diff --git a/.gitignore b/.gitignore index 7b54a3d..86feca0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST \ No newline at end of file +MANIFEST diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5382310 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-added-large-files + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + args: ["--profile", "black"] + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + args: ["--max-line-length=160"] diff --git a/README.md b/README.md index 610016a..e22e4ba 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ poetry run python -m unittest discover -s tests ``` - ## Data Structures For a very short and simplified example of the complete library JSON that the API provides, see [example.json](./example.json). Below you will find the fields of each main topic. @@ -129,6 +128,3 @@ For a very short and simplified example of the complete library JSON that the AP "artist_id": 2 } ``` -``` - -Made changes. \ No newline at end of file diff --git a/ibroadcastaio/__init__.py b/ibroadcastaio/__init__.py index 7919254..f97fa47 100644 --- a/ibroadcastaio/__init__.py +++ b/ibroadcastaio/__init__.py @@ -1,2 +1,7 @@ """Provide a package for ibroadcastaio.""" -from .client import IBroadcastClient \ No newline at end of file + +from .client import IBroadcastClient + +__all__ = [ + "IBroadcastClient", +] diff --git a/ibroadcastaio/client.py b/ibroadcastaio/client.py index 73a78dd..87b4d27 100644 --- a/ibroadcastaio/client.py +++ b/ibroadcastaio/client.py @@ -1,12 +1,20 @@ import json -from aiohttp import ClientSession from typing import Any, AsyncGenerator -import logging -from ibroadcastaio.const import BASE_API_URL, BASE_LIBRARY_URL, REFERER, STATUS_API, VERSION +from aiohttp import ClientSession + +from ibroadcastaio.const import ( + BASE_API_URL, + BASE_LIBRARY_URL, + REFERER, + STATUS_API, + VERSION, +) + class IBroadcastClient: """iBroadcast API Client to use the API in an async manner""" + _user_id = None _token = None @@ -22,7 +30,7 @@ def __init__(self, http_session: ClientSession) -> None: self.http_session = http_session async def login(self, username: str, password: str) -> dict[str, Any]: - data={ + data = { "mode": "status", "email_address": username, "password": password, @@ -31,7 +39,9 @@ async def login(self, username: str, password: str) -> dict[str, Any]: "supported_types": False, } - status = await self.__post(f"{BASE_API_URL}{STATUS_API}", { "content_type": "application/json" }, data) + status = await self.__post( + f"{BASE_API_URL}{STATUS_API}", {"content_type": "application/json"}, data + ) if "user" not in status: raise ValueError("Invalid credentials") @@ -57,16 +67,40 @@ async def refresh_library(self): For now we fetch the complete librady and split it into in memory class members. Later, we remove this step and rewrite methods such as _get_albums(album_id) to directly fetch it from the API. """ - library = await self.__post(f"{BASE_LIBRARY_URL}", { "content_type": "application/json" }, data) - - self._albums = {album['album_id']: album async for album in self.__jsonToDict(library['library']['albums'], 'album_id')} - self._artists = {artist['artist_id']: artist async for artist in self.__jsonToDict(library['library']['artists'], 'artist_id')} - self._playlists = {playlist['playlist_id']: playlist async for playlist in self.__jsonToDict(library['library']['playlists'], 'playlist_id')} - self._tags = {tag['tag_id']: tag async for tag in self.__jsonToDict(library['library']['tags'], 'tag_id')} - self._tracks = {track['track_id']: track async for track in self.__jsonToDict(library['library']['tracks'], 'track_id')} - - self._settings = library['settings'] + library = await self.__post( + f"{BASE_LIBRARY_URL}", {"content_type": "application/json"}, data + ) + + self._albums = { + album["album_id"]: album + async for album in self.__jsonToDict( + library["library"]["albums"], "album_id" + ) + } + self._artists = { + artist["artist_id"]: artist + async for artist in self.__jsonToDict( + library["library"]["artists"], "artist_id" + ) + } + self._playlists = { + playlist["playlist_id"]: playlist + async for playlist in self.__jsonToDict( + library["library"]["playlists"], "playlist_id" + ) + } + self._tags = { + tag["tag_id"]: tag + async for tag in self.__jsonToDict(library["library"]["tags"], "tag_id") + } + self._tracks = { + track["track_id"]: track + async for track in self.__jsonToDict( + library["library"]["tracks"], "track_id" + ) + } + self._settings = library["settings"] async def get_settings(self): self._check_library_loaded() @@ -76,13 +110,20 @@ async def get_album(self, album_id: int): self._check_library_loaded() return self._albums.get(album_id) - - async def __post(self, url: str, headers: dict[str, Any] | None = None, data: dict[str, Any] | None = None): - async with self.http_session.post(url=url, data=json.dumps(data), headers=headers) as response: + async def __post( + self, + url: str, + headers: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ): + async with self.http_session.post( + url=url, data=json.dumps(data), headers=headers + ) as response: return await response.json() - - async def __jsonToDict(self, data: list[dict[str, Any]], main_key: str) -> AsyncGenerator[dict[str, Any], None]: + async def __jsonToDict( + self, data: list[dict[str, Any]], main_key: str + ) -> AsyncGenerator[dict[str, Any], None]: """ Convert the library json into python dicts. See the readme for all fields. @@ -138,10 +179,10 @@ async def __jsonToDict(self, data: list[dict[str, Any]], main_key: str) -> Async } } """ - if not 'map' in data or type(data['map']) is not dict: + if "map" not in data or type(data["map"]) is not dict: return - keymap = {v: k for (k, v) in data['map'].items() if not isinstance(v, dict)} + keymap = {v: k for (k, v) in data["map"].items() if not isinstance(v, dict)} for key, value in data.items(): if type(value) is list: @@ -152,4 +193,4 @@ async def __jsonToDict(self, data: list[dict[str, Any]], main_key: str) -> Async def _check_library_loaded(self): """Check if the library is loaded""" if self._settings is None: - raise ValueError("Library not loaded. Please call refresh_library first.") \ No newline at end of file + raise ValueError("Library not loaded. Please call refresh_library first.") diff --git a/ibroadcastaio/const.py b/ibroadcastaio/const.py index 0fd8146..dba963a 100644 --- a/ibroadcastaio/const.py +++ b/ibroadcastaio/const.py @@ -6,4 +6,4 @@ STATUS_API = "/s/JSON/status" REFERER = "ibroadcastaio-client" -VERSION = "0.1.0" \ No newline at end of file +VERSION = "0.1.0" diff --git a/poetry.lock b/poetry.lock index acd8477..1746a86 100644 --- a/poetry.lock +++ b/poetry.lock @@ -177,6 +177,52 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "cfgv" version = "3.4.0" @@ -188,6 +234,20 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -241,6 +301,22 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2)"] +[[package]] +name = "flake8" +version = "7.1.1" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + [[package]] name = "frozenlist" version = "1.5.0" @@ -510,6 +586,17 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {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]] name = "nodeenv" version = "1.9.1" @@ -532,6 +619,17 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -699,6 +797,28 @@ files = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + [[package]] name = "pylint" version = "3.3.1" @@ -977,4 +1097,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3e499831a0c50d60990baf03af02e89f721a051f067447580964a8d4282c2182" +content-hash = "52ef386f015cdc2383bd0720affa0ac498c11ee145d808d7fd78b5a9e9907f94" diff --git a/pyproject.toml b/pyproject.toml index 0865ee8..2cde117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,9 @@ pylint = "^3.3.1" [tool.poetry.dev-dependencies] pytest = "^6.2.4" +black = "^24.10.0" +isort = "^5.13.2" +flake8 = "^7.1.1" [tool.poetry.scripts] example = "example:main" diff --git a/tests/example.json b/tests/example.json index c97e2c2..2a894e6 100644 --- a/tests/example.json +++ b/tests/example.json @@ -534,4 +534,4 @@ "sessionkey": "", "user": "" } -} \ No newline at end of file +} diff --git a/tests/test_client.py b/tests/test_client.py index ca2b2d5..fcffbd5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,9 +1,12 @@ import json import unittest -from unittest.mock import patch, AsyncMock +from unittest.mock import AsyncMock, patch + from aiohttp import ClientSession + from ibroadcastaio.client import IBroadcastClient + class TestIBroadcastClient(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): @@ -13,29 +16,33 @@ async def asyncSetUp(self): async def asyncTearDown(self): await self.session.close() - @patch('ibroadcastaio.client.IBroadcastClient._IBroadcastClient__post', new_callable=AsyncMock) + @patch( + "ibroadcastaio.client.IBroadcastClient._IBroadcastClient__post", + new_callable=AsyncMock, + ) async def test_login_success(self, mock_post): - mock_post.return_value = { - "user": { - "token": "fake_token", - "id": "fake_id" - } - } + mock_post.return_value = {"user": {"token": "fake_token", "id": "fake_id"}} result = await self.client.login("test@example.com", "password") self.assertEqual(self.client._token, "fake_token") self.assertEqual(self.client._user_id, "fake_id") self.assertIn("user", result) - @patch('ibroadcastaio.client.IBroadcastClient._IBroadcastClient__post', new_callable=AsyncMock) + @patch( + "ibroadcastaio.client.IBroadcastClient._IBroadcastClient__post", + new_callable=AsyncMock, + ) async def test_login_failure(self, mock_post): mock_post.return_value = {} with self.assertRaises(ValueError): await self.client.login("test@example.com", "password") - @patch('ibroadcastaio.client.IBroadcastClient._IBroadcastClient__post', new_callable=AsyncMock) + @patch( + "ibroadcastaio.client.IBroadcastClient._IBroadcastClient__post", + new_callable=AsyncMock, + ) async def test_refresh_library(self, mock_post): - with open('tests/example.json', 'r') as file: + with open("tests/example.json", "r") as file: mock_library = json.load(file) mock_post.return_value = mock_library @@ -58,7 +65,7 @@ async def test_json_to_dict(self): None, None, 456, - 1 + 1, ], "map": { "artwork_id": 7, @@ -69,8 +76,8 @@ async def test_json_to_dict(self): "system_created": 3, "tracks": 1, "type": 5, - "uid": 2 - } + "uid": 2, + }, } expected_result = { @@ -83,14 +90,15 @@ async def test_json_to_dict(self): "type": None, "description": None, "artwork_id": 456, - "sort": 1 + "sort": 1, } result = [] - async for item in self.client._IBroadcastClient__jsonToDict(data, 'album_id'): + async for item in self.client._IBroadcastClient__jsonToDict(data, "album_id"): result.append(item) self.assertEqual(result[0], expected_result) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main()