Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automatic check for server version when instantiating client #51

Merged
merged 8 commits into from
Jan 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ parameters:
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")


asyncio.run(main())
Expand All @@ -80,12 +80,12 @@ The `Client` object provides easy access to several bookmark-related API operati
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Get all bookmarks:
bookmarks = await client.bookmarks.async_get_all()
Expand All @@ -106,12 +106,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Get all archived bookmarks:
bookmarks = await client.bookmarks.async_get_archived()
Expand All @@ -132,12 +132,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Get a single bookmark:
bookmark = await client.bookmarks.async_get_single(37)
Expand All @@ -152,12 +152,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Create a new bookmark:
created_bookmark = await client.bookmarks.async_create(
Expand Down Expand Up @@ -189,12 +189,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Update an existing bookmark:
updated_bookmark = await client.bookmarks.async_update(
Expand Down Expand Up @@ -228,12 +228,12 @@ will change that value for the existing bookmark):
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Archive a bookmark by ID:
await client.bookmarks.async_archive(37)
Expand All @@ -250,12 +250,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Delete a bookmark by ID:
await client.bookmarks.async_delete(37)
Expand All @@ -273,12 +273,12 @@ The `Client` object also provides easy access to several tag-related API operati
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Get all tags:
tags = await client.tags.async_get_all()
Expand All @@ -298,12 +298,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Get a single tag:
tag = await client.tags.async_get_single(22)
Expand All @@ -318,12 +318,12 @@ asyncio.run(main())
```python
import asyncio

from aiolinkding import Client
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
client = Client("http://127.0.0.1:8000", "token_abcde12345")
client = await async_get_client("http://127.0.0.1:8000", "token_abcde12345")

# Create a new tag:
created_tag = await client.tags.async_create("example-tag")
Expand All @@ -343,14 +343,16 @@ connection pooling:
```python
import asyncio

from aiohttp import ClientSession
from aiolinkding import Client
from aiohttp import async_get_clientSession
from aiolinkding import async_get_client


async def main() -> None:
"""Use aiolinkding for fun and profit."""
async with ClientSession() as session:
client = Client("http://127.0.0.1:8000", "token_abcde12345", session=session)
client = await async_get_client(
"http://127.0.0.1:8000", "token_abcde12345", session=session
)

# Get to work...

Expand Down
2 changes: 1 addition & 1 deletion aiolinkding/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Define the aiowatttime package."""
from .client import Client # noqa
from .client import Client, async_get_client # noqa
61 changes: 59 additions & 2 deletions aiolinkding/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@

from aiohttp import ClientSession, ClientTimeout
from aiohttp.client_exceptions import ClientResponseError
from packaging import version

from aiolinkding.bookmark import BookmarkManager
from aiolinkding.const import LOGGER
from aiolinkding.errors import InvalidTokenError, RequestError
from aiolinkding.errors import (
InvalidServerVersionError,
InvalidTokenError,
RequestError,
UnknownEndpointError,
)
from aiolinkding.tag import TagManager

DEFAULT_REQUEST_TIMEOUT = 10

SERVER_VERSION_HEALTH_CHECK_INTRODUCED = version.parse("1.17.0")
SERVER_VERSION_MINIMUM_REQUIRED = version.parse("1.17.0")

INVALID_SERVER_VERSION_MESSAGE = (
"Server version ({0}) is below the minimum version required "
f"({SERVER_VERSION_MINIMUM_REQUIRED})"
)


class Client: # pylint: disable=too-few-public-methods
"""Define a client for the linkding API."""
Expand Down Expand Up @@ -50,6 +64,7 @@ async def async_request(
Raises:
InvalidTokenError: Raised upon an invalid API token.
RequestError: Raised upon an underlying HTTP error.
UnknownEndpointError: Raised when requesting an unknown API endpoint.
"""
kwargs.setdefault("headers", {})
kwargs["headers"]["Authorization"] = f"Token {self._token}"
Expand All @@ -74,8 +89,13 @@ async def async_request(
# An HTTP 204 will not return parsable JSON data, but it's still a
# successful response, so we swallow the exception and return:
return {}
if err.status == 401:
if resp.status == 401:
raise InvalidTokenError("Invalid API token") from err
if resp.status == 404:
# We break out this particular response for the health check; if we
# catch this when querying GET /health, we can raise a better final
# exception:
raise UnknownEndpointError(f"Unknown API endpoint: {endpoint}") from err
raise RequestError(f"Error while requesting {endpoint}: {data}") from err
finally:
if not use_running_session:
Expand All @@ -84,3 +104,40 @@ async def async_request(
LOGGER.debug("Data received for %s: %s", endpoint, data)

return data


async def async_get_client(
url: str, token: str, *, session: ClientSession | None = None
) -> Client:
"""Get an authenticated, version-checked client.

Args:
url: The full URL to a linkding instance.
token: A linkding API token.
session: An optional aiohttp ClientSession.

Returns:
A Client object.

Raises:
InvalidServerVersionError: Raised when the server version is too low.
"""
client = Client(url, token, session=session)

try:
health_resp = await client.async_request("get", "/health")
except UnknownEndpointError as err:
raise InvalidServerVersionError(
INVALID_SERVER_VERSION_MESSAGE.format(
f"older than {SERVER_VERSION_HEALTH_CHECK_INTRODUCED}"
)
) from err

server_version = version.parse(health_resp["version"])

if server_version < SERVER_VERSION_MINIMUM_REQUIRED:
raise InvalidServerVersionError(
INVALID_SERVER_VERSION_MESSAGE.format(server_version)
)

return client
12 changes: 12 additions & 0 deletions aiolinkding/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ class LinkDingError(Exception):
pass


class InvalidServerVersionError(LinkDingError):
"""Define an error related to an invalid server version."""

pass


class InvalidTokenError(LinkDingError):
"""Define an error related to an invalid API token."""

Expand All @@ -17,3 +23,9 @@ class RequestError(LinkDingError):
"""An error related to invalid requests."""

pass


class UnknownEndpointError(RequestError):
"""An error related to an unknown endpoint."""

pass
4 changes: 2 additions & 2 deletions examples/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from aiohttp import ClientSession

from aiolinkding import Client
from aiolinkding import async_get_client
from aiolinkding.errors import LinkDingError

_LOGGER = logging.getLogger()
Expand All @@ -18,7 +18,7 @@ async def main() -> None:
logging.basicConfig(level=logging.INFO)
async with ClientSession() as session:
try:
client = Client(URL, TOKEN, session=session)
client = await async_get_client(URL, TOKEN, session=session)

bookmarks = await client.bookmarks.async_get_all()
_LOGGER.info("Bookmarks: %s", bookmarks)
Expand Down
Loading