Skip to content

Commit

Permalink
feat: ✨ Discover registered links when available (#112)
Browse files Browse the repository at this point in the history
- add links map to the discovery model
- load links when the discover is called

As Links are new addtion to the API, we do not fail when server returns 404 and only have no links available. All this to have backward compatibility.

RESOLVES h2oai/cloud-discovery#694
  • Loading branch information
zoido authored Jan 8, 2025
1 parent ccb4012 commit 8d584dd
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 3 deletions.
50 changes: 47 additions & 3 deletions src/h2o_discovery/_internal/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import types
from typing import Iterable
from typing import List
from typing import Mapping
from typing import Optional

Expand All @@ -18,11 +19,13 @@ def load_discovery(cl: client.Client) -> model.Discovery:
environment = cl.get_environment()
services = _get_service_map(cl.list_services())
clients = _get_client_map(cl.list_clients())
links = _get_link_map(_list_links(cl))
except Exception as e:
_handle_specific_client_exceptions(e)
raise

return model.Discovery(environment=environment, services=services, clients=clients)
return model.Discovery(
environment=environment, services=services, clients=clients, links=links
)


async def load_discovery_async(cl: client.AsyncClient) -> model.Discovery:
Expand All @@ -31,11 +34,36 @@ async def load_discovery_async(cl: client.AsyncClient) -> model.Discovery:
environment = await cl.get_environment()
services = _get_service_map(await cl.list_services())
clients = _get_client_map(await cl.list_clients())
links = _get_link_map(await _list_links_async(cl))
except Exception as e:
_handle_specific_client_exceptions(e)
raise

return model.Discovery(environment=environment, services=services, clients=clients)
return model.Discovery(
environment=environment, services=services, clients=clients, links=links
)


def _list_links(cl: client.Client) -> List[model.Link]:
try:
return cl.list_links()
except Exception as e:
# Links are added in the server version 2.5.0. In order to have client that
# is backwards compatible, we won't fail if the links are not available.
if isinstance(e, httpx.HTTPStatusError) and e.response.status_code == 404:
return []
raise


async def _list_links_async(cl: client.AsyncClient) -> List[model.Link]:
try:
return await cl.list_links()
except Exception as e:
# Links are added in the server version 2.5.0. In order to have client that
# is backwards compatible, we won't fail if the links are not available.
if isinstance(e, httpx.HTTPStatusError) and e.response.status_code == 404:
return []
raise


def load_credentials(
Expand Down Expand Up @@ -71,6 +99,13 @@ def _get_client_map(clients: Iterable[model.Client]) -> Mapping[str, model.Clien
return types.MappingProxyType(out)


def _get_link_map(links: Iterable[model.Link]) -> Mapping[str, model.Link]:
out = {}
for ln in links:
out[_link_key(ln.name)] = ln
return types.MappingProxyType(out)


_SERVICES_COLLECTION_PREFIX = "services/"


Expand All @@ -89,6 +124,15 @@ def _client_key(name: str) -> str:
raise ValueError(f"invalid client name: {name}")


_LINKS_COLLECTION_PREFIX = "links/"


def _link_key(name: str) -> str:
if name.startswith(_LINKS_COLLECTION_PREFIX):
return name[len(_LINKS_COLLECTION_PREFIX) :]
raise ValueError(f"invalid link name: {name}")


_ENV_ERROR = error.H2OCloudEnvironmentError(
"Received an unexpected response from the server."
" Please make sure that the environment you are trying to connect to is"
Expand Down
3 changes: 3 additions & 0 deletions src/h2o_discovery/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ class Discovery:
#: Map of registered clients in the `{"client-identifier": Client(...)}` format.
clients: Mapping[str, Client]

#: Map of registered links in the `{"link-identifier": Link(...)}` format.
links: Mapping[str, Link]

#: Map of credentials in the `{"client-identifier": Credentials(...)}` format.
credentials: Mapping[str, Credentials] = dataclasses.field(
default_factory=_empty_credentials_factory
Expand Down
66 changes: 66 additions & 0 deletions tests/_internal/load/test_load_discovery.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
from unittest import mock

import httpx
import pytest

from h2o_discovery import model
Expand All @@ -15,6 +16,8 @@ def mock_async_client():
client.list_services.return_value.set_result({})
client.list_clients.return_value = asyncio.Future()
client.list_clients.return_value.set_result({})
client.list_links.return_value = asyncio.Future()
client.list_links.return_value.set_result({})

return client

Expand All @@ -25,6 +28,7 @@ def mock_sync_client():
client.list_services.return_value = {}
client.list_clients.return_value = {}
client.list_clients.return_value = {}
client.list_links.return_value = {}

return client

Expand Down Expand Up @@ -130,3 +134,65 @@ async def test_load_clients_async():

# Then
assert discovery.clients["test-client"] == CLIENT_RECORD


LINK_RECORD = model.Link(
name="links/test-link", uri="http://test-link.domain:1234", text="Test Link"
)


def test_load_links():
# Given
mock_client = mock_sync_client()
mock_client.list_links.return_value = [LINK_RECORD]

# When
discovery = load.load_discovery(mock_client)

# Then
assert discovery.links["test-link"] == LINK_RECORD


@pytest.mark.asyncio
async def test_load_links_async():
# Given
mock_client = mock_async_client()
mock_client.list_links.return_value = asyncio.Future()
mock_client.list_links.return_value.set_result([LINK_RECORD])

# When
discovery = await load.load_discovery_async(mock_client)

# Then
assert discovery.links["test-link"] == LINK_RECORD


def test_load_links_not_found_returns_empty_map():
# Given
not_found_exception = httpx.HTTPStatusError(
"Test Error", request=mock.Mock(), response=mock.Mock(status_code=404)
)
mock_client = mock_sync_client()
mock_client.list_links.side_effect = not_found_exception

# When
discovery = load.load_discovery(mock_client)

# Then
assert discovery.links == {}


@pytest.mark.asyncio
async def test_load_links_async_not_found_returns_empty_map():
# Given
not_found_exception = httpx.HTTPStatusError(
"Test Error", request=mock.Mock(), response=mock.Mock(status_code=404)
)
mock_client = mock_async_client()
mock_client.list_links.side_effect = not_found_exception

# When
discovery = await load.load_discovery_async(mock_client)

# Then
assert discovery.links == {}

0 comments on commit 8d584dd

Please sign in to comment.