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.
- bloomy/__init__.py +78 -0
- bloomy/client.py +95 -0
- bloomy/configuration.py +137 -0
- bloomy/exceptions.py +27 -0
- bloomy/models.py +371 -0
- bloomy/operations/__init__.py +19 -0
- bloomy/operations/goals.py +270 -0
- bloomy/operations/headlines.py +199 -0
- bloomy/operations/issues.py +187 -0
- bloomy/operations/meetings.py +304 -0
- bloomy/operations/scorecard.py +152 -0
- bloomy/operations/todos.py +229 -0
- bloomy/operations/users.py +154 -0
- bloomy/py.typed +0 -0
- bloomy/utils/__init__.py +1 -0
- bloomy/utils/base_operations.py +43 -0
- bloomy_python-0.12.1.dist-info/METADATA +253 -0
- bloomy_python-0.12.1.dist-info/RECORD +20 -0
- bloomy_python-0.12.1.dist-info/WHEEL +4 -0
- bloomy_python-0.12.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""API operations for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
from .goals import GoalOperations
|
|
4
|
+
from .headlines import HeadlineOperations
|
|
5
|
+
from .issues import IssueOperations
|
|
6
|
+
from .meetings import MeetingOperations
|
|
7
|
+
from .scorecard import ScorecardOperations
|
|
8
|
+
from .todos import TodoOperations
|
|
9
|
+
from .users import UserOperations
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"GoalOperations",
|
|
13
|
+
"HeadlineOperations",
|
|
14
|
+
"IssueOperations",
|
|
15
|
+
"MeetingOperations",
|
|
16
|
+
"ScorecardOperations",
|
|
17
|
+
"TodoOperations",
|
|
18
|
+
"UserOperations",
|
|
19
|
+
]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Goal operations for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..models import ArchivedGoalInfo, CreatedGoalInfo, GoalInfo, GoalListResponse
|
|
9
|
+
from ..utils.base_operations import BaseOperations
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GoalOperations(BaseOperations):
|
|
16
|
+
"""Class to handle all the operations related to goals (also known as "rocks").
|
|
17
|
+
|
|
18
|
+
Note:
|
|
19
|
+
This class is already initialized via the client and usable as
|
|
20
|
+
`client.goal.method`
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def list(
|
|
24
|
+
self, user_id: int | None = None, archived: bool = False
|
|
25
|
+
) -> builtins.list[GoalInfo] | GoalListResponse:
|
|
26
|
+
"""List all goals for a specific user.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
user_id: The ID of the user (default is the initialized user ID)
|
|
30
|
+
archived: Whether to include archived goals (default: False)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Either:
|
|
34
|
+
- A list of GoalInfo model instances if archived is false
|
|
35
|
+
- A GoalListResponse model with 'active' and 'archived' lists of
|
|
36
|
+
GoalInfo instances if archived is true
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
List active goals:
|
|
40
|
+
```python
|
|
41
|
+
client.goal.list()
|
|
42
|
+
# Returns: [GoalInfo(id=1, title='Complete project', ...)]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
List both active and archived goals:
|
|
46
|
+
```python
|
|
47
|
+
client.goal.list(archived=True)
|
|
48
|
+
# Returns: GoalListResponse(
|
|
49
|
+
# active=[GoalInfo(id=1, ...)],
|
|
50
|
+
# archived=[ArchivedGoalInfo(id=2, ...)]
|
|
51
|
+
# )
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
if user_id is None:
|
|
55
|
+
user_id = self.user_id
|
|
56
|
+
|
|
57
|
+
response = self._client.get(
|
|
58
|
+
f"rocks/user/{user_id}", params={"include_origin": True}
|
|
59
|
+
)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
data = response.json()
|
|
62
|
+
|
|
63
|
+
active_goals: list[GoalInfo] = []
|
|
64
|
+
for goal in data:
|
|
65
|
+
active_goals.append(
|
|
66
|
+
GoalInfo(
|
|
67
|
+
id=goal["Id"],
|
|
68
|
+
user_id=goal["Owner"]["Id"],
|
|
69
|
+
user_name=goal["Owner"]["Name"],
|
|
70
|
+
title=goal["Name"],
|
|
71
|
+
created_at=goal["CreateTime"],
|
|
72
|
+
due_date=goal["DueDate"],
|
|
73
|
+
status="Completed" if goal.get("Complete") else "Incomplete",
|
|
74
|
+
meeting_id=goal["Origins"][0]["Id"]
|
|
75
|
+
if goal.get("Origins")
|
|
76
|
+
else None,
|
|
77
|
+
meeting_title=goal["Origins"][0]["Name"]
|
|
78
|
+
if goal.get("Origins")
|
|
79
|
+
else None,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if archived:
|
|
84
|
+
archived_goals = self._get_archived_goals(user_id)
|
|
85
|
+
return GoalListResponse(active=active_goals, archived=archived_goals)
|
|
86
|
+
|
|
87
|
+
return active_goals
|
|
88
|
+
|
|
89
|
+
def create(
|
|
90
|
+
self, title: str, meeting_id: int, user_id: int | None = None
|
|
91
|
+
) -> CreatedGoalInfo:
|
|
92
|
+
"""Create a new goal.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
title: The title of the new goal
|
|
96
|
+
meeting_id: The ID of the meeting associated with the goal
|
|
97
|
+
user_id: The ID of the user responsible for the goal (default:
|
|
98
|
+
initialized user ID)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A CreatedGoalInfo model instance representing the newly created goal
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
```python
|
|
105
|
+
client.goal.create(title="New Goal", meeting_id=1)
|
|
106
|
+
# Returns: CreatedGoalInfo(id=1, title='New Goal', meeting_id=1, ...)
|
|
107
|
+
```
|
|
108
|
+
"""
|
|
109
|
+
if user_id is None:
|
|
110
|
+
user_id = self.user_id
|
|
111
|
+
|
|
112
|
+
payload = {"title": title, "accountableUserId": user_id}
|
|
113
|
+
response = self._client.post(f"L10/{meeting_id}/rocks", json=payload)
|
|
114
|
+
response.raise_for_status()
|
|
115
|
+
data = response.json()
|
|
116
|
+
|
|
117
|
+
# Map completion status
|
|
118
|
+
completion_map = {2: "complete", 1: "on", 0: "off"}
|
|
119
|
+
status = completion_map.get(data.get("Completion", 0), "off")
|
|
120
|
+
|
|
121
|
+
return CreatedGoalInfo(
|
|
122
|
+
id=data["Id"],
|
|
123
|
+
user_id=user_id,
|
|
124
|
+
user_name=data["Owner"]["Name"],
|
|
125
|
+
title=title,
|
|
126
|
+
meeting_id=meeting_id,
|
|
127
|
+
meeting_title=data["Origins"][0]["Name"],
|
|
128
|
+
status=status,
|
|
129
|
+
created_at=data["CreateTime"],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def delete(self, goal_id: int) -> bool:
|
|
133
|
+
"""Delete a goal.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
goal_id: The ID of the goal to delete
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if deletion was successful
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
```python
|
|
143
|
+
client.goal.delete(1)
|
|
144
|
+
# Returns: True
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
response = self._client.delete(f"rocks/{goal_id}")
|
|
148
|
+
response.raise_for_status()
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def update(
|
|
152
|
+
self,
|
|
153
|
+
goal_id: int,
|
|
154
|
+
title: str | None = None,
|
|
155
|
+
accountable_user: int | None = None,
|
|
156
|
+
status: str | None = None,
|
|
157
|
+
) -> bool:
|
|
158
|
+
"""Update a goal.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
goal_id: The ID of the goal to update
|
|
162
|
+
title: The new title of the goal
|
|
163
|
+
accountable_user: The ID of the user responsible for the goal
|
|
164
|
+
(default: initialized user ID)
|
|
165
|
+
status: The status value ('on', 'off', or 'complete')
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if the update was successful
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If an invalid status value is provided
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
```python
|
|
175
|
+
client.goal.update(goal_id=1, title="Updated Goal", status='on')
|
|
176
|
+
# Returns: True
|
|
177
|
+
```
|
|
178
|
+
"""
|
|
179
|
+
if accountable_user is None:
|
|
180
|
+
accountable_user = self.user_id
|
|
181
|
+
|
|
182
|
+
payload: dict[str, Any] = {"accountableUserId": accountable_user}
|
|
183
|
+
|
|
184
|
+
if title is not None:
|
|
185
|
+
payload["title"] = title
|
|
186
|
+
|
|
187
|
+
if status is not None:
|
|
188
|
+
valid_status = {"on": "OnTrack", "off": "AtRisk", "complete": "Complete"}
|
|
189
|
+
status_key = status.lower()
|
|
190
|
+
if status_key not in valid_status:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"Invalid status value. Must be 'on', 'off', or 'complete'."
|
|
193
|
+
)
|
|
194
|
+
payload["completion"] = valid_status[status_key]
|
|
195
|
+
|
|
196
|
+
response = self._client.put(f"rocks/{goal_id}", json=payload)
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
def archive(self, goal_id: int) -> bool:
|
|
201
|
+
"""Archive a rock with the specified goal ID.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
goal_id: The ID of the goal/rock to archive
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if the archival was successful, False otherwise
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
```python
|
|
211
|
+
goals.archive(123)
|
|
212
|
+
# Returns: True
|
|
213
|
+
```
|
|
214
|
+
"""
|
|
215
|
+
response = self._client.put(f"rocks/{goal_id}/archive")
|
|
216
|
+
response.raise_for_status()
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
def restore(self, goal_id: int) -> bool:
|
|
220
|
+
"""Restore a previously archived goal identified by the provided goal ID.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
goal_id: The unique identifier of the goal to restore
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if the restore operation was successful, False otherwise
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
```python
|
|
230
|
+
goals.restore(123)
|
|
231
|
+
# Returns: True
|
|
232
|
+
```
|
|
233
|
+
"""
|
|
234
|
+
response = self._client.put(f"rocks/{goal_id}/restore")
|
|
235
|
+
response.raise_for_status()
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
def _get_archived_goals(self, user_id: int | None = None) -> list[ArchivedGoalInfo]:
|
|
239
|
+
"""Retrieve all archived goals for a specific user (private method).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
user_id: The ID of the user (default is the initialized user ID)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
A list of ArchivedGoalInfo model instances containing archived goal details
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
```python
|
|
249
|
+
goal._get_archived_goals()
|
|
250
|
+
# Returns: [ArchivedGoalInfo(id=1, title='Archived Goal',
|
|
251
|
+
# created_at='2024-06-10', ...), ...]
|
|
252
|
+
```
|
|
253
|
+
"""
|
|
254
|
+
if user_id is None:
|
|
255
|
+
user_id = self.user_id
|
|
256
|
+
|
|
257
|
+
response = self._client.get(f"archivedrocks/user/{user_id}")
|
|
258
|
+
response.raise_for_status()
|
|
259
|
+
data = response.json()
|
|
260
|
+
|
|
261
|
+
return [
|
|
262
|
+
ArchivedGoalInfo(
|
|
263
|
+
id=goal["Id"],
|
|
264
|
+
title=goal["Name"],
|
|
265
|
+
created_at=goal["CreateTime"],
|
|
266
|
+
due_date=goal["DueDate"],
|
|
267
|
+
status="Complete" if goal.get("Complete") else "Incomplete",
|
|
268
|
+
)
|
|
269
|
+
for goal in data
|
|
270
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Headline operations for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
|
|
7
|
+
from ..models import (
|
|
8
|
+
HeadlineDetails,
|
|
9
|
+
HeadlineInfo,
|
|
10
|
+
HeadlineListItem,
|
|
11
|
+
MeetingInfo,
|
|
12
|
+
OwnerDetails,
|
|
13
|
+
)
|
|
14
|
+
from ..utils.base_operations import BaseOperations
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HeadlineOperations(BaseOperations):
|
|
18
|
+
"""Class to handle all operations related to headlines."""
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
meeting_id: int,
|
|
23
|
+
title: str,
|
|
24
|
+
owner_id: int | None = None,
|
|
25
|
+
notes: str | None = None,
|
|
26
|
+
) -> HeadlineInfo:
|
|
27
|
+
"""Create a new headline.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
meeting_id: The ID of the meeting
|
|
31
|
+
title: The title of the headline
|
|
32
|
+
owner_id: The ID of the headline owner (default: current user ID)
|
|
33
|
+
notes: Additional notes for the headline
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A HeadlineInfo model instance containing id, title, owner_details,
|
|
37
|
+
and notes_url
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
httpx.HTTPStatusError: If the API request fails
|
|
41
|
+
"""
|
|
42
|
+
if owner_id is None:
|
|
43
|
+
owner_id = self.user_id
|
|
44
|
+
|
|
45
|
+
payload = {"title": title, "ownerId": owner_id}
|
|
46
|
+
if notes is not None:
|
|
47
|
+
payload["notes"] = notes
|
|
48
|
+
|
|
49
|
+
response = self._client.post(f"L10/{meeting_id}/headlines", json=payload)
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
data = response.json()
|
|
52
|
+
|
|
53
|
+
return HeadlineInfo(
|
|
54
|
+
id=data["Id"],
|
|
55
|
+
title=data["Name"],
|
|
56
|
+
owner_details=OwnerDetails(id=owner_id, name=None),
|
|
57
|
+
notes_url=data.get("DetailsUrl", ""),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def update(self, headline_id: int, title: str) -> bool:
|
|
61
|
+
"""Update a headline.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
headline_id: The ID of the headline to update
|
|
65
|
+
title: The new title of the headline
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if update was successful
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
httpx.HTTPStatusError: If the API request fails
|
|
72
|
+
"""
|
|
73
|
+
payload = {"title": title}
|
|
74
|
+
response = self._client.put(f"headline/{headline_id}", json=payload)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def details(self, headline_id: int) -> HeadlineDetails:
|
|
79
|
+
"""Get headline details.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
headline_id: The ID of the headline
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A HeadlineDetails model instance containing id, title, notes_url,
|
|
86
|
+
meeting_details, owner_details, archived, created_at, and closed_at
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
httpx.HTTPStatusError: If the API request fails
|
|
90
|
+
"""
|
|
91
|
+
response = self._client.get(
|
|
92
|
+
f"headline/{headline_id}", params={"Include_Origin": "true"}
|
|
93
|
+
)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
data = response.json()
|
|
96
|
+
|
|
97
|
+
return HeadlineDetails(
|
|
98
|
+
id=data["Id"],
|
|
99
|
+
title=data["Name"],
|
|
100
|
+
notes_url=data["DetailsUrl"],
|
|
101
|
+
meeting_details=MeetingInfo(
|
|
102
|
+
id=data["OriginId"],
|
|
103
|
+
title=data["Origin"],
|
|
104
|
+
),
|
|
105
|
+
owner_details=OwnerDetails(
|
|
106
|
+
id=data["Owner"]["Id"],
|
|
107
|
+
name=data["Owner"]["Name"],
|
|
108
|
+
),
|
|
109
|
+
archived=data["Archived"],
|
|
110
|
+
created_at=data["CreateTime"],
|
|
111
|
+
closed_at=data["CloseTime"],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def list(
|
|
115
|
+
self, user_id: int | None = None, meeting_id: int | None = None
|
|
116
|
+
) -> builtins.list[HeadlineListItem]:
|
|
117
|
+
"""Get headlines for a user or a meeting.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
user_id: The ID of the user (defaults to initialized user_id)
|
|
121
|
+
meeting_id: The ID of the meeting
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A list of HeadlineListItem model instances containing:
|
|
125
|
+
- id
|
|
126
|
+
- title
|
|
127
|
+
- meeting_details
|
|
128
|
+
- owner_details
|
|
129
|
+
- archived
|
|
130
|
+
- created_at
|
|
131
|
+
- closed_at
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If both user_id and meeting_id are provided
|
|
135
|
+
httpx.HTTPStatusError: If the API request fails
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
```python
|
|
139
|
+
client.headline.list()
|
|
140
|
+
# Returns: [
|
|
141
|
+
# HeadlineListItem(
|
|
142
|
+
# id=1,
|
|
143
|
+
# title='Headline Title',
|
|
144
|
+
# meeting_details=MeetingInfo(id=1, title='Team Meeting'),
|
|
145
|
+
# owner_details=OwnerDetails(id=1, name='John Doe'),
|
|
146
|
+
# archived=False,
|
|
147
|
+
# created_at='2023-01-01',
|
|
148
|
+
# closed_at=None
|
|
149
|
+
# )
|
|
150
|
+
# ]
|
|
151
|
+
```
|
|
152
|
+
"""
|
|
153
|
+
if user_id and meeting_id:
|
|
154
|
+
raise ValueError("Please provide either user_id or meeting_id, not both.")
|
|
155
|
+
|
|
156
|
+
if meeting_id:
|
|
157
|
+
response = self._client.get(f"l10/{meeting_id}/headlines")
|
|
158
|
+
else:
|
|
159
|
+
if user_id is None:
|
|
160
|
+
user_id = self.user_id
|
|
161
|
+
response = self._client.get(f"headline/users/{user_id}")
|
|
162
|
+
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
data = response.json()
|
|
165
|
+
|
|
166
|
+
return [
|
|
167
|
+
HeadlineListItem(
|
|
168
|
+
id=headline["Id"],
|
|
169
|
+
title=headline["Name"],
|
|
170
|
+
meeting_details=MeetingInfo(
|
|
171
|
+
id=headline["OriginId"],
|
|
172
|
+
title=headline["Origin"],
|
|
173
|
+
),
|
|
174
|
+
owner_details=OwnerDetails(
|
|
175
|
+
id=headline["Owner"]["Id"],
|
|
176
|
+
name=headline["Owner"]["Name"],
|
|
177
|
+
),
|
|
178
|
+
archived=headline["Archived"],
|
|
179
|
+
created_at=headline["CreateTime"],
|
|
180
|
+
closed_at=headline["CloseTime"],
|
|
181
|
+
)
|
|
182
|
+
for headline in data
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
def delete(self, headline_id: int) -> bool:
|
|
186
|
+
"""Delete a headline.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
headline_id: The ID of the headline to delete
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if the deletion was successful
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
httpx.HTTPStatusError: If the API request fails
|
|
196
|
+
"""
|
|
197
|
+
response = self._client.delete(f"headline/{headline_id}")
|
|
198
|
+
response.raise_for_status()
|
|
199
|
+
return True
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Issue operations for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
|
|
7
|
+
from ..models import CreatedIssue, IssueDetails, IssueListItem
|
|
8
|
+
from ..utils.base_operations import BaseOperations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IssueOperations(BaseOperations):
|
|
12
|
+
"""Class to handle all operations related to issues.
|
|
13
|
+
|
|
14
|
+
Provides functionality to create, retrieve, list, and solve issues
|
|
15
|
+
associated with meetings and users.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def details(self, issue_id: int) -> IssueDetails:
|
|
19
|
+
"""Retrieve detailed information about a specific issue.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
issue_id: Unique identifier of the issue
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
An IssueDetails model instance containing detailed information
|
|
26
|
+
about the issue
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
httpx.HTTPError: When the API request fails or returns invalid data
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
```python
|
|
33
|
+
client.issue.details(123)
|
|
34
|
+
# Returns: IssueDetails(id=123, title='Issue Title',
|
|
35
|
+
# created_at='2024-06-10', ...)
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
response = self._client.get(f"issues/{issue_id}")
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
data = response.json()
|
|
41
|
+
|
|
42
|
+
return IssueDetails(
|
|
43
|
+
id=data["Id"],
|
|
44
|
+
title=data["Name"],
|
|
45
|
+
notes_url=data["DetailsUrl"],
|
|
46
|
+
created_at=data["CreateTime"],
|
|
47
|
+
completed_at=data["CloseTime"],
|
|
48
|
+
meeting_id=data["OriginId"],
|
|
49
|
+
meeting_title=data["Origin"],
|
|
50
|
+
user_id=data["Owner"]["Id"],
|
|
51
|
+
user_name=data["Owner"]["Name"],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def list(
|
|
55
|
+
self, user_id: int | None = None, meeting_id: int | None = None
|
|
56
|
+
) -> builtins.list[IssueListItem]:
|
|
57
|
+
"""List issues filtered by user or meeting.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
user_id: Unique identifier of the user (optional)
|
|
61
|
+
meeting_id: Unique identifier of the meeting (optional)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A list of IssueListItem model instances matching the filter criteria
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: When both user_id and meeting_id are provided
|
|
68
|
+
httpx.HTTPError: When the API request fails or returns invalid data
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
```python
|
|
72
|
+
# List issues for current user
|
|
73
|
+
client.issue.list()
|
|
74
|
+
# Returns: [IssueListItem(id=1, title='Issue 1', ...), ...]
|
|
75
|
+
|
|
76
|
+
# List issues for specific meeting
|
|
77
|
+
client.issue.list(meeting_id=456)
|
|
78
|
+
# Returns: [IssueListItem(id=2, title='Issue 2', ...), ...]
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
if user_id and meeting_id:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"Please provide either `user_id` or `meeting_id`, not both."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if meeting_id:
|
|
87
|
+
response = self._client.get(f"l10/{meeting_id}/issues")
|
|
88
|
+
else:
|
|
89
|
+
if user_id is None:
|
|
90
|
+
user_id = self.user_id
|
|
91
|
+
response = self._client.get(f"issues/users/{user_id}")
|
|
92
|
+
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
data = response.json()
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
IssueListItem(
|
|
98
|
+
id=issue["Id"],
|
|
99
|
+
title=issue["Name"],
|
|
100
|
+
notes_url=issue["DetailsUrl"],
|
|
101
|
+
created_at=issue["CreateTime"],
|
|
102
|
+
meeting_id=issue["OriginId"],
|
|
103
|
+
meeting_title=issue["Origin"],
|
|
104
|
+
)
|
|
105
|
+
for issue in data
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def solve(self, issue_id: int) -> bool:
|
|
109
|
+
"""Mark an issue as completed/solved.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
issue_id: Unique identifier of the issue to be solved
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if issue was successfully solved
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
httpx.HTTPError: When the API request fails
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
```python
|
|
122
|
+
client.issue.solve(123)
|
|
123
|
+
# Returns: True
|
|
124
|
+
```
|
|
125
|
+
"""
|
|
126
|
+
response = self._client.post(
|
|
127
|
+
f"issues/{issue_id}/complete", json={"complete": True}
|
|
128
|
+
)
|
|
129
|
+
response.raise_for_status()
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def create(
|
|
133
|
+
self,
|
|
134
|
+
meeting_id: int,
|
|
135
|
+
title: str,
|
|
136
|
+
user_id: int | None = None,
|
|
137
|
+
notes: str | None = None,
|
|
138
|
+
) -> CreatedIssue:
|
|
139
|
+
"""Create a new issue in the system.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
meeting_id: Unique identifier of the associated meeting
|
|
143
|
+
title: Title/name of the issue
|
|
144
|
+
user_id: Unique identifier of the issue owner (defaults to current user)
|
|
145
|
+
notes: Additional notes or description for the issue (optional)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A CreatedIssue model instance containing the newly created issue details
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
httpx.HTTPError: When the API request fails or returns invalid data
|
|
152
|
+
ValueError: When required parameters are missing or invalid
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
```python
|
|
156
|
+
client.issue.create(
|
|
157
|
+
meeting_id=123,
|
|
158
|
+
title="New Issue",
|
|
159
|
+
notes="This is a detailed description"
|
|
160
|
+
)
|
|
161
|
+
# Returns: CreatedIssue(id=456, title='New Issue', meeting_id=123, ...)
|
|
162
|
+
```
|
|
163
|
+
"""
|
|
164
|
+
if user_id is None:
|
|
165
|
+
user_id = self.user_id
|
|
166
|
+
|
|
167
|
+
payload = {
|
|
168
|
+
"title": title,
|
|
169
|
+
"meetingid": meeting_id,
|
|
170
|
+
"ownerid": user_id,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if notes is not None:
|
|
174
|
+
payload["notes"] = notes
|
|
175
|
+
|
|
176
|
+
response = self._client.post("issues/create", json=payload)
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
data = response.json()
|
|
179
|
+
|
|
180
|
+
return CreatedIssue(
|
|
181
|
+
id=data["Id"],
|
|
182
|
+
meeting_id=data["OriginId"],
|
|
183
|
+
meeting_title=data["Origin"],
|
|
184
|
+
title=data["Name"],
|
|
185
|
+
user_id=data["Owner"]["Id"],
|
|
186
|
+
notes_url=data["DetailsUrl"],
|
|
187
|
+
)
|