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: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "bloomy-python"
version = "0.21.1"
version = "0.21.2"
description = "Python SDK for Bloom Growth API"
readme = "README.md"
authors = [{ name = "Franccesco Orozco", email = "franccesco@codingdose.info" }]
Expand Down
25 changes: 24 additions & 1 deletion src/bloomy/operations/async_/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,31 @@ async def delete(self, goal_id: int) -> None:
response = await self._client.delete(f"rocks/{goal_id}")
response.raise_for_status()

async def details(self, goal_id: int) -> GoalInfo:
"""Get details for a specific goal.

Args:
goal_id: The ID of the goal

Returns:
A GoalInfo model instance containing the goal details

"""
response = await self._client.get(
f"rocks/{goal_id}", params={"include_origin": True}
)
response.raise_for_status()
data = response.json()

return self._transform_goal_details(data)

async def update(
self,
goal_id: int,
title: str | None = None,
accountable_user: int | None = None,
status: GoalStatus | str | None = None,
) -> None:
) -> GoalInfo:
"""Update a goal.

Args:
Expand All @@ -110,12 +128,17 @@ async def update(
GoalStatus.AT_RISK, or GoalStatus.COMPLETE for type safety.
Invalid values will raise ValueError via the update payload builder.

Returns:
A GoalInfo model instance containing the updated goal details

"""
payload = self._build_goal_update_payload(accountable_user, title, status)

response = await self._client.put(f"rocks/{goal_id}", json=payload)
response.raise_for_status()

return await self.details(goal_id)

async def archive(self, goal_id: int) -> None:
"""Archive a rock with the specified goal ID.

Expand Down
7 changes: 6 additions & 1 deletion src/bloomy/operations/async_/headlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,23 @@ async def create(
notes_url=data.get("DetailsUrl") or "",
)

async def update(self, headline_id: int, title: str) -> None:
async def update(self, headline_id: int, title: str) -> HeadlineDetails:
"""Update a headline.

Args:
headline_id: The ID of the headline to update
title: The new title of the headline

Returns:
A HeadlineDetails model instance containing the updated headline

"""
payload = {"title": title}
response = await self._client.put(f"headline/{headline_id}", json=payload)
response.raise_for_status()

return await self.details(headline_id)

async def details(self, headline_id: int) -> HeadlineDetails:
"""Get headline details.

Expand Down
29 changes: 28 additions & 1 deletion src/bloomy/operations/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,35 @@ def delete(self, goal_id: int) -> None:
response = self._client.delete(f"rocks/{goal_id}")
response.raise_for_status()

def details(self, goal_id: int) -> GoalInfo:
"""Get details for a specific goal.

Args:
goal_id: The ID of the goal

Returns:
A GoalInfo model instance containing the goal details

Example:
```python
client.goal.details(1)
# Returns: GoalInfo(id=1, title='Complete project', ...)
```

"""
response = self._client.get(f"rocks/{goal_id}", params={"include_origin": True})
response.raise_for_status()
data = response.json()

return self._transform_goal_details(data)

def update(
self,
goal_id: int,
title: str | None = None,
accountable_user: int | None = None,
status: GoalStatus | str | None = None,
) -> None:
) -> GoalInfo:
"""Update a goal.

Args:
Expand All @@ -143,6 +165,9 @@ def update(
GoalStatus.AT_RISK, or GoalStatus.COMPLETE for type safety.
Invalid values will raise ValueError via the update payload builder.

Returns:
A GoalInfo model instance containing the updated goal details

Example:
```python
from bloomy import GoalStatus
Expand All @@ -160,6 +185,8 @@ def update(
response = self._client.put(f"rocks/{goal_id}", json=payload)
response.raise_for_status()

return self.details(goal_id)

def archive(self, goal_id: int) -> None:
"""Archive a rock with the specified goal ID.

Expand Down
7 changes: 6 additions & 1 deletion src/bloomy/operations/headlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,23 @@ def create(
notes_url=data.get("DetailsUrl") or "",
)

def update(self, headline_id: int, title: str) -> None:
def update(self, headline_id: int, title: str) -> HeadlineDetails:
"""Update a headline.

Args:
headline_id: The ID of the headline to update
title: The new title of the headline

Returns:
A HeadlineDetails model instance containing the updated headline

"""
payload = {"title": title}
response = self._client.put(f"headline/{headline_id}", json=payload)
response.raise_for_status()

return self.details(headline_id)

def details(self, headline_id: int) -> HeadlineDetails:
"""Get headline details.

Expand Down
30 changes: 30 additions & 0 deletions src/bloomy/operations/mixins/goals_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ def _transform_goal_list(self, data: Sequence[dict[str, Any]]) -> list[GoalInfo]
for goal in data
]

def _transform_goal_details(self, data: dict[str, Any]) -> GoalInfo:
"""Transform a single goal API response to a GoalInfo model.

Args:
data: The raw API response data for a single goal.

Returns:
A GoalInfo model.

"""
return GoalInfo(
id=data["Id"],
user_id=data["Owner"]["Id"],
user_name=data["Owner"]["Name"],
title=data["Name"],
created_at=data["CreateTime"],
due_date=data["DueDate"],
status="Completed" if data.get("Complete") else "Incomplete",
meeting_id=(
data["Origins"][0]["Id"]
if data.get("Origins") and data["Origins"][0]
else None
),
meeting_title=(
data["Origins"][0]["Name"]
if data.get("Origins") and data["Origins"][0]
else None
),
)

def _transform_archived_goals(
self, data: Sequence[dict[str, Any]]
) -> list[ArchivedGoalInfo]:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_adversarial_todos_goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ class TestGoalUpdateOverwritesOwner:
overwriting the goal owner. This has been fixed.
"""

@pytest.fixture(autouse=True)
def _setup_goal_get_response(self, mock_http_client: Mock) -> None:
"""Configure GET to return a valid goal response for the details() re-fetch."""
get_response = Mock()
get_response.raise_for_status = Mock()
get_response.json.return_value = {
"Id": 1,
"Owner": {"Id": 123, "Name": "Alice"},
"Name": "Goal Title",
"CreateTime": "2024-01-01T00:00:00Z",
"DueDate": "2024-12-31",
"Complete": False,
"Origins": [{"Id": 100, "Name": "Meeting A"}],
}
mock_http_client.get.return_value = get_response

def test_update_title_only_should_not_overwrite_owner(
self, mock_http_client: Mock, mock_user_id: PropertyMock
) -> None:
Expand Down
23 changes: 19 additions & 4 deletions tests/test_async_goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,30 @@ async def test_update(
self, async_client: AsyncClient, mock_async_client: AsyncMock
):
"""Test updating a goal."""
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_async_client.put.return_value = mock_response
mock_put_response = MagicMock()
mock_put_response.raise_for_status = MagicMock()
mock_async_client.put.return_value = mock_put_response

mock_get_response = MagicMock()
mock_get_response.raise_for_status = MagicMock()
mock_get_response.json.return_value = {
"Id": 123,
"Owner": {"Id": 1, "Name": "John Doe"},
"Name": "Updated Goal",
"CreateTime": "2024-01-01T00:00:00Z",
"DueDate": "2024-06-01",
"Complete": False,
"Origins": [{"Id": 10, "Name": "Team Meeting"}],
}
mock_async_client.get.return_value = mock_get_response

result = await async_client.goal.update(
goal_id=123, title="Updated Goal", status="on"
)

assert result is None
assert isinstance(result, GoalInfo)
assert result.id == 123
assert result.title == "Updated Goal"
mock_async_client.put.assert_called_once_with(
"rocks/123",
json={
Expand Down
24 changes: 20 additions & 4 deletions tests/test_async_headlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,31 @@ async def test_update(
self, async_client: AsyncClient, mock_async_client: AsyncMock
):
"""Test updating a headline."""
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_async_client.put.return_value = mock_response
mock_put_response = MagicMock()
mock_put_response.raise_for_status = MagicMock()
mock_async_client.put.return_value = mock_put_response

mock_get_response = MagicMock()
mock_get_response.raise_for_status = MagicMock()
mock_get_response.json.return_value = {
"Id": 501,
"Name": "Updated headline",
"DetailsUrl": "https://example.com/headline/501",
"Owner": {"Id": 123, "Name": "John Doe"},
"Origin": "Product Meeting",
"OriginId": 456,
"Archived": False,
"CreateTime": "2024-06-01T10:00:00Z",
"CloseTime": None,
}
mock_async_client.get.return_value = mock_get_response

result = await async_client.headline.update(
headline_id=501, title="Updated headline"
)

assert result is None
assert isinstance(result, HeadlineDetails)
assert result.id == 501
mock_async_client.put.assert_called_once_with(
"headline/501", json={"title": "Updated headline"}
)
Expand Down
19 changes: 15 additions & 4 deletions tests/test_goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,27 @@ def test_delete_goal(self, mock_http_client: Mock) -> None:
assert result is None
mock_http_client.delete.assert_called_once_with("rocks/101")

def test_update_goal(self, mock_http_client: Mock, mock_user_id: Mock) -> None:
def test_update_goal(
self,
mock_http_client: Mock,
mock_user_id: Mock,
sample_goal_data: dict[str, Any],
) -> None:
"""Test updating a goal."""
mock_response = Mock()
mock_http_client.put.return_value = mock_response
mock_put_response = Mock()
mock_http_client.put.return_value = mock_put_response

mock_get_response = Mock()
mock_get_response.json.return_value = sample_goal_data
mock_http_client.get.return_value = mock_get_response

goal_ops = GoalOperations(mock_http_client)

from bloomy.models import GoalInfo

result = goal_ops.update(goal_id=101, title="Updated Goal", status="complete")

assert result is None
assert isinstance(result, GoalInfo)
mock_http_client.put.assert_called_once_with(
"rocks/101",
json={
Expand Down
18 changes: 13 additions & 5 deletions tests/test_headlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,24 @@ def test_create_default_owner(
json={"title": "Product launch successful", "ownerId": 123},
)

def test_update(self, mock_http_client: Mock) -> None:
def test_update(
self, mock_http_client: Mock, sample_headline_data: dict[str, Any]
) -> None:
"""Test updating a headline."""
mock_response = Mock()
mock_response.json.return_value = {"Id": 501, "Name": "Updated headline"}
mock_http_client.put.return_value = mock_response
mock_put_response = Mock()
mock_http_client.put.return_value = mock_put_response

mock_get_response = Mock()
mock_get_response.json.return_value = sample_headline_data
mock_http_client.get.return_value = mock_get_response

headline_ops = HeadlineOperations(mock_http_client)
result = headline_ops.update(headline_id=501, title="Updated headline")

assert result is None
from bloomy.models import HeadlineDetails

assert isinstance(result, HeadlineDetails)
assert result.id == 501

mock_http_client.put.assert_called_once_with(
"headline/501", json={"title": "Updated headline"}
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading