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
18 changes: 15 additions & 3 deletions src/bloomy/operations/async_/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

import asyncio

from ...models import (
DirectReport,
Position,
Expand All @@ -25,6 +27,9 @@ async def details(
) -> UserDetails:
"""Retrieve details of a specific user.

When both direct reports and positions are requested, the two
sub-resource fetches run concurrently via ``asyncio.gather``.

Args:
user_id: The ID of the user (default: the current user ID)
include_direct_reports: Whether to include direct reports
Expand All @@ -44,13 +49,20 @@ async def details(
response.raise_for_status()
data = response.json()

fetch_reports = include_direct_reports or include_all
fetch_positions = include_positions or include_all

direct_reports_data = None
positions_data = None

if include_direct_reports or include_all:
if fetch_reports and fetch_positions:
direct_reports_data, positions_data = await asyncio.gather(
self.direct_reports(user_id),
self.positions(user_id),
)
elif fetch_reports:
direct_reports_data = await self.direct_reports(user_id)

if include_positions or include_all:
elif fetch_positions:
positions_data = await self.positions(user_id)

return self._transform_user_details(data, direct_reports_data, positions_data)
Expand Down
62 changes: 62 additions & 0 deletions tests/test_async_users_extra.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Additional tests for async user operations to improve coverage."""

import asyncio
from unittest.mock import AsyncMock, MagicMock

import pytest
Expand Down Expand Up @@ -209,3 +210,64 @@ async def test_list_users(
mock_async_client.get.assert_called_once_with(
"search/all", params={"term": "%"}
)

@pytest.mark.asyncio
async def test_details_include_all_fetches_subresources_concurrently(
self,
async_client: AsyncClient,
mock_async_client: AsyncMock,
) -> None:
"""When both flags are set, direct reports and positions run concurrently."""
user_data = {
"Id": 123,
"Name": "John Doe",
"Description": "CEO",
"Email": "john@example.com",
"OrganizationId": 1,
"ImageUrl": "https://example.com/avatar.jpg",
}
reports_data = [
{
"Id": 456,
"Name": "Jane Smith",
"Description": "VP Sales",
"Email": "jane@example.com",
"OrganizationId": 1,
"ImageUrl": "https://example.com/jane.jpg",
}
]
positions_data = [{"Group": {"Position": {"Id": 1, "Name": "CEO"}}}]

in_flight = 0
max_in_flight = 0

async def get_side_effect(url: str) -> MagicMock:
nonlocal in_flight, max_in_flight
in_flight += 1
max_in_flight = max(max_in_flight, in_flight)
await asyncio.sleep(0.02)
in_flight -= 1

resp = MagicMock()
resp.raise_for_status = MagicMock()
if url == "users/123":
resp.json.return_value = user_data
elif url == "users/123/directreports":
resp.json.return_value = reports_data
elif url == "users/123/seats":
resp.json.return_value = positions_data
else:
raise ValueError(f"Unexpected URL: {url}")
return resp

mock_async_client.get.side_effect = get_side_effect

result = await async_client.user.details(user_id=123, include_all=True)

assert isinstance(result, UserDetails)
assert result.direct_reports is not None
assert result.positions is not None
assert len(result.direct_reports) == 1
assert len(result.positions) == 1
# User details first (sequential), then the two sub-resources overlap.
assert max_in_flight >= 2
Loading