diff --git a/miniflux.py b/miniflux.py index b01f416..fadba57 100644 --- a/miniflux.py +++ b/miniflux.py @@ -133,14 +133,10 @@ def __init__( ValueError: If neither `api_key` nor both `username` and `password` are provided. """ if not base_url.startswith(("http://", "https://")): - raise ValueError( - "base_url must be a valid URL starting with http:// or https://" - ) + raise ValueError("base_url must be a valid URL starting with http:// or https://") if not api_key and not (username and password): - raise ValueError( - "Either api_key or both username and password must be provided" - ) + raise ValueError("Either api_key or both username and password must be provided") self._base_url = base_url.rstrip("/") self._timeout = timeout @@ -391,9 +387,7 @@ def get_icon_by_feed_id(self, feed_id: int) -> dict: """ return self.get_feed_icon(feed_id) - def create_feed( - self, feed_url: str, category_id: Optional[int] = None, **kwargs - ) -> int: + def create_feed(self, feed_url: str, category_id: Optional[int] = None, **kwargs) -> int: """ Create a new feed. @@ -596,9 +590,7 @@ def get_entries(self, **kwargs) -> dict: return response.json() self._handle_error_response(response) - def update_entry( - self, entry_id: int, title: Optional[str] = None, content: Optional[str] = None - ) -> dict: + def update_entry(self, entry_id: int, title: Optional[str] = None, content: Optional[str] = None) -> dict: """ Update an entry. @@ -718,9 +710,7 @@ def get_enclosure(self, enclosure_id: int) -> dict: return response.json() self._handle_error_response(response) - def update_enclosure( - self, enclosure_id: int, media_progression: Optional[int] = None - ) -> bool: + def update_enclosure(self, enclosure_id: int, media_progression: Optional[int] = None) -> bool: """ Update an enclosure. @@ -1023,6 +1013,57 @@ def get_integrations_status(self) -> bool: return response.json()["has_integrations"] self._handle_error_response(response) + def get_api_keys(self) -> List[dict]: + """ + Get all API keys for the current user. + + Returns: + List[dict]: A list of API keys. + Raises: + ClientError: If the request fails. + """ + endpoint = self._get_endpoint("/api-keys") + response = self._session.get(endpoint, timeout=self._timeout) + if response.status_code == 200: + return response.json() + self._handle_error_response(response) + + def create_api_key(self, description: str) -> dict: + """ + Create a new API key. + + Args: + description (str): The description for the API key. + Returns: + dict: The created API key. + Raises: + ClientError: If the request fails. + """ + endpoint = self._get_endpoint("/api-keys") + data = {"description": description} + response = self._session.post( + endpoint, + data=json.dumps(data), + timeout=self._timeout, + ) + if response.status_code == 201: + return response.json() + self._handle_error_response(response) + + def delete_api_key(self, api_key_id: int) -> None: + """ + Delete an API key. + + Args: + api_key_id (int): The ID of the API key to delete. + Raises: + ClientError: If the request fails. + """ + endpoint = self._get_endpoint(f"/api-keys/{api_key_id}") + response = self._session.delete(endpoint, timeout=self._timeout) + if response.status_code != 204: + self._handle_error_response(response) + def close(self) -> None: """ Close the underlying session diff --git a/pyproject.toml b/pyproject.toml index 1832868..bbcc17b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "miniflux" -version = "1.1.3" +version = "1.1.4" description = "Client library for Miniflux" readme = "README.md" requires-python = ">=3.8" diff --git a/tests/test_client.py b/tests/test_client.py index d8878bf..c85ca5b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -65,9 +65,7 @@ def test_get_error_with_bad_response(self): def test_base_url_with_trailing_slash(self): session = requests.Session() - expected_result = [ - {"url": "http://example.org/feed", "title": "Example", "type": "RSS"} - ] + expected_result = [{"url": "http://example.org/feed", "title": "Example", "type": "RSS"}] response = mock.Mock() response.status_code = 200 @@ -76,9 +74,7 @@ def test_base_url_with_trailing_slash(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost/", "username", "password", session=session - ) + client = miniflux.Client("http://localhost/", "username", "password", session=session) result = client.discover("http://example.org/") session.post.assert_called_once_with( @@ -148,9 +144,7 @@ def test_get_me(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.me() session.get.assert_called_once_with( @@ -169,18 +163,14 @@ def test_get_me_with_server_error(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(ClientError): client.me() def test_discover(self): session = requests.Session() - expected_result = [ - {"url": "http://example.org/feed", "title": "Example", "type": "RSS"} - ] + expected_result = [{"url": "http://example.org/feed", "title": "Example", "type": "RSS"}] response = mock.Mock() response.status_code = 200 @@ -189,9 +179,7 @@ def test_discover(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.discover("http://example.org/") session.post.assert_called_once_with( @@ -210,9 +198,7 @@ def test_discover(self): def test_discover_with_credentials(self): session = requests.Session() - expected_result = [ - {"url": "http://example.org/feed", "title": "Example", "type": "RSS"} - ] + expected_result = [{"url": "http://example.org/feed", "title": "Example", "type": "RSS"}] response = mock.Mock() response.status_code = 200 @@ -221,9 +207,7 @@ def test_discover_with_credentials(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.discover( "http://example.org/", username="foobar", @@ -257,9 +241,7 @@ def test_discover_with_server_error(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(ClientError): client.discover("http://example.org/") @@ -275,9 +257,7 @@ def test_export(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.export() session.get.assert_called_once_with( @@ -297,9 +277,7 @@ def test_import(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) client.import_feeds(input_data) session.post.assert_called_once_with( @@ -319,9 +297,7 @@ def test_import_failure(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(ClientError): client.import_feeds(input_data) @@ -343,9 +319,7 @@ def test_get_feed(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_feed(123) session.get.assert_called_once_with( @@ -370,9 +344,7 @@ def test_get_feed_icon(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_icon_by_feed_id(123) session.get.assert_called_once_with( @@ -397,9 +369,7 @@ def test_get_icon(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_icon(11) session.get.assert_called_once_with( @@ -420,9 +390,7 @@ def test_create_feed(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.create_feed("http://example.org/feed", 123) session.post.assert_called_once_with( @@ -452,9 +420,7 @@ def test_create_feed_with_no_category(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.create_feed("http://example.org/feed") session.post.assert_called_once_with( @@ -484,12 +450,8 @@ def test_create_feed_with_credentials(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) - result = client.create_feed( - "http://example.org/feed", 123, username="foobar", password="secret" - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) + result = client.create_feed("http://example.org/feed", 123, username="foobar", password="secret") session.post.assert_called_once_with( "http://localhost/v1/feeds", @@ -518,9 +480,7 @@ def test_create_feed_with_crawler_enabled(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.create_feed("http://example.org/feed", 123, crawler=True) session.post.assert_called_once_with( @@ -550,12 +510,8 @@ def test_create_feed_with_custom_user_agent_and_crawler_disabled(self): session.post = mock.Mock() session.post.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) - result = client.create_feed( - "http://example.org/feed", 123, crawler=False, user_agent="GoogleBot" - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) + result = client.create_feed("http://example.org/feed", 123, crawler=False, user_agent="GoogleBot") session.post.assert_called_once_with( "http://localhost/v1/feeds", @@ -585,9 +541,7 @@ def test_update_feed(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.update_feed(123, crawler=True, username="test") session.put.assert_called_once_with( @@ -616,9 +570,7 @@ def test_refresh_all_feeds(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.refresh_all_feeds() session.put.assert_called_once_with( @@ -639,9 +591,7 @@ def test_refresh_feed(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.refresh_feed(123) session.put.assert_called_once_with( @@ -662,9 +612,7 @@ def test_refresh_category(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.refresh_category(123) session.put.assert_called_once_with( @@ -685,9 +633,7 @@ def test_get_feed_entry(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_feed_entry(123, 456) session.get.assert_called_once_with( @@ -708,9 +654,7 @@ def test_get_feed_entries(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_feed_entries(123) session.get.assert_called_once_with( @@ -732,9 +676,7 @@ def test_get_feed_entries_with_direction_param(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_feed_entries(123, direction="asc") session.get.assert_called_once_with( @@ -810,9 +752,7 @@ def test_get_entry(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_entry(123) session.get.assert_called_once_with( @@ -833,9 +773,7 @@ def test_fetch_entry_content(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.fetch_entry_content(123) session.get.assert_called_once_with( @@ -856,9 +794,7 @@ def test_get_entries(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_entries(status="unread", limit=10, offset=5) session.get.assert_called_once_with( @@ -881,9 +817,7 @@ def test_get_entries_with_before_param(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_entries(before=param_value) session.get.assert_called_once_with( @@ -905,9 +839,7 @@ def test_get_entries_with_starred_param(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_entries(starred=True) session.get.assert_called_once_with( @@ -929,9 +861,7 @@ def test_get_entries_with_starred_param_at_false(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_entries(starred=False, after_entry_id=123) session.get.assert_called_once_with( @@ -953,9 +883,7 @@ def test_get_user_by_id(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_user_by_id(123) session.get.assert_called_once_with( @@ -975,9 +903,7 @@ def test_get_inexisting_user(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(ResourceNotFound): client.get_user_by_id(123) @@ -993,9 +919,7 @@ def test_get_user_by_username(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_user_by_username("foobar") session.get.assert_called_once_with( @@ -1016,9 +940,7 @@ def test_update_user(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.update_user(123, theme="black", language="fr_FR") session.put.assert_called_once_with( @@ -1041,9 +963,7 @@ def test_timeout(self): session.get = mock.Mock() session.get.side_effect = Timeout() - client = miniflux.Client( - "http://localhost", "username", "password", 1.0, session=session - ) + client = miniflux.Client("http://localhost", "username", "password", 1.0, session=session) with self.assertRaises(Timeout): client.export() @@ -1101,9 +1021,7 @@ def test_get_category_entry(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_category_entry(123, 456) session.get.assert_called_once_with( @@ -1124,9 +1042,7 @@ def test_get_category_entries(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_category_entries(123) session.get.assert_called_once_with( @@ -1148,9 +1064,7 @@ def test_update_entry_title(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.update_entry(entry_id=123, title="New title") session.put.assert_called_once_with( @@ -1176,9 +1090,7 @@ def test_update_entry_content(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.update_entry(entry_id=123, content="New content") session.put.assert_called_once_with( @@ -1202,9 +1114,7 @@ def test_update_entries_status(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.update_entries(entry_ids=[123, 456], status="read") session.put.assert_called_once_with( @@ -1231,9 +1141,7 @@ def test_get_enclosure(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_enclosure(123) session.get.assert_called_once_with( @@ -1252,9 +1160,7 @@ def test_update_enclosure(self): session.put = mock.Mock() session.put.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) self.assertTrue(client.update_enclosure(123, media_progression=42)) session.put.assert_called_once_with( @@ -1274,9 +1180,7 @@ def test_get_integrations_status(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) result = client.get_integrations_status() session.get.assert_called_once_with( @@ -1286,6 +1190,67 @@ def test_get_integrations_status(self): self.assertTrue(result) + def test_get_api_keys(self): + session = requests.Session() + expected_result = [{"id": 1, "description": "Test API Key"}] + + response = mock.Mock() + response.status_code = 200 + response.json.return_value = expected_result + + session.get = mock.Mock() + session.get.return_value = response + + client = miniflux.Client("http://localhost", api_key="secret", session=session) + result = client.get_api_keys() + + session.get.assert_called_once_with( + "http://localhost/v1/api-keys", + timeout=30.0, + ) + self.assertEqual(session.headers.get("X-Auth-Token"), "secret") + self.assertEqual(result, expected_result) + + def test_create_api_key(self): + session = requests.Session() + expected_result = {"id": 2, "description": "New API Key", "token": "some-token"} + + response = mock.Mock() + response.status_code = 201 + response.json.return_value = expected_result + + session.post = mock.Mock() + session.post.return_value = response + + client = miniflux.Client("http://localhost", api_key="secret", session=session) + result = client.create_api_key("New API Key") + + session.post.assert_called_once_with( + "http://localhost/v1/api-keys", + data=json.dumps({"description": "New API Key"}), + timeout=30.0, + ) + self.assertEqual(session.headers.get("X-Auth-Token"), "secret") + self.assertEqual(result, expected_result) + + def test_delete_api_key(self): + session = requests.Session() + + response = mock.Mock() + response.status_code = 204 + + session.delete = mock.Mock() + session.delete.return_value = response + + client = miniflux.Client("http://localhost", api_key="secret", session=session) + client.delete_api_key(1) + + session.delete.assert_called_once_with( + "http://localhost/v1/api-keys/1", + timeout=30.0, + ) + self.assertEqual(session.headers.get("X-Auth-Token"), "secret") + def test_not_found_response(self): session = requests.Session() @@ -1296,9 +1261,7 @@ def test_not_found_response(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(ResourceNotFound): client.get_version() @@ -1313,9 +1276,7 @@ def test_unauthorized_response(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(AccessUnauthorized): client.get_version() @@ -1330,9 +1291,7 @@ def test_forbidden_response(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(AccessForbidden): client.get_version() @@ -1347,9 +1306,7 @@ def test_bad_request_response(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(BadRequest): client.get_version() @@ -1364,9 +1321,7 @@ def test_server_error_response(self): session.get = mock.Mock() session.get.return_value = response - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) with self.assertRaises(ServerError): client.get_version() @@ -1374,9 +1329,7 @@ def test_server_error_response(self): def test_session_closed(self): session = mock.Mock() - client = miniflux.Client( - "http://localhost", "username", "password", session=session - ) + client = miniflux.Client("http://localhost", "username", "password", session=session) client.close() session.close.assert_called() @@ -1389,9 +1342,7 @@ def test_context_manager_exit_on_error(self): session = mock.Mock() session.get.return_value = response - with miniflux.Client( - "http://localhost", "username", "password", session=session - ) as client: + with miniflux.Client("http://localhost", "username", "password", session=session) as client: with self.assertRaises(ServerError): client.get_version()