Skip to content

Commit 54facee

Browse files
franccescoclaude
andcommitted
refactor(operations): simplify using mixins and bulk helpers
- Update all operations to inherit from transformation mixins - Replace inline transformation logic with mixin method calls - Simplify create_many methods using generic bulk helpers - Standardize error handling with raise_for_status() - Remove redundant __init__ methods from async operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f7ee739 commit 54facee

13 files changed

Lines changed: 162 additions & 931 deletions

File tree

src/bloomy/operations/async_/goals.py

Lines changed: 21 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,27 @@
22

33
from __future__ import annotations
44

5-
import asyncio
65
import builtins
76
from typing import TYPE_CHECKING
87

98
from ...models import (
109
ArchivedGoalInfo,
11-
BulkCreateError,
1210
BulkCreateResult,
1311
CreatedGoalInfo,
1412
GoalInfo,
1513
GoalListResponse,
1614
GoalStatus,
1715
)
1816
from ...utils.async_base_operations import AsyncBaseOperations
17+
from ..mixins.goals_transform import GoalOperationsMixin
1918

2019
if TYPE_CHECKING:
2120
from typing import Any
2221

23-
import httpx
2422

25-
26-
class AsyncGoalOperations(AsyncBaseOperations):
23+
class AsyncGoalOperations(AsyncBaseOperations, GoalOperationsMixin):
2724
"""Async class to handle all operations related to goals (aka "rocks")."""
2825

29-
def __init__(self, client: httpx.AsyncClient) -> None:
30-
"""Initialize the async goal operations.
31-
32-
Args:
33-
client: The async HTTP client to use for API requests.
34-
35-
"""
36-
super().__init__(client)
37-
3826
async def list(
3927
self, user_id: int | None = None, archived: bool = False
4028
) -> builtins.list[GoalInfo] | GoalListResponse:
@@ -60,22 +48,7 @@ async def list(
6048
response.raise_for_status()
6149
data = response.json()
6250

63-
active_goals: list[GoalInfo] = [
64-
GoalInfo(
65-
id=goal["Id"],
66-
user_id=goal["Owner"]["Id"],
67-
user_name=goal["Owner"]["Name"],
68-
title=goal["Name"],
69-
created_at=goal["CreateTime"],
70-
due_date=goal["DueDate"],
71-
status="Completed" if goal.get("Complete") else "Incomplete",
72-
meeting_id=goal["Origins"][0]["Id"] if goal.get("Origins") else None,
73-
meeting_title=(
74-
goal["Origins"][0]["Name"] if goal.get("Origins") else None
75-
),
76-
)
77-
for goal in data
78-
]
51+
active_goals = self._transform_goal_list(data)
7952

8053
if archived:
8154
archived_goals = await self._get_archived_goals(user_id)
@@ -106,20 +79,7 @@ async def create(
10679
response.raise_for_status()
10780
data = response.json()
10881

109-
# Map completion status
110-
completion_map = {2: "complete", 1: "on", 0: "off"}
111-
status = completion_map.get(data.get("Completion", 0), "off")
112-
113-
return CreatedGoalInfo(
114-
id=data["Id"],
115-
user_id=user_id,
116-
user_name=data["Owner"]["Name"],
117-
title=title,
118-
meeting_id=meeting_id,
119-
meeting_title=data["Origins"][0]["Name"],
120-
status=status,
121-
created_at=data["CreateTime"],
122-
)
82+
return self._transform_created_goal(data, title, meeting_id, user_id)
12383

12484
async def delete(self, goal_id: int) -> None:
12585
"""Delete a goal.
@@ -148,29 +108,13 @@ async def update(
148108
status: The status value. Can be a GoalStatus enum member or string
149109
('on', 'off', or 'complete'). Use GoalStatus.ON_TRACK,
150110
GoalStatus.AT_RISK, or GoalStatus.COMPLETE for type safety.
151-
152-
Raises:
153-
ValueError: If an invalid status value is provided
111+
Invalid values will raise ValueError via the update payload builder.
154112
155113
"""
156114
if accountable_user is None:
157115
accountable_user = await self.get_user_id()
158116

159-
payload: dict[str, Any] = {"accountableUserId": accountable_user}
160-
161-
if title is not None:
162-
payload["title"] = title
163-
164-
if status is not None:
165-
valid_status = {"on": "OnTrack", "off": "AtRisk", "complete": "Complete"}
166-
# Handle both GoalStatus enum and string
167-
status_value = status.value if isinstance(status, GoalStatus) else status
168-
status_key = status_value.lower()
169-
if status_key not in valid_status:
170-
raise ValueError(
171-
"Invalid status value. Must be 'on', 'off', or 'complete'."
172-
)
173-
payload["completion"] = valid_status[status_key]
117+
payload = self._build_goal_update_payload(accountable_user, title, status)
174118

175119
response = await self._client.put(f"rocks/{goal_id}", json=payload)
176120
response.raise_for_status()
@@ -214,16 +158,7 @@ async def _get_archived_goals(
214158
response.raise_for_status()
215159
data = response.json()
216160

217-
return [
218-
ArchivedGoalInfo(
219-
id=goal["Id"],
220-
title=goal["Name"],
221-
created_at=goal["CreateTime"],
222-
due_date=goal["DueDate"],
223-
status="Complete" if goal.get("Complete") else "Incomplete",
224-
)
225-
for goal in data
226-
]
161+
return self._transform_archived_goals(data)
227162

228163
async def create_many(
229164
self, goals: builtins.list[dict[str, Any]], max_concurrent: int = 5
@@ -260,67 +195,17 @@ async def create_many(
260195
```
261196
262197
"""
263-
# Create a semaphore to limit concurrent requests
264-
semaphore = asyncio.Semaphore(max_concurrent)
265-
266-
async def create_single_goal(
267-
index: int, goal_data: dict[str, Any]
268-
) -> tuple[int, CreatedGoalInfo | BulkCreateError]:
269-
"""Create a single goal with error handling.
270-
271-
Returns:
272-
Tuple of (index, result) where result is either CreatedGoalInfo
273-
or BulkCreateError.
274-
275-
Raises:
276-
ValueError: When required parameters are missing.
277-
278-
"""
279-
async with semaphore:
280-
try:
281-
# Extract parameters from the goal data
282-
title = goal_data.get("title")
283-
meeting_id = goal_data.get("meeting_id")
284-
user_id = goal_data.get("user_id")
285-
286-
# Validate required parameters
287-
if title is None:
288-
raise ValueError("title is required")
289-
if meeting_id is None:
290-
raise ValueError("meeting_id is required")
291-
292-
# Create the goal
293-
created_goal = await self.create(
294-
title=title, meeting_id=meeting_id, user_id=user_id
295-
)
296-
return (index, created_goal)
297-
298-
except Exception as e:
299-
error = BulkCreateError(
300-
index=index, input_data=goal_data, error=str(e)
301-
)
302-
return (index, error)
303-
304-
# Create tasks for all goals
305-
tasks = [
306-
create_single_goal(index, goal_data)
307-
for index, goal_data in enumerate(goals)
308-
]
309-
310-
# Execute all tasks concurrently
311-
results = await asyncio.gather(*tasks)
312-
313-
# Sort results to maintain order
314-
results.sort(key=lambda x: x[0])
315-
316-
# Separate successful and failed results
317-
successful: builtins.list[CreatedGoalInfo] = []
318-
failed: builtins.list[BulkCreateError] = []
319-
320-
for _, result in results:
321-
if isinstance(result, CreatedGoalInfo):
322-
successful.append(result)
323-
else:
324-
failed.append(result)
325-
326-
return BulkCreateResult(successful=successful, failed=failed)
198+
199+
async def _create_single(data: dict[str, Any]) -> CreatedGoalInfo:
200+
return await self.create(
201+
title=data["title"],
202+
meeting_id=data["meeting_id"],
203+
user_id=data.get("user_id"),
204+
)
205+
206+
return await self._process_bulk_async(
207+
goals,
208+
_create_single,
209+
required_fields=["title", "meeting_id"],
210+
max_concurrent=max_concurrent,
211+
)

src/bloomy/operations/async_/headlines.py

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,18 @@
99
HeadlineDetails,
1010
HeadlineInfo,
1111
HeadlineListItem,
12-
MeetingInfo,
1312
OwnerDetails,
1413
)
1514
from ...utils.async_base_operations import AsyncBaseOperations
15+
from ..mixins.headlines_transform import HeadlineOperationsMixin
1616

1717
if TYPE_CHECKING:
18-
import httpx
18+
pass
1919

2020

21-
class AsyncHeadlineOperations(AsyncBaseOperations):
21+
class AsyncHeadlineOperations(AsyncBaseOperations, HeadlineOperationsMixin):
2222
"""Async class to handle all operations related to headlines."""
2323

24-
def __init__(self, client: httpx.AsyncClient) -> None:
25-
"""Initialize the async headline operations.
26-
27-
Args:
28-
client: The async HTTP client to use for API requests.
29-
30-
"""
31-
super().__init__(client)
32-
3324
async def create(
3425
self,
3526
meeting_id: int,
@@ -97,22 +88,7 @@ async def details(self, headline_id: int) -> HeadlineDetails:
9788
response.raise_for_status()
9889
data = response.json()
9990

100-
return HeadlineDetails(
101-
id=data["Id"],
102-
title=data["Name"],
103-
notes_url=data["DetailsUrl"],
104-
meeting_details=MeetingInfo(
105-
id=data["OriginId"],
106-
title=data["Origin"],
107-
),
108-
owner_details=OwnerDetails(
109-
id=data["Owner"]["Id"],
110-
name=data["Owner"]["Name"],
111-
),
112-
archived=data["Archived"],
113-
created_at=data["CreateTime"],
114-
closed_at=data["CloseTime"],
115-
)
91+
return self._transform_headline_details(data)
11692

11793
async def list(
11894
self, user_id: int | None = None, meeting_id: int | None = None
@@ -143,24 +119,7 @@ async def list(
143119
response.raise_for_status()
144120
data = response.json()
145121

146-
return [
147-
HeadlineListItem(
148-
id=headline["Id"],
149-
title=headline["Name"],
150-
meeting_details=MeetingInfo(
151-
id=headline["OriginId"],
152-
title=headline["Origin"],
153-
),
154-
owner_details=OwnerDetails(
155-
id=headline["Owner"]["Id"],
156-
name=headline["Owner"]["Name"],
157-
),
158-
archived=headline["Archived"],
159-
created_at=headline["CreateTime"],
160-
closed_at=headline["CloseTime"],
161-
)
162-
for headline in data
163-
]
122+
return self._transform_headline_list(data)
164123

165124
async def delete(self, headline_id: int) -> None:
166125
"""Delete a headline.

0 commit comments

Comments
 (0)