Skip to content

Commit

Permalink
refactor: 🚧 Add support for link entity to client
Browse files Browse the repository at this point in the history
- defines the model for the link entity
- add methods for fetching links from the server

PART OF h2oai/cloud-discovery#694
  • Loading branch information
zoido committed Jan 7, 2025
1 parent 5e81a63 commit 3961d9b
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 1 deletion.
13 changes: 13 additions & 0 deletions src/h2o_discovery/_internal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
_ENVIRONMENT_ENDPOINT = "v1/environment"
_SERVICES_ENDPOINT = "v1/services"
_CLIENTS_ENDPOINT = "v1/clients"
_LINKS_ENDPOINT = "v1/links"

DEFAULT_HTTP_TIMEOUT = datetime.timedelta(seconds=5)

Expand Down Expand Up @@ -58,6 +59,12 @@ def list_clients(self) -> List[model.Client]:
_CLIENTS_ENDPOINT, "clients", model.Client.from_json_dict
)

def list_links(self) -> List[model.Link]:
"""Returns the list of all registered links."""
return self._get_all_entities(
_LINKS_ENDPOINT, "links", model.Link.from_json_dict
)

def _get_all_entities(
self, endpoint: str, collection_key: str, factory: Callable[[dict], _T]
) -> List[_T]:
Expand Down Expand Up @@ -123,6 +130,12 @@ async def list_clients(self) -> List[model.Client]:
_CLIENTS_ENDPOINT, "clients", model.Client.from_json_dict
)

async def list_links(self) -> List[model.Link]:
"""Returns the list of all registered links."""
return await self._get_all_entities(
_LINKS_ENDPOINT, "links", model.Link.from_json_dict
)

async def _get_all_entities(
self, endpoint: str, collection_key: str, factory: Callable[[dict], _T]
) -> List[_T]:
Expand Down
22 changes: 22 additions & 0 deletions src/h2o_discovery/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ def from_json_dict(cls, json: Mapping[str, str]) -> "Environment":
)


@dataclasses.dataclass(frozen=True)
class Link:
"""Representation of the navigation link."""

#: Canonical name of the Link. For example: "links/app-link".
name: str

#: Value that applications can put into the HTML anchor's href
#: attribute to navigate to link.
uri: str

#: Preferred text to use as a link text. This usually make sense
#: only as part of the navigation menus.
#: Example: <a href="{{ link.uri }}">{{ link.text }}</a>
text: Optional[str]

@classmethod
def from_json_dict(cls, json: Mapping[str, str]) -> "Link":
"""Create a Link from a JSON dict returned by the server."""
return cls(name=json["name"], uri=json["uri"], text=json.get("text"))


@dataclasses.dataclass(frozen=True)
class Credentials:
"""Contain credentials associated with single registered client.
Expand Down
157 changes: 156 additions & 1 deletion tests/_internal/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async def test_async_client_list_services_internal():

@respx.mock
@pytest.mark.asyncio
async def test_async_client_list_services_pubclic():
async def test_async_client_list_services_public():
# Given
route = respx.get("https://test.example.com/.ai.h2o.cloud.discovery/v1/services")
route.side_effect = SERVICES_RESPONSES
Expand Down Expand Up @@ -208,6 +208,76 @@ async def test_async_client_list_clients_public():
_assert_pagination_api_calls(route)


@respx.mock
def test_client_list_links_internal():
# Given
route = respx.get("http://test.example.com:1234/v1/links")
route.side_effect = LINKS_RESPONSES

cl = client.Client("http://test.example.com:1234")

# When
links = cl.list_links()

# Then

assert links == EXPECTED_LINKS_RECORDS
_assert_pagination_api_calls(route)


@respx.mock
def test_client_list_links_public():
# Given
route = respx.get("https://test.example.com/.ai.h2o.cloud.discovery/v1/links")
route.side_effect = LINKS_RESPONSES

cl = client.Client("https://test.example.com/.ai.h2o.cloud.discovery")

# When
links = cl.list_links()

# Then

assert links == EXPECTED_LINKS_RECORDS
_assert_pagination_api_calls(route)


@respx.mock
@pytest.mark.asyncio
async def test_async_client_list_links_internal():
# Given
route = respx.get("https://test.example.com:1234/v1/links")
route.side_effect = LINKS_RESPONSES

cl = client.AsyncClient("https://test.example.com:1234")

# When
links = await cl.list_links()

# Then

assert links == EXPECTED_LINKS_RECORDS
_assert_pagination_api_calls(route)


@respx.mock
@pytest.mark.asyncio
async def test_async_client_list_links_public():
# Given
route = respx.get("https://test.example.com/.ai.h2o.cloud.discovery/v1/links")
route.side_effect = LINKS_RESPONSES

cl = client.AsyncClient("https://test.example.com/.ai.h2o.cloud.discovery")

# When
links = await cl.list_links()

# Then

assert links == EXPECTED_LINKS_RECORDS
_assert_pagination_api_calls(route)


@respx.mock
def test_client_list_services_can_handle_empty_response():
# Given
Expand Down Expand Up @@ -262,6 +332,33 @@ async def test_async_client_list_clients_can_handle_empty_response():
assert services == []


@respx.mock
def test_client_list_links_can_handle_empty_response():
# Given
respx.get("https://test.example.com/v1/links").respond(json={})
cl = client.Client("https://test.example.com")

# When
services = cl.list_links()

# Then
assert services == []


@respx.mock
@pytest.mark.asyncio
async def test_async_client_list_links_can_handle_empty_response():
# Given
respx.get("https://test.example.com/v1/links").respond(json={})
cl = client.AsyncClient("https://test.example.com")

# When
services = await cl.list_links()

# Then
assert services == []


def _assert_pagination_api_calls(route):
assert route.call_count == 3
assert not route.calls[0].request.url.query
Expand Down Expand Up @@ -437,3 +534,61 @@ def _assert_pagination_api_calls(route):
oauth2_client_id="test-client-4",
),
]


LINKS_RESPONSES = [
httpx.Response(
200,
json={
"links": [
{"name": "links/test-link-1", "uri": "http://test-link-1.domain:1234"},
{
"name": "links/test-link-2",
"uri": "http://test-link-2.domain:1234",
"text": "Test Link 2",
},
],
"nextPageToken": "next-page-token-1",
},
),
httpx.Response(
200,
json={
"links": [
{
"name": "links/test-link-3",
"uri": "http://test-link-3.domain:1234",
"text": "Test Link 3",
}
],
"nextPageToken": "next-page-token-2",
},
),
httpx.Response(
200,
json={
"links": [
{"name": "links/test-link-4", "uri": "http://test-link-4.domain:1234"}
]
},
),
]

EXPECTED_LINKS_RECORDS = [
model.Link(
name="links/test-link-1", uri="http://test-link-1.domain:1234", text=None
),
model.Link(
name="links/test-link-2",
uri="http://test-link-2.domain:1234",
text="Test Link 2",
),
model.Link(
name="links/test-link-3",
uri="http://test-link-3.domain:1234",
text="Test Link 3",
),
model.Link(
name="links/test-link-4", uri="http://test-link-4.domain:1234", text=None
),
]
30 changes: 30 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,33 @@ def test_environment_from_json_dict_with_missing_version():
h2o_cloud_platform_oauth2_scope="test-platform-scope",
h2o_cloud_version=None,
)


def test_link_from_json_dictt():
# Given
json = {
"name": "links/test-link",
"uri": "http://test-link.domain:1234",
"text": "Test Link",
}

# When
result = model.Link.from_json_dict(json)

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


def test_link_from_json_dict_with_missing_text():
# Given
json = {"name": "links/test-link", "uri": "http://test-link.domain:1234"}

# When
result = model.Link.from_json_dict(json)

# Then
assert result == model.Link(
name="links/test-link", uri="http://test-link.domain:1234", text=None
)

0 comments on commit 3961d9b

Please sign in to comment.