Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ All notable changes to this project are documented here. The format follows
and added cursor-aware membership page and collection helpers.
- Parsed both standard `Retry-After` forms into safe, non-negative rate-limit
delays and ignored malformed or non-finite values.
- Cached lazily created resource-service accessors per client while keeping
class-scoped object services on demand.

### Security

Expand Down
5 changes: 5 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ same in both modes.
Create and close an `AsyncClient` within the same application and event-loop
lifetime.

Resource-service accessors such as `client.classes` are created lazily and
cached for the lifetime of each client. Class-scoped services such as
`client.classes.by_id(class_id).objects` remain created on demand for the
requested class.

## Identity scopes

Provider-scoped authentication uses the optional `identity_scope` value:
Expand Down
15 changes: 8 additions & 7 deletions src/hubuum_client/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import AsyncIterator, Mapping
from contextlib import asynccontextmanager
from functools import cached_property
from typing import Any, Self, TypeVar, overload

import httpx
Expand Down Expand Up @@ -107,31 +108,31 @@ async def config(self) -> dict[str, Any]:
value = await self.request("GET", "/api/v1/config", options=_PUBLIC_REQUEST)
return value if isinstance(value, dict) else {}

@property
@cached_property
def collections(self) -> AsyncCollectionsService:
return AsyncCollectionsService(self)

@property
@cached_property
def classes(self) -> AsyncClassesService:
return AsyncClassesService(self)

@property
@cached_property
def users(self) -> AsyncUsersService:
return AsyncUsersService(self)

@property
@cached_property
def groups(self) -> AsyncGroupsService:
return AsyncGroupsService(self)

@property
@cached_property
def class_relations(self) -> AsyncClassRelationsService:
return AsyncClassRelationsService(self)

@property
@cached_property
def object_relations(self) -> AsyncObjectRelationsService:
return AsyncObjectRelationsService(self)

@property
@cached_property
def tasks(self) -> AsyncTasksService:
return AsyncTasksService(self)

Expand Down
15 changes: 8 additions & 7 deletions src/hubuum_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from functools import cached_property
from typing import Any, Self, TypeVar, overload

import httpx
Expand Down Expand Up @@ -110,31 +111,31 @@ def config(self) -> dict[str, Any]:
value = self.request("GET", "/api/v1/config", options=_PUBLIC_REQUEST)
return value if isinstance(value, dict) else {}

@property
@cached_property
def collections(self) -> CollectionsService:
return CollectionsService(self)

@property
@cached_property
def classes(self) -> ClassesService:
return ClassesService(self)

@property
@cached_property
def users(self) -> UsersService:
return UsersService(self)

@property
@cached_property
def groups(self) -> GroupsService:
return GroupsService(self)

@property
@cached_property
def class_relations(self) -> ClassRelationsService:
return ClassRelationsService(self)

@property
@cached_property
def object_relations(self) -> ObjectRelationsService:
return ObjectRelationsService(self)

@property
@cached_property
def tasks(self) -> TasksService:
return TasksService(self)

Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,33 @@ def _client(
return AsyncClient("https://hubuum.test", token=token, transport=httpx.MockTransport(handler))


async def test_async_resource_services_are_lazily_cached_per_client() -> None:
names = (
"collections",
"classes",
"users",
"groups",
"class_relations",
"object_relations",
"tasks",
)

def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={})

async with _client(handler) as client, _client(handler) as other_client:
for name in names:
assert name not in vars(client)
service = getattr(client, name)
assert service is getattr(client, name)
assert vars(client)[name] is service

assert client.classes is not other_client.classes
assert client.classes.by_id(1) is not client.classes.by_id(1)
selected = client.classes.by_id(1)
assert selected.objects is not selected.objects


async def test_async_login_and_typed_service(class_json: dict[str, Any]) -> None:
requests: list[httpx.Request] = []

Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@ def _client(
return Client("https://hubuum.test", token=token, transport=httpx.MockTransport(handler))


def test_resource_services_are_lazily_cached_per_client() -> None:
names = (
"collections",
"classes",
"users",
"groups",
"class_relations",
"object_relations",
"tasks",
)

def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={})

with _client(handler) as client, _client(handler) as other_client:
for name in names:
assert name not in vars(client)
service = getattr(client, name)
assert service is getattr(client, name)
assert vars(client)[name] is service

assert client.classes is not other_client.classes
assert client.classes.by_id(1) is not client.classes.by_id(1)
selected = client.classes.by_id(1)
assert selected.objects is not selected.objects


def test_login_authentication_and_logout_are_redacted() -> None:
requests: list[httpx.Request] = []

Expand Down