devrev-Python-SDK 2.14.0__py3-none-any.whl → 3.0.0__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.
@@ -8,6 +8,7 @@ from typing import Any
8
8
  from pydantic import Field
9
9
 
10
10
  from devrev.models.base import (
11
+ DateFilter,
11
12
  DevRevBaseModel,
12
13
  DevRevResponseModel,
13
14
  PaginatedResponse,
@@ -63,6 +64,13 @@ class ConversationsListRequest(DevRevBaseModel):
63
64
 
64
65
  cursor: str | None = Field(default=None, description="Pagination cursor")
65
66
  limit: int | None = Field(default=None, ge=1, le=100, description="Max results")
67
+ modified_date: DateFilter | None = Field(
68
+ default=None, description="Filter by modification date"
69
+ )
70
+ sort_by: list[str] | None = Field(
71
+ default=None,
72
+ description='Sort order (e.g., ["modified_date:desc"])',
73
+ )
66
74
 
67
75
 
68
76
  class ConversationsUpdateRequest(DevRevBaseModel):
devrev/models/works.py CHANGED
@@ -167,15 +167,18 @@ class WorksListRequest(DevRevBaseModel):
167
167
  type: list[WorkType] | None = Field(default=None, description="Filter by work types")
168
168
  applies_to_part: list[str] | None = Field(default=None, description="Filter by part IDs")
169
169
  created_by: list[str] | None = Field(default=None, description="Filter by creator user IDs")
170
- created_date: DateFilter | None = Field(default=None, description="Filter by creation date")
171
170
  cursor: str | None = Field(default=None, description="Pagination cursor")
172
171
  external_ref: list[str] | None = Field(default=None, description="Filter by external refs")
173
172
  limit: int | None = Field(default=None, ge=1, le=100, description="Max results to return")
174
- modified_date: DateFilter | None = Field(
175
- default=None, description="Filter by modification date"
176
- )
177
173
  owned_by: list[str] | None = Field(default=None, description="Filter by owner user IDs")
178
- sort_by: list[str] | None = Field(default=None, description="Sort order")
174
+ sort_by: list[str] | None = Field(
175
+ default=None,
176
+ description=(
177
+ "Sort order. Each entry uses the server form 'field:asc' or 'field:desc' "
178
+ "(e.g. 'modified_date:desc'). The legacy '-field' form is accepted by the "
179
+ "service and normalized before being sent."
180
+ ),
181
+ )
179
182
  stage_name: list[str] | None = Field(default=None, description="Filter by stage names")
180
183
  target_close_date: DateFilter | None = Field(
181
184
  default=None, description="Filter by target close date"
@@ -226,8 +229,15 @@ class WorksExportRequest(DevRevBaseModel):
226
229
  type: list[WorkType] | None = Field(default=None, description="Filter by work types")
227
230
  applies_to_part: list[str] | None = Field(default=None, description="Filter by part IDs")
228
231
  created_by: list[str] | None = Field(default=None, description="Filter by creator user IDs")
229
- created_date: DateFilter | None = Field(default=None, description="Filter by creation date")
230
232
  first: int | None = Field(default=None, ge=1, le=10000, description="Max results")
233
+ sort_by: list[str] | None = Field(
234
+ default=None,
235
+ description=(
236
+ "Sort order. Each entry uses the server form 'field:asc' or 'field:desc' "
237
+ "(e.g. 'modified_date:desc'). The legacy '-field' form is accepted by the "
238
+ "service and normalized before being sent."
239
+ ),
240
+ )
231
241
 
232
242
 
233
243
  class WorksCountRequest(DevRevBaseModel):
devrev/services/base.py CHANGED
@@ -72,7 +72,9 @@ class BaseService:
72
72
  Returns:
73
73
  Parsed response model or raw dict if no response_type
74
74
  """
75
- data = request.model_dump(exclude_none=True, by_alias=True) if request else None
75
+ data = (
76
+ request.model_dump(exclude_none=True, by_alias=True, mode="json") if request else None
77
+ )
76
78
  response = self._http.post(endpoint, data=data)
77
79
 
78
80
  # Handle empty responses (204 No Content or empty body)
@@ -185,7 +187,9 @@ class AsyncBaseService:
185
187
  Returns:
186
188
  Parsed response model or raw dict if no response_type
187
189
  """
188
- data = request.model_dump(exclude_none=True, by_alias=True) if request else None
190
+ data = (
191
+ request.model_dump(exclude_none=True, by_alias=True, mode="json") if request else None
192
+ )
189
193
  response = await self._http.post(endpoint, data=data)
190
194
 
191
195
  # Handle empty responses (204 No Content or empty body)
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Sequence
6
+ from datetime import datetime
6
7
  from typing import overload
7
8
 
9
+ from devrev.models.base import DateFilter
8
10
  from devrev.models.conversations import (
9
11
  Conversation,
10
12
  ConversationExportItem,
@@ -24,6 +26,68 @@ from devrev.models.conversations import (
24
26
  from devrev.services.base import AsyncBaseService, BaseService
25
27
 
26
28
 
29
+ def _normalize_sort_by(sort_by: Sequence[str] | None) -> list[str] | None:
30
+ """Normalize sort_by entries to the ``field:direction`` format.
31
+
32
+ Accepts entries already in ``field:direction`` form (e.g.
33
+ ``"modified_date:desc"``), the legacy ``"-field"`` shorthand for
34
+ descending order, and bare field names (e.g. ``"modified_date"``) which
35
+ default to ascending order.
36
+ """
37
+ if sort_by is None:
38
+ return None
39
+ normalized: list[str] = []
40
+ for entry in sort_by:
41
+ if ":" in entry:
42
+ normalized.append(entry)
43
+ elif entry.startswith("-"):
44
+ normalized.append(f"{entry[1:]}:desc")
45
+ else:
46
+ normalized.append(f"{entry}:asc")
47
+ return normalized
48
+
49
+
50
+ # ``ConversationsListRequest.limit`` is pydantic-constrained to ``le=100``;
51
+ # clamp any computed per-page limit so pagination loops do not construct a
52
+ # request body that fails validation before it is ever sent.
53
+ _CONVERSATIONS_MAX_PAGE = 100
54
+
55
+
56
+ def _resolve_page_limit(
57
+ overall_limit: int | None,
58
+ collected: int,
59
+ page_size: int | None,
60
+ ) -> int | None:
61
+ """Compute the ``limit`` to send for the next page request.
62
+
63
+ The returned value is always clamped to ``_CONVERSATIONS_MAX_PAGE`` so
64
+ callers using an ``overall_limit`` greater than the server maximum (e.g.
65
+ ``limit=200, page_size=None``) still paginate correctly.
66
+ """
67
+ if overall_limit is None:
68
+ if page_size is None:
69
+ return None
70
+ return min(page_size, _CONVERSATIONS_MAX_PAGE)
71
+ remaining = overall_limit - collected
72
+ if page_size is None:
73
+ return min(remaining, _CONVERSATIONS_MAX_PAGE)
74
+ return min(page_size, remaining, _CONVERSATIONS_MAX_PAGE)
75
+
76
+
77
+ def _is_before_cutoff(modified_date: datetime | None, cutoff: datetime) -> bool:
78
+ """Return True if ``modified_date`` is strictly older than ``cutoff``.
79
+
80
+ Returns False when the timestamp is unknown or when the two datetimes have
81
+ incompatible tz-awareness; the server-side filter remains authoritative.
82
+ """
83
+ if modified_date is None:
84
+ return False
85
+ try:
86
+ return modified_date < cutoff
87
+ except TypeError:
88
+ return False
89
+
90
+
27
91
  class ConversationsService(BaseService):
28
92
  """Service for managing DevRev Conversations."""
29
93
 
@@ -41,9 +105,55 @@ class ConversationsService(BaseService):
41
105
  """List conversations."""
42
106
  if request is None:
43
107
  request = ConversationsListRequest()
108
+ if request.sort_by is not None:
109
+ request.sort_by = _normalize_sort_by(request.sort_by)
44
110
  response = self._post("/conversations.list", request, ConversationsListResponse)
45
111
  return response.conversations
46
112
 
113
+ def list_modified_since(
114
+ self,
115
+ after: datetime,
116
+ *,
117
+ limit: int | None = None,
118
+ page_size: int | None = None,
119
+ ) -> Sequence[Conversation]:
120
+ """List conversations modified after a given datetime, newest first.
121
+
122
+ Streams pages via cursor until the server returns no further cursor,
123
+ ``limit`` is reached, or a conversation older than ``after`` is seen.
124
+
125
+ Args:
126
+ after: Only include conversations modified after this datetime.
127
+ limit: Maximum number of conversations to return overall.
128
+ page_size: Number of results per API request; ``None`` defers to server.
129
+
130
+ Returns:
131
+ List of Conversation objects modified after ``after``, newest first.
132
+ """
133
+ results: list[Conversation] = []
134
+ cursor: str | None = None
135
+ while True:
136
+ if limit is not None and len(results) >= limit:
137
+ break
138
+ request_limit = _resolve_page_limit(limit, len(results), page_size)
139
+ request = ConversationsListRequest(
140
+ cursor=cursor,
141
+ limit=request_limit,
142
+ modified_date=DateFilter(after=after),
143
+ sort_by=_normalize_sort_by(["modified_date:desc"]),
144
+ )
145
+ response = self._post("/conversations.list", request, ConversationsListResponse)
146
+ for conversation in response.conversations:
147
+ if _is_before_cutoff(conversation.modified_date, after):
148
+ return results
149
+ results.append(conversation)
150
+ if limit is not None and len(results) >= limit:
151
+ return results
152
+ if not response.next_cursor:
153
+ break
154
+ cursor = response.next_cursor
155
+ return results
156
+
47
157
  def update(self, request: ConversationsUpdateRequest) -> Conversation:
48
158
  """Update a conversation."""
49
159
  response = self._post("/conversations.update", request, ConversationsUpdateResponse)
@@ -127,9 +237,55 @@ class AsyncConversationsService(AsyncBaseService):
127
237
  """List conversations."""
128
238
  if request is None:
129
239
  request = ConversationsListRequest()
240
+ if request.sort_by is not None:
241
+ request.sort_by = _normalize_sort_by(request.sort_by)
130
242
  response = await self._post("/conversations.list", request, ConversationsListResponse)
131
243
  return response.conversations
132
244
 
245
+ async def list_modified_since(
246
+ self,
247
+ after: datetime,
248
+ *,
249
+ limit: int | None = None,
250
+ page_size: int | None = None,
251
+ ) -> Sequence[Conversation]:
252
+ """List conversations modified after a given datetime, newest first.
253
+
254
+ Streams pages via cursor until the server returns no further cursor,
255
+ ``limit`` is reached, or a conversation older than ``after`` is seen.
256
+
257
+ Args:
258
+ after: Only include conversations modified after this datetime.
259
+ limit: Maximum number of conversations to return overall.
260
+ page_size: Number of results per API request; ``None`` defers to server.
261
+
262
+ Returns:
263
+ List of Conversation objects modified after ``after``, newest first.
264
+ """
265
+ results: list[Conversation] = []
266
+ cursor: str | None = None
267
+ while True:
268
+ if limit is not None and len(results) >= limit:
269
+ break
270
+ request_limit = _resolve_page_limit(limit, len(results), page_size)
271
+ request = ConversationsListRequest(
272
+ cursor=cursor,
273
+ limit=request_limit,
274
+ modified_date=DateFilter(after=after),
275
+ sort_by=_normalize_sort_by(["modified_date:desc"]),
276
+ )
277
+ response = await self._post("/conversations.list", request, ConversationsListResponse)
278
+ for conversation in response.conversations:
279
+ if _is_before_cutoff(conversation.modified_date, after):
280
+ return results
281
+ results.append(conversation)
282
+ if limit is not None and len(results) >= limit:
283
+ return results
284
+ if not response.next_cursor:
285
+ break
286
+ cursor = response.next_cursor
287
+ return results
288
+
133
289
  async def update(self, request: ConversationsUpdateRequest) -> Conversation:
134
290
  """Update a conversation."""
135
291
  response = await self._post("/conversations.update", request, ConversationsUpdateResponse)
devrev/services/works.py CHANGED
@@ -9,7 +9,7 @@ from collections.abc import Sequence
9
9
  from datetime import datetime
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
- from devrev.models.base import DateFilter, StageUpdate
12
+ from devrev.models.base import StageUpdate
13
13
  from devrev.models.works import (
14
14
  IssuePriority,
15
15
  TicketSeverity,
@@ -37,6 +37,74 @@ if TYPE_CHECKING:
37
37
  from devrev.utils.http import AsyncHTTPClient, HTTPClient
38
38
 
39
39
 
40
+ # Module-level type aliases. Declared here so class-scoped annotations can
41
+ # refer to ``list[...]`` without colliding with the ``list`` method defined on
42
+ # :class:`WorksService` / :class:`AsyncWorksService`.
43
+ _WorkList = list[Work]
44
+ _StrList = list[str]
45
+
46
+
47
+ def _is_before_cutoff(timestamp: datetime | None, cutoff: datetime) -> bool:
48
+ """Return True if ``timestamp`` is strictly older than ``cutoff``.
49
+
50
+ Returns False when the timestamp is unknown or when the two datetimes have
51
+ incompatible tz-awareness; the server-side filter remains authoritative.
52
+ """
53
+ if timestamp is None:
54
+ return False
55
+ try:
56
+ return timestamp < cutoff
57
+ except TypeError:
58
+ return False
59
+
60
+
61
+ # ``WorksListRequest.limit`` is pydantic-constrained to ``le=100``; clamp any
62
+ # computed per-page limit so pagination loops do not construct a request body
63
+ # that fails validation before it is ever sent.
64
+ _WORKS_MAX_PAGE = 100
65
+
66
+
67
+ def _resolve_page_limit(
68
+ overall_limit: int | None,
69
+ collected: int,
70
+ page_size: int | None,
71
+ ) -> int | None:
72
+ """Compute the ``limit`` to send for the next page request.
73
+
74
+ The returned value is always clamped to ``_WORKS_MAX_PAGE`` so callers using
75
+ an ``overall_limit`` greater than the server maximum (e.g. ``limit=200,
76
+ page_size=None``) still paginate correctly.
77
+ """
78
+ if page_size is not None:
79
+ return min(page_size, _WORKS_MAX_PAGE)
80
+ if overall_limit is None:
81
+ return None
82
+ remaining = overall_limit - collected
83
+ return min(remaining, _WORKS_MAX_PAGE)
84
+
85
+
86
+ def _normalize_sort_by(sort_by: Sequence[str] | None) -> _StrList | None:
87
+ """Normalize sort_by entries to the server-expected ``field:direction`` form.
88
+
89
+ Accepts both the legacy ``"-field"`` (descending) / ``"field"`` (ascending)
90
+ shorthand and the explicit ``"field:asc"`` / ``"field:desc"`` form. Returns
91
+ a new list with every entry in the ``"field:direction"`` form. Returns
92
+ ``None`` when the input is ``None`` so callers can pass the result straight
93
+ through to request models with no-op semantics.
94
+ """
95
+ if sort_by is None:
96
+ return None
97
+ normalized: list[str] = []
98
+ for entry in sort_by:
99
+ if ":" in entry:
100
+ normalized.append(entry)
101
+ elif entry.startswith("-"):
102
+ normalized.append(f"{entry[1:]}:desc")
103
+ else:
104
+ normalized.append(f"{entry}:asc")
105
+ return normalized
106
+
107
+
40
108
  class WorksService(BaseService):
41
109
  """Synchronous service for managing DevRev work items.
42
110
 
@@ -109,10 +177,10 @@ class WorksService(BaseService):
109
177
  type: Sequence[WorkType] | None = None,
110
178
  applies_to_part: Sequence[str] | None = None,
111
179
  created_by: Sequence[str] | None = None,
112
- created_date: DateFilter | None = None,
113
180
  cursor: str | None = None,
114
181
  limit: int | None = None,
115
182
  owned_by: Sequence[str] | None = None,
183
+ sort_by: Sequence[str] | None = None,
116
184
  stage_name: Sequence[str] | None = None,
117
185
  ) -> WorksListResponse:
118
186
  """List work items.
@@ -121,10 +189,12 @@ class WorksService(BaseService):
121
189
  type: Filter by work types
122
190
  applies_to_part: Filter by part IDs
123
191
  created_by: Filter by creator user IDs
124
- created_date: Filter by creation date
125
192
  cursor: Pagination cursor
126
193
  limit: Maximum number of results
127
194
  owned_by: Filter by owner user IDs
195
+ sort_by: Sort order. Accepts either the server form
196
+ ``"field:asc"`` / ``"field:desc"`` or the legacy
197
+ ``"-field"`` shorthand; the client normalizes before sending.
128
198
  stage_name: Filter by stage names
129
199
 
130
200
  Returns:
@@ -134,10 +204,10 @@ class WorksService(BaseService):
134
204
  type=type,
135
205
  applies_to_part=applies_to_part,
136
206
  created_by=created_by,
137
- created_date=created_date,
138
207
  cursor=cursor,
139
208
  limit=limit,
140
209
  owned_by=owned_by,
210
+ sort_by=_normalize_sort_by(sort_by),
141
211
  stage_name=stage_name,
142
212
  )
143
213
  return self._post("/works.list", request, WorksListResponse)
@@ -198,8 +268,8 @@ class WorksService(BaseService):
198
268
  type: Sequence[WorkType] | None = None,
199
269
  applies_to_part: Sequence[str] | None = None,
200
270
  created_by: Sequence[str] | None = None,
201
- created_date: DateFilter | None = None,
202
271
  first: int | None = None,
272
+ sort_by: Sequence[str] | None = None,
203
273
  ) -> Sequence[Work]:
204
274
  """Export work items.
205
275
 
@@ -207,8 +277,10 @@ class WorksService(BaseService):
207
277
  type: Filter by work types
208
278
  applies_to_part: Filter by part IDs
209
279
  created_by: Filter by creator user IDs
210
- created_date: Filter by creation date
211
280
  first: Maximum number of results
281
+ sort_by: Sort order. Accepts either the server form
282
+ ``"field:asc"`` / ``"field:desc"`` or the legacy
283
+ ``"-field"`` shorthand; the client normalizes before sending.
212
284
 
213
285
  Returns:
214
286
  List of exported work items
@@ -217,8 +289,8 @@ class WorksService(BaseService):
217
289
  type=type,
218
290
  applies_to_part=applies_to_part,
219
291
  created_by=created_by,
220
- created_date=created_date,
221
292
  first=first,
293
+ sort_by=_normalize_sort_by(sort_by),
222
294
  )
223
295
  response = self._post("/works.export", request, WorksExportResponse)
224
296
  return response.works
@@ -251,6 +323,104 @@ class WorksService(BaseService):
251
323
  response = self._post("/works.count", request, WorksCountResponse)
252
324
  return response.count
253
325
 
326
+ def _list_since(
327
+ self,
328
+ after: datetime,
329
+ timestamp_field: str,
330
+ *,
331
+ type: Sequence[WorkType] | None,
332
+ owned_by: Sequence[str] | None,
333
+ applies_to_part: Sequence[str] | None,
334
+ limit: int | None,
335
+ page_size: int | None,
336
+ ) -> _WorkList:
337
+ """Shared cursor-paginated fetcher for ``list_*_since`` helpers.
338
+
339
+ Streams pages sorted ``{timestamp_field}:desc`` and early-exits as soon
340
+ as a record's timestamp is strictly older than ``after``. Respects
341
+ ``limit`` as a hard cap on returned items.
342
+ """
343
+ sort_by = [f"{timestamp_field}:desc"]
344
+ collected: _WorkList = []
345
+ cursor: str | None = None
346
+ while True:
347
+ if limit is not None and len(collected) >= limit:
348
+ break
349
+ page = self.list(
350
+ type=type,
351
+ owned_by=owned_by,
352
+ applies_to_part=applies_to_part,
353
+ cursor=cursor,
354
+ limit=_resolve_page_limit(limit, len(collected), page_size),
355
+ sort_by=sort_by,
356
+ )
357
+ stop = False
358
+ for work in page.works:
359
+ timestamp = getattr(work, timestamp_field, None)
360
+ if _is_before_cutoff(timestamp, after):
361
+ stop = True
362
+ break
363
+ collected.append(work)
364
+ if limit is not None and len(collected) >= limit:
365
+ stop = True
366
+ break
367
+ if stop or not page.next_cursor:
368
+ break
369
+ cursor = page.next_cursor
370
+ return collected
371
+
372
+ def list_modified_since(
373
+ self,
374
+ after: datetime,
375
+ *,
376
+ type: Sequence[WorkType] | None = None,
377
+ owned_by: Sequence[str] | None = None,
378
+ applies_to_part: Sequence[str] | None = None,
379
+ limit: int | None = None,
380
+ page_size: int | None = None,
381
+ ) -> _WorkList:
382
+ """Return work items modified at or after ``after``.
383
+
384
+ Pages through ``works.list`` sorted by ``modified_date:desc`` and stops
385
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
386
+ hard cap when provided.
387
+ """
388
+ return self._list_since(
389
+ after,
390
+ "modified_date",
391
+ type=type,
392
+ owned_by=owned_by,
393
+ applies_to_part=applies_to_part,
394
+ limit=limit,
395
+ page_size=page_size,
396
+ )
397
+
398
+ def list_created_since(
399
+ self,
400
+ after: datetime,
401
+ *,
402
+ type: Sequence[WorkType] | None = None,
403
+ owned_by: Sequence[str] | None = None,
404
+ applies_to_part: Sequence[str] | None = None,
405
+ limit: int | None = None,
406
+ page_size: int | None = None,
407
+ ) -> _WorkList:
408
+ """Return work items created at or after ``after``.
409
+
410
+ Pages through ``works.list`` sorted by ``created_date:desc`` and stops
411
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
412
+ hard cap when provided.
413
+ """
414
+ return self._list_since(
415
+ after,
416
+ "created_date",
417
+ type=type,
418
+ owned_by=owned_by,
419
+ applies_to_part=applies_to_part,
420
+ limit=limit,
421
+ page_size=page_size,
422
+ )
423
+
254
424
 
255
425
  class AsyncWorksService(AsyncBaseService):
256
426
  """Asynchronous service for managing DevRev work items."""
@@ -301,14 +471,21 @@ class AsyncWorksService(AsyncBaseService):
301
471
  cursor: str | None = None,
302
472
  limit: int | None = None,
303
473
  owned_by: Sequence[str] | None = None,
474
+ sort_by: Sequence[str] | None = None,
304
475
  ) -> WorksListResponse:
305
- """List work items."""
476
+ """List work items.
477
+
478
+ ``sort_by`` accepts either the server form ``"field:asc"`` /
479
+ ``"field:desc"`` or the legacy ``"-field"`` shorthand; the client
480
+ normalizes before sending.
481
+ """
306
482
  request = WorksListRequest(
307
483
  type=type,
308
484
  applies_to_part=applies_to_part,
309
485
  cursor=cursor,
310
486
  limit=limit,
311
487
  owned_by=owned_by,
488
+ sort_by=_normalize_sort_by(sort_by),
312
489
  )
313
490
  return await self._post("/works.list", request, WorksListResponse)
314
491
 
@@ -345,9 +522,19 @@ class AsyncWorksService(AsyncBaseService):
345
522
  *,
346
523
  type: Sequence[WorkType] | None = None,
347
524
  first: int | None = None,
525
+ sort_by: Sequence[str] | None = None,
348
526
  ) -> Sequence[Work]:
349
- """Export work items."""
350
- request = WorksExportRequest(type=type, first=first)
527
+ """Export work items.
528
+
529
+ ``sort_by`` accepts either the server form ``"field:asc"`` /
530
+ ``"field:desc"`` or the legacy ``"-field"`` shorthand; the client
531
+ normalizes before sending.
532
+ """
533
+ request = WorksExportRequest(
534
+ type=type,
535
+ first=first,
536
+ sort_by=_normalize_sort_by(sort_by),
537
+ )
351
538
  response = await self._post("/works.export", request, WorksExportResponse)
352
539
  return response.works
353
540
 
@@ -361,3 +548,101 @@ class AsyncWorksService(AsyncBaseService):
361
548
  request = WorksCountRequest(type=type, owned_by=owned_by)
362
549
  response = await self._post("/works.count", request, WorksCountResponse)
363
550
  return response.count
551
+
552
+ async def _list_since(
553
+ self,
554
+ after: datetime,
555
+ timestamp_field: str,
556
+ *,
557
+ type: Sequence[WorkType] | None,
558
+ owned_by: Sequence[str] | None,
559
+ applies_to_part: Sequence[str] | None,
560
+ limit: int | None,
561
+ page_size: int | None,
562
+ ) -> _WorkList:
563
+ """Shared cursor-paginated fetcher for async ``list_*_since`` helpers.
564
+
565
+ Streams pages sorted ``{timestamp_field}:desc`` and early-exits as soon
566
+ as a record's timestamp is strictly older than ``after``. Respects
567
+ ``limit`` as a hard cap on returned items.
568
+ """
569
+ sort_by = [f"{timestamp_field}:desc"]
570
+ collected: _WorkList = []
571
+ cursor: str | None = None
572
+ while True:
573
+ if limit is not None and len(collected) >= limit:
574
+ break
575
+ page = await self.list(
576
+ type=type,
577
+ owned_by=owned_by,
578
+ applies_to_part=applies_to_part,
579
+ cursor=cursor,
580
+ limit=_resolve_page_limit(limit, len(collected), page_size),
581
+ sort_by=sort_by,
582
+ )
583
+ stop = False
584
+ for work in page.works:
585
+ timestamp = getattr(work, timestamp_field, None)
586
+ if _is_before_cutoff(timestamp, after):
587
+ stop = True
588
+ break
589
+ collected.append(work)
590
+ if limit is not None and len(collected) >= limit:
591
+ stop = True
592
+ break
593
+ if stop or not page.next_cursor:
594
+ break
595
+ cursor = page.next_cursor
596
+ return collected
597
+
598
+ async def list_modified_since(
599
+ self,
600
+ after: datetime,
601
+ *,
602
+ type: Sequence[WorkType] | None = None,
603
+ owned_by: Sequence[str] | None = None,
604
+ applies_to_part: Sequence[str] | None = None,
605
+ limit: int | None = None,
606
+ page_size: int | None = None,
607
+ ) -> _WorkList:
608
+ """Return work items modified at or after ``after``.
609
+
610
+ Pages through ``works.list`` sorted by ``modified_date:desc`` and stops
611
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
612
+ hard cap when provided.
613
+ """
614
+ return await self._list_since(
615
+ after,
616
+ "modified_date",
617
+ type=type,
618
+ owned_by=owned_by,
619
+ applies_to_part=applies_to_part,
620
+ limit=limit,
621
+ page_size=page_size,
622
+ )
623
+
624
+ async def list_created_since(
625
+ self,
626
+ after: datetime,
627
+ *,
628
+ type: Sequence[WorkType] | None = None,
629
+ owned_by: Sequence[str] | None = None,
630
+ applies_to_part: Sequence[str] | None = None,
631
+ limit: int | None = None,
632
+ page_size: int | None = None,
633
+ ) -> _WorkList:
634
+ """Return work items created at or after ``after``.
635
+
636
+ Pages through ``works.list`` sorted by ``created_date:desc`` and stops
637
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
638
+ hard cap when provided.
639
+ """
640
+ return await self._list_since(
641
+ after,
642
+ "created_date",
643
+ type=type,
644
+ owned_by=owned_by,
645
+ applies_to_part=applies_to_part,
646
+ limit=limit,
647
+ page_size=page_size,
648
+ )
@@ -3,11 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from datetime import datetime
6
7
  from typing import Any
7
8
 
8
9
  from mcp.server.fastmcp import Context
9
10
 
10
11
  from devrev.exceptions import DevRevError
12
+ from devrev.models.base import DateFilter
11
13
  from devrev.models.conversations import (
12
14
  ConversationsCreateRequest,
13
15
  ConversationsDeleteRequest,
@@ -24,25 +26,71 @@ from devrev_mcp.utils.pagination import clamp_page_size, paginated_response
24
26
  logger = logging.getLogger(__name__)
25
27
 
26
28
 
29
+ def _parse_iso_datetime(value: str, field_name: str) -> datetime:
30
+ """Parse an ISO-8601 datetime string, accepting a trailing ``Z`` for UTC.
31
+
32
+ Args:
33
+ value: The ISO-8601 datetime string.
34
+ field_name: Name of the parameter being parsed (for error messages).
35
+
36
+ Returns:
37
+ A parsed ``datetime`` instance.
38
+
39
+ Raises:
40
+ RuntimeError: If the string cannot be parsed.
41
+ """
42
+ try:
43
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
44
+ except ValueError as e:
45
+ raise RuntimeError(
46
+ f"Invalid {field_name} format: {value}. "
47
+ "Use ISO-8601 format (e.g., 2025-01-01T00:00:00Z)."
48
+ ) from e
49
+
50
+
27
51
  @mcp.tool()
28
52
  async def devrev_conversations_list(
29
53
  ctx: Context[Any, Any, Any],
30
54
  cursor: str | None = None,
31
55
  limit: int | None = None,
56
+ modified_date_after: str | None = None,
57
+ modified_date_before: str | None = None,
58
+ sort_by: list[str] | None = None,
32
59
  ) -> dict[str, Any]:
33
60
  """List DevRev conversations.
34
61
 
35
62
  Args:
36
63
  cursor: Pagination cursor from a previous response.
37
64
  limit: Maximum number of items to return (default: 25, max: 100).
65
+ modified_date_after: Only include conversations modified after this
66
+ ISO-8601 timestamp (e.g., "2025-01-01T00:00:00Z").
67
+ modified_date_before: Only include conversations modified before this
68
+ ISO-8601 timestamp.
69
+ sort_by: Sort order entries (e.g., ["modified_date:desc"] or
70
+ ["-modified_date"]).
38
71
  """
39
72
  app = ctx.request_context.lifespan_context
40
73
  try:
74
+ modified_date: DateFilter | None = None
75
+ if modified_date_after is not None or modified_date_before is not None:
76
+ after_dt = (
77
+ _parse_iso_datetime(modified_date_after, "modified_date_after")
78
+ if modified_date_after is not None
79
+ else None
80
+ )
81
+ before_dt = (
82
+ _parse_iso_datetime(modified_date_before, "modified_date_before")
83
+ if modified_date_before is not None
84
+ else None
85
+ )
86
+ modified_date = DateFilter(after=after_dt, before=before_dt)
41
87
  request = ConversationsListRequest(
42
88
  cursor=cursor,
43
89
  limit=clamp_page_size(
44
90
  limit, default=app.config.default_page_size, maximum=app.config.max_page_size
45
91
  ),
92
+ modified_date=modified_date,
93
+ sort_by=sort_by,
46
94
  )
47
95
  conversations = await app.get_client().conversations.list(request)
48
96
  items = serialize_models(list(conversations))
@@ -51,6 +99,35 @@ async def devrev_conversations_list(
51
99
  raise RuntimeError(format_devrev_error(e)) from e
52
100
 
53
101
 
102
+ @mcp.tool()
103
+ async def devrev_conversations_list_modified_since(
104
+ ctx: Context[Any, Any, Any],
105
+ after: str,
106
+ limit: int | None = None,
107
+ ) -> dict[str, Any]:
108
+ """List DevRev conversations modified after a given ISO-8601 datetime.
109
+
110
+ Streams pages newest-first, stopping when the cutoff is crossed or the
111
+ ``limit`` is reached. The cutoff is supplied as an ISO-8601 string.
112
+
113
+ Args:
114
+ after: ISO-8601 datetime; only conversations modified after this are
115
+ returned (e.g., "2025-01-01T00:00:00Z").
116
+ limit: Maximum number of conversations to return overall.
117
+ """
118
+ app = ctx.request_context.lifespan_context
119
+ after_dt = _parse_iso_datetime(after, "after")
120
+ try:
121
+ conversations = await app.get_client().conversations.list_modified_since(
122
+ after=after_dt,
123
+ limit=limit,
124
+ )
125
+ items = serialize_models(list(conversations))
126
+ return {"count": len(items), "conversations": items}
127
+ except DevRevError as e:
128
+ raise RuntimeError(format_devrev_error(e)) from e
129
+
130
+
54
131
  @mcp.tool()
55
132
  async def devrev_conversations_get(
56
133
  ctx: Context[Any, Any, Any],
devrev_mcp/tools/works.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from datetime import datetime
6
7
  from typing import Any
7
8
 
8
9
  from mcp.server.fastmcp import Context
@@ -18,6 +19,17 @@ from devrev_mcp.utils.pagination import clamp_page_size, paginated_response
18
19
  logger = logging.getLogger(__name__)
19
20
 
20
21
 
22
+ def _parse_iso_after(value: str, param_name: str) -> datetime:
23
+ """Parse an ISO-8601 timestamp, accepting a trailing ``Z`` for UTC."""
24
+ try:
25
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
26
+ except ValueError as e:
27
+ raise RuntimeError(
28
+ f"Invalid {param_name} format: {value}. "
29
+ "Use ISO-8601 (e.g., 2024-01-15T00:00:00Z or 2024-01-15T00:00:00+00:00)."
30
+ ) from e
31
+
32
+
21
33
  @mcp.tool()
22
34
  async def devrev_works_list(
23
35
  ctx: Context[Any, Any, Any],
@@ -26,8 +38,12 @@ async def devrev_works_list(
26
38
  owned_by: list[str] | None = None,
27
39
  cursor: str | None = None,
28
40
  limit: int | None = None,
41
+ sort_by: list[str] | None = None,
29
42
  ) -> dict[str, Any]:
30
- """List DevRev work items (tickets, issues, tasks).
43
+ """List DevRev work items (tickets, issues, and tasks).
44
+
45
+ Work items in DevRev include tickets (customer-facing), issues (internal
46
+ engineering), and tasks. Use the ``type`` filter to scope results.
31
47
 
32
48
  Args:
33
49
  type: Filter by work type(s): TICKET, ISSUE, TASK, OPPORTUNITY.
@@ -35,6 +51,8 @@ async def devrev_works_list(
35
51
  owned_by: Filter by owner user ID(s).
36
52
  cursor: Pagination cursor from a previous response.
37
53
  limit: Maximum number of items to return (default: 25, max: 100).
54
+ sort_by: Sort order as a list of ``"field:asc"`` / ``"field:desc"``
55
+ entries (or legacy ``"-field"`` shorthand), forwarded to the SDK.
38
56
  """
39
57
  app = ctx.request_context.lifespan_context
40
58
  try:
@@ -56,6 +74,7 @@ async def devrev_works_list(
56
74
  limit=clamp_page_size(
57
75
  limit, default=app.config.default_page_size, maximum=app.config.max_page_size
58
76
  ),
77
+ sort_by=sort_by,
59
78
  )
60
79
  items = serialize_models(response.works)
61
80
  return paginated_response(items, next_cursor=response.next_cursor, total_label="works")
@@ -68,7 +87,7 @@ async def devrev_works_get(
68
87
  ctx: Context[Any, Any, Any],
69
88
  id: str,
70
89
  ) -> dict[str, Any]:
71
- """Get a DevRev work item by ID.
90
+ """Get a DevRev work item (ticket, issue, or task) by ID.
72
91
 
73
92
  Args:
74
93
  id: The work item ID (e.g., don:core:dvrv-us-1:devo/1:issue/123).
@@ -161,7 +180,7 @@ if _config.enable_destructive_tools:
161
180
  priority: str | None = None,
162
181
  severity: str | None = None,
163
182
  ) -> dict[str, Any]:
164
- """Update an existing DevRev work item.
183
+ """Update an existing DevRev work item (ticket, issue, or task).
165
184
 
166
185
  Only provided fields will be updated; others remain unchanged.
167
186
 
@@ -214,7 +233,7 @@ if _config.enable_destructive_tools:
214
233
  ctx: Context[Any, Any, Any],
215
234
  id: str,
216
235
  ) -> dict[str, Any]:
217
- """Delete a DevRev work item.
236
+ """Delete a DevRev work item (ticket, issue, or task).
218
237
 
219
238
  Args:
220
239
  id: The work item ID to delete.
@@ -234,7 +253,7 @@ async def devrev_works_count(
234
253
  type: list[str] | None = None,
235
254
  owned_by: list[str] | None = None,
236
255
  ) -> dict[str, Any]:
237
- """Count DevRev work items matching filters.
256
+ """Count DevRev work items (tickets, issues, and tasks) matching filters.
238
257
 
239
258
  Args:
240
259
  type: Filter by work type(s): TICKET, ISSUE, TASK, OPPORTUNITY.
@@ -263,14 +282,17 @@ async def devrev_works_export(
263
282
  ctx: Context[Any, Any, Any],
264
283
  type: list[str] | None = None,
265
284
  first: int | None = None,
285
+ sort_by: list[str] | None = None,
266
286
  ) -> dict[str, Any]:
267
- """Export DevRev work items.
287
+ """Export DevRev work items (tickets, issues, and tasks).
268
288
 
269
289
  Returns a bulk export of work items. Use for large data retrieval.
270
290
 
271
291
  Args:
272
292
  type: Filter by work type(s): TICKET, ISSUE, TASK, OPPORTUNITY.
273
293
  first: Maximum number of items to export.
294
+ sort_by: Sort order as a list of ``"field:asc"`` / ``"field:desc"``
295
+ entries (or legacy ``"-field"`` shorthand), forwarded to the SDK.
274
296
  """
275
297
  app = ctx.request_context.lifespan_context
276
298
  try:
@@ -284,8 +306,102 @@ async def devrev_works_export(
284
306
  f"Invalid work type: {e.args[0]}. "
285
307
  f"Valid types: {', '.join(wt.name for wt in WorkType)}"
286
308
  ) from e
287
- works = await app.get_client().works.export(type=work_types, first=first)
309
+ works = await app.get_client().works.export(type=work_types, first=first, sort_by=sort_by)
288
310
  items = serialize_models(list(works))
289
311
  return {"count": len(items), "works": items}
290
312
  except DevRevError as e:
291
313
  raise RuntimeError(format_devrev_error(e)) from e
314
+
315
+
316
+ @mcp.tool()
317
+ async def devrev_works_list_modified_since(
318
+ ctx: Context[Any, Any, Any],
319
+ after: str,
320
+ type: list[str] | None = None,
321
+ owned_by: list[str] | None = None,
322
+ applies_to_part: list[str] | None = None,
323
+ limit: int | None = None,
324
+ ) -> dict[str, Any]:
325
+ """List DevRev work items (tickets, issues, and tasks) modified at or after a timestamp.
326
+
327
+ Pages through results server-side sorted by ``modified_date:desc`` and
328
+ returns all matches. Use ``limit`` as a hard cap on total items.
329
+
330
+ Args:
331
+ after: ISO-8601 timestamp (e.g., ``2024-01-15T00:00:00Z``). Items with
332
+ ``modified_date >= after`` are returned.
333
+ type: Filter by work type(s): TICKET, ISSUE, TASK, OPPORTUNITY.
334
+ owned_by: Filter by owner user ID(s).
335
+ applies_to_part: Filter by part ID(s) the work applies to.
336
+ limit: Maximum total items to return across all pages.
337
+ """
338
+ after_dt = _parse_iso_after(after, "after")
339
+ app = ctx.request_context.lifespan_context
340
+ try:
341
+ work_types = None
342
+ if type:
343
+ try:
344
+ work_types = [WorkType[t.upper()] for t in type]
345
+ except KeyError as e:
346
+ raise RuntimeError(
347
+ f"Invalid work type: {e.args[0]}. "
348
+ f"Valid types: {', '.join(wt.name for wt in WorkType)}"
349
+ ) from e
350
+ works = await app.get_client().works.list_modified_since(
351
+ after_dt,
352
+ type=work_types,
353
+ owned_by=owned_by,
354
+ applies_to_part=applies_to_part,
355
+ limit=limit,
356
+ )
357
+ items = serialize_models(list(works))
358
+ return paginated_response(items, next_cursor=None, total_label="works")
359
+ except DevRevError as e:
360
+ raise RuntimeError(format_devrev_error(e)) from e
361
+
362
+
363
+ @mcp.tool()
364
+ async def devrev_works_list_created_since(
365
+ ctx: Context[Any, Any, Any],
366
+ after: str,
367
+ type: list[str] | None = None,
368
+ owned_by: list[str] | None = None,
369
+ applies_to_part: list[str] | None = None,
370
+ limit: int | None = None,
371
+ ) -> dict[str, Any]:
372
+ """List DevRev work items (tickets, issues, and tasks) created at or after a timestamp.
373
+
374
+ Pages through results server-side sorted by ``created_date:desc`` and
375
+ returns all matches. Use ``limit`` as a hard cap on total items.
376
+
377
+ Args:
378
+ after: ISO-8601 timestamp (e.g., ``2024-01-15T00:00:00Z``). Items with
379
+ ``created_date >= after`` are returned.
380
+ type: Filter by work type(s): TICKET, ISSUE, TASK, OPPORTUNITY.
381
+ owned_by: Filter by owner user ID(s).
382
+ applies_to_part: Filter by part ID(s) the work applies to.
383
+ limit: Maximum total items to return across all pages.
384
+ """
385
+ after_dt = _parse_iso_after(after, "after")
386
+ app = ctx.request_context.lifespan_context
387
+ try:
388
+ work_types = None
389
+ if type:
390
+ try:
391
+ work_types = [WorkType[t.upper()] for t in type]
392
+ except KeyError as e:
393
+ raise RuntimeError(
394
+ f"Invalid work type: {e.args[0]}. "
395
+ f"Valid types: {', '.join(wt.name for wt in WorkType)}"
396
+ ) from e
397
+ works = await app.get_client().works.list_created_since(
398
+ after_dt,
399
+ type=work_types,
400
+ owned_by=owned_by,
401
+ applies_to_part=applies_to_part,
402
+ limit=limit,
403
+ )
404
+ items = serialize_models(list(works))
405
+ return paginated_response(items, next_cursor=None, total_label="works")
406
+ except DevRevError as e:
407
+ raise RuntimeError(format_devrev_error(e)) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devrev-Python-SDK
3
- Version: 2.14.0
3
+ Version: 3.0.0
4
4
  Summary: A modern, type-safe Python SDK for the DevRev API
5
5
  Project-URL: Homepage, https://github.com/mgmonteleone/py-dev-rev
6
6
  Project-URL: Documentation, https://github.com/mgmonteleone/py-dev-rev
@@ -10,7 +10,7 @@ devrev/models/artifacts.py,sha256=3iA58RyVlX0S0U5iID3Cnoc096Y70QfGqYfKEhxfE9g,43
10
10
  devrev/models/base.py,sha256=qMxfptmsWimH-MMv84-qojy9Ip858ZYMe6GvTQkDAjo,4511
11
11
  devrev/models/brands.py,sha256=6nYVGoy7s8iDYLotfXefMTPIMIJoI8978TXm4RtIswY,3021
12
12
  devrev/models/code_changes.py,sha256=cJsv5aX6o859cmhbNwuQBLdhIi5XrN26lBvVF1IfiYs,3420
13
- devrev/models/conversations.py,sha256=55Hg4xlbZBXcAjqMQcK2mKbCF11y8b2U3gQ7nZqYiPo,4060
13
+ devrev/models/conversations.py,sha256=SwyMFbqziaAo75daxdEl6DS-pEl9a-GFXE4Z-AKACMg,4324
14
14
  devrev/models/dev_users.py,sha256=0qQtd7pl2v2SF0pFjlzDWaVOUzI_U_uvNu4kXuXFfBs,9226
15
15
  devrev/models/engagements.py,sha256=8mz1KC_lCQIWHcSqX5q_ViQoV4Q9AtW3mcEEQECoAxc,5201
16
16
  devrev/models/groups.py,sha256=oB3eCuX14po5E8WhqkuuC-GlC1WXEPVfpqB5tn4NZ-o,4486
@@ -34,15 +34,15 @@ devrev/models/track_events.py,sha256=bb9belz1W3XvrUEgp9mGdOKELhRcWL-59wS9cIa1geY
34
34
  devrev/models/uoms.py,sha256=bxjk2YhJ3qrX6C52xuokgLCHD3K4-8DRDeu2QwjnK4E,5700
35
35
  devrev/models/webhooks.py,sha256=DzrLJiW1GYWvfaB-aVPcYCf4XkEGHz3dkyzbBKKdhFI,3655
36
36
  devrev/models/widgets.py,sha256=7WWN17_ySqnu1pjYSIS5B8J5dEaqkoB8X8GWzvI0ZYc,4577
37
- devrev/models/works.py,sha256=0PypJjDA5T3d-mmuvo5tl6_EUoz22YREmTWdFHx5jCc,10924
37
+ devrev/models/works.py,sha256=sHZ_eJ6eCadWxPkM6HGcmbkiGRDYT2RnH3QZRO4T3tY,11198
38
38
  devrev/services/__init__.py,sha256=LWy_0xGNH692d6Wc4S4E9v6AoxjgfExFMzFmbrL2NTY,3975
39
39
  devrev/services/accounts.py,sha256=X7FgcODex0XKLiV_VvXKDl2Jm8XsNpn9qp40oRjZqME,9704
40
40
  devrev/services/articles.py,sha256=xOpJOG9f29a5W3IFROplL1a9eJdGucbUFkK-MAIIYlc,41637
41
41
  devrev/services/artifacts.py,sha256=SJzIi5M4np0ENoOTTGEAcoqoRFMVd8pe-BCo9vvhYzk,14124
42
- devrev/services/base.py,sha256=g55dfhd2-uAhgXuguiBacSP4yOAa03Q7hzKDG2vbZ8U,7100
42
+ devrev/services/base.py,sha256=pzUOkOA0kc7yxQQS8AP65L-inpW1WxpsYwtZwA_LXk4,7174
43
43
  devrev/services/brands.py,sha256=W6FB9XXTtEzGOfm9IBI6dqY8WXrMeH6urwjiZKrDVgI,5679
44
44
  devrev/services/code_changes.py,sha256=ahDniXCz54GGmTFmNh88heUg0mHQcIAgaqwlqm1CP6E,3199
45
- devrev/services/conversations.py,sha256=yuQ8Sn0ZZ2gDQjG2C8Hjt_UcvD-fJ9R7yWwh7nCH4_o,7546
45
+ devrev/services/conversations.py,sha256=Kx1pOyBc1Wkk32PmPhjXlM2KnLlLuN5Ydzrm9y5iJhM,13635
46
46
  devrev/services/dev_users.py,sha256=YDJUNk6Crz2TN_uwkWQIBmztIYrLd8EX3toNLdb_HEk,11870
47
47
  devrev/services/engagements.py,sha256=nT6AElHbFO40utp6R_QCGwTGunXjVci73I8YAtkqu8A,9094
48
48
  devrev/services/groups.py,sha256=0VpOKTwHiRjrKXrO1kdHo8V_z2I02GphYQhyDFnImxg,4960
@@ -62,7 +62,7 @@ devrev/services/timeline_entries.py,sha256=6nbVPcWlE3-ohLhvkM421eqYS5ztKtHmGsPek
62
62
  devrev/services/track_events.py,sha256=lI4wXkWu3uUuXtuRg1MGNkTZ7B0Lc1PjM8Kw-6sUnWc,1439
63
63
  devrev/services/uoms.py,sha256=AA3ymoHj24FIbsZpYC4tg2elSdQ3iINTVOz7MraZcj8,8163
64
64
  devrev/services/webhooks.py,sha256=-TSkcaya1y48WB24_vHd-bqO5xSqxRsCLiilncNzQZU,3917
65
- devrev/services/works.py,sha256=b-HWpPMLnda-1P6iB-_QzHu_MoB7z4g_OQGh26rqhtQ,11252
65
+ devrev/services/works.py,sha256=EUlDaUOttiRhvFLa0tkZXtgF3PpLLZlyUqjf3Hp4zRo,21239
66
66
  devrev/utils/__init__.py,sha256=NOrbpkjDVLH8n9xf-xpZJiIIa_GVI_6vqTm3E8L3Udw,857
67
67
  devrev/utils/content_converter.py,sha256=emRBLiVoOfDGpPDzrMRnqQr4-QkqN13OdWlYOyU_LCg,28141
68
68
  devrev/utils/deprecation.py,sha256=7qB2Dx531oP7mNi7q2txOYsOKC9YwdHqlKPMFHOW9Ws,1275
@@ -95,7 +95,7 @@ devrev_mcp/resources/user.py,sha256=0Paq2w_nbj_dCQ8R0S81zlgjUhDAUzvn1_NmshadqM8,
95
95
  devrev_mcp/tools/__init__.py,sha256=wiou4HHy6HeOQY0El3KYqy_S7c2IC4hjsYHjMm7aH-w,54
96
96
  devrev_mcp/tools/accounts.py,sha256=CKD5bKxCNpm_60lakcrG9pBwNTrWOncH6MVQF4rT5wQ,6173
97
97
  devrev_mcp/tools/articles.py,sha256=tna2ZLcefAaYbvICbRicM-0gk1HEx5HJSLS5Sz9t0iI,14063
98
- devrev_mcp/tools/conversations.py,sha256=qpY12ZEOLv5IwVtokneZQpI3rmTWs3bFcWlxnRI97fU,5885
98
+ devrev_mcp/tools/conversations.py,sha256=aEH8MOAQbdOEyIWbqGLYK2JjXcTaxAJqB5prBzmXvpY,8836
99
99
  devrev_mcp/tools/engagements.py,sha256=21zsMLlgY1FwKzSIxPf6XVdMY77p8NXrWZ7l8PRKI-0,8928
100
100
  devrev_mcp/tools/groups.py,sha256=E29eptwl_1rbmMPPElcSblWs71t1XdvSZIl1UR8TZr0,8456
101
101
  devrev_mcp/tools/incidents.py,sha256=XO9FaZlLHCXULCFpMpCOftYNfvajH5PeXOkW2uUZ_Z4,7984
@@ -110,13 +110,13 @@ devrev_mcp/tools/slas.py,sha256=q7eYC7nMxCGoHMDp1QbtCgSWiG4oHk78sydPtYwsh10,4902
110
110
  devrev_mcp/tools/tags.py,sha256=zj_my9CkPQu8_TTOqszfLULPNgxm83kHux-rWFxHvXQ,4711
111
111
  devrev_mcp/tools/timeline.py,sha256=p7P699Oq205jrzCVyYS6v4bXHEQizircHVyS5JE-GJ4,5326
112
112
  devrev_mcp/tools/users.py,sha256=7CtrxjgxniEb5-DaPh0rwL680a2fDFLJSIU5huuTjYE,4896
113
- devrev_mcp/tools/works.py,sha256=B1og_hy4gfCW9e38TzLVg8CGppKP6QKEMO2uCwZ5H5I,10446
113
+ devrev_mcp/tools/works.py,sha256=IFaIXYR4j_2pZ39ykZgdHdo5nCk8OfgWU5bxBdHwp5E,15241
114
114
  devrev_mcp/utils/__init__.py,sha256=2_5b1KC5kjoUqFY1ZSdB2Tefd2ekjbZ-eHyFWBKI-0A,49
115
115
  devrev_mcp/utils/don_id.py,sha256=PAVzOSgaiqJQSdo1fwohFHq2x7PDNUZoBTN4LFGYM48,6294
116
116
  devrev_mcp/utils/errors.py,sha256=5mRAo76rJvvEVi6b1ZokPxDtX5JKkptaqmiYDLCkwBE,2110
117
117
  devrev_mcp/utils/formatting.py,sha256=6JssG5x1BxjdgSiQ8Ou3H-9Wo3wgWTWmejsrGez4wKc,2431
118
118
  devrev_mcp/utils/pagination.py,sha256=EOUgL-ZdSToM1Q-ydXmjhibsef5K1u1g3CaS9K8I2fY,1286
119
- devrev_python_sdk-2.14.0.dist-info/METADATA,sha256=0DK8rjdaH_pV5HiIfZ6-XGXdZOYiGA7NMoIss2dGo-M,40907
120
- devrev_python_sdk-2.14.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
121
- devrev_python_sdk-2.14.0.dist-info/entry_points.txt,sha256=XiV4J_yy0yzVZVxg7T66YERVIlqdPNp3O-NHTHkllqQ,63
122
- devrev_python_sdk-2.14.0.dist-info/RECORD,,
119
+ devrev_python_sdk-3.0.0.dist-info/METADATA,sha256=X6qGjwru56IRRe8VAMJqh8AQTWKhjn0r4S_-GjGkbc4,40906
120
+ devrev_python_sdk-3.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
121
+ devrev_python_sdk-3.0.0.dist-info/entry_points.txt,sha256=XiV4J_yy0yzVZVxg7T66YERVIlqdPNp3O-NHTHkllqQ,63
122
+ devrev_python_sdk-3.0.0.dist-info/RECORD,,