devrev-Python-SDK 2.14.1__py3-none-any.whl → 3.0.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.
@@ -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):
@@ -0,0 +1,54 @@
1
+ """Shared pagination helpers for cursor-streaming list helpers.
2
+
3
+ The DevRev ``*.list`` endpoints cap per-page responses at 100 items and the
4
+ corresponding ``*ListRequest.limit`` fields are pydantic-constrained to
5
+ ``le=100``. Any service that drives cursor pagination from a user-supplied
6
+ ``overall_limit`` / ``page_size`` pair must therefore clamp the value it puts
7
+ on the request body before construction, or the request will fail validation
8
+ locally before it is ever sent.
9
+
10
+ :func:`resolve_page_limit` centralises that clamping so every ``list_*_since``
11
+ helper resolves the per-page ``limit`` identically.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Final
17
+
18
+ _MAX_PAGE: Final[int] = 100
19
+
20
+
21
+ def resolve_page_limit(
22
+ overall_limit: int | None,
23
+ collected: int,
24
+ page_size: int | None,
25
+ ) -> int | None:
26
+ """Compute the ``limit`` to send for the next page request.
27
+
28
+ The returned value is always clamped to :data:`_MAX_PAGE` so callers using
29
+ an ``overall_limit`` greater than the server maximum (e.g. ``limit=200,
30
+ page_size=None``) still paginate correctly. When both ``overall_limit`` and
31
+ ``page_size`` are supplied the tightest of ``{page_size, remaining,
32
+ _MAX_PAGE}`` wins.
33
+
34
+ Args:
35
+ overall_limit: Caller-supplied hard cap on total items to return across
36
+ all pages, or ``None`` to stream until the server runs out of
37
+ results.
38
+ collected: Number of items already accumulated across prior pages.
39
+ page_size: Caller-supplied per-page size, or ``None`` to defer to the
40
+ server default (still clamped to :data:`_MAX_PAGE`).
41
+
42
+ Returns:
43
+ The ``limit`` to put on the next ``*ListRequest``, or ``None`` to omit
44
+ it entirely (only when both ``overall_limit`` and ``page_size`` are
45
+ ``None``).
46
+ """
47
+ if overall_limit is None:
48
+ if page_size is None:
49
+ return None
50
+ return min(page_size, _MAX_PAGE)
51
+ remaining = overall_limit - collected
52
+ if page_size is None:
53
+ return min(remaining, _MAX_PAGE)
54
+ return min(page_size, remaining, _MAX_PAGE)
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,
@@ -21,9 +23,45 @@ from devrev.models.conversations import (
21
23
  ConversationsUpdateRequest,
22
24
  ConversationsUpdateResponse,
23
25
  )
26
+ from devrev.services._pagination import resolve_page_limit
24
27
  from devrev.services.base import AsyncBaseService, BaseService
25
28
 
26
29
 
30
+ def _normalize_sort_by(sort_by: Sequence[str] | None) -> list[str] | None:
31
+ """Normalize sort_by entries to the ``field:direction`` format.
32
+
33
+ Accepts entries already in ``field:direction`` form (e.g.
34
+ ``"modified_date:desc"``), the legacy ``"-field"`` shorthand for
35
+ descending order, and bare field names (e.g. ``"modified_date"``) which
36
+ default to ascending order.
37
+ """
38
+ if sort_by is None:
39
+ return None
40
+ normalized: list[str] = []
41
+ for entry in sort_by:
42
+ if ":" in entry:
43
+ normalized.append(entry)
44
+ elif entry.startswith("-"):
45
+ normalized.append(f"{entry[1:]}:desc")
46
+ else:
47
+ normalized.append(f"{entry}:asc")
48
+ return normalized
49
+
50
+
51
+ def _is_before_cutoff(modified_date: datetime | None, cutoff: datetime) -> bool:
52
+ """Return True if ``modified_date`` is strictly older than ``cutoff``.
53
+
54
+ Returns False when the timestamp is unknown or when the two datetimes have
55
+ incompatible tz-awareness; the server-side filter remains authoritative.
56
+ """
57
+ if modified_date is None:
58
+ return False
59
+ try:
60
+ return modified_date < cutoff
61
+ except TypeError:
62
+ return False
63
+
64
+
27
65
  class ConversationsService(BaseService):
28
66
  """Service for managing DevRev Conversations."""
29
67
 
@@ -41,9 +79,55 @@ class ConversationsService(BaseService):
41
79
  """List conversations."""
42
80
  if request is None:
43
81
  request = ConversationsListRequest()
82
+ if request.sort_by is not None:
83
+ request.sort_by = _normalize_sort_by(request.sort_by)
44
84
  response = self._post("/conversations.list", request, ConversationsListResponse)
45
85
  return response.conversations
46
86
 
87
+ def list_modified_since(
88
+ self,
89
+ after: datetime,
90
+ *,
91
+ limit: int | None = None,
92
+ page_size: int | None = None,
93
+ ) -> Sequence[Conversation]:
94
+ """List conversations modified after a given datetime, newest first.
95
+
96
+ Streams pages via cursor until the server returns no further cursor,
97
+ ``limit`` is reached, or a conversation older than ``after`` is seen.
98
+
99
+ Args:
100
+ after: Only include conversations modified after this datetime.
101
+ limit: Maximum number of conversations to return overall.
102
+ page_size: Number of results per API request; ``None`` defers to server.
103
+
104
+ Returns:
105
+ List of Conversation objects modified after ``after``, newest first.
106
+ """
107
+ results: list[Conversation] = []
108
+ cursor: str | None = None
109
+ while True:
110
+ if limit is not None and len(results) >= limit:
111
+ break
112
+ request_limit = resolve_page_limit(limit, len(results), page_size)
113
+ request = ConversationsListRequest(
114
+ cursor=cursor,
115
+ limit=request_limit,
116
+ modified_date=DateFilter(after=after),
117
+ sort_by=_normalize_sort_by(["modified_date:desc"]),
118
+ )
119
+ response = self._post("/conversations.list", request, ConversationsListResponse)
120
+ for conversation in response.conversations:
121
+ if _is_before_cutoff(conversation.modified_date, after):
122
+ return results
123
+ results.append(conversation)
124
+ if limit is not None and len(results) >= limit:
125
+ return results
126
+ if not response.next_cursor:
127
+ break
128
+ cursor = response.next_cursor
129
+ return results
130
+
47
131
  def update(self, request: ConversationsUpdateRequest) -> Conversation:
48
132
  """Update a conversation."""
49
133
  response = self._post("/conversations.update", request, ConversationsUpdateResponse)
@@ -127,9 +211,55 @@ class AsyncConversationsService(AsyncBaseService):
127
211
  """List conversations."""
128
212
  if request is None:
129
213
  request = ConversationsListRequest()
214
+ if request.sort_by is not None:
215
+ request.sort_by = _normalize_sort_by(request.sort_by)
130
216
  response = await self._post("/conversations.list", request, ConversationsListResponse)
131
217
  return response.conversations
132
218
 
219
+ async def list_modified_since(
220
+ self,
221
+ after: datetime,
222
+ *,
223
+ limit: int | None = None,
224
+ page_size: int | None = None,
225
+ ) -> Sequence[Conversation]:
226
+ """List conversations modified after a given datetime, newest first.
227
+
228
+ Streams pages via cursor until the server returns no further cursor,
229
+ ``limit`` is reached, or a conversation older than ``after`` is seen.
230
+
231
+ Args:
232
+ after: Only include conversations modified after this datetime.
233
+ limit: Maximum number of conversations to return overall.
234
+ page_size: Number of results per API request; ``None`` defers to server.
235
+
236
+ Returns:
237
+ List of Conversation objects modified after ``after``, newest first.
238
+ """
239
+ results: list[Conversation] = []
240
+ cursor: str | None = None
241
+ while True:
242
+ if limit is not None and len(results) >= limit:
243
+ break
244
+ request_limit = resolve_page_limit(limit, len(results), page_size)
245
+ request = ConversationsListRequest(
246
+ cursor=cursor,
247
+ limit=request_limit,
248
+ modified_date=DateFilter(after=after),
249
+ sort_by=_normalize_sort_by(["modified_date:desc"]),
250
+ )
251
+ response = await self._post("/conversations.list", request, ConversationsListResponse)
252
+ for conversation in response.conversations:
253
+ if _is_before_cutoff(conversation.modified_date, after):
254
+ return results
255
+ results.append(conversation)
256
+ if limit is not None and len(results) >= limit:
257
+ return results
258
+ if not response.next_cursor:
259
+ break
260
+ cursor = response.next_cursor
261
+ return results
262
+
133
263
  async def update(self, request: ConversationsUpdateRequest) -> Conversation:
134
264
  """Update a conversation."""
135
265
  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,
@@ -31,12 +31,56 @@ from devrev.models.works import (
31
31
  WorksUpdateResponse,
32
32
  WorkType,
33
33
  )
34
+ from devrev.services._pagination import resolve_page_limit
34
35
  from devrev.services.base import AsyncBaseService, BaseService
35
36
 
36
37
  if TYPE_CHECKING:
37
38
  from devrev.utils.http import AsyncHTTPClient, HTTPClient
38
39
 
39
40
 
41
+ # Module-level type aliases. Declared here so class-scoped annotations can
42
+ # refer to ``list[...]`` without colliding with the ``list`` method defined on
43
+ # :class:`WorksService` / :class:`AsyncWorksService`.
44
+ _WorkList = list[Work]
45
+ _StrList = list[str]
46
+
47
+
48
+ def _is_before_cutoff(timestamp: datetime | None, cutoff: datetime) -> bool:
49
+ """Return True if ``timestamp`` is strictly older than ``cutoff``.
50
+
51
+ Returns False when the timestamp is unknown or when the two datetimes have
52
+ incompatible tz-awareness; the server-side filter remains authoritative.
53
+ """
54
+ if timestamp is None:
55
+ return False
56
+ try:
57
+ return timestamp < cutoff
58
+ except TypeError:
59
+ return False
60
+
61
+
62
+ def _normalize_sort_by(sort_by: Sequence[str] | None) -> _StrList | None:
63
+ """Normalize sort_by entries to the server-expected ``field:direction`` form.
64
+
65
+ Accepts both the legacy ``"-field"`` (descending) / ``"field"`` (ascending)
66
+ shorthand and the explicit ``"field:asc"`` / ``"field:desc"`` form. Returns
67
+ a new list with every entry in the ``"field:direction"`` form. Returns
68
+ ``None`` when the input is ``None`` so callers can pass the result straight
69
+ through to request models with no-op semantics.
70
+ """
71
+ if sort_by is None:
72
+ return None
73
+ normalized: list[str] = []
74
+ for entry in sort_by:
75
+ if ":" in entry:
76
+ normalized.append(entry)
77
+ elif entry.startswith("-"):
78
+ normalized.append(f"{entry[1:]}:desc")
79
+ else:
80
+ normalized.append(f"{entry}:asc")
81
+ return normalized
82
+
83
+
40
84
  class WorksService(BaseService):
41
85
  """Synchronous service for managing DevRev work items.
42
86
 
@@ -109,10 +153,10 @@ class WorksService(BaseService):
109
153
  type: Sequence[WorkType] | None = None,
110
154
  applies_to_part: Sequence[str] | None = None,
111
155
  created_by: Sequence[str] | None = None,
112
- created_date: DateFilter | None = None,
113
156
  cursor: str | None = None,
114
157
  limit: int | None = None,
115
158
  owned_by: Sequence[str] | None = None,
159
+ sort_by: Sequence[str] | None = None,
116
160
  stage_name: Sequence[str] | None = None,
117
161
  ) -> WorksListResponse:
118
162
  """List work items.
@@ -121,10 +165,12 @@ class WorksService(BaseService):
121
165
  type: Filter by work types
122
166
  applies_to_part: Filter by part IDs
123
167
  created_by: Filter by creator user IDs
124
- created_date: Filter by creation date
125
168
  cursor: Pagination cursor
126
169
  limit: Maximum number of results
127
170
  owned_by: Filter by owner user IDs
171
+ sort_by: Sort order. Accepts either the server form
172
+ ``"field:asc"`` / ``"field:desc"`` or the legacy
173
+ ``"-field"`` shorthand; the client normalizes before sending.
128
174
  stage_name: Filter by stage names
129
175
 
130
176
  Returns:
@@ -134,10 +180,10 @@ class WorksService(BaseService):
134
180
  type=type,
135
181
  applies_to_part=applies_to_part,
136
182
  created_by=created_by,
137
- created_date=created_date,
138
183
  cursor=cursor,
139
184
  limit=limit,
140
185
  owned_by=owned_by,
186
+ sort_by=_normalize_sort_by(sort_by),
141
187
  stage_name=stage_name,
142
188
  )
143
189
  return self._post("/works.list", request, WorksListResponse)
@@ -198,8 +244,8 @@ class WorksService(BaseService):
198
244
  type: Sequence[WorkType] | None = None,
199
245
  applies_to_part: Sequence[str] | None = None,
200
246
  created_by: Sequence[str] | None = None,
201
- created_date: DateFilter | None = None,
202
247
  first: int | None = None,
248
+ sort_by: Sequence[str] | None = None,
203
249
  ) -> Sequence[Work]:
204
250
  """Export work items.
205
251
 
@@ -207,8 +253,10 @@ class WorksService(BaseService):
207
253
  type: Filter by work types
208
254
  applies_to_part: Filter by part IDs
209
255
  created_by: Filter by creator user IDs
210
- created_date: Filter by creation date
211
256
  first: Maximum number of results
257
+ sort_by: Sort order. Accepts either the server form
258
+ ``"field:asc"`` / ``"field:desc"`` or the legacy
259
+ ``"-field"`` shorthand; the client normalizes before sending.
212
260
 
213
261
  Returns:
214
262
  List of exported work items
@@ -217,8 +265,8 @@ class WorksService(BaseService):
217
265
  type=type,
218
266
  applies_to_part=applies_to_part,
219
267
  created_by=created_by,
220
- created_date=created_date,
221
268
  first=first,
269
+ sort_by=_normalize_sort_by(sort_by),
222
270
  )
223
271
  response = self._post("/works.export", request, WorksExportResponse)
224
272
  return response.works
@@ -251,6 +299,104 @@ class WorksService(BaseService):
251
299
  response = self._post("/works.count", request, WorksCountResponse)
252
300
  return response.count
253
301
 
302
+ def _list_since(
303
+ self,
304
+ after: datetime,
305
+ timestamp_field: str,
306
+ *,
307
+ type: Sequence[WorkType] | None,
308
+ owned_by: Sequence[str] | None,
309
+ applies_to_part: Sequence[str] | None,
310
+ limit: int | None,
311
+ page_size: int | None,
312
+ ) -> _WorkList:
313
+ """Shared cursor-paginated fetcher for ``list_*_since`` helpers.
314
+
315
+ Streams pages sorted ``{timestamp_field}:desc`` and early-exits as soon
316
+ as a record's timestamp is strictly older than ``after``. Respects
317
+ ``limit`` as a hard cap on returned items.
318
+ """
319
+ sort_by = [f"{timestamp_field}:desc"]
320
+ collected: _WorkList = []
321
+ cursor: str | None = None
322
+ while True:
323
+ if limit is not None and len(collected) >= limit:
324
+ break
325
+ page = self.list(
326
+ type=type,
327
+ owned_by=owned_by,
328
+ applies_to_part=applies_to_part,
329
+ cursor=cursor,
330
+ limit=resolve_page_limit(limit, len(collected), page_size),
331
+ sort_by=sort_by,
332
+ )
333
+ stop = False
334
+ for work in page.works:
335
+ timestamp = getattr(work, timestamp_field, None)
336
+ if _is_before_cutoff(timestamp, after):
337
+ stop = True
338
+ break
339
+ collected.append(work)
340
+ if limit is not None and len(collected) >= limit:
341
+ stop = True
342
+ break
343
+ if stop or not page.next_cursor:
344
+ break
345
+ cursor = page.next_cursor
346
+ return collected
347
+
348
+ def list_modified_since(
349
+ self,
350
+ after: datetime,
351
+ *,
352
+ type: Sequence[WorkType] | None = None,
353
+ owned_by: Sequence[str] | None = None,
354
+ applies_to_part: Sequence[str] | None = None,
355
+ limit: int | None = None,
356
+ page_size: int | None = None,
357
+ ) -> _WorkList:
358
+ """Return work items modified at or after ``after``.
359
+
360
+ Pages through ``works.list`` sorted by ``modified_date:desc`` and stops
361
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
362
+ hard cap when provided.
363
+ """
364
+ return self._list_since(
365
+ after,
366
+ "modified_date",
367
+ type=type,
368
+ owned_by=owned_by,
369
+ applies_to_part=applies_to_part,
370
+ limit=limit,
371
+ page_size=page_size,
372
+ )
373
+
374
+ def list_created_since(
375
+ self,
376
+ after: datetime,
377
+ *,
378
+ type: Sequence[WorkType] | None = None,
379
+ owned_by: Sequence[str] | None = None,
380
+ applies_to_part: Sequence[str] | None = None,
381
+ limit: int | None = None,
382
+ page_size: int | None = None,
383
+ ) -> _WorkList:
384
+ """Return work items created at or after ``after``.
385
+
386
+ Pages through ``works.list`` sorted by ``created_date:desc`` and stops
387
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
388
+ hard cap when provided.
389
+ """
390
+ return self._list_since(
391
+ after,
392
+ "created_date",
393
+ type=type,
394
+ owned_by=owned_by,
395
+ applies_to_part=applies_to_part,
396
+ limit=limit,
397
+ page_size=page_size,
398
+ )
399
+
254
400
 
255
401
  class AsyncWorksService(AsyncBaseService):
256
402
  """Asynchronous service for managing DevRev work items."""
@@ -301,14 +447,21 @@ class AsyncWorksService(AsyncBaseService):
301
447
  cursor: str | None = None,
302
448
  limit: int | None = None,
303
449
  owned_by: Sequence[str] | None = None,
450
+ sort_by: Sequence[str] | None = None,
304
451
  ) -> WorksListResponse:
305
- """List work items."""
452
+ """List work items.
453
+
454
+ ``sort_by`` accepts either the server form ``"field:asc"`` /
455
+ ``"field:desc"`` or the legacy ``"-field"`` shorthand; the client
456
+ normalizes before sending.
457
+ """
306
458
  request = WorksListRequest(
307
459
  type=type,
308
460
  applies_to_part=applies_to_part,
309
461
  cursor=cursor,
310
462
  limit=limit,
311
463
  owned_by=owned_by,
464
+ sort_by=_normalize_sort_by(sort_by),
312
465
  )
313
466
  return await self._post("/works.list", request, WorksListResponse)
314
467
 
@@ -345,9 +498,19 @@ class AsyncWorksService(AsyncBaseService):
345
498
  *,
346
499
  type: Sequence[WorkType] | None = None,
347
500
  first: int | None = None,
501
+ sort_by: Sequence[str] | None = None,
348
502
  ) -> Sequence[Work]:
349
- """Export work items."""
350
- request = WorksExportRequest(type=type, first=first)
503
+ """Export work items.
504
+
505
+ ``sort_by`` accepts either the server form ``"field:asc"`` /
506
+ ``"field:desc"`` or the legacy ``"-field"`` shorthand; the client
507
+ normalizes before sending.
508
+ """
509
+ request = WorksExportRequest(
510
+ type=type,
511
+ first=first,
512
+ sort_by=_normalize_sort_by(sort_by),
513
+ )
351
514
  response = await self._post("/works.export", request, WorksExportResponse)
352
515
  return response.works
353
516
 
@@ -361,3 +524,101 @@ class AsyncWorksService(AsyncBaseService):
361
524
  request = WorksCountRequest(type=type, owned_by=owned_by)
362
525
  response = await self._post("/works.count", request, WorksCountResponse)
363
526
  return response.count
527
+
528
+ async def _list_since(
529
+ self,
530
+ after: datetime,
531
+ timestamp_field: str,
532
+ *,
533
+ type: Sequence[WorkType] | None,
534
+ owned_by: Sequence[str] | None,
535
+ applies_to_part: Sequence[str] | None,
536
+ limit: int | None,
537
+ page_size: int | None,
538
+ ) -> _WorkList:
539
+ """Shared cursor-paginated fetcher for async ``list_*_since`` helpers.
540
+
541
+ Streams pages sorted ``{timestamp_field}:desc`` and early-exits as soon
542
+ as a record's timestamp is strictly older than ``after``. Respects
543
+ ``limit`` as a hard cap on returned items.
544
+ """
545
+ sort_by = [f"{timestamp_field}:desc"]
546
+ collected: _WorkList = []
547
+ cursor: str | None = None
548
+ while True:
549
+ if limit is not None and len(collected) >= limit:
550
+ break
551
+ page = await self.list(
552
+ type=type,
553
+ owned_by=owned_by,
554
+ applies_to_part=applies_to_part,
555
+ cursor=cursor,
556
+ limit=resolve_page_limit(limit, len(collected), page_size),
557
+ sort_by=sort_by,
558
+ )
559
+ stop = False
560
+ for work in page.works:
561
+ timestamp = getattr(work, timestamp_field, None)
562
+ if _is_before_cutoff(timestamp, after):
563
+ stop = True
564
+ break
565
+ collected.append(work)
566
+ if limit is not None and len(collected) >= limit:
567
+ stop = True
568
+ break
569
+ if stop or not page.next_cursor:
570
+ break
571
+ cursor = page.next_cursor
572
+ return collected
573
+
574
+ async def list_modified_since(
575
+ self,
576
+ after: datetime,
577
+ *,
578
+ type: Sequence[WorkType] | None = None,
579
+ owned_by: Sequence[str] | None = None,
580
+ applies_to_part: Sequence[str] | None = None,
581
+ limit: int | None = None,
582
+ page_size: int | None = None,
583
+ ) -> _WorkList:
584
+ """Return work items modified at or after ``after``.
585
+
586
+ Pages through ``works.list`` sorted by ``modified_date:desc`` and stops
587
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
588
+ hard cap when provided.
589
+ """
590
+ return await self._list_since(
591
+ after,
592
+ "modified_date",
593
+ type=type,
594
+ owned_by=owned_by,
595
+ applies_to_part=applies_to_part,
596
+ limit=limit,
597
+ page_size=page_size,
598
+ )
599
+
600
+ async def list_created_since(
601
+ self,
602
+ after: datetime,
603
+ *,
604
+ type: Sequence[WorkType] | None = None,
605
+ owned_by: Sequence[str] | None = None,
606
+ applies_to_part: Sequence[str] | None = None,
607
+ limit: int | None = None,
608
+ page_size: int | None = None,
609
+ ) -> _WorkList:
610
+ """Return work items created at or after ``after``.
611
+
612
+ Pages through ``works.list`` sorted by ``created_date:desc`` and stops
613
+ as soon as it sees a record older than ``after``. Uses ``limit`` as a
614
+ hard cap when provided.
615
+ """
616
+ return await self._list_since(
617
+ after,
618
+ "created_date",
619
+ type=type,
620
+ owned_by=owned_by,
621
+ applies_to_part=applies_to_part,
622
+ limit=limit,
623
+ page_size=page_size,
624
+ )
@@ -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.1
3
+ Version: 3.0.1
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,16 @@ 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
+ devrev/services/_pagination.py,sha256=J6AXu1P3iNRyC8_nH709Z8nOLhHeSctOLRmY_eYI6QA,2100
39
40
  devrev/services/accounts.py,sha256=X7FgcODex0XKLiV_VvXKDl2Jm8XsNpn9qp40oRjZqME,9704
40
41
  devrev/services/articles.py,sha256=xOpJOG9f29a5W3IFROplL1a9eJdGucbUFkK-MAIIYlc,41637
41
42
  devrev/services/artifacts.py,sha256=SJzIi5M4np0ENoOTTGEAcoqoRFMVd8pe-BCo9vvhYzk,14124
42
- devrev/services/base.py,sha256=g55dfhd2-uAhgXuguiBacSP4yOAa03Q7hzKDG2vbZ8U,7100
43
+ devrev/services/base.py,sha256=pzUOkOA0kc7yxQQS8AP65L-inpW1WxpsYwtZwA_LXk4,7174
43
44
  devrev/services/brands.py,sha256=W6FB9XXTtEzGOfm9IBI6dqY8WXrMeH6urwjiZKrDVgI,5679
44
45
  devrev/services/code_changes.py,sha256=ahDniXCz54GGmTFmNh88heUg0mHQcIAgaqwlqm1CP6E,3199
45
- devrev/services/conversations.py,sha256=yuQ8Sn0ZZ2gDQjG2C8Hjt_UcvD-fJ9R7yWwh7nCH4_o,7546
46
+ devrev/services/conversations.py,sha256=Xw8hDZuwte57fH2Z3D19gd5nrm91OURcWkBrKAcbqAQ,12716
46
47
  devrev/services/dev_users.py,sha256=YDJUNk6Crz2TN_uwkWQIBmztIYrLd8EX3toNLdb_HEk,11870
47
48
  devrev/services/engagements.py,sha256=nT6AElHbFO40utp6R_QCGwTGunXjVci73I8YAtkqu8A,9094
48
49
  devrev/services/groups.py,sha256=0VpOKTwHiRjrKXrO1kdHo8V_z2I02GphYQhyDFnImxg,4960
@@ -62,7 +63,7 @@ devrev/services/timeline_entries.py,sha256=6nbVPcWlE3-ohLhvkM421eqYS5ztKtHmGsPek
62
63
  devrev/services/track_events.py,sha256=lI4wXkWu3uUuXtuRg1MGNkTZ7B0Lc1PjM8Kw-6sUnWc,1439
63
64
  devrev/services/uoms.py,sha256=AA3ymoHj24FIbsZpYC4tg2elSdQ3iINTVOz7MraZcj8,8163
64
65
  devrev/services/webhooks.py,sha256=-TSkcaya1y48WB24_vHd-bqO5xSqxRsCLiilncNzQZU,3917
65
- devrev/services/works.py,sha256=b-HWpPMLnda-1P6iB-_QzHu_MoB7z4g_OQGh26rqhtQ,11252
66
+ devrev/services/works.py,sha256=hAK7ZUqLph5Db15p4RDbeRFLZUALAF5tCFOQSYytj_g,20456
66
67
  devrev/utils/__init__.py,sha256=NOrbpkjDVLH8n9xf-xpZJiIIa_GVI_6vqTm3E8L3Udw,857
67
68
  devrev/utils/content_converter.py,sha256=emRBLiVoOfDGpPDzrMRnqQr4-QkqN13OdWlYOyU_LCg,28141
68
69
  devrev/utils/deprecation.py,sha256=7qB2Dx531oP7mNi7q2txOYsOKC9YwdHqlKPMFHOW9Ws,1275
@@ -95,7 +96,7 @@ devrev_mcp/resources/user.py,sha256=0Paq2w_nbj_dCQ8R0S81zlgjUhDAUzvn1_NmshadqM8,
95
96
  devrev_mcp/tools/__init__.py,sha256=wiou4HHy6HeOQY0El3KYqy_S7c2IC4hjsYHjMm7aH-w,54
96
97
  devrev_mcp/tools/accounts.py,sha256=CKD5bKxCNpm_60lakcrG9pBwNTrWOncH6MVQF4rT5wQ,6173
97
98
  devrev_mcp/tools/articles.py,sha256=tna2ZLcefAaYbvICbRicM-0gk1HEx5HJSLS5Sz9t0iI,14063
98
- devrev_mcp/tools/conversations.py,sha256=qpY12ZEOLv5IwVtokneZQpI3rmTWs3bFcWlxnRI97fU,5885
99
+ devrev_mcp/tools/conversations.py,sha256=aEH8MOAQbdOEyIWbqGLYK2JjXcTaxAJqB5prBzmXvpY,8836
99
100
  devrev_mcp/tools/engagements.py,sha256=21zsMLlgY1FwKzSIxPf6XVdMY77p8NXrWZ7l8PRKI-0,8928
100
101
  devrev_mcp/tools/groups.py,sha256=E29eptwl_1rbmMPPElcSblWs71t1XdvSZIl1UR8TZr0,8456
101
102
  devrev_mcp/tools/incidents.py,sha256=XO9FaZlLHCXULCFpMpCOftYNfvajH5PeXOkW2uUZ_Z4,7984
@@ -110,13 +111,13 @@ devrev_mcp/tools/slas.py,sha256=q7eYC7nMxCGoHMDp1QbtCgSWiG4oHk78sydPtYwsh10,4902
110
111
  devrev_mcp/tools/tags.py,sha256=zj_my9CkPQu8_TTOqszfLULPNgxm83kHux-rWFxHvXQ,4711
111
112
  devrev_mcp/tools/timeline.py,sha256=p7P699Oq205jrzCVyYS6v4bXHEQizircHVyS5JE-GJ4,5326
112
113
  devrev_mcp/tools/users.py,sha256=7CtrxjgxniEb5-DaPh0rwL680a2fDFLJSIU5huuTjYE,4896
113
- devrev_mcp/tools/works.py,sha256=B1og_hy4gfCW9e38TzLVg8CGppKP6QKEMO2uCwZ5H5I,10446
114
+ devrev_mcp/tools/works.py,sha256=IFaIXYR4j_2pZ39ykZgdHdo5nCk8OfgWU5bxBdHwp5E,15241
114
115
  devrev_mcp/utils/__init__.py,sha256=2_5b1KC5kjoUqFY1ZSdB2Tefd2ekjbZ-eHyFWBKI-0A,49
115
116
  devrev_mcp/utils/don_id.py,sha256=PAVzOSgaiqJQSdo1fwohFHq2x7PDNUZoBTN4LFGYM48,6294
116
117
  devrev_mcp/utils/errors.py,sha256=5mRAo76rJvvEVi6b1ZokPxDtX5JKkptaqmiYDLCkwBE,2110
117
118
  devrev_mcp/utils/formatting.py,sha256=6JssG5x1BxjdgSiQ8Ou3H-9Wo3wgWTWmejsrGez4wKc,2431
118
119
  devrev_mcp/utils/pagination.py,sha256=EOUgL-ZdSToM1Q-ydXmjhibsef5K1u1g3CaS9K8I2fY,1286
119
- devrev_python_sdk-2.14.1.dist-info/METADATA,sha256=AKxypqfyTxpsuDnpU4Fr2ni_0ZPpJtDx9gnqTz4oH4A,40907
120
- devrev_python_sdk-2.14.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
121
- devrev_python_sdk-2.14.1.dist-info/entry_points.txt,sha256=XiV4J_yy0yzVZVxg7T66YERVIlqdPNp3O-NHTHkllqQ,63
122
- devrev_python_sdk-2.14.1.dist-info/RECORD,,
120
+ devrev_python_sdk-3.0.1.dist-info/METADATA,sha256=lTWWDMuQ7SvBTLsT1ape1IhGmY4b8OixjrHpqvkQL2o,40906
121
+ devrev_python_sdk-3.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
122
+ devrev_python_sdk-3.0.1.dist-info/entry_points.txt,sha256=XiV4J_yy0yzVZVxg7T66YERVIlqdPNp3O-NHTHkllqQ,63
123
+ devrev_python_sdk-3.0.1.dist-info/RECORD,,