bloomy-python 0.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,304 @@
1
+ """Meeting operations for the Bloomy SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import Any
7
+
8
+ from ..exceptions import APIError
9
+ from ..models import (
10
+ Issue,
11
+ MeetingAttendee,
12
+ MeetingDetails,
13
+ MeetingListItem,
14
+ ScorecardMetric,
15
+ Todo,
16
+ )
17
+ from ..utils.base_operations import BaseOperations
18
+
19
+
20
+ class MeetingOperations(BaseOperations):
21
+ """Class to handle all operations related to meetings.
22
+
23
+ Note:
24
+ This class is already initialized via the client and usable as
25
+ `client.meeting.method`
26
+ """
27
+
28
+ def list(self, user_id: int | None = None) -> builtins.list[MeetingListItem]:
29
+ """List all meetings for a specific user.
30
+
31
+ Args:
32
+ user_id: The ID of the user (default is the initialized user ID)
33
+
34
+ Returns:
35
+ A list of MeetingListItem model instances
36
+
37
+ Example:
38
+ ```python
39
+ client.meeting.list()
40
+ # Returns: [MeetingListItem(id=123, name="Team Meeting", ...), ...]
41
+ ```
42
+ """
43
+ if user_id is None:
44
+ user_id = self.user_id
45
+
46
+ response = self._client.get(f"L10/{user_id}/list")
47
+ response.raise_for_status()
48
+ data: Any = response.json()
49
+
50
+ return [MeetingListItem.model_validate(meeting) for meeting in data]
51
+
52
+ def attendees(self, meeting_id: int) -> builtins.list[MeetingAttendee]:
53
+ """List all attendees for a specific meeting.
54
+
55
+ Args:
56
+ meeting_id: The ID of the meeting
57
+
58
+ Returns:
59
+ A list of MeetingAttendee model instances
60
+
61
+ Example:
62
+ ```python
63
+ client.meeting.attendees(1)
64
+ # Returns: [MeetingAttendee(user_id=1, name='John Doe',
65
+ # image_url='...'), ...]
66
+ ```
67
+ """
68
+ response = self._client.get(f"L10/{meeting_id}/attendees")
69
+ response.raise_for_status()
70
+ data: Any = response.json()
71
+
72
+ return [
73
+ MeetingAttendee(
74
+ UserId=attendee["Id"],
75
+ Name=attendee["Name"],
76
+ ImageUrl=attendee.get("ImageUrl", ""),
77
+ )
78
+ for attendee in data
79
+ ]
80
+
81
+ def issues(
82
+ self, meeting_id: int, include_closed: bool = False
83
+ ) -> builtins.list[Issue]:
84
+ """List all issues for a specific meeting.
85
+
86
+ Args:
87
+ meeting_id: The ID of the meeting
88
+ include_closed: Whether to include closed issues (default: False)
89
+
90
+ Returns:
91
+ A list of Issue model instances
92
+
93
+ Example:
94
+ ```python
95
+ client.meeting.issues(1)
96
+ # Returns: [Issue(id=1, name='Issue Title',
97
+ # created_at='2024-06-10', ...), ...]
98
+ ```
99
+ """
100
+ response = self._client.get(
101
+ f"L10/{meeting_id}/issues",
102
+ params={"include_resolved": include_closed},
103
+ )
104
+ response.raise_for_status()
105
+ data: Any = response.json()
106
+
107
+ return [
108
+ Issue(
109
+ Id=issue["Id"],
110
+ Name=issue["Name"],
111
+ DetailsUrl=issue["DetailsUrl"],
112
+ CreateDate=issue["CreateTime"],
113
+ ClosedDate=issue["CloseTime"],
114
+ CompletionDate=issue["CloseTime"],
115
+ OwnerId=issue.get("Owner", {}).get("Id", 0),
116
+ OwnerName=issue.get("Owner", {}).get("Name", ""),
117
+ OwnerImageUrl=issue.get("Owner", {}).get("ImageUrl", ""),
118
+ MeetingId=meeting_id,
119
+ MeetingName=issue["Origin"],
120
+ )
121
+ for issue in data
122
+ ]
123
+
124
+ def todos(
125
+ self, meeting_id: int, include_closed: bool = False
126
+ ) -> builtins.list[Todo]:
127
+ """List all todos for a specific meeting.
128
+
129
+ Args:
130
+ meeting_id: The ID of the meeting
131
+ include_closed: Whether to include closed todos (default: False)
132
+
133
+ Returns:
134
+ A list of Todo model instances
135
+
136
+ Example:
137
+ ```python
138
+ client.meeting.todos(1)
139
+ # Returns: [Todo(id=1, name='Todo Title', due_date='2024-06-12', ...), ...]
140
+ ```
141
+ """
142
+ response = self._client.get(
143
+ f"L10/{meeting_id}/todos",
144
+ params={"INCLUDE_CLOSED": include_closed},
145
+ )
146
+ response.raise_for_status()
147
+ data: Any = response.json()
148
+
149
+ return [Todo.model_validate(todo) for todo in data]
150
+
151
+ def metrics(self, meeting_id: int) -> builtins.list[ScorecardMetric]:
152
+ """List all metrics for a specific meeting.
153
+
154
+ Args:
155
+ meeting_id: The ID of the meeting
156
+
157
+ Returns:
158
+ A list of ScorecardMetric model instances
159
+
160
+ Example:
161
+ ```python
162
+ client.meeting.metrics(1)
163
+ # Returns: [ScorecardMetric(id=1, title='Sales', target=100.0,
164
+ # metric_type='>', unit='currency', ...), ...]
165
+ ```
166
+ """
167
+ response = self._client.get(f"L10/{meeting_id}/measurables")
168
+ response.raise_for_status()
169
+ raw_data = response.json()
170
+
171
+ if not isinstance(raw_data, list):
172
+ return []
173
+
174
+ metrics: list[ScorecardMetric] = []
175
+ # Type the list explicitly
176
+ data_list: list[Any] = raw_data # type: ignore[assignment]
177
+ for item in data_list:
178
+ if not isinstance(item, dict):
179
+ continue
180
+
181
+ # Cast to Any dict to satisfy type checker
182
+ item_dict: dict[str, Any] = item # type: ignore[assignment]
183
+ measurable_id = item_dict.get("Id")
184
+ measurable_name = item_dict.get("Name")
185
+
186
+ if not measurable_id or not measurable_name:
187
+ continue
188
+
189
+ owner_data = item_dict.get("Owner", {})
190
+ if not isinstance(owner_data, dict):
191
+ owner_data = {}
192
+ owner_dict: dict[str, Any] = owner_data # type: ignore[assignment]
193
+
194
+ metrics.append(
195
+ ScorecardMetric(
196
+ Id=int(measurable_id),
197
+ Title=str(measurable_name).strip(),
198
+ Target=float(item_dict.get("Target", 0)),
199
+ Unit=str(item_dict.get("Modifiers", "")),
200
+ WeekNumber=0, # Not provided in this endpoint
201
+ Value=None,
202
+ MetricType=str(item_dict.get("Direction", "")),
203
+ AccountableUserId=int(owner_dict.get("Id") or 0),
204
+ AccountableUserName=str(owner_dict.get("Name") or ""),
205
+ IsInverse=False,
206
+ )
207
+ )
208
+
209
+ return metrics
210
+
211
+ def details(self, meeting_id: int, include_closed: bool = False) -> MeetingDetails:
212
+ """Retrieve details of a specific meeting.
213
+
214
+ Args:
215
+ meeting_id: The ID of the meeting
216
+ include_closed: Whether to include closed issues and todos (default: False)
217
+
218
+ Returns:
219
+ A MeetingDetails model instance with comprehensive meeting information
220
+
221
+ Example:
222
+ ```python
223
+ client.meeting.details(1)
224
+ # Returns: MeetingDetails(id=1, name='Team Meeting', attendees=[...],
225
+ # issues=[...], todos=[...], metrics=[...])
226
+ ```
227
+ """
228
+ meetings = self.list()
229
+ meeting = next((m for m in meetings if m.id == meeting_id), None)
230
+
231
+ if not meeting:
232
+ raise APIError(f"Meeting with ID {meeting_id} not found", status_code=404)
233
+
234
+ return MeetingDetails(
235
+ id=meeting.id,
236
+ name=meeting.name,
237
+ start_date_utc=getattr(meeting, "start_date_utc", None),
238
+ created_date=getattr(meeting, "created_date", None),
239
+ organization_id=getattr(meeting, "organization_id", None),
240
+ attendees=self.attendees(meeting_id),
241
+ issues=self.issues(meeting_id, include_closed=include_closed),
242
+ todos=self.todos(meeting_id, include_closed=include_closed),
243
+ metrics=self.metrics(meeting_id),
244
+ )
245
+
246
+ def create(
247
+ self,
248
+ title: str,
249
+ add_self: bool = True,
250
+ attendees: builtins.list[int] | None = None,
251
+ ) -> dict[str, Any]:
252
+ """Create a new meeting.
253
+
254
+ Args:
255
+ title: The title of the new meeting
256
+ add_self: Whether to add the current user as an attendee (default: True)
257
+ attendees: A list of user IDs to add as attendees
258
+
259
+ Returns:
260
+ A dictionary containing meeting_id, title and attendees array
261
+
262
+ Example:
263
+ ```python
264
+ client.meeting.create("New Meeting", attendees=[2, 3])
265
+ # Returns: {"meeting_id": 1, "title": "New Meeting", "attendees": [2, 3]}
266
+ ```
267
+ """
268
+ if attendees is None:
269
+ attendees = []
270
+
271
+ payload = {"title": title, "addSelf": add_self}
272
+ response = self._client.post("L10/create", json=payload)
273
+ response.raise_for_status()
274
+ data: Any = response.json()
275
+
276
+ meeting_id = data["meetingId"]
277
+
278
+ # Add attendees
279
+ for attendee_id in attendees:
280
+ attendee_response = self._client.post(
281
+ f"L10/{meeting_id}/attendees/{attendee_id}"
282
+ )
283
+ attendee_response.raise_for_status()
284
+
285
+ return {"meeting_id": meeting_id, "title": title, "attendees": attendees}
286
+
287
+ def delete(self, meeting_id: int) -> bool:
288
+ """Delete a meeting.
289
+
290
+ Args:
291
+ meeting_id: The ID of the meeting to delete
292
+
293
+ Returns:
294
+ True if deletion was successful
295
+
296
+ Example:
297
+ ```python
298
+ client.meeting.delete(1)
299
+ # Returns: True
300
+ ```
301
+ """
302
+ response = self._client.delete(f"L10/{meeting_id}")
303
+ response.raise_for_status()
304
+ return True
@@ -0,0 +1,152 @@
1
+ """Scorecard operations for the Bloomy SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+
7
+ from ..models import ScorecardItem, ScorecardWeek
8
+ from ..utils.base_operations import BaseOperations
9
+
10
+
11
+ class ScorecardOperations(BaseOperations):
12
+ """Class to handle all operations related to scorecards.
13
+
14
+ Note:
15
+ This class is already initialized via the client and usable as
16
+ `client.scorecard.method`
17
+ """
18
+
19
+ def current_week(self) -> ScorecardWeek:
20
+ """Retrieve the current week details.
21
+
22
+ Returns:
23
+ A ScorecardWeek model instance containing current week details
24
+
25
+ Example:
26
+ ```python
27
+ client.scorecard.current_week()
28
+ # Returns: ScorecardWeek(id=123, week_number=24, week_start='2024-06-10',
29
+ # week_end='2024-06-16')
30
+ ```
31
+ """
32
+ response = self._client.get("weeks/current")
33
+ response.raise_for_status()
34
+ data = response.json()
35
+
36
+ return ScorecardWeek(
37
+ id=data["Id"],
38
+ week_number=data["ForWeekNumber"],
39
+ week_start=data["LocalDate"]["Date"],
40
+ week_end=data["ForWeek"],
41
+ )
42
+
43
+ def list(
44
+ self,
45
+ user_id: int | None = None,
46
+ meeting_id: int | None = None,
47
+ show_empty: bool = False,
48
+ week_offset: int | None = None,
49
+ ) -> builtins.list[ScorecardItem]:
50
+ """Retrieve the scorecards for a user or a meeting.
51
+
52
+ Args:
53
+ user_id: The ID of the user (defaults to initialized user_id)
54
+ meeting_id: The ID of the meeting
55
+ show_empty: Whether to include scores with None values (default: False)
56
+ week_offset: Offset for the week number to filter scores
57
+
58
+ Returns:
59
+ A list of ScorecardItem model instances
60
+
61
+ Raises:
62
+ ValueError: If both user_id and meeting_id are provided
63
+
64
+ Example:
65
+ ```python
66
+ # Fetch scorecards for the current user
67
+ client.scorecard.list()
68
+
69
+ # Fetch scorecards for a specific user
70
+ client.scorecard.list(user_id=42)
71
+
72
+ # Fetch scorecards for a specific meeting
73
+ client.scorecard.list(meeting_id=99)
74
+ ```
75
+
76
+ Note:
77
+ The week_offset parameter is useful when fetching scores for
78
+ previous or future weeks.
79
+ For example, to fetch scores for the previous week, you can set
80
+ week_offset to -1.
81
+ To fetch scores for a future week, you can set week_offset to a
82
+ positive value.
83
+ """
84
+ if user_id and meeting_id:
85
+ raise ValueError(
86
+ "Please provide either `user_id` or `meeting_id`, not both."
87
+ )
88
+
89
+ if meeting_id:
90
+ response = self._client.get(f"scorecard/meeting/{meeting_id}")
91
+ else:
92
+ if user_id is None:
93
+ user_id = self.user_id
94
+ response = self._client.get(f"scorecard/user/{user_id}")
95
+
96
+ response.raise_for_status()
97
+ data = response.json()
98
+
99
+ scorecards: list[ScorecardItem] = [
100
+ ScorecardItem(
101
+ id=scorecard["Id"],
102
+ measurable_id=scorecard["MeasurableId"],
103
+ accountable_user_id=scorecard["AccountableUserId"],
104
+ title=scorecard["MeasurableName"],
105
+ target=scorecard["Target"],
106
+ value=scorecard["Measured"],
107
+ week=scorecard["Week"],
108
+ week_id=scorecard["ForWeek"],
109
+ updated_at=scorecard["DateEntered"],
110
+ )
111
+ for scorecard in data["Scores"]
112
+ ]
113
+
114
+ # Filter by week offset if provided
115
+ if week_offset is not None:
116
+ week_data = self.current_week()
117
+ target_week_id = week_data.week_number + week_offset
118
+ scorecards = [s for s in scorecards if s.week_id == target_week_id]
119
+
120
+ # Filter out empty values unless show_empty is True
121
+ if not show_empty:
122
+ scorecards = [s for s in scorecards if s.value is not None]
123
+
124
+ return scorecards
125
+
126
+ def score(self, measurable_id: int, score: float, week_offset: int = 0) -> bool:
127
+ """Update the score for a measurable item for a specific week.
128
+
129
+ Args:
130
+ measurable_id: The ID of the measurable item
131
+ score: The score to be assigned to the measurable item
132
+ week_offset: The number of weeks to offset from the current week
133
+ (default: 0)
134
+
135
+ Returns:
136
+ True if the score was successfully updated
137
+
138
+ Example:
139
+ ```python
140
+ client.scorecard.score(measurable_id=123, score=5)
141
+ # Returns: True
142
+ ```
143
+ """
144
+ week_data = self.current_week()
145
+ week_id = week_data.week_number + week_offset
146
+
147
+ response = self._client.put(
148
+ f"measurables/{measurable_id}/week/{week_id}",
149
+ json={"value": score},
150
+ )
151
+ response.raise_for_status()
152
+ return response.is_success
@@ -0,0 +1,229 @@
1
+ """Todo operations for the Bloomy SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING
8
+
9
+ from ..models import Todo
10
+ from ..utils.base_operations import BaseOperations
11
+
12
+ if TYPE_CHECKING:
13
+ from typing import Any
14
+
15
+
16
+ class TodoOperations(BaseOperations):
17
+ """Class to handle all operations related to todos.
18
+
19
+ Note:
20
+ This class is already initialized via the client and usable as
21
+ `client.todo.method`
22
+ """
23
+
24
+ def list(
25
+ self, user_id: int | None = None, meeting_id: int | None = None
26
+ ) -> builtins.list[Todo]:
27
+ """List all todos for a specific user or meeting.
28
+
29
+ Args:
30
+ user_id: The ID of the user (default is the initialized user ID)
31
+ meeting_id: The ID of the meeting
32
+
33
+ Returns:
34
+ A list of Todo model instances
35
+
36
+ Raises:
37
+ ValueError: If both user_id and meeting_id are provided
38
+
39
+ Example:
40
+ ```python
41
+ # Fetch todos for the current user
42
+ client.todo.list()
43
+ # Returns: [Todo(id=1, name='New Todo', due_date='2024-06-15', ...)]
44
+ ```
45
+ """
46
+ if user_id is not None and meeting_id is not None:
47
+ raise ValueError(
48
+ "Please provide either `user_id` or `meeting_id`, not both."
49
+ )
50
+
51
+ if meeting_id is not None:
52
+ response = self._client.get(f"l10/{meeting_id}/todos")
53
+ else:
54
+ if user_id is None:
55
+ user_id = self.user_id
56
+ response = self._client.get(f"todo/user/{user_id}")
57
+
58
+ response.raise_for_status()
59
+ data = response.json()
60
+
61
+ return [Todo.model_validate(todo) for todo in data]
62
+
63
+ def create(
64
+ self,
65
+ title: str,
66
+ meeting_id: int,
67
+ due_date: str | None = None,
68
+ user_id: int | None = None,
69
+ notes: str | None = None,
70
+ ) -> Todo:
71
+ """Create a new todo.
72
+
73
+ Args:
74
+ title: The title of the new todo
75
+ meeting_id: The ID of the meeting associated with the todo
76
+ due_date: The due date of the todo (optional)
77
+ user_id: The ID of the user responsible for the todo
78
+ (default: initialized user ID)
79
+ notes: Additional notes for the todo (optional)
80
+
81
+ Returns:
82
+ A Todo model instance representing the newly created todo
83
+
84
+ Example:
85
+ ```python
86
+ client.todo.create(
87
+ title="New Todo", meeting_id=1, due_date="2024-06-15"
88
+ )
89
+ # Returns: Todo(id=1, name='New Todo', due_date='2024-06-15', ...)
90
+ ```
91
+ """
92
+ if user_id is None:
93
+ user_id = self.user_id
94
+
95
+ payload: dict[str, Any] = {
96
+ "title": title,
97
+ "accountableUserId": user_id,
98
+ "notes": notes,
99
+ }
100
+
101
+ if due_date is not None:
102
+ payload["dueDate"] = due_date
103
+
104
+ response = self._client.post(f"L10/{meeting_id}/todos", json=payload)
105
+ response.raise_for_status()
106
+ data = response.json()
107
+
108
+ # Add default values for fields that might be missing in create response
109
+ todo_data = {
110
+ "Id": data["Id"],
111
+ "Name": data["Name"],
112
+ "DetailsUrl": data.get("DetailsUrl"),
113
+ "DueDate": data.get("DueDate"),
114
+ "CompleteTime": None,
115
+ "CreateTime": datetime.now().isoformat(),
116
+ "OriginId": meeting_id,
117
+ "Origin": None,
118
+ "Complete": False,
119
+ }
120
+
121
+ return Todo.model_validate(todo_data)
122
+
123
+ def complete(self, todo_id: int) -> bool:
124
+ """Mark a todo as complete.
125
+
126
+ Args:
127
+ todo_id: The ID of the todo to complete
128
+
129
+ Returns:
130
+ True if the operation was successful
131
+
132
+ Example:
133
+ ```python
134
+ client.todo.complete(1)
135
+ # Returns: True
136
+ ```
137
+ """
138
+ response = self._client.post(f"todo/{todo_id}/complete?status=true")
139
+ response.raise_for_status()
140
+ return response.is_success
141
+
142
+ def update(
143
+ self,
144
+ todo_id: int,
145
+ title: str | None = None,
146
+ due_date: str | None = None,
147
+ ) -> Todo:
148
+ """Update an existing todo.
149
+
150
+ Args:
151
+ todo_id: The ID of the todo to update
152
+ title: The new title of the todo (optional)
153
+ due_date: The new due date of the todo (optional)
154
+
155
+ Returns:
156
+ A Todo model instance containing the updated todo details
157
+
158
+ Raises:
159
+ ValueError: If no update fields are provided
160
+ RuntimeError: If the update request fails
161
+
162
+ Example:
163
+ ```python
164
+ client.todo.update(
165
+ todo_id=1, title="Updated Todo", due_date="2024-11-01"
166
+ )
167
+ # Returns: Todo(id=1, name='Updated Todo', due_date='2024-11-01', ...)
168
+ ```
169
+ """
170
+ payload: dict[str, Any] = {}
171
+
172
+ if title is not None:
173
+ payload["title"] = title
174
+
175
+ if due_date is not None:
176
+ payload["dueDate"] = due_date
177
+
178
+ if not payload:
179
+ raise ValueError("At least one field must be provided")
180
+
181
+ response = self._client.put(f"todo/{todo_id}", json=payload)
182
+
183
+ if response.status_code != 200:
184
+ raise RuntimeError(f"Failed to update todo. Status: {response.status_code}")
185
+
186
+ # Construct todo data for validation
187
+ todo_data = {
188
+ "Id": todo_id,
189
+ "Name": title or "",
190
+ "DetailsUrl": "",
191
+ "DueDate": due_date,
192
+ "CompleteTime": None,
193
+ "CreateTime": datetime.now().isoformat(),
194
+ "OriginId": None,
195
+ "Origin": None,
196
+ "Complete": False,
197
+ }
198
+
199
+ return Todo.model_validate(todo_data)
200
+
201
+ def details(self, todo_id: int) -> Todo:
202
+ """Retrieve the details of a specific todo item by its ID.
203
+
204
+ Args:
205
+ todo_id: The ID of the todo item to retrieve
206
+
207
+ Returns:
208
+ A Todo model instance containing the todo details
209
+
210
+ Raises:
211
+ RuntimeError: If the request to retrieve the todo details fails
212
+
213
+ Example:
214
+ ```python
215
+ client.todo.details(1)
216
+ # Returns: Todo(id=1, name='Updated Todo', due_date='2024-11-01', ...)
217
+ ```
218
+ """
219
+ response = self._client.get(f"todo/{todo_id}")
220
+
221
+ if not response.is_success:
222
+ raise RuntimeError(
223
+ f"Failed to get todo details. Status: {response.status_code}"
224
+ )
225
+
226
+ response.raise_for_status()
227
+ todo = response.json()
228
+
229
+ return Todo.model_validate(todo)