Skip to content

Commit 49844b5

Browse files
authored
Merge pull request #16 from franccesco/fix/update-return-consistency
fix: make update() return updated objects consistently
2 parents df740f4 + f273543 commit 49844b5

12 files changed

Lines changed: 179 additions & 23 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "bloomy-python"
3-
version = "0.21.1"
3+
version = "0.21.2"
44
description = "Python SDK for Bloom Growth API"
55
readme = "README.md"
66
authors = [{ name = "Franccesco Orozco", email = "franccesco@codingdose.info" }]

src/bloomy/operations/async_/goals.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,31 @@ async def delete(self, goal_id: int) -> None:
9191
response = await self._client.delete(f"rocks/{goal_id}")
9292
response.raise_for_status()
9393

94+
async def details(self, goal_id: int) -> GoalInfo:
95+
"""Get details for a specific goal.
96+
97+
Args:
98+
goal_id: The ID of the goal
99+
100+
Returns:
101+
A GoalInfo model instance containing the goal details
102+
103+
"""
104+
response = await self._client.get(
105+
f"rocks/{goal_id}", params={"include_origin": True}
106+
)
107+
response.raise_for_status()
108+
data = response.json()
109+
110+
return self._transform_goal_details(data)
111+
94112
async def update(
95113
self,
96114
goal_id: int,
97115
title: str | None = None,
98116
accountable_user: int | None = None,
99117
status: GoalStatus | str | None = None,
100-
) -> None:
118+
) -> GoalInfo:
101119
"""Update a goal.
102120
103121
Args:
@@ -110,12 +128,17 @@ async def update(
110128
GoalStatus.AT_RISK, or GoalStatus.COMPLETE for type safety.
111129
Invalid values will raise ValueError via the update payload builder.
112130
131+
Returns:
132+
A GoalInfo model instance containing the updated goal details
133+
113134
"""
114135
payload = self._build_goal_update_payload(accountable_user, title, status)
115136

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

140+
return await self.details(goal_id)
141+
119142
async def archive(self, goal_id: int) -> None:
120143
"""Archive a rock with the specified goal ID.
121144

src/bloomy/operations/async_/headlines.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,23 @@ async def create(
5959
notes_url=data.get("DetailsUrl") or "",
6060
)
6161

62-
async def update(self, headline_id: int, title: str) -> None:
62+
async def update(self, headline_id: int, title: str) -> HeadlineDetails:
6363
"""Update a headline.
6464
6565
Args:
6666
headline_id: The ID of the headline to update
6767
title: The new title of the headline
6868
69+
Returns:
70+
A HeadlineDetails model instance containing the updated headline
71+
6972
"""
7073
payload = {"title": title}
7174
response = await self._client.put(f"headline/{headline_id}", json=payload)
7275
response.raise_for_status()
7376

77+
return await self.details(headline_id)
78+
7479
async def details(self, headline_id: int) -> HeadlineDetails:
7580
"""Get headline details.
7681

src/bloomy/operations/goals.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,35 @@ def delete(self, goal_id: int) -> None:
124124
response = self._client.delete(f"rocks/{goal_id}")
125125
response.raise_for_status()
126126

127+
def details(self, goal_id: int) -> GoalInfo:
128+
"""Get details for a specific goal.
129+
130+
Args:
131+
goal_id: The ID of the goal
132+
133+
Returns:
134+
A GoalInfo model instance containing the goal details
135+
136+
Example:
137+
```python
138+
client.goal.details(1)
139+
# Returns: GoalInfo(id=1, title='Complete project', ...)
140+
```
141+
142+
"""
143+
response = self._client.get(f"rocks/{goal_id}", params={"include_origin": True})
144+
response.raise_for_status()
145+
data = response.json()
146+
147+
return self._transform_goal_details(data)
148+
127149
def update(
128150
self,
129151
goal_id: int,
130152
title: str | None = None,
131153
accountable_user: int | None = None,
132154
status: GoalStatus | str | None = None,
133-
) -> None:
155+
) -> GoalInfo:
134156
"""Update a goal.
135157
136158
Args:
@@ -143,6 +165,9 @@ def update(
143165
GoalStatus.AT_RISK, or GoalStatus.COMPLETE for type safety.
144166
Invalid values will raise ValueError via the update payload builder.
145167
168+
Returns:
169+
A GoalInfo model instance containing the updated goal details
170+
146171
Example:
147172
```python
148173
from bloomy import GoalStatus
@@ -160,6 +185,8 @@ def update(
160185
response = self._client.put(f"rocks/{goal_id}", json=payload)
161186
response.raise_for_status()
162187

188+
return self.details(goal_id)
189+
163190
def archive(self, goal_id: int) -> None:
164191
"""Archive a rock with the specified goal ID.
165192

src/bloomy/operations/headlines.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,23 @@ def create(
5555
notes_url=data.get("DetailsUrl") or "",
5656
)
5757

58-
def update(self, headline_id: int, title: str) -> None:
58+
def update(self, headline_id: int, title: str) -> HeadlineDetails:
5959
"""Update a headline.
6060
6161
Args:
6262
headline_id: The ID of the headline to update
6363
title: The new title of the headline
6464
65+
Returns:
66+
A HeadlineDetails model instance containing the updated headline
67+
6568
"""
6669
payload = {"title": title}
6770
response = self._client.put(f"headline/{headline_id}", json=payload)
6871
response.raise_for_status()
6972

73+
return self.details(headline_id)
74+
7075
def details(self, headline_id: int) -> HeadlineDetails:
7176
"""Get headline details.
7277

src/bloomy/operations/mixins/goals_transform.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,36 @@ def _transform_goal_list(self, data: Sequence[dict[str, Any]]) -> list[GoalInfo]
5151
for goal in data
5252
]
5353

54+
def _transform_goal_details(self, data: dict[str, Any]) -> GoalInfo:
55+
"""Transform a single goal API response to a GoalInfo model.
56+
57+
Args:
58+
data: The raw API response data for a single goal.
59+
60+
Returns:
61+
A GoalInfo model.
62+
63+
"""
64+
return GoalInfo(
65+
id=data["Id"],
66+
user_id=data["Owner"]["Id"],
67+
user_name=data["Owner"]["Name"],
68+
title=data["Name"],
69+
created_at=data["CreateTime"],
70+
due_date=data["DueDate"],
71+
status="Completed" if data.get("Complete") else "Incomplete",
72+
meeting_id=(
73+
data["Origins"][0]["Id"]
74+
if data.get("Origins") and data["Origins"][0]
75+
else None
76+
),
77+
meeting_title=(
78+
data["Origins"][0]["Name"]
79+
if data.get("Origins") and data["Origins"][0]
80+
else None
81+
),
82+
)
83+
5484
def _transform_archived_goals(
5585
self, data: Sequence[dict[str, Any]]
5686
) -> list[ArchivedGoalInfo]:

tests/test_adversarial_todos_goals.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ class TestGoalUpdateOverwritesOwner:
6868
overwriting the goal owner. This has been fixed.
6969
"""
7070

71+
@pytest.fixture(autouse=True)
72+
def _setup_goal_get_response(self, mock_http_client: Mock) -> None:
73+
"""Configure GET to return a valid goal response for the details() re-fetch."""
74+
get_response = Mock()
75+
get_response.raise_for_status = Mock()
76+
get_response.json.return_value = {
77+
"Id": 1,
78+
"Owner": {"Id": 123, "Name": "Alice"},
79+
"Name": "Goal Title",
80+
"CreateTime": "2024-01-01T00:00:00Z",
81+
"DueDate": "2024-12-31",
82+
"Complete": False,
83+
"Origins": [{"Id": 100, "Name": "Meeting A"}],
84+
}
85+
mock_http_client.get.return_value = get_response
86+
7187
def test_update_title_only_should_not_overwrite_owner(
7288
self, mock_http_client: Mock, mock_user_id: PropertyMock
7389
) -> None:

tests/test_async_goals.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,30 @@ async def test_update(
190190
self, async_client: AsyncClient, mock_async_client: AsyncMock
191191
):
192192
"""Test updating a goal."""
193-
mock_response = MagicMock()
194-
mock_response.raise_for_status = MagicMock()
195-
mock_async_client.put.return_value = mock_response
193+
mock_put_response = MagicMock()
194+
mock_put_response.raise_for_status = MagicMock()
195+
mock_async_client.put.return_value = mock_put_response
196+
197+
mock_get_response = MagicMock()
198+
mock_get_response.raise_for_status = MagicMock()
199+
mock_get_response.json.return_value = {
200+
"Id": 123,
201+
"Owner": {"Id": 1, "Name": "John Doe"},
202+
"Name": "Updated Goal",
203+
"CreateTime": "2024-01-01T00:00:00Z",
204+
"DueDate": "2024-06-01",
205+
"Complete": False,
206+
"Origins": [{"Id": 10, "Name": "Team Meeting"}],
207+
}
208+
mock_async_client.get.return_value = mock_get_response
196209

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

201-
assert result is None
214+
assert isinstance(result, GoalInfo)
215+
assert result.id == 123
216+
assert result.title == "Updated Goal"
202217
mock_async_client.put.assert_called_once_with(
203218
"rocks/123",
204219
json={

tests/test_async_headlines.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,31 @@ async def test_update(
8686
self, async_client: AsyncClient, mock_async_client: AsyncMock
8787
):
8888
"""Test updating a headline."""
89-
mock_response = MagicMock()
90-
mock_response.raise_for_status = MagicMock()
91-
mock_async_client.put.return_value = mock_response
89+
mock_put_response = MagicMock()
90+
mock_put_response.raise_for_status = MagicMock()
91+
mock_async_client.put.return_value = mock_put_response
92+
93+
mock_get_response = MagicMock()
94+
mock_get_response.raise_for_status = MagicMock()
95+
mock_get_response.json.return_value = {
96+
"Id": 501,
97+
"Name": "Updated headline",
98+
"DetailsUrl": "https://example.com/headline/501",
99+
"Owner": {"Id": 123, "Name": "John Doe"},
100+
"Origin": "Product Meeting",
101+
"OriginId": 456,
102+
"Archived": False,
103+
"CreateTime": "2024-06-01T10:00:00Z",
104+
"CloseTime": None,
105+
}
106+
mock_async_client.get.return_value = mock_get_response
92107

93108
result = await async_client.headline.update(
94109
headline_id=501, title="Updated headline"
95110
)
96111

97-
assert result is None
112+
assert isinstance(result, HeadlineDetails)
113+
assert result.id == 501
98114
mock_async_client.put.assert_called_once_with(
99115
"headline/501", json={"title": "Updated headline"}
100116
)

tests/test_goals.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,27 @@ def test_delete_goal(self, mock_http_client: Mock) -> None:
109109
assert result is None
110110
mock_http_client.delete.assert_called_once_with("rocks/101")
111111

112-
def test_update_goal(self, mock_http_client: Mock, mock_user_id: Mock) -> None:
112+
def test_update_goal(
113+
self,
114+
mock_http_client: Mock,
115+
mock_user_id: Mock,
116+
sample_goal_data: dict[str, Any],
117+
) -> None:
113118
"""Test updating a goal."""
114-
mock_response = Mock()
115-
mock_http_client.put.return_value = mock_response
119+
mock_put_response = Mock()
120+
mock_http_client.put.return_value = mock_put_response
121+
122+
mock_get_response = Mock()
123+
mock_get_response.json.return_value = sample_goal_data
124+
mock_http_client.get.return_value = mock_get_response
116125

117126
goal_ops = GoalOperations(mock_http_client)
118127

128+
from bloomy.models import GoalInfo
129+
119130
result = goal_ops.update(goal_id=101, title="Updated Goal", status="complete")
120131

121-
assert result is None
132+
assert isinstance(result, GoalInfo)
122133
mock_http_client.put.assert_called_once_with(
123134
"rocks/101",
124135
json={

0 commit comments

Comments
 (0)