Skip to content

Commit c9f3b2d

Browse files
feat(api): manual updates
1 parent 58799fa commit c9f3b2d

File tree

8 files changed

+121
-102
lines changed

8 files changed

+121
-102
lines changed

.stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 15
22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml
33
openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef
4-
config_hash: 738402ade5ac9528c8ef1677aa1d70f7
4+
config_hash: 4fb2010b528ce4358300ddd10e750265

README.md

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -118,85 +118,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
118118

119119
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
120120

121-
## Pagination
122-
123-
List methods in the Beeper Desktop API are paginated.
124-
125-
This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
126-
127-
```python
128-
from beeper_desktop_api import BeeperDesktop
129-
130-
client = BeeperDesktop()
131-
132-
all_messages = []
133-
# Automatically fetches more pages as needed.
134-
for message in client.messages.search(
135-
account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
136-
limit=10,
137-
query="deployment",
138-
):
139-
# Do something with message here
140-
all_messages.append(message)
141-
print(all_messages)
142-
```
143-
144-
Or, asynchronously:
145-
146-
```python
147-
import asyncio
148-
from beeper_desktop_api import AsyncBeeperDesktop
149-
150-
client = AsyncBeeperDesktop()
151-
152-
153-
async def main() -> None:
154-
all_messages = []
155-
# Iterate through items across all pages, issuing requests as needed.
156-
async for message in client.messages.search(
157-
account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
158-
limit=10,
159-
query="deployment",
160-
):
161-
all_messages.append(message)
162-
print(all_messages)
163-
164-
165-
asyncio.run(main())
166-
```
167-
168-
Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
169-
170-
```python
171-
first_page = await client.messages.search(
172-
account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
173-
limit=10,
174-
query="deployment",
175-
)
176-
if first_page.has_next_page():
177-
print(f"will fetch next page using these details: {first_page.next_page_info()}")
178-
next_page = await first_page.get_next_page()
179-
print(f"number of items we just fetched: {len(next_page.items)}")
180-
181-
# Remove `await` for non-async usage.
182-
```
183-
184-
Or just work directly with the returned data:
185-
186-
```python
187-
first_page = await client.messages.search(
188-
account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
189-
limit=10,
190-
query="deployment",
191-
)
192-
193-
print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..."
194-
for message in first_page.items:
195-
print(message.id)
196-
197-
# Remove `await` for non-async usage.
198-
```
199-
200121
## Nested params
201122

202123
Nested parameters are dictionaries, typed using `TypedDict`, for example:

api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ Methods:
7070
Types:
7171

7272
```python
73-
from beeper_desktop_api.types import MessageSendResponse
73+
from beeper_desktop_api.types import MessageSearchResponse, MessageSendResponse
7474
```
7575

7676
Methods:
7777

7878
- <code title="get /v1/messages">client.messages.<a href="./src/beeper_desktop_api/resources/messages.py">list</a>(\*\*<a href="src/beeper_desktop_api/types/message_list_params.py">params</a>) -> <a href="./src/beeper_desktop_api/types/shared/message.py">SyncCursor[Message]</a></code>
79-
- <code title="get /v1/messages/search">client.messages.<a href="./src/beeper_desktop_api/resources/messages.py">search</a>(\*\*<a href="src/beeper_desktop_api/types/message_search_params.py">params</a>) -> <a href="./src/beeper_desktop_api/types/shared/message.py">SyncCursor[Message]</a></code>
79+
- <code title="get /v1/messages/search">client.messages.<a href="./src/beeper_desktop_api/resources/messages.py">search</a>(\*\*<a href="src/beeper_desktop_api/types/message_search_params.py">params</a>) -> <a href="./src/beeper_desktop_api/types/message_search_response.py">MessageSearchResponse</a></code>
8080
- <code title="post /v1/messages">client.messages.<a href="./src/beeper_desktop_api/resources/messages.py">send</a>(\*\*<a href="src/beeper_desktop_api/types/message_send_params.py">params</a>) -> <a href="./src/beeper_desktop_api/types/message_send_response.py">MessageSendResponse</a></code>

src/beeper_desktop_api/pagination.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
from typing import List, Generic, TypeVar, Optional
3+
from typing import Dict, List, Generic, TypeVar, Optional
44
from typing_extensions import override
55

66
from pydantic import Field as FieldInfo
77

8+
from .types.chat import Chat
89
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
910

10-
__all__ = ["SyncCursor", "AsyncCursor"]
11+
__all__ = ["SyncCursor", "AsyncCursor", "SyncCursorWithChats", "AsyncCursorWithChats"]
1112

1213
_T = TypeVar("_T")
1314

@@ -70,3 +71,65 @@ def next_page_info(self) -> Optional[PageInfo]:
7071
return None
7172

7273
return PageInfo(params={"cursor": oldest_cursor})
74+
75+
76+
class SyncCursorWithChats(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
77+
items: List[_T]
78+
chats: Optional[Dict[str, Chat]] = None
79+
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
80+
oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
81+
newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
82+
83+
@override
84+
def _get_page_items(self) -> List[_T]:
85+
items = self.items
86+
if not items:
87+
return []
88+
return items
89+
90+
@override
91+
def has_next_page(self) -> bool:
92+
has_more = self.has_more
93+
if has_more is not None and has_more is False:
94+
return False
95+
96+
return super().has_next_page()
97+
98+
@override
99+
def next_page_info(self) -> Optional[PageInfo]:
100+
oldest_cursor = self.oldest_cursor
101+
if not oldest_cursor:
102+
return None
103+
104+
return PageInfo(params={"cursor": oldest_cursor})
105+
106+
107+
class AsyncCursorWithChats(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
108+
items: List[_T]
109+
chats: Optional[Dict[str, Chat]] = None
110+
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
111+
oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
112+
newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
113+
114+
@override
115+
def _get_page_items(self) -> List[_T]:
116+
items = self.items
117+
if not items:
118+
return []
119+
return items
120+
121+
@override
122+
def has_next_page(self) -> bool:
123+
has_more = self.has_more
124+
if has_more is not None and has_more is False:
125+
return False
126+
127+
return super().has_next_page()
128+
129+
@override
130+
def next_page_info(self) -> Optional[PageInfo]:
131+
oldest_cursor = self.oldest_cursor
132+
if not oldest_cursor:
133+
return None
134+
135+
return PageInfo(params={"cursor": oldest_cursor})

src/beeper_desktop_api/resources/messages.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .._base_client import AsyncPaginator, make_request_options
2424
from ..types.shared.message import Message
2525
from ..types.message_send_response import MessageSendResponse
26+
from ..types.message_search_response import MessageSearchResponse
2627

2728
__all__ = ["MessagesResource", "AsyncMessagesResource"]
2829

@@ -129,7 +130,7 @@ def search(
129130
extra_query: Query | None = None,
130131
extra_body: Body | None = None,
131132
timeout: float | httpx.Timeout | None | NotGiven = not_given,
132-
) -> SyncCursor[Message]:
133+
) -> MessageSearchResponse:
133134
"""
134135
Search messages across chats using Beeper's message index
135136
@@ -179,9 +180,8 @@ def search(
179180
180181
timeout: Override the client-level default timeout for this request, in seconds
181182
"""
182-
return self._get_api_list(
183+
return self._get(
183184
"/v1/messages/search",
184-
page=SyncCursor[Message],
185185
options=make_request_options(
186186
extra_headers=extra_headers,
187187
extra_query=extra_query,
@@ -206,7 +206,7 @@ def search(
206206
message_search_params.MessageSearchParams,
207207
),
208208
),
209-
model=Message,
209+
cast_to=MessageSearchResponse,
210210
)
211211

212212
def send(
@@ -339,7 +339,7 @@ def list(
339339
model=Message,
340340
)
341341

342-
def search(
342+
async def search(
343343
self,
344344
*,
345345
account_ids: SequenceNotStr[str] | Omit = omit,
@@ -361,7 +361,7 @@ def search(
361361
extra_query: Query | None = None,
362362
extra_body: Body | None = None,
363363
timeout: float | httpx.Timeout | None | NotGiven = not_given,
364-
) -> AsyncPaginator[Message, AsyncCursor[Message]]:
364+
) -> MessageSearchResponse:
365365
"""
366366
Search messages across chats using Beeper's message index
367367
@@ -411,15 +411,14 @@ def search(
411411
412412
timeout: Override the client-level default timeout for this request, in seconds
413413
"""
414-
return self._get_api_list(
414+
return await self._get(
415415
"/v1/messages/search",
416-
page=AsyncCursor[Message],
417416
options=make_request_options(
418417
extra_headers=extra_headers,
419418
extra_query=extra_query,
420419
extra_body=extra_body,
421420
timeout=timeout,
422-
query=maybe_transform(
421+
query=await async_maybe_transform(
423422
{
424423
"account_ids": account_ids,
425424
"chat_ids": chat_ids,
@@ -438,7 +437,7 @@ def search(
438437
message_search_params.MessageSearchParams,
439438
),
440439
),
441-
model=Message,
440+
cast_to=MessageSearchResponse,
442441
)
443442

444443
async def send(

src/beeper_desktop_api/types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@
3131
from .message_send_response import MessageSendResponse as MessageSendResponse
3232
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
3333
from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
34+
from .message_search_response import MessageSearchResponse as MessageSearchResponse
3435
from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from typing import Dict, List, Optional
4+
5+
from pydantic import Field as FieldInfo
6+
7+
from .chat import Chat
8+
from .._models import BaseModel
9+
from .shared.message import Message
10+
11+
__all__ = ["MessageSearchResponse"]
12+
13+
14+
class MessageSearchResponse(BaseModel):
15+
chats: Dict[str, Chat]
16+
"""Map of chatID -> chat details for chats referenced in items."""
17+
18+
has_more: bool = FieldInfo(alias="hasMore")
19+
"""True if additional results can be fetched using the provided cursors."""
20+
21+
items: List[Message]
22+
"""Messages matching the query and filters."""
23+
24+
newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
25+
"""Cursor for fetching newer results (use with direction='after').
26+
27+
Opaque string; do not inspect.
28+
"""
29+
30+
oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
31+
"""Cursor for fetching older results (use with direction='before').
32+
33+
Opaque string; do not inspect.
34+
"""

tests/api_resources/test_messages.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
1212
from beeper_desktop_api.types import (
1313
MessageSendResponse,
14+
MessageSearchResponse,
1415
)
1516
from beeper_desktop_api._utils import parse_datetime
1617
from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
@@ -66,7 +67,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
6667
@parametrize
6768
def test_method_search(self, client: BeeperDesktop) -> None:
6869
message = client.messages.search()
69-
assert_matches_type(SyncCursor[Message], message, path=["response"])
70+
assert_matches_type(MessageSearchResponse, message, path=["response"])
7071

7172
@parametrize
7273
def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
@@ -89,7 +90,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
8990
query="dinner",
9091
sender="me",
9192
)
92-
assert_matches_type(SyncCursor[Message], message, path=["response"])
93+
assert_matches_type(MessageSearchResponse, message, path=["response"])
9394

9495
@parametrize
9596
def test_raw_response_search(self, client: BeeperDesktop) -> None:
@@ -98,7 +99,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None:
9899
assert response.is_closed is True
99100
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
100101
message = response.parse()
101-
assert_matches_type(SyncCursor[Message], message, path=["response"])
102+
assert_matches_type(MessageSearchResponse, message, path=["response"])
102103

103104
@parametrize
104105
def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@@ -107,7 +108,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
107108
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
108109

109110
message = response.parse()
110-
assert_matches_type(SyncCursor[Message], message, path=["response"])
111+
assert_matches_type(MessageSearchResponse, message, path=["response"])
111112

112113
assert cast(Any, response.is_closed) is True
113114

@@ -201,7 +202,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
201202
@parametrize
202203
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
203204
message = await async_client.messages.search()
204-
assert_matches_type(AsyncCursor[Message], message, path=["response"])
205+
assert_matches_type(MessageSearchResponse, message, path=["response"])
205206

206207
@parametrize
207208
async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -224,7 +225,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk
224225
query="dinner",
225226
sender="me",
226227
)
227-
assert_matches_type(AsyncCursor[Message], message, path=["response"])
228+
assert_matches_type(MessageSearchResponse, message, path=["response"])
228229

229230
@parametrize
230231
async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -233,7 +234,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No
233234
assert response.is_closed is True
234235
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
235236
message = await response.parse()
236-
assert_matches_type(AsyncCursor[Message], message, path=["response"])
237+
assert_matches_type(MessageSearchResponse, message, path=["response"])
237238

238239
@parametrize
239240
async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -242,7 +243,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
242243
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
243244

244245
message = await response.parse()
245-
assert_matches_type(AsyncCursor[Message], message, path=["response"])
246+
assert_matches_type(MessageSearchResponse, message, path=["response"])
246247

247248
assert cast(Any, response.is_closed) is True
248249

0 commit comments

Comments
 (0)