From 545ed69d7251f47a309f2f46ee4f3b8e4cf1cc60 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 13:58:21 +0000
Subject: [PATCH 01/20] feat(api): manual updates
---
.github/workflows/publish-pypi.yml | 31 +++++++++++
.github/workflows/release-doctor.yml | 21 ++++++++
.release-please-manifest.json | 3 ++
.stats.yml | 2 +-
CONTRIBUTING.md | 4 +-
README.md | 14 ++---
bin/check-release-environment | 21 ++++++++
pyproject.toml | 6 +--
release-please-config.json | 66 +++++++++++++++++++++++
src/beeper_desktop_api/_version.py | 2 +-
src/beeper_desktop_api/resources/token.py | 8 +--
11 files changed, 160 insertions(+), 18 deletions(-)
create mode 100644 .github/workflows/publish-pypi.yml
create mode 100644 .github/workflows/release-doctor.yml
create mode 100644 .release-please-manifest.json
create mode 100644 bin/check-release-environment
create mode 100644 release-please-config.json
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
new file mode 100644
index 0000000..c22364e
--- /dev/null
+++ b/.github/workflows/publish-pypi.yml
@@ -0,0 +1,31 @@
+# This workflow is triggered when a GitHub release is created.
+# It can also be run manually to re-publish to PyPI in case it failed for some reason.
+# You can run this workflow by navigating to https://www.github.com/beeper/desktop-api-python/actions/workflows/publish-pypi.yml
+name: Publish PyPI
+on:
+ workflow_dispatch:
+
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ name: publish
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rye
+ run: |
+ curl -sSf https://rye.astral.sh/get | bash
+ echo "$HOME/.rye/shims" >> $GITHUB_PATH
+ env:
+ RYE_VERSION: '0.44.0'
+ RYE_INSTALL_OPTION: '--yes'
+
+ - name: Publish to PyPI
+ run: |
+ bash ./bin/publish-pypi
+ env:
+ PYPI_TOKEN: ${{ secrets.BEEPER_DESKTOP_PYPI_TOKEN || secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
new file mode 100644
index 0000000..c6b3e44
--- /dev/null
+++ b/.github/workflows/release-doctor.yml
@@ -0,0 +1,21 @@
+name: Release Doctor
+on:
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ release_doctor:
+ name: release doctor
+ runs-on: ubuntu-latest
+ if: github.repository == 'beeper/desktop-api-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check release environment
+ run: |
+ bash ./bin/check-release-environment
+ env:
+ PYPI_TOKEN: ${{ secrets.BEEPER_DESKTOP_PYPI_TOKEN || secrets.PYPI_TOKEN }}
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..1332969
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.0.1"
+}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index abfa216..ab027c2 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 1
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml
openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311
-config_hash: def03aa92de3408ec65438763617f5c7
+config_hash: f83b2b6eb86f2dd68101065998479cb2
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7899f17..81bc94a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g
To install via git:
```sh
-$ pip install git+ssh://git@github.com/stainless-sdks/beeper-desktop-api-python.git
+$ pip install git+ssh://git@github.com/beeper/desktop-api-python.git
```
Alternatively, you can build from source and install the wheel file:
@@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel
### Publish with a GitHub workflow
-You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/beeper-desktop-api-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up.
+You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/beeper/desktop-api-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up.
### Publish manually
diff --git a/README.md b/README.md
index c634600..51fb670 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@ The REST API documentation can be found on [developers.beeper.com](https://devel
## Installation
```sh
-# install from this staging repo
-pip install git+ssh://git@github.com/stainless-sdks/beeper-desktop-api-python.git
+# install from the production repo
+pip install git+ssh://git@github.com/beeper/desktop-api-python.git
```
> [!NOTE]
@@ -73,8 +73,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv
You can enable this by installing `aiohttp`:
```sh
-# install from this staging repo
-pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/stainless-sdks/beeper-desktop-api-python.git'
+# install from the production repo
+pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/desktop-api-python.git'
```
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
@@ -236,9 +236,9 @@ token = response.parse() # get the object that `token.info()` would have return
print(token.sub)
```
-These methods return an [`APIResponse`](https://github.com/stainless-sdks/beeper-desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) object.
+These methods return an [`APIResponse`](https://github.com/beeper/desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) object.
-The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/beeper-desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.
+The async client returns an [`AsyncAPIResponse`](https://github.com/beeper/desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.
#### `.with_streaming_response`
@@ -342,7 +342,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
-We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/beeper-desktop-api-python/issues) with questions, bugs, or suggestions.
+We are keen for your feedback; please open an [issue](https://www.github.com/beeper/desktop-api-python/issues) with questions, bugs, or suggestions.
### Determining the installed version
diff --git a/bin/check-release-environment b/bin/check-release-environment
new file mode 100644
index 0000000..b845b0f
--- /dev/null
+++ b/bin/check-release-environment
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+errors=()
+
+if [ -z "${PYPI_TOKEN}" ]; then
+ errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
+fi
+
+lenErrors=${#errors[@]}
+
+if [[ lenErrors -gt 0 ]]; then
+ echo -e "Found the following errors in the release environment:\n"
+
+ for error in "${errors[@]}"; do
+ echo -e "- $error\n"
+ done
+
+ exit 1
+fi
+
+echo "The environment is ready to push releases!"
diff --git a/pyproject.toml b/pyproject.toml
index b90ac16..d3a4a85 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,8 +35,8 @@ classifiers = [
]
[project.urls]
-Homepage = "https://github.com/stainless-sdks/beeper-desktop-api-python"
-Repository = "https://github.com/stainless-sdks/beeper-desktop-api-python"
+Homepage = "https://github.com/beeper/desktop-api-python"
+Repository = "https://github.com/beeper/desktop-api-python"
[project.optional-dependencies]
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
@@ -124,7 +124,7 @@ path = "README.md"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
# replace relative links with absolute links
pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
-replacement = '[\1](https://github.com/stainless-sdks/beeper-desktop-api-python/tree/main/\g<2>)'
+replacement = '[\1](https://github.com/beeper/desktop-api-python/tree/main/\g<2>)'
[tool.pytest.ini_options]
testpaths = ["tests"]
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..fd672c1
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,66 @@
+{
+ "packages": {
+ ".": {}
+ },
+ "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json",
+ "include-v-in-tag": true,
+ "include-component-in-tag": false,
+ "versioning": "prerelease",
+ "prerelease": true,
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": false,
+ "pull-request-header": "Automated Release PR",
+ "pull-request-title-pattern": "release: ${version}",
+ "changelog-sections": [
+ {
+ "type": "feat",
+ "section": "Features"
+ },
+ {
+ "type": "fix",
+ "section": "Bug Fixes"
+ },
+ {
+ "type": "perf",
+ "section": "Performance Improvements"
+ },
+ {
+ "type": "revert",
+ "section": "Reverts"
+ },
+ {
+ "type": "chore",
+ "section": "Chores"
+ },
+ {
+ "type": "docs",
+ "section": "Documentation"
+ },
+ {
+ "type": "style",
+ "section": "Styles"
+ },
+ {
+ "type": "refactor",
+ "section": "Refactors"
+ },
+ {
+ "type": "test",
+ "section": "Tests",
+ "hidden": true
+ },
+ {
+ "type": "build",
+ "section": "Build System"
+ },
+ {
+ "type": "ci",
+ "section": "Continuous Integration",
+ "hidden": true
+ }
+ ],
+ "release-type": "python",
+ "extra-files": [
+ "src/beeper_desktop_api/_version.py"
+ ]
+}
\ No newline at end of file
diff --git a/src/beeper_desktop_api/_version.py b/src/beeper_desktop_api/_version.py
index 72a9009..3ba6273 100644
--- a/src/beeper_desktop_api/_version.py
+++ b/src/beeper_desktop_api/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "beeper_desktop_api"
-__version__ = "0.0.1"
+__version__ = "0.0.1" # x-release-please-version
diff --git a/src/beeper_desktop_api/resources/token.py b/src/beeper_desktop_api/resources/token.py
index fbf0425..5648872 100644
--- a/src/beeper_desktop_api/resources/token.py
+++ b/src/beeper_desktop_api/resources/token.py
@@ -28,7 +28,7 @@ def with_raw_response(self) -> TokenResourceWithRawResponse:
This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
- For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#accessing-raw-response-data-eg-headers
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
"""
return TokenResourceWithRawResponse(self)
@@ -37,7 +37,7 @@ def with_streaming_response(self) -> TokenResourceWithStreamingResponse:
"""
An alternative to `.with_raw_response` that doesn't eagerly read the response body.
- For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#with_streaming_response
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
"""
return TokenResourceWithStreamingResponse(self)
@@ -70,7 +70,7 @@ def with_raw_response(self) -> AsyncTokenResourceWithRawResponse:
This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
- For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#accessing-raw-response-data-eg-headers
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
"""
return AsyncTokenResourceWithRawResponse(self)
@@ -79,7 +79,7 @@ def with_streaming_response(self) -> AsyncTokenResourceWithStreamingResponse:
"""
An alternative to `.with_raw_response` that doesn't eagerly read the response body.
- For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#with_streaming_response
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
"""
return AsyncTokenResourceWithStreamingResponse(self)
From b1ba1c0584b99ab402f7c1643c13c19881baa600 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 13:58:48 +0000
Subject: [PATCH 02/20] feat(api): manual updates
---
.stats.yml | 6 +-
README.md | 136 +++-
api.md | 61 +-
src/beeper_desktop_api/_client.py | 351 ++++++++-
src/beeper_desktop_api/resources/__init__.py | 56 ++
src/beeper_desktop_api/resources/accounts.py | 145 ++++
.../resources/chats/__init__.py | 33 +
.../resources/chats/chats.py | 668 ++++++++++++++++++
.../resources/chats/reminders.py | 267 +++++++
src/beeper_desktop_api/resources/contacts.py | 197 ++++++
src/beeper_desktop_api/resources/messages.py | 423 +++++++++++
src/beeper_desktop_api/types/__init__.py | 19 +
src/beeper_desktop_api/types/account.py | 19 +
.../types/account_list_response.py | 10 +
src/beeper_desktop_api/types/chat.py | 67 ++
.../types/chat_archive_params.py | 12 +
.../types/chat_create_params.py | 30 +
.../types/chat_create_response.py | 14 +
.../types/chat_retrieve_params.py | 18 +
.../types/chat_search_params.py | 81 +++
.../types/chats/__init__.py | 2 +
.../types/chats/reminder_create_params.py | 22 +
.../types/client_download_asset_params.py | 12 +
.../types/client_open_params.py | 26 +
.../types/client_search_params.py | 12 +
.../types/contact_search_params.py | 17 +
.../types/contact_search_response.py | 12 +
.../types/download_asset_response.py | 17 +
.../types/message_search_params.py | 85 +++
.../types/message_send_params.py | 20 +
.../types/message_send_response.py | 15 +
src/beeper_desktop_api/types/open_response.py | 10 +
.../types/search_response.py | 48 ++
src/beeper_desktop_api/types/shared/error.py | 12 +-
tests/api_resources/chats/__init__.py | 1 +
tests/api_resources/chats/test_reminders.py | 206 ++++++
tests/api_resources/test_accounts.py | 74 ++
tests/api_resources/test_chats.py | 402 +++++++++++
tests/api_resources/test_client.py | 222 ++++++
tests/api_resources/test_contacts.py | 92 +++
tests/api_resources/test_messages.py | 203 ++++++
tests/test_client.py | 40 +-
42 files changed, 4113 insertions(+), 50 deletions(-)
create mode 100644 src/beeper_desktop_api/resources/accounts.py
create mode 100644 src/beeper_desktop_api/resources/chats/__init__.py
create mode 100644 src/beeper_desktop_api/resources/chats/chats.py
create mode 100644 src/beeper_desktop_api/resources/chats/reminders.py
create mode 100644 src/beeper_desktop_api/resources/contacts.py
create mode 100644 src/beeper_desktop_api/resources/messages.py
create mode 100644 src/beeper_desktop_api/types/account.py
create mode 100644 src/beeper_desktop_api/types/account_list_response.py
create mode 100644 src/beeper_desktop_api/types/chat.py
create mode 100644 src/beeper_desktop_api/types/chat_archive_params.py
create mode 100644 src/beeper_desktop_api/types/chat_create_params.py
create mode 100644 src/beeper_desktop_api/types/chat_create_response.py
create mode 100644 src/beeper_desktop_api/types/chat_retrieve_params.py
create mode 100644 src/beeper_desktop_api/types/chat_search_params.py
create mode 100644 src/beeper_desktop_api/types/chats/reminder_create_params.py
create mode 100644 src/beeper_desktop_api/types/client_download_asset_params.py
create mode 100644 src/beeper_desktop_api/types/client_open_params.py
create mode 100644 src/beeper_desktop_api/types/client_search_params.py
create mode 100644 src/beeper_desktop_api/types/contact_search_params.py
create mode 100644 src/beeper_desktop_api/types/contact_search_response.py
create mode 100644 src/beeper_desktop_api/types/download_asset_response.py
create mode 100644 src/beeper_desktop_api/types/message_search_params.py
create mode 100644 src/beeper_desktop_api/types/message_send_params.py
create mode 100644 src/beeper_desktop_api/types/message_send_response.py
create mode 100644 src/beeper_desktop_api/types/open_response.py
create mode 100644 src/beeper_desktop_api/types/search_response.py
create mode 100644 tests/api_resources/chats/__init__.py
create mode 100644 tests/api_resources/chats/test_reminders.py
create mode 100644 tests/api_resources/test_accounts.py
create mode 100644 tests/api_resources/test_chats.py
create mode 100644 tests/api_resources/test_client.py
create mode 100644 tests/api_resources/test_contacts.py
create mode 100644 tests/api_resources/test_messages.py
diff --git a/.stats.yml b/.stats.yml
index ab027c2..8526f3e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 1
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml
-openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311
+configured_endpoints: 14
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
+openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
config_hash: f83b2b6eb86f2dd68101065998479cb2
diff --git a/README.md b/README.md
index 51fb670..9f0e7ba 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,12 @@ client = BeeperDesktop(
access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted
)
-user_info = client.token.info()
-print(user_info.sub)
+page = client.chats.search(
+ include_muted=True,
+ limit=3,
+ type="single",
+)
+print(page.items)
```
While you can provide a `access_token` keyword argument,
@@ -57,8 +61,12 @@ client = AsyncBeeperDesktop(
async def main() -> None:
- user_info = await client.token.info()
- print(user_info.sub)
+ page = await client.chats.search(
+ include_muted=True,
+ limit=3,
+ type="single",
+ )
+ print(page.items)
asyncio.run(main())
@@ -90,8 +98,12 @@ async def main() -> None:
access_token="My Access Token",
http_client=DefaultAioHttpClient(),
) as client:
- user_info = await client.token.info()
- print(user_info.sub)
+ page = await client.chats.search(
+ include_muted=True,
+ limit=3,
+ type="single",
+ )
+ print(page.items)
asyncio.run(main())
@@ -106,6 +118,101 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
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`.
+## Pagination
+
+List methods in the Beeper Desktop API are paginated.
+
+This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
+
+```python
+from beeper_desktop_api import BeeperDesktop
+
+client = BeeperDesktop()
+
+all_messages = []
+# Automatically fetches more pages as needed.
+for message in client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+):
+ # Do something with message here
+ all_messages.append(message)
+print(all_messages)
+```
+
+Or, asynchronously:
+
+```python
+import asyncio
+from beeper_desktop_api import AsyncBeeperDesktop
+
+client = AsyncBeeperDesktop()
+
+
+async def main() -> None:
+ all_messages = []
+ # Iterate through items across all pages, issuing requests as needed.
+ async for message in client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+ ):
+ all_messages.append(message)
+ print(all_messages)
+
+
+asyncio.run(main())
+```
+
+Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
+
+```python
+first_page = await client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+)
+if first_page.has_next_page():
+ print(f"will fetch next page using these details: {first_page.next_page_info()}")
+ next_page = await first_page.get_next_page()
+ print(f"number of items we just fetched: {len(next_page.items)}")
+
+# Remove `await` for non-async usage.
+```
+
+Or just work directly with the returned data:
+
+```python
+first_page = await client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+)
+
+print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..."
+for message in first_page.items:
+ print(message.id)
+
+# Remove `await` for non-async usage.
+```
+
+## Nested params
+
+Nested parameters are dictionaries, typed using `TypedDict`, for example:
+
+```python
+from beeper_desktop_api import BeeperDesktop
+
+client = BeeperDesktop()
+
+base_response = client.chats.reminders.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+)
+print(base_response.reminder)
+```
+
## Handling errors
When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `beeper_desktop_api.APIConnectionError` is raised.
@@ -122,7 +229,10 @@ from beeper_desktop_api import BeeperDesktop
client = BeeperDesktop()
try:
- client.token.info()
+ client.messages.send(
+ chat_id="1229391",
+ text="Hello! Just checking in on the project status.",
+ )
except beeper_desktop_api.APIConnectionError as e:
print("The server could not be reached")
print(e.__cause__) # an underlying Exception, likely raised within httpx.
@@ -165,7 +275,7 @@ client = BeeperDesktop(
)
# Or, configure per-request:
-client.with_options(max_retries=5).token.info()
+client.with_options(max_retries=5).accounts.list()
```
### Timeouts
@@ -188,7 +298,7 @@ client = BeeperDesktop(
)
# Override per-request:
-client.with_options(timeout=5.0).token.info()
+client.with_options(timeout=5.0).accounts.list()
```
On timeout, an `APITimeoutError` is thrown.
@@ -229,11 +339,11 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to
from beeper_desktop_api import BeeperDesktop
client = BeeperDesktop()
-response = client.token.with_raw_response.info()
+response = client.accounts.with_raw_response.list()
print(response.headers.get('X-My-Header'))
-token = response.parse() # get the object that `token.info()` would have returned
-print(token.sub)
+account = response.parse() # get the object that `accounts.list()` would have returned
+print(account)
```
These methods return an [`APIResponse`](https://github.com/beeper/desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) object.
@@ -247,7 +357,7 @@ The above interface eagerly reads the full response body when you make the reque
To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods.
```python
-with client.token.with_streaming_response.info() as response:
+with client.accounts.with_streaming_response.list() as response:
print(response.headers.get("X-My-Header"))
for line in response.iter_lines():
diff --git a/api.md b/api.md
index 83f0189..cfe2dc3 100644
--- a/api.md
+++ b/api.md
@@ -4,22 +4,79 @@
from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, Reaction, User
```
+# BeeperDesktop
+
+Types:
+
+```python
+from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse
+```
+
+Methods:
+
+- client.download_asset(\*\*params) -> DownloadAssetResponse
+- client.open(\*\*params) -> OpenResponse
+- client.search(\*\*params) -> SearchResponse
+
# Accounts
Types:
```python
-from beeper_desktop_api.types import Account
+from beeper_desktop_api.types import Account, AccountListResponse
```
+Methods:
+
+- client.accounts.list() -> AccountListResponse
+
+# Contacts
+
+Types:
+
+```python
+from beeper_desktop_api.types import ContactSearchResponse
+```
+
+Methods:
+
+- client.contacts.search(\*\*params) -> ContactSearchResponse
+
# Chats
Types:
```python
-from beeper_desktop_api.types import Chat
+from beeper_desktop_api.types import Chat, ChatCreateResponse
+```
+
+Methods:
+
+- client.chats.create(\*\*params) -> ChatCreateResponse
+- client.chats.retrieve(chat_id, \*\*params) -> Chat
+- client.chats.archive(chat_id, \*\*params) -> BaseResponse
+- client.chats.search(\*\*params) -> SyncCursor[Chat]
+
+## Reminders
+
+Methods:
+
+- client.chats.reminders.create(chat_id, \*\*params) -> BaseResponse
+- client.chats.reminders.delete(chat_id) -> BaseResponse
+
+# Messages
+
+Types:
+
+```python
+from beeper_desktop_api.types import MessageSendResponse
```
+Methods:
+
+- client.messages.search(\*\*params) -> SyncCursor[Message]
+- client.messages.send(\*\*params) -> MessageSendResponse
+
# Token
Types:
diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py
index 44866d0..7a1a10e 100644
--- a/src/beeper_desktop_api/_client.py
+++ b/src/beeper_desktop_api/_client.py
@@ -10,25 +10,46 @@
from . import _exceptions
from ._qs import Querystring
+from .types import client_open_params, client_search_params, client_download_asset_params
from ._types import (
+ Body,
Omit,
+ Query,
+ Headers,
Timeout,
NotGiven,
Transport,
ProxiesTypes,
RequestOptions,
+ omit,
not_given,
)
-from ._utils import is_given, get_async_library
+from ._utils import (
+ is_given,
+ maybe_transform,
+ get_async_library,
+ async_maybe_transform,
+)
from ._version import __version__
-from .resources import token
+from ._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from .resources import token, accounts, contacts, messages
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import APIStatusError, BeeperDesktopError
from ._base_client import (
DEFAULT_MAX_RETRIES,
SyncAPIClient,
AsyncAPIClient,
+ make_request_options,
)
+from .resources.chats import chats
+from .types.open_response import OpenResponse
+from .types.search_response import SearchResponse
+from .types.download_asset_response import DownloadAssetResponse
__all__ = [
"Timeout",
@@ -43,6 +64,10 @@
class BeeperDesktop(SyncAPIClient):
+ accounts: accounts.AccountsResource
+ contacts: contacts.ContactsResource
+ chats: chats.ChatsResource
+ messages: messages.MessagesResource
token: token.TokenResource
with_raw_response: BeeperDesktopWithRawResponse
with_streaming_response: BeeperDesktopWithStreamedResponse
@@ -101,6 +126,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
+ self.accounts = accounts.AccountsResource(self)
+ self.contacts = contacts.ContactsResource(self)
+ self.chats = chats.ChatsResource(self)
+ self.messages = messages.MessagesResource(self)
self.token = token.TokenResource(self)
self.with_raw_response = BeeperDesktopWithRawResponse(self)
self.with_streaming_response = BeeperDesktopWithStreamedResponse(self)
@@ -176,6 +205,133 @@ def copy(
# client.with_options(timeout=10).foo.create(...)
with_options = copy
+ def download_asset(
+ self,
+ *,
+ url: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> DownloadAssetResponse:
+ """
+ Download a Matrix asset using its mxc:// or localmxc:// URL and return the local
+ file URL.
+
+ Args:
+ url: Matrix content URL (mxc:// or localmxc://) for the asset to download.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self.post(
+ "/v1/app/download-asset",
+ body=maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DownloadAssetResponse,
+ )
+
+ def open(
+ self,
+ *,
+ chat_id: str | Omit = omit,
+ draft_attachment_path: str | Omit = omit,
+ draft_text: str | Omit = omit,
+ message_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OpenResponse:
+ """
+ Open Beeper Desktop and optionally navigate to a specific chat, message, or
+ pre-fill draft text and attachment.
+
+ Args:
+ chat_id: Optional Beeper chat ID (or local chat ID) to focus after opening the app. If
+ omitted, only opens/focuses the app.
+
+ draft_attachment_path: Optional draft attachment path to populate in the message input field.
+
+ draft_text: Optional draft text to populate in the message input field.
+
+ message_id: Optional message ID. Jumps to that message in the chat when opening.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self.post(
+ "/v1/app/open",
+ body=maybe_transform(
+ {
+ "chat_id": chat_id,
+ "draft_attachment_path": draft_attachment_path,
+ "draft_text": draft_text,
+ "message_id": message_id,
+ },
+ client_open_params.ClientOpenParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OpenResponse,
+ )
+
+ def search(
+ self,
+ *,
+ query: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SearchResponse:
+ """
+ Returns matching chats, participant name matches in groups, and the first page
+ of messages in one call. Paginate messages via search-messages. Paginate chats
+ via search-chats. Uses the same sorting as the chat search in the app.
+
+ Args:
+ query: User-typed search text. Literal word matching (NOT semantic).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self.get(
+ "/v1/search",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"query": query}, client_search_params.ClientSearchParams),
+ ),
+ cast_to=SearchResponse,
+ )
+
@override
def _make_status_error(
self,
@@ -211,6 +367,10 @@ def _make_status_error(
class AsyncBeeperDesktop(AsyncAPIClient):
+ accounts: accounts.AsyncAccountsResource
+ contacts: contacts.AsyncContactsResource
+ chats: chats.AsyncChatsResource
+ messages: messages.AsyncMessagesResource
token: token.AsyncTokenResource
with_raw_response: AsyncBeeperDesktopWithRawResponse
with_streaming_response: AsyncBeeperDesktopWithStreamedResponse
@@ -269,6 +429,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
+ self.accounts = accounts.AsyncAccountsResource(self)
+ self.contacts = contacts.AsyncContactsResource(self)
+ self.chats = chats.AsyncChatsResource(self)
+ self.messages = messages.AsyncMessagesResource(self)
self.token = token.AsyncTokenResource(self)
self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self)
self.with_streaming_response = AsyncBeeperDesktopWithStreamedResponse(self)
@@ -344,6 +508,133 @@ def copy(
# client.with_options(timeout=10).foo.create(...)
with_options = copy
+ async def download_asset(
+ self,
+ *,
+ url: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> DownloadAssetResponse:
+ """
+ Download a Matrix asset using its mxc:// or localmxc:// URL and return the local
+ file URL.
+
+ Args:
+ url: Matrix content URL (mxc:// or localmxc://) for the asset to download.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self.post(
+ "/v1/app/download-asset",
+ body=await async_maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DownloadAssetResponse,
+ )
+
+ async def open(
+ self,
+ *,
+ chat_id: str | Omit = omit,
+ draft_attachment_path: str | Omit = omit,
+ draft_text: str | Omit = omit,
+ message_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OpenResponse:
+ """
+ Open Beeper Desktop and optionally navigate to a specific chat, message, or
+ pre-fill draft text and attachment.
+
+ Args:
+ chat_id: Optional Beeper chat ID (or local chat ID) to focus after opening the app. If
+ omitted, only opens/focuses the app.
+
+ draft_attachment_path: Optional draft attachment path to populate in the message input field.
+
+ draft_text: Optional draft text to populate in the message input field.
+
+ message_id: Optional message ID. Jumps to that message in the chat when opening.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self.post(
+ "/v1/app/open",
+ body=await async_maybe_transform(
+ {
+ "chat_id": chat_id,
+ "draft_attachment_path": draft_attachment_path,
+ "draft_text": draft_text,
+ "message_id": message_id,
+ },
+ client_open_params.ClientOpenParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OpenResponse,
+ )
+
+ async def search(
+ self,
+ *,
+ query: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SearchResponse:
+ """
+ Returns matching chats, participant name matches in groups, and the first page
+ of messages in one call. Paginate messages via search-messages. Paginate chats
+ via search-chats. Uses the same sorting as the chat search in the app.
+
+ Args:
+ query: User-typed search text. Literal word matching (NOT semantic).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self.get(
+ "/v1/search",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"query": query}, client_search_params.ClientSearchParams),
+ ),
+ cast_to=SearchResponse,
+ )
+
@override
def _make_status_error(
self,
@@ -380,23 +671,79 @@ def _make_status_error(
class BeeperDesktopWithRawResponse:
def __init__(self, client: BeeperDesktop) -> None:
+ self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts)
+ self.contacts = contacts.ContactsResourceWithRawResponse(client.contacts)
+ self.chats = chats.ChatsResourceWithRawResponse(client.chats)
+ self.messages = messages.MessagesResourceWithRawResponse(client.messages)
self.token = token.TokenResourceWithRawResponse(client.token)
+ self.download_asset = to_raw_response_wrapper(
+ client.download_asset,
+ )
+ self.open = to_raw_response_wrapper(
+ client.open,
+ )
+ self.search = to_raw_response_wrapper(
+ client.search,
+ )
+
class AsyncBeeperDesktopWithRawResponse:
def __init__(self, client: AsyncBeeperDesktop) -> None:
+ self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts)
+ self.contacts = contacts.AsyncContactsResourceWithRawResponse(client.contacts)
+ self.chats = chats.AsyncChatsResourceWithRawResponse(client.chats)
+ self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages)
self.token = token.AsyncTokenResourceWithRawResponse(client.token)
+ self.download_asset = async_to_raw_response_wrapper(
+ client.download_asset,
+ )
+ self.open = async_to_raw_response_wrapper(
+ client.open,
+ )
+ self.search = async_to_raw_response_wrapper(
+ client.search,
+ )
+
class BeeperDesktopWithStreamedResponse:
def __init__(self, client: BeeperDesktop) -> None:
+ self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts)
+ self.contacts = contacts.ContactsResourceWithStreamingResponse(client.contacts)
+ self.chats = chats.ChatsResourceWithStreamingResponse(client.chats)
+ self.messages = messages.MessagesResourceWithStreamingResponse(client.messages)
self.token = token.TokenResourceWithStreamingResponse(client.token)
+ self.download_asset = to_streamed_response_wrapper(
+ client.download_asset,
+ )
+ self.open = to_streamed_response_wrapper(
+ client.open,
+ )
+ self.search = to_streamed_response_wrapper(
+ client.search,
+ )
+
class AsyncBeeperDesktopWithStreamedResponse:
def __init__(self, client: AsyncBeeperDesktop) -> None:
+ self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts)
+ self.contacts = contacts.AsyncContactsResourceWithStreamingResponse(client.contacts)
+ self.chats = chats.AsyncChatsResourceWithStreamingResponse(client.chats)
+ self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages)
self.token = token.AsyncTokenResourceWithStreamingResponse(client.token)
+ self.download_asset = async_to_streamed_response_wrapper(
+ client.download_asset,
+ )
+ self.open = async_to_streamed_response_wrapper(
+ client.open,
+ )
+ self.search = async_to_streamed_response_wrapper(
+ client.search,
+ )
+
Client = BeeperDesktop
diff --git a/src/beeper_desktop_api/resources/__init__.py b/src/beeper_desktop_api/resources/__init__.py
index 7c3b25f..24ab242 100644
--- a/src/beeper_desktop_api/resources/__init__.py
+++ b/src/beeper_desktop_api/resources/__init__.py
@@ -1,5 +1,13 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from .chats import (
+ ChatsResource,
+ AsyncChatsResource,
+ ChatsResourceWithRawResponse,
+ AsyncChatsResourceWithRawResponse,
+ ChatsResourceWithStreamingResponse,
+ AsyncChatsResourceWithStreamingResponse,
+)
from .token import (
TokenResource,
AsyncTokenResource,
@@ -8,8 +16,56 @@
TokenResourceWithStreamingResponse,
AsyncTokenResourceWithStreamingResponse,
)
+from .accounts import (
+ AccountsResource,
+ AsyncAccountsResource,
+ AccountsResourceWithRawResponse,
+ AsyncAccountsResourceWithRawResponse,
+ AccountsResourceWithStreamingResponse,
+ AsyncAccountsResourceWithStreamingResponse,
+)
+from .contacts import (
+ ContactsResource,
+ AsyncContactsResource,
+ ContactsResourceWithRawResponse,
+ AsyncContactsResourceWithRawResponse,
+ ContactsResourceWithStreamingResponse,
+ AsyncContactsResourceWithStreamingResponse,
+)
+from .messages import (
+ MessagesResource,
+ AsyncMessagesResource,
+ MessagesResourceWithRawResponse,
+ AsyncMessagesResourceWithRawResponse,
+ MessagesResourceWithStreamingResponse,
+ AsyncMessagesResourceWithStreamingResponse,
+)
__all__ = [
+ "AccountsResource",
+ "AsyncAccountsResource",
+ "AccountsResourceWithRawResponse",
+ "AsyncAccountsResourceWithRawResponse",
+ "AccountsResourceWithStreamingResponse",
+ "AsyncAccountsResourceWithStreamingResponse",
+ "ContactsResource",
+ "AsyncContactsResource",
+ "ContactsResourceWithRawResponse",
+ "AsyncContactsResourceWithRawResponse",
+ "ContactsResourceWithStreamingResponse",
+ "AsyncContactsResourceWithStreamingResponse",
+ "ChatsResource",
+ "AsyncChatsResource",
+ "ChatsResourceWithRawResponse",
+ "AsyncChatsResourceWithRawResponse",
+ "ChatsResourceWithStreamingResponse",
+ "AsyncChatsResourceWithStreamingResponse",
+ "MessagesResource",
+ "AsyncMessagesResource",
+ "MessagesResourceWithRawResponse",
+ "AsyncMessagesResourceWithRawResponse",
+ "MessagesResourceWithStreamingResponse",
+ "AsyncMessagesResourceWithStreamingResponse",
"TokenResource",
"AsyncTokenResource",
"TokenResourceWithRawResponse",
diff --git a/src/beeper_desktop_api/resources/accounts.py b/src/beeper_desktop_api/resources/accounts.py
new file mode 100644
index 0000000..49a5df2
--- /dev/null
+++ b/src/beeper_desktop_api/resources/accounts.py
@@ -0,0 +1,145 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from .._types import Body, Query, Headers, NotGiven, not_given
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from .._base_client import make_request_options
+from ..types.account_list_response import AccountListResponse
+
+__all__ = ["AccountsResource", "AsyncAccountsResource"]
+
+
+class AccountsResource(SyncAPIResource):
+ """Accounts operations"""
+
+ @cached_property
+ def with_raw_response(self) -> AccountsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AccountsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AccountsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AccountsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AccountListResponse:
+ """
+ Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.)
+ actively connected to this Beeper Desktop instance
+ """
+ return self._get(
+ "/v1/accounts",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AccountListResponse,
+ )
+
+
+class AsyncAccountsResource(AsyncAPIResource):
+ """Accounts operations"""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncAccountsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncAccountsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncAccountsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AsyncAccountsResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AccountListResponse:
+ """
+ Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.)
+ actively connected to this Beeper Desktop instance
+ """
+ return await self._get(
+ "/v1/accounts",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AccountListResponse,
+ )
+
+
+class AccountsResourceWithRawResponse:
+ def __init__(self, accounts: AccountsResource) -> None:
+ self._accounts = accounts
+
+ self.list = to_raw_response_wrapper(
+ accounts.list,
+ )
+
+
+class AsyncAccountsResourceWithRawResponse:
+ def __init__(self, accounts: AsyncAccountsResource) -> None:
+ self._accounts = accounts
+
+ self.list = async_to_raw_response_wrapper(
+ accounts.list,
+ )
+
+
+class AccountsResourceWithStreamingResponse:
+ def __init__(self, accounts: AccountsResource) -> None:
+ self._accounts = accounts
+
+ self.list = to_streamed_response_wrapper(
+ accounts.list,
+ )
+
+
+class AsyncAccountsResourceWithStreamingResponse:
+ def __init__(self, accounts: AsyncAccountsResource) -> None:
+ self._accounts = accounts
+
+ self.list = async_to_streamed_response_wrapper(
+ accounts.list,
+ )
diff --git a/src/beeper_desktop_api/resources/chats/__init__.py b/src/beeper_desktop_api/resources/chats/__init__.py
new file mode 100644
index 0000000..e26ae7f
--- /dev/null
+++ b/src/beeper_desktop_api/resources/chats/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .chats import (
+ ChatsResource,
+ AsyncChatsResource,
+ ChatsResourceWithRawResponse,
+ AsyncChatsResourceWithRawResponse,
+ ChatsResourceWithStreamingResponse,
+ AsyncChatsResourceWithStreamingResponse,
+)
+from .reminders import (
+ RemindersResource,
+ AsyncRemindersResource,
+ RemindersResourceWithRawResponse,
+ AsyncRemindersResourceWithRawResponse,
+ RemindersResourceWithStreamingResponse,
+ AsyncRemindersResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "RemindersResource",
+ "AsyncRemindersResource",
+ "RemindersResourceWithRawResponse",
+ "AsyncRemindersResourceWithRawResponse",
+ "RemindersResourceWithStreamingResponse",
+ "AsyncRemindersResourceWithStreamingResponse",
+ "ChatsResource",
+ "AsyncChatsResource",
+ "ChatsResourceWithRawResponse",
+ "AsyncChatsResourceWithRawResponse",
+ "ChatsResourceWithStreamingResponse",
+ "AsyncChatsResourceWithStreamingResponse",
+]
diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py
new file mode 100644
index 0000000..b5ec602
--- /dev/null
+++ b/src/beeper_desktop_api/resources/chats/chats.py
@@ -0,0 +1,668 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+import httpx
+
+from ...types import chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from .reminders import (
+ RemindersResource,
+ AsyncRemindersResource,
+ RemindersResourceWithRawResponse,
+ AsyncRemindersResourceWithRawResponse,
+ RemindersResourceWithStreamingResponse,
+ AsyncRemindersResourceWithStreamingResponse,
+)
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncCursor, AsyncCursor
+from ...types.chat import Chat
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.chat_create_response import ChatCreateResponse
+from ...types.shared.base_response import BaseResponse
+
+__all__ = ["ChatsResource", "AsyncChatsResource"]
+
+
+class ChatsResource(SyncAPIResource):
+ """Chats operations"""
+
+ @cached_property
+ def reminders(self) -> RemindersResource:
+ """Reminders operations"""
+ return RemindersResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> ChatsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return ChatsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ChatsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return ChatsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ account_id: str,
+ participant_ids: SequenceNotStr[str],
+ type: Literal["single", "group"],
+ message_text: str | Omit = omit,
+ title: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ChatCreateResponse:
+ """
+ Create a single or group chat on a specific account using participant IDs and
+ optional title.
+
+ Args:
+ account_id: Account to create the chat on.
+
+ participant_ids: User IDs to include in the new chat.
+
+ type: Chat type to create: 'single' requires exactly one participantID; 'group'
+ supports multiple participants and optional title.
+
+ message_text: Optional first message content if the platform requires it to create the chat.
+
+ title: Optional title for group chats; ignored for single chats on most platforms.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._post(
+ "/v1/chats",
+ body=maybe_transform(
+ {
+ "account_id": account_id,
+ "participant_ids": participant_ids,
+ "type": type,
+ "message_text": message_text,
+ "title": title,
+ },
+ chat_create_params.ChatCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ChatCreateResponse,
+ )
+
+ def retrieve(
+ self,
+ chat_id: str,
+ *,
+ max_participant_count: Optional[int] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Chat:
+ """
+ Retrieve chat details including metadata, participants, and latest message
+
+ Args:
+ chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats.
+ Participants are limited by 'maxParticipantCount'.
+
+ max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500.
+ Defaults to 20.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return self._get(
+ f"/v1/chats/{chat_id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {"max_participant_count": max_participant_count}, chat_retrieve_params.ChatRetrieveParams
+ ),
+ ),
+ cast_to=Chat,
+ )
+
+ def archive(
+ self,
+ chat_id: str,
+ *,
+ archived: bool | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BaseResponse:
+ """Archive or unarchive a chat.
+
+ Set archived=true to move to archive,
+ archived=false to move back to inbox
+
+ Args:
+ chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and
+ local chat ID)
+
+ archived: True to archive, false to unarchive
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return self._post(
+ f"/v1/chats/{chat_id}/archive",
+ body=maybe_transform({"archived": archived}, chat_archive_params.ChatArchiveParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseResponse,
+ )
+
+ def search(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ inbox: Literal["primary", "low-priority", "archive"] | Omit = omit,
+ include_muted: Optional[bool] | Omit = omit,
+ last_activity_after: Union[str, datetime] | Omit = omit,
+ last_activity_before: Union[str, datetime] | Omit = omit,
+ limit: int | Omit = omit,
+ query: str | Omit = omit,
+ scope: Literal["titles", "participants"] | Omit = omit,
+ type: Literal["single", "group", "any"] | Omit = omit,
+ unread_only: Optional[bool] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncCursor[Chat]:
+ """
+ Search chats by title/network or participants using Beeper Desktop's renderer
+ algorithm.
+
+ Args:
+ account_ids: Provide an array of account IDs to filter chats from specific messaging accounts
+ only
+
+ cursor: Pagination cursor from previous response. Use with direction to navigate results
+
+ direction: Pagination direction: "after" for newer page, "before" for older page. Defaults
+ to "before" when only cursor is provided.
+
+ inbox: Filter by inbox type: "primary" (non-archived, non-low-priority),
+ "low-priority", or "archive". If not specified, shows all chats.
+
+ include_muted: Include chats marked as Muted by the user, which are usually less important.
+ Default: true. Set to false if the user wants a more refined search.
+
+ last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after
+ this time
+
+ last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before
+ this time
+
+ limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50
+
+ query: Literal token search (non-semantic). Use single words users type (e.g.,
+ "dinner"). When multiple words provided, ALL must match. Case-insensitive.
+
+ scope: Search scope: 'titles' matches title + network; 'participants' matches
+ participant names.
+
+ type: Specify the type of chats to retrieve: use "single" for direct messages, "group"
+ for group chats, or "any" to get all types
+
+ unread_only: Set to true to only retrieve chats that have unread messages
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/chats/search",
+ page=SyncCursor[Chat],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "cursor": cursor,
+ "direction": direction,
+ "inbox": inbox,
+ "include_muted": include_muted,
+ "last_activity_after": last_activity_after,
+ "last_activity_before": last_activity_before,
+ "limit": limit,
+ "query": query,
+ "scope": scope,
+ "type": type,
+ "unread_only": unread_only,
+ },
+ chat_search_params.ChatSearchParams,
+ ),
+ ),
+ model=Chat,
+ )
+
+
+class AsyncChatsResource(AsyncAPIResource):
+ """Chats operations"""
+
+ @cached_property
+ def reminders(self) -> AsyncRemindersResource:
+ """Reminders operations"""
+ return AsyncRemindersResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncChatsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncChatsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncChatsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AsyncChatsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ account_id: str,
+ participant_ids: SequenceNotStr[str],
+ type: Literal["single", "group"],
+ message_text: str | Omit = omit,
+ title: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ChatCreateResponse:
+ """
+ Create a single or group chat on a specific account using participant IDs and
+ optional title.
+
+ Args:
+ account_id: Account to create the chat on.
+
+ participant_ids: User IDs to include in the new chat.
+
+ type: Chat type to create: 'single' requires exactly one participantID; 'group'
+ supports multiple participants and optional title.
+
+ message_text: Optional first message content if the platform requires it to create the chat.
+
+ title: Optional title for group chats; ignored for single chats on most platforms.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._post(
+ "/v1/chats",
+ body=await async_maybe_transform(
+ {
+ "account_id": account_id,
+ "participant_ids": participant_ids,
+ "type": type,
+ "message_text": message_text,
+ "title": title,
+ },
+ chat_create_params.ChatCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ChatCreateResponse,
+ )
+
+ async def retrieve(
+ self,
+ chat_id: str,
+ *,
+ max_participant_count: Optional[int] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Chat:
+ """
+ Retrieve chat details including metadata, participants, and latest message
+
+ Args:
+ chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats.
+ Participants are limited by 'maxParticipantCount'.
+
+ max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500.
+ Defaults to 20.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return await self._get(
+ f"/v1/chats/{chat_id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"max_participant_count": max_participant_count}, chat_retrieve_params.ChatRetrieveParams
+ ),
+ ),
+ cast_to=Chat,
+ )
+
+ async def archive(
+ self,
+ chat_id: str,
+ *,
+ archived: bool | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BaseResponse:
+ """Archive or unarchive a chat.
+
+ Set archived=true to move to archive,
+ archived=false to move back to inbox
+
+ Args:
+ chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and
+ local chat ID)
+
+ archived: True to archive, false to unarchive
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return await self._post(
+ f"/v1/chats/{chat_id}/archive",
+ body=await async_maybe_transform({"archived": archived}, chat_archive_params.ChatArchiveParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseResponse,
+ )
+
+ def search(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ inbox: Literal["primary", "low-priority", "archive"] | Omit = omit,
+ include_muted: Optional[bool] | Omit = omit,
+ last_activity_after: Union[str, datetime] | Omit = omit,
+ last_activity_before: Union[str, datetime] | Omit = omit,
+ limit: int | Omit = omit,
+ query: str | Omit = omit,
+ scope: Literal["titles", "participants"] | Omit = omit,
+ type: Literal["single", "group", "any"] | Omit = omit,
+ unread_only: Optional[bool] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Chat, AsyncCursor[Chat]]:
+ """
+ Search chats by title/network or participants using Beeper Desktop's renderer
+ algorithm.
+
+ Args:
+ account_ids: Provide an array of account IDs to filter chats from specific messaging accounts
+ only
+
+ cursor: Pagination cursor from previous response. Use with direction to navigate results
+
+ direction: Pagination direction: "after" for newer page, "before" for older page. Defaults
+ to "before" when only cursor is provided.
+
+ inbox: Filter by inbox type: "primary" (non-archived, non-low-priority),
+ "low-priority", or "archive". If not specified, shows all chats.
+
+ include_muted: Include chats marked as Muted by the user, which are usually less important.
+ Default: true. Set to false if the user wants a more refined search.
+
+ last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after
+ this time
+
+ last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before
+ this time
+
+ limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50
+
+ query: Literal token search (non-semantic). Use single words users type (e.g.,
+ "dinner"). When multiple words provided, ALL must match. Case-insensitive.
+
+ scope: Search scope: 'titles' matches title + network; 'participants' matches
+ participant names.
+
+ type: Specify the type of chats to retrieve: use "single" for direct messages, "group"
+ for group chats, or "any" to get all types
+
+ unread_only: Set to true to only retrieve chats that have unread messages
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/chats/search",
+ page=AsyncCursor[Chat],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "cursor": cursor,
+ "direction": direction,
+ "inbox": inbox,
+ "include_muted": include_muted,
+ "last_activity_after": last_activity_after,
+ "last_activity_before": last_activity_before,
+ "limit": limit,
+ "query": query,
+ "scope": scope,
+ "type": type,
+ "unread_only": unread_only,
+ },
+ chat_search_params.ChatSearchParams,
+ ),
+ ),
+ model=Chat,
+ )
+
+
+class ChatsResourceWithRawResponse:
+ def __init__(self, chats: ChatsResource) -> None:
+ self._chats = chats
+
+ self.create = to_raw_response_wrapper(
+ chats.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ chats.retrieve,
+ )
+ self.archive = to_raw_response_wrapper(
+ chats.archive,
+ )
+ self.search = to_raw_response_wrapper(
+ chats.search,
+ )
+
+ @cached_property
+ def reminders(self) -> RemindersResourceWithRawResponse:
+ """Reminders operations"""
+ return RemindersResourceWithRawResponse(self._chats.reminders)
+
+
+class AsyncChatsResourceWithRawResponse:
+ def __init__(self, chats: AsyncChatsResource) -> None:
+ self._chats = chats
+
+ self.create = async_to_raw_response_wrapper(
+ chats.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ chats.retrieve,
+ )
+ self.archive = async_to_raw_response_wrapper(
+ chats.archive,
+ )
+ self.search = async_to_raw_response_wrapper(
+ chats.search,
+ )
+
+ @cached_property
+ def reminders(self) -> AsyncRemindersResourceWithRawResponse:
+ """Reminders operations"""
+ return AsyncRemindersResourceWithRawResponse(self._chats.reminders)
+
+
+class ChatsResourceWithStreamingResponse:
+ def __init__(self, chats: ChatsResource) -> None:
+ self._chats = chats
+
+ self.create = to_streamed_response_wrapper(
+ chats.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ chats.retrieve,
+ )
+ self.archive = to_streamed_response_wrapper(
+ chats.archive,
+ )
+ self.search = to_streamed_response_wrapper(
+ chats.search,
+ )
+
+ @cached_property
+ def reminders(self) -> RemindersResourceWithStreamingResponse:
+ """Reminders operations"""
+ return RemindersResourceWithStreamingResponse(self._chats.reminders)
+
+
+class AsyncChatsResourceWithStreamingResponse:
+ def __init__(self, chats: AsyncChatsResource) -> None:
+ self._chats = chats
+
+ self.create = async_to_streamed_response_wrapper(
+ chats.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ chats.retrieve,
+ )
+ self.archive = async_to_streamed_response_wrapper(
+ chats.archive,
+ )
+ self.search = async_to_streamed_response_wrapper(
+ chats.search,
+ )
+
+ @cached_property
+ def reminders(self) -> AsyncRemindersResourceWithStreamingResponse:
+ """Reminders operations"""
+ return AsyncRemindersResourceWithStreamingResponse(self._chats.reminders)
diff --git a/src/beeper_desktop_api/resources/chats/reminders.py b/src/beeper_desktop_api/resources/chats/reminders.py
new file mode 100644
index 0000000..e9da3b4
--- /dev/null
+++ b/src/beeper_desktop_api/resources/chats/reminders.py
@@ -0,0 +1,267 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Query, Headers, NotGiven, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...types.chats import reminder_create_params
+from ..._base_client import make_request_options
+from ...types.shared.base_response import BaseResponse
+
+__all__ = ["RemindersResource", "AsyncRemindersResource"]
+
+
+class RemindersResource(SyncAPIResource):
+ """Reminders operations"""
+
+ @cached_property
+ def with_raw_response(self) -> RemindersResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return RemindersResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> RemindersResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return RemindersResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ chat_id: str,
+ *,
+ reminder: reminder_create_params.Reminder,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BaseResponse:
+ """
+ Set a reminder for a chat at a specific time
+
+ Args:
+ chat_id: The identifier of the chat to set reminder for (accepts both chatID and local
+ chat ID)
+
+ reminder: Reminder configuration
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return self._post(
+ f"/v1/chats/{chat_id}/reminders",
+ body=maybe_transform({"reminder": reminder}, reminder_create_params.ReminderCreateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseResponse,
+ )
+
+ def delete(
+ self,
+ chat_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BaseResponse:
+ """
+ Clear an existing reminder from a chat
+
+ Args:
+ chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local
+ chat ID)
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return self._delete(
+ f"/v1/chats/{chat_id}/reminders",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseResponse,
+ )
+
+
+class AsyncRemindersResource(AsyncAPIResource):
+ """Reminders operations"""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncRemindersResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncRemindersResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncRemindersResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AsyncRemindersResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ chat_id: str,
+ *,
+ reminder: reminder_create_params.Reminder,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BaseResponse:
+ """
+ Set a reminder for a chat at a specific time
+
+ Args:
+ chat_id: The identifier of the chat to set reminder for (accepts both chatID and local
+ chat ID)
+
+ reminder: Reminder configuration
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return await self._post(
+ f"/v1/chats/{chat_id}/reminders",
+ body=await async_maybe_transform({"reminder": reminder}, reminder_create_params.ReminderCreateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseResponse,
+ )
+
+ async def delete(
+ self,
+ chat_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BaseResponse:
+ """
+ Clear an existing reminder from a chat
+
+ Args:
+ chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local
+ chat ID)
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return await self._delete(
+ f"/v1/chats/{chat_id}/reminders",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseResponse,
+ )
+
+
+class RemindersResourceWithRawResponse:
+ def __init__(self, reminders: RemindersResource) -> None:
+ self._reminders = reminders
+
+ self.create = to_raw_response_wrapper(
+ reminders.create,
+ )
+ self.delete = to_raw_response_wrapper(
+ reminders.delete,
+ )
+
+
+class AsyncRemindersResourceWithRawResponse:
+ def __init__(self, reminders: AsyncRemindersResource) -> None:
+ self._reminders = reminders
+
+ self.create = async_to_raw_response_wrapper(
+ reminders.create,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ reminders.delete,
+ )
+
+
+class RemindersResourceWithStreamingResponse:
+ def __init__(self, reminders: RemindersResource) -> None:
+ self._reminders = reminders
+
+ self.create = to_streamed_response_wrapper(
+ reminders.create,
+ )
+ self.delete = to_streamed_response_wrapper(
+ reminders.delete,
+ )
+
+
+class AsyncRemindersResourceWithStreamingResponse:
+ def __init__(self, reminders: AsyncRemindersResource) -> None:
+ self._reminders = reminders
+
+ self.create = async_to_streamed_response_wrapper(
+ reminders.create,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ reminders.delete,
+ )
diff --git a/src/beeper_desktop_api/resources/contacts.py b/src/beeper_desktop_api/resources/contacts.py
new file mode 100644
index 0000000..db84950
--- /dev/null
+++ b/src/beeper_desktop_api/resources/contacts.py
@@ -0,0 +1,197 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..types import contact_search_params
+from .._types import Body, Query, Headers, NotGiven, not_given
+from .._utils import maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from .._base_client import make_request_options
+from ..types.contact_search_response import ContactSearchResponse
+
+__all__ = ["ContactsResource", "AsyncContactsResource"]
+
+
+class ContactsResource(SyncAPIResource):
+ """Contacts operations"""
+
+ @cached_property
+ def with_raw_response(self) -> ContactsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return ContactsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ContactsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return ContactsResourceWithStreamingResponse(self)
+
+ def search(
+ self,
+ *,
+ account_id: str,
+ query: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ContactSearchResponse:
+ """
+ Search contacts across on a specific account using the network's search API.
+ Only use for creating new chats.
+
+ Args:
+ account_id: Account ID this resource belongs to.
+
+ query: Text to search users by. Network-specific behavior.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get(
+ "/v1/contacts/search",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_id": account_id,
+ "query": query,
+ },
+ contact_search_params.ContactSearchParams,
+ ),
+ ),
+ cast_to=ContactSearchResponse,
+ )
+
+
+class AsyncContactsResource(AsyncAPIResource):
+ """Contacts operations"""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncContactsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncContactsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AsyncContactsResourceWithStreamingResponse(self)
+
+ async def search(
+ self,
+ *,
+ account_id: str,
+ query: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ContactSearchResponse:
+ """
+ Search contacts across on a specific account using the network's search API.
+ Only use for creating new chats.
+
+ Args:
+ account_id: Account ID this resource belongs to.
+
+ query: Text to search users by. Network-specific behavior.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._get(
+ "/v1/contacts/search",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "account_id": account_id,
+ "query": query,
+ },
+ contact_search_params.ContactSearchParams,
+ ),
+ ),
+ cast_to=ContactSearchResponse,
+ )
+
+
+class ContactsResourceWithRawResponse:
+ def __init__(self, contacts: ContactsResource) -> None:
+ self._contacts = contacts
+
+ self.search = to_raw_response_wrapper(
+ contacts.search,
+ )
+
+
+class AsyncContactsResourceWithRawResponse:
+ def __init__(self, contacts: AsyncContactsResource) -> None:
+ self._contacts = contacts
+
+ self.search = async_to_raw_response_wrapper(
+ contacts.search,
+ )
+
+
+class ContactsResourceWithStreamingResponse:
+ def __init__(self, contacts: ContactsResource) -> None:
+ self._contacts = contacts
+
+ self.search = to_streamed_response_wrapper(
+ contacts.search,
+ )
+
+
+class AsyncContactsResourceWithStreamingResponse:
+ def __init__(self, contacts: AsyncContactsResource) -> None:
+ self._contacts = contacts
+
+ self.search = async_to_streamed_response_wrapper(
+ contacts.search,
+ )
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
new file mode 100644
index 0000000..ea1ea25
--- /dev/null
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -0,0 +1,423 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import message_send_params, message_search_params
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from .._utils import maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncCursor, AsyncCursor
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.shared.message import Message
+from ..types.message_send_response import MessageSendResponse
+
+__all__ = ["MessagesResource", "AsyncMessagesResource"]
+
+
+class MessagesResource(SyncAPIResource):
+ """Messages operations"""
+
+ @cached_property
+ def with_raw_response(self) -> MessagesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return MessagesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return MessagesResourceWithStreamingResponse(self)
+
+ def search(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ chat_ids: SequenceNotStr[str] | Omit = omit,
+ chat_type: Literal["group", "single"] | Omit = omit,
+ cursor: str | Omit = omit,
+ date_after: Union[str, datetime] | Omit = omit,
+ date_before: Union[str, datetime] | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ exclude_low_priority: Optional[bool] | Omit = omit,
+ include_muted: Optional[bool] | Omit = omit,
+ limit: int | Omit = omit,
+ media_types: List[Literal["any", "video", "image", "link", "file"]] | Omit = omit,
+ query: str | Omit = omit,
+ sender: Union[Literal["me", "others"], str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncCursor[Message]:
+ """
+ Search messages across chats using Beeper's message index
+
+ Args:
+ account_ids: Limit search to specific account IDs.
+
+ chat_ids: Limit search to specific chat IDs.
+
+ chat_type: Filter by chat type: 'group' for group chats, 'single' for 1:1 chats.
+
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
+
+ date_after: Only include messages with timestamp strictly after this ISO 8601 datetime
+ (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00').
+
+ date_before: Only include messages with timestamp strictly before this ISO 8601 datetime
+ (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00').
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+
+ exclude_low_priority: Exclude messages marked Low Priority by the user. Default: true. Set to false to
+ include all.
+
+ include_muted: Include messages in chats marked as Muted by the user, which are usually less
+ important. Default: true. Set to false if the user wants a more refined search.
+
+ limit: Maximum number of messages to return (1–500). Defaults to 20. The current
+ implementation caps each page at 20 items even if a higher limit is requested.
+
+ media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact
+ types like ['video', 'image']. Omit for no media filtering.
+
+ query: Literal word search (NOT semantic). Finds messages containing these EXACT words
+ in any order. Use single words users actually type, not concepts or phrases.
+ Example: use "dinner" not "dinner plans", use "sick" not "health issues". If
+ omitted, returns results filtered only by other parameters.
+
+ sender: Filter by sender: 'me' (messages sent by the authenticated user), 'others'
+ (messages sent by others), or a specific user ID string (user.id).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/messages/search",
+ page=SyncCursor[Message],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "chat_ids": chat_ids,
+ "chat_type": chat_type,
+ "cursor": cursor,
+ "date_after": date_after,
+ "date_before": date_before,
+ "direction": direction,
+ "exclude_low_priority": exclude_low_priority,
+ "include_muted": include_muted,
+ "limit": limit,
+ "media_types": media_types,
+ "query": query,
+ "sender": sender,
+ },
+ message_search_params.MessageSearchParams,
+ ),
+ ),
+ model=Message,
+ )
+
+ def send(
+ self,
+ *,
+ chat_id: str,
+ reply_to_message_id: str | Omit = omit,
+ text: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> MessageSendResponse:
+ """Send a text message to a specific chat.
+
+ Supports replying to existing messages.
+ Returns the sent message ID.
+
+ Args:
+ chat_id: Unique identifier of the chat.
+
+ reply_to_message_id: Provide a message ID to send this as a reply to an existing message
+
+ text: Text content of the message you want to send. You may use markdown.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._post(
+ "/v1/messages",
+ body=maybe_transform(
+ {
+ "chat_id": chat_id,
+ "reply_to_message_id": reply_to_message_id,
+ "text": text,
+ },
+ message_send_params.MessageSendParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MessageSendResponse,
+ )
+
+
+class AsyncMessagesResource(AsyncAPIResource):
+ """Messages operations"""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncMessagesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AsyncMessagesResourceWithStreamingResponse(self)
+
+ def search(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ chat_ids: SequenceNotStr[str] | Omit = omit,
+ chat_type: Literal["group", "single"] | Omit = omit,
+ cursor: str | Omit = omit,
+ date_after: Union[str, datetime] | Omit = omit,
+ date_before: Union[str, datetime] | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ exclude_low_priority: Optional[bool] | Omit = omit,
+ include_muted: Optional[bool] | Omit = omit,
+ limit: int | Omit = omit,
+ media_types: List[Literal["any", "video", "image", "link", "file"]] | Omit = omit,
+ query: str | Omit = omit,
+ sender: Union[Literal["me", "others"], str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Message, AsyncCursor[Message]]:
+ """
+ Search messages across chats using Beeper's message index
+
+ Args:
+ account_ids: Limit search to specific account IDs.
+
+ chat_ids: Limit search to specific chat IDs.
+
+ chat_type: Filter by chat type: 'group' for group chats, 'single' for 1:1 chats.
+
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
+
+ date_after: Only include messages with timestamp strictly after this ISO 8601 datetime
+ (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00').
+
+ date_before: Only include messages with timestamp strictly before this ISO 8601 datetime
+ (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00').
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+
+ exclude_low_priority: Exclude messages marked Low Priority by the user. Default: true. Set to false to
+ include all.
+
+ include_muted: Include messages in chats marked as Muted by the user, which are usually less
+ important. Default: true. Set to false if the user wants a more refined search.
+
+ limit: Maximum number of messages to return (1–500). Defaults to 20. The current
+ implementation caps each page at 20 items even if a higher limit is requested.
+
+ media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact
+ types like ['video', 'image']. Omit for no media filtering.
+
+ query: Literal word search (NOT semantic). Finds messages containing these EXACT words
+ in any order. Use single words users actually type, not concepts or phrases.
+ Example: use "dinner" not "dinner plans", use "sick" not "health issues". If
+ omitted, returns results filtered only by other parameters.
+
+ sender: Filter by sender: 'me' (messages sent by the authenticated user), 'others'
+ (messages sent by others), or a specific user ID string (user.id).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/messages/search",
+ page=AsyncCursor[Message],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "chat_ids": chat_ids,
+ "chat_type": chat_type,
+ "cursor": cursor,
+ "date_after": date_after,
+ "date_before": date_before,
+ "direction": direction,
+ "exclude_low_priority": exclude_low_priority,
+ "include_muted": include_muted,
+ "limit": limit,
+ "media_types": media_types,
+ "query": query,
+ "sender": sender,
+ },
+ message_search_params.MessageSearchParams,
+ ),
+ ),
+ model=Message,
+ )
+
+ async def send(
+ self,
+ *,
+ chat_id: str,
+ reply_to_message_id: str | Omit = omit,
+ text: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> MessageSendResponse:
+ """Send a text message to a specific chat.
+
+ Supports replying to existing messages.
+ Returns the sent message ID.
+
+ Args:
+ chat_id: Unique identifier of the chat.
+
+ reply_to_message_id: Provide a message ID to send this as a reply to an existing message
+
+ text: Text content of the message you want to send. You may use markdown.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._post(
+ "/v1/messages",
+ body=await async_maybe_transform(
+ {
+ "chat_id": chat_id,
+ "reply_to_message_id": reply_to_message_id,
+ "text": text,
+ },
+ message_send_params.MessageSendParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MessageSendResponse,
+ )
+
+
+class MessagesResourceWithRawResponse:
+ def __init__(self, messages: MessagesResource) -> None:
+ self._messages = messages
+
+ self.search = to_raw_response_wrapper(
+ messages.search,
+ )
+ self.send = to_raw_response_wrapper(
+ messages.send,
+ )
+
+
+class AsyncMessagesResourceWithRawResponse:
+ def __init__(self, messages: AsyncMessagesResource) -> None:
+ self._messages = messages
+
+ self.search = async_to_raw_response_wrapper(
+ messages.search,
+ )
+ self.send = async_to_raw_response_wrapper(
+ messages.send,
+ )
+
+
+class MessagesResourceWithStreamingResponse:
+ def __init__(self, messages: MessagesResource) -> None:
+ self._messages = messages
+
+ self.search = to_streamed_response_wrapper(
+ messages.search,
+ )
+ self.send = to_streamed_response_wrapper(
+ messages.send,
+ )
+
+
+class AsyncMessagesResourceWithStreamingResponse:
+ def __init__(self, messages: AsyncMessagesResource) -> None:
+ self._messages = messages
+
+ self.search = async_to_streamed_response_wrapper(
+ messages.search,
+ )
+ self.send = async_to_streamed_response_wrapper(
+ messages.send,
+ )
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index bb86833..5bede4c 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+from .chat import Chat as Chat
from .shared import (
User as User,
Error as Error,
@@ -10,4 +11,22 @@
Attachment as Attachment,
BaseResponse as BaseResponse,
)
+from .account import Account as Account
from .user_info import UserInfo as UserInfo
+from .open_response import OpenResponse as OpenResponse
+from .search_response import SearchResponse as SearchResponse
+from .chat_create_params import ChatCreateParams as ChatCreateParams
+from .chat_search_params import ChatSearchParams as ChatSearchParams
+from .client_open_params import ClientOpenParams as ClientOpenParams
+from .chat_archive_params import ChatArchiveParams as ChatArchiveParams
+from .message_send_params import MessageSendParams as MessageSendParams
+from .chat_create_response import ChatCreateResponse as ChatCreateResponse
+from .chat_retrieve_params import ChatRetrieveParams as ChatRetrieveParams
+from .client_search_params import ClientSearchParams as ClientSearchParams
+from .account_list_response import AccountListResponse as AccountListResponse
+from .contact_search_params import ContactSearchParams as ContactSearchParams
+from .message_search_params import MessageSearchParams as MessageSearchParams
+from .message_send_response import MessageSendResponse as MessageSendResponse
+from .contact_search_response import ContactSearchResponse as ContactSearchResponse
+from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
+from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
diff --git a/src/beeper_desktop_api/types/account.py b/src/beeper_desktop_api/types/account.py
new file mode 100644
index 0000000..97336b7
--- /dev/null
+++ b/src/beeper_desktop_api/types/account.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .shared.user import User
+
+__all__ = ["Account"]
+
+
+class Account(BaseModel):
+ account_id: str = FieldInfo(alias="accountID")
+ """Chat account added to Beeper. Use this to route account-scoped actions."""
+
+ network: str
+ """Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger')."""
+
+ user: User
+ """User the account belongs to."""
diff --git a/src/beeper_desktop_api/types/account_list_response.py b/src/beeper_desktop_api/types/account_list_response.py
new file mode 100644
index 0000000..8268843
--- /dev/null
+++ b/src/beeper_desktop_api/types/account_list_response.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import TypeAlias
+
+from .account import Account
+
+__all__ = ["AccountListResponse"]
+
+AccountListResponse: TypeAlias = List[Account]
diff --git a/src/beeper_desktop_api/types/chat.py b/src/beeper_desktop_api/types/chat.py
new file mode 100644
index 0000000..d580426
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat.py
@@ -0,0 +1,67 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .shared.user import User
+
+__all__ = ["Chat", "Participants"]
+
+
+class Participants(BaseModel):
+ has_more: bool = FieldInfo(alias="hasMore")
+ """True if there are more participants than included in items."""
+
+ items: List[User]
+ """Participants returned for this chat (limited by the request; may be a subset)."""
+
+ total: int
+ """Total number of participants in the chat."""
+
+
+class Chat(BaseModel):
+ id: str
+ """Unique identifier of the chat (room/thread ID, same as id) across Beeper."""
+
+ account_id: str = FieldInfo(alias="accountID")
+ """Beeper account ID this chat belongs to."""
+
+ network: str
+ """Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger')."""
+
+ participants: Participants
+ """Chat participants information."""
+
+ title: str
+ """Display title of the chat as computed by the client/server."""
+
+ type: Literal["single", "group"]
+ """Chat type: 'single' for direct messages, 'group' for group chats."""
+
+ unread_count: int = FieldInfo(alias="unreadCount")
+ """Number of unread messages."""
+
+ is_archived: Optional[bool] = FieldInfo(alias="isArchived", default=None)
+ """True if chat is archived."""
+
+ is_muted: Optional[bool] = FieldInfo(alias="isMuted", default=None)
+ """True if chat notifications are muted."""
+
+ is_pinned: Optional[bool] = FieldInfo(alias="isPinned", default=None)
+ """True if chat is pinned."""
+
+ last_activity: Optional[datetime] = FieldInfo(alias="lastActivity", default=None)
+ """Timestamp of last activity.
+
+ Chats with more recent activity are often more important.
+ """
+
+ last_read_message_sort_key: Union[int, str, None] = FieldInfo(alias="lastReadMessageSortKey", default=None)
+ """Last read message sortKey (hsOrder). Used to compute 'isUnread'."""
+
+ local_chat_id: Optional[str] = FieldInfo(alias="localChatID", default=None)
+ """Local chat ID specific to this Beeper Desktop installation."""
diff --git a/src/beeper_desktop_api/types/chat_archive_params.py b/src/beeper_desktop_api/types/chat_archive_params.py
new file mode 100644
index 0000000..38cc168
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_archive_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["ChatArchiveParams"]
+
+
+class ChatArchiveParams(TypedDict, total=False):
+ archived: bool
+ """True to archive, false to unarchive"""
diff --git a/src/beeper_desktop_api/types/chat_create_params.py b/src/beeper_desktop_api/types/chat_create_params.py
new file mode 100644
index 0000000..686bfaa
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_create_params.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["ChatCreateParams"]
+
+
+class ChatCreateParams(TypedDict, total=False):
+ account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]]
+ """Account to create the chat on."""
+
+ participant_ids: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="participantIDs")]]
+ """User IDs to include in the new chat."""
+
+ type: Required[Literal["single", "group"]]
+ """
+ Chat type to create: 'single' requires exactly one participantID; 'group'
+ supports multiple participants and optional title.
+ """
+
+ message_text: Annotated[str, PropertyInfo(alias="messageText")]
+ """Optional first message content if the platform requires it to create the chat."""
+
+ title: str
+ """Optional title for group chats; ignored for single chats on most platforms."""
diff --git a/src/beeper_desktop_api/types/chat_create_response.py b/src/beeper_desktop_api/types/chat_create_response.py
new file mode 100644
index 0000000..64b6981
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_create_response.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from pydantic import Field as FieldInfo
+
+from .shared.base_response import BaseResponse
+
+__all__ = ["ChatCreateResponse"]
+
+
+class ChatCreateResponse(BaseResponse):
+ chat_id: Optional[str] = FieldInfo(alias="chatID", default=None)
+ """Newly created chat if available."""
diff --git a/src/beeper_desktop_api/types/chat_retrieve_params.py b/src/beeper_desktop_api/types/chat_retrieve_params.py
new file mode 100644
index 0000000..ea22752
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_retrieve_params.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["ChatRetrieveParams"]
+
+
+class ChatRetrieveParams(TypedDict, total=False):
+ max_participant_count: Annotated[Optional[int], PropertyInfo(alias="maxParticipantCount")]
+ """Maximum number of participants to return.
+
+ Use -1 for all; otherwise 0–500. Defaults to 20.
+ """
diff --git a/src/beeper_desktop_api/types/chat_search_params.py b/src/beeper_desktop_api/types/chat_search_params.py
new file mode 100644
index 0000000..de94b8d
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_search_params.py
@@ -0,0 +1,81 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["ChatSearchParams"]
+
+
+class ChatSearchParams(TypedDict, total=False):
+ account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")]
+ """
+ Provide an array of account IDs to filter chats from specific messaging accounts
+ only
+ """
+
+ cursor: str
+ """Pagination cursor from previous response.
+
+ Use with direction to navigate results
+ """
+
+ direction: Literal["after", "before"]
+ """Pagination direction: "after" for newer page, "before" for older page.
+
+ Defaults to "before" when only cursor is provided.
+ """
+
+ inbox: Literal["primary", "low-priority", "archive"]
+ """
+ Filter by inbox type: "primary" (non-archived, non-low-priority),
+ "low-priority", or "archive". If not specified, shows all chats.
+ """
+
+ include_muted: Annotated[Optional[bool], PropertyInfo(alias="includeMuted")]
+ """Include chats marked as Muted by the user, which are usually less important.
+
+ Default: true. Set to false if the user wants a more refined search.
+ """
+
+ last_activity_after: Annotated[Union[str, datetime], PropertyInfo(alias="lastActivityAfter", format="iso8601")]
+ """
+ Provide an ISO datetime string to only retrieve chats with last activity after
+ this time
+ """
+
+ last_activity_before: Annotated[Union[str, datetime], PropertyInfo(alias="lastActivityBefore", format="iso8601")]
+ """
+ Provide an ISO datetime string to only retrieve chats with last activity before
+ this time
+ """
+
+ limit: int
+ """Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50"""
+
+ query: str
+ """Literal token search (non-semantic).
+
+ Use single words users type (e.g., "dinner"). When multiple words provided, ALL
+ must match. Case-insensitive.
+ """
+
+ scope: Literal["titles", "participants"]
+ """
+ Search scope: 'titles' matches title + network; 'participants' matches
+ participant names.
+ """
+
+ type: Literal["single", "group", "any"]
+ """
+ Specify the type of chats to retrieve: use "single" for direct messages, "group"
+ for group chats, or "any" to get all types
+ """
+
+ unread_only: Annotated[Optional[bool], PropertyInfo(alias="unreadOnly")]
+ """Set to true to only retrieve chats that have unread messages"""
diff --git a/src/beeper_desktop_api/types/chats/__init__.py b/src/beeper_desktop_api/types/chats/__init__.py
index f8ee8b1..848b361 100644
--- a/src/beeper_desktop_api/types/chats/__init__.py
+++ b/src/beeper_desktop_api/types/chats/__init__.py
@@ -1,3 +1,5 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
+
+from .reminder_create_params import ReminderCreateParams as ReminderCreateParams
diff --git a/src/beeper_desktop_api/types/chats/reminder_create_params.py b/src/beeper_desktop_api/types/chats/reminder_create_params.py
new file mode 100644
index 0000000..810263e
--- /dev/null
+++ b/src/beeper_desktop_api/types/chats/reminder_create_params.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["ReminderCreateParams", "Reminder"]
+
+
+class ReminderCreateParams(TypedDict, total=False):
+ reminder: Required[Reminder]
+ """Reminder configuration"""
+
+
+class Reminder(TypedDict, total=False):
+ remind_at_ms: Required[Annotated[float, PropertyInfo(alias="remindAtMs")]]
+ """Unix timestamp in milliseconds when reminder should trigger"""
+
+ dismiss_on_incoming_message: Annotated[bool, PropertyInfo(alias="dismissOnIncomingMessage")]
+ """Cancel reminder if someone messages in the chat"""
diff --git a/src/beeper_desktop_api/types/client_download_asset_params.py b/src/beeper_desktop_api/types/client_download_asset_params.py
new file mode 100644
index 0000000..fe824e0
--- /dev/null
+++ b/src/beeper_desktop_api/types/client_download_asset_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["ClientDownloadAssetParams"]
+
+
+class ClientDownloadAssetParams(TypedDict, total=False):
+ url: Required[str]
+ """Matrix content URL (mxc:// or localmxc://) for the asset to download."""
diff --git a/src/beeper_desktop_api/types/client_open_params.py b/src/beeper_desktop_api/types/client_open_params.py
new file mode 100644
index 0000000..84dea5f
--- /dev/null
+++ b/src/beeper_desktop_api/types/client_open_params.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["ClientOpenParams"]
+
+
+class ClientOpenParams(TypedDict, total=False):
+ chat_id: Annotated[str, PropertyInfo(alias="chatID")]
+ """Optional Beeper chat ID (or local chat ID) to focus after opening the app.
+
+ If omitted, only opens/focuses the app.
+ """
+
+ draft_attachment_path: Annotated[str, PropertyInfo(alias="draftAttachmentPath")]
+ """Optional draft attachment path to populate in the message input field."""
+
+ draft_text: Annotated[str, PropertyInfo(alias="draftText")]
+ """Optional draft text to populate in the message input field."""
+
+ message_id: Annotated[str, PropertyInfo(alias="messageID")]
+ """Optional message ID. Jumps to that message in the chat when opening."""
diff --git a/src/beeper_desktop_api/types/client_search_params.py b/src/beeper_desktop_api/types/client_search_params.py
new file mode 100644
index 0000000..06d58e4
--- /dev/null
+++ b/src/beeper_desktop_api/types/client_search_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["ClientSearchParams"]
+
+
+class ClientSearchParams(TypedDict, total=False):
+ query: Required[str]
+ """User-typed search text. Literal word matching (NOT semantic)."""
diff --git a/src/beeper_desktop_api/types/contact_search_params.py b/src/beeper_desktop_api/types/contact_search_params.py
new file mode 100644
index 0000000..53d052f
--- /dev/null
+++ b/src/beeper_desktop_api/types/contact_search_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["ContactSearchParams"]
+
+
+class ContactSearchParams(TypedDict, total=False):
+ account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]]
+ """Account ID this resource belongs to."""
+
+ query: Required[str]
+ """Text to search users by. Network-specific behavior."""
diff --git a/src/beeper_desktop_api/types/contact_search_response.py b/src/beeper_desktop_api/types/contact_search_response.py
new file mode 100644
index 0000000..71c609e
--- /dev/null
+++ b/src/beeper_desktop_api/types/contact_search_response.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .._models import BaseModel
+from .shared.user import User
+
+__all__ = ["ContactSearchResponse"]
+
+
+class ContactSearchResponse(BaseModel):
+ items: List[User]
diff --git a/src/beeper_desktop_api/types/download_asset_response.py b/src/beeper_desktop_api/types/download_asset_response.py
new file mode 100644
index 0000000..47bc22e
--- /dev/null
+++ b/src/beeper_desktop_api/types/download_asset_response.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+
+__all__ = ["DownloadAssetResponse"]
+
+
+class DownloadAssetResponse(BaseModel):
+ error: Optional[str] = None
+ """Error message if the download failed."""
+
+ src_url: Optional[str] = FieldInfo(alias="srcURL", default=None)
+ """Local file URL to the downloaded asset."""
diff --git a/src/beeper_desktop_api/types/message_search_params.py b/src/beeper_desktop_api/types/message_search_params.py
new file mode 100644
index 0000000..650775f
--- /dev/null
+++ b/src/beeper_desktop_api/types/message_search_params.py
@@ -0,0 +1,85 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["MessageSearchParams"]
+
+
+class MessageSearchParams(TypedDict, total=False):
+ account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")]
+ """Limit search to specific account IDs."""
+
+ chat_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="chatIDs")]
+ """Limit search to specific chat IDs."""
+
+ chat_type: Annotated[Literal["group", "single"], PropertyInfo(alias="chatType")]
+ """Filter by chat type: 'group' for group chats, 'single' for 1:1 chats."""
+
+ cursor: str
+ """Opaque pagination cursor; do not inspect. Use together with 'direction'."""
+
+ date_after: Annotated[Union[str, datetime], PropertyInfo(alias="dateAfter", format="iso8601")]
+ """
+ Only include messages with timestamp strictly after this ISO 8601 datetime
+ (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00').
+ """
+
+ date_before: Annotated[Union[str, datetime], PropertyInfo(alias="dateBefore", format="iso8601")]
+ """
+ Only include messages with timestamp strictly before this ISO 8601 datetime
+ (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00').
+ """
+
+ direction: Literal["after", "before"]
+ """
+ Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+ """
+
+ exclude_low_priority: Annotated[Optional[bool], PropertyInfo(alias="excludeLowPriority")]
+ """Exclude messages marked Low Priority by the user.
+
+ Default: true. Set to false to include all.
+ """
+
+ include_muted: Annotated[Optional[bool], PropertyInfo(alias="includeMuted")]
+ """
+ Include messages in chats marked as Muted by the user, which are usually less
+ important. Default: true. Set to false if the user wants a more refined search.
+ """
+
+ limit: int
+ """Maximum number of messages to return (1–500).
+
+ Defaults to 20. The current implementation caps each page at 20 items even if a
+ higher limit is requested.
+ """
+
+ media_types: Annotated[List[Literal["any", "video", "image", "link", "file"]], PropertyInfo(alias="mediaTypes")]
+ """Filter messages by media types.
+
+ Use ['any'] for any media type, or specify exact types like ['video', 'image'].
+ Omit for no media filtering.
+ """
+
+ query: str
+ """Literal word search (NOT semantic).
+
+ Finds messages containing these EXACT words in any order. Use single words users
+ actually type, not concepts or phrases. Example: use "dinner" not "dinner
+ plans", use "sick" not "health issues". If omitted, returns results filtered
+ only by other parameters.
+ """
+
+ sender: Union[Literal["me", "others"], str]
+ """
+ Filter by sender: 'me' (messages sent by the authenticated user), 'others'
+ (messages sent by others), or a specific user ID string (user.id).
+ """
diff --git a/src/beeper_desktop_api/types/message_send_params.py b/src/beeper_desktop_api/types/message_send_params.py
new file mode 100644
index 0000000..8b05d6a
--- /dev/null
+++ b/src/beeper_desktop_api/types/message_send_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["MessageSendParams"]
+
+
+class MessageSendParams(TypedDict, total=False):
+ chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]]
+ """Unique identifier of the chat."""
+
+ reply_to_message_id: Annotated[str, PropertyInfo(alias="replyToMessageID")]
+ """Provide a message ID to send this as a reply to an existing message"""
+
+ text: str
+ """Text content of the message you want to send. You may use markdown."""
diff --git a/src/beeper_desktop_api/types/message_send_response.py b/src/beeper_desktop_api/types/message_send_response.py
new file mode 100644
index 0000000..05cc535
--- /dev/null
+++ b/src/beeper_desktop_api/types/message_send_response.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from pydantic import Field as FieldInfo
+
+from .shared.base_response import BaseResponse
+
+__all__ = ["MessageSendResponse"]
+
+
+class MessageSendResponse(BaseResponse):
+ chat_id: str = FieldInfo(alias="chatID")
+ """Unique identifier of the chat."""
+
+ pending_message_id: str = FieldInfo(alias="pendingMessageID")
+ """Pending message ID"""
diff --git a/src/beeper_desktop_api/types/open_response.py b/src/beeper_desktop_api/types/open_response.py
new file mode 100644
index 0000000..970f2ba
--- /dev/null
+++ b/src/beeper_desktop_api/types/open_response.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["OpenResponse"]
+
+
+class OpenResponse(BaseModel):
+ success: bool
+ """Whether the app was successfully opened/focused."""
diff --git a/src/beeper_desktop_api/types/search_response.py b/src/beeper_desktop_api/types/search_response.py
new file mode 100644
index 0000000..fe5113c
--- /dev/null
+++ b/src/beeper_desktop_api/types/search_response.py
@@ -0,0 +1,48 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Optional
+
+from pydantic import Field as FieldInfo
+
+from .chat import Chat
+from .._models import BaseModel
+from .shared.message import Message
+
+__all__ = ["SearchResponse", "Results", "ResultsMessages"]
+
+
+class ResultsMessages(BaseModel):
+ chats: Dict[str, Chat]
+ """Map of chatID -> chat details for chats referenced in items."""
+
+ has_more: bool = FieldInfo(alias="hasMore")
+ """True if additional results can be fetched using the provided cursors."""
+
+ items: List[Message]
+ """Messages matching the query and filters."""
+
+ newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
+ """Cursor for fetching newer results (use with direction='after').
+
+ Opaque string; do not inspect.
+ """
+
+ oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
+ """Cursor for fetching older results (use with direction='before').
+
+ Opaque string; do not inspect.
+ """
+
+
+class Results(BaseModel):
+ chats: List[Chat]
+ """Top chat results."""
+
+ in_groups: List[Chat]
+ """Top group results by participant matches."""
+
+ messages: ResultsMessages
+
+
+class SearchResponse(BaseModel):
+ results: Results
diff --git a/src/beeper_desktop_api/types/shared/error.py b/src/beeper_desktop_api/types/shared/error.py
index 1f82efd..e5b5a77 100644
--- a/src/beeper_desktop_api/types/shared/error.py
+++ b/src/beeper_desktop_api/types/shared/error.py
@@ -1,18 +1,10 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
-
from ..._models import BaseModel
__all__ = ["Error"]
class Error(BaseModel):
- error: str
- """Error message"""
-
- code: Optional[str] = None
- """Error code"""
-
- details: Optional[Dict[str, str]] = None
- """Additional error details"""
+ error: Error
+ """Error details"""
diff --git a/tests/api_resources/chats/__init__.py b/tests/api_resources/chats/__init__.py
new file mode 100644
index 0000000..fd8019a
--- /dev/null
+++ b/tests/api_resources/chats/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/chats/test_reminders.py b/tests/api_resources/chats/test_reminders.py
new file mode 100644
index 0000000..fea1bcb
--- /dev/null
+++ b/tests/api_resources/chats/test_reminders.py
@@ -0,0 +1,206 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types.shared import BaseResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestReminders:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: BeeperDesktop) -> None:
+ reminder = client.chats.reminders.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+ )
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: BeeperDesktop) -> None:
+ reminder = client.chats.reminders.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={
+ "remind_at_ms": 0,
+ "dismiss_on_incoming_message": True,
+ },
+ )
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: BeeperDesktop) -> None:
+ response = client.chats.reminders.with_raw_response.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ reminder = response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: BeeperDesktop) -> None:
+ with client.chats.reminders.with_streaming_response.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ reminder = response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_create(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ client.chats.reminders.with_raw_response.create(
+ chat_id="",
+ reminder={"remind_at_ms": 0},
+ )
+
+ @parametrize
+ def test_method_delete(self, client: BeeperDesktop) -> None:
+ reminder = client.chats.reminders.delete(
+ "!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ def test_raw_response_delete(self, client: BeeperDesktop) -> None:
+ response = client.chats.reminders.with_raw_response.delete(
+ "!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ reminder = response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ def test_streaming_response_delete(self, client: BeeperDesktop) -> None:
+ with client.chats.reminders.with_streaming_response.delete(
+ "!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ reminder = response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_delete(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ client.chats.reminders.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncReminders:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncBeeperDesktop) -> None:
+ reminder = await async_client.chats.reminders.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+ )
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ reminder = await async_client.chats.reminders.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={
+ "remind_at_ms": 0,
+ "dismiss_on_incoming_message": True,
+ },
+ )
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.reminders.with_raw_response.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ reminder = await response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.reminders.with_streaming_response.create(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reminder={"remind_at_ms": 0},
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ reminder = await response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ await async_client.chats.reminders.with_raw_response.create(
+ chat_id="",
+ reminder={"remind_at_ms": 0},
+ )
+
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncBeeperDesktop) -> None:
+ reminder = await async_client.chats.reminders.delete(
+ "!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.reminders.with_raw_response.delete(
+ "!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ reminder = await response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.reminders.with_streaming_response.delete(
+ "!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ reminder = await response.parse()
+ assert_matches_type(BaseResponse, reminder, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ await async_client.chats.reminders.with_raw_response.delete(
+ "",
+ )
diff --git a/tests/api_resources/test_accounts.py b/tests/api_resources/test_accounts.py
new file mode 100644
index 0000000..46ac702
--- /dev/null
+++ b/tests/api_resources/test_accounts.py
@@ -0,0 +1,74 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types import AccountListResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestAccounts:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_list(self, client: BeeperDesktop) -> None:
+ account = client.accounts.list()
+ assert_matches_type(AccountListResponse, account, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: BeeperDesktop) -> None:
+ response = client.accounts.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ account = response.parse()
+ assert_matches_type(AccountListResponse, account, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
+ with client.accounts.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ account = response.parse()
+ assert_matches_type(AccountListResponse, account, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncAccounts:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
+ account = await async_client.accounts.list()
+ assert_matches_type(AccountListResponse, account, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.accounts.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ account = await response.parse()
+ assert_matches_type(AccountListResponse, account, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.accounts.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ account = await response.parse()
+ assert_matches_type(AccountListResponse, account, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py
new file mode 100644
index 0000000..3eba100
--- /dev/null
+++ b/tests/api_resources/test_chats.py
@@ -0,0 +1,402 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types import (
+ Chat,
+ ChatCreateResponse,
+)
+from beeper_desktop_api._utils import parse_datetime
+from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
+from beeper_desktop_api.types.shared import BaseResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestChats:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: BeeperDesktop) -> None:
+ chat = client.chats.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ )
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: BeeperDesktop) -> None:
+ chat = client.chats.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ message_text="messageText",
+ title="title",
+ )
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: BeeperDesktop) -> None:
+ response = client.chats.with_raw_response.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = response.parse()
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: BeeperDesktop) -> None:
+ with client.chats.with_streaming_response.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = response.parse()
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_retrieve(self, client: BeeperDesktop) -> None:
+ chat = client.chats.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(Chat, chat, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: BeeperDesktop) -> None:
+ chat = client.chats.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ max_participant_count=50,
+ )
+ assert_matches_type(Chat, chat, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: BeeperDesktop) -> None:
+ response = client.chats.with_raw_response.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = response.parse()
+ assert_matches_type(Chat, chat, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: BeeperDesktop) -> None:
+ with client.chats.with_streaming_response.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = response.parse()
+ assert_matches_type(Chat, chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ client.chats.with_raw_response.retrieve(
+ chat_id="",
+ )
+
+ @parametrize
+ def test_method_archive(self, client: BeeperDesktop) -> None:
+ chat = client.chats.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ @parametrize
+ def test_method_archive_with_all_params(self, client: BeeperDesktop) -> None:
+ chat = client.chats.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ archived=True,
+ )
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ @parametrize
+ def test_raw_response_archive(self, client: BeeperDesktop) -> None:
+ response = client.chats.with_raw_response.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = response.parse()
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ @parametrize
+ def test_streaming_response_archive(self, client: BeeperDesktop) -> None:
+ with client.chats.with_streaming_response.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = response.parse()
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_archive(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ client.chats.with_raw_response.archive(
+ chat_id="",
+ )
+
+ @parametrize
+ def test_method_search(self, client: BeeperDesktop) -> None:
+ chat = client.chats.search()
+ assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+
+ @parametrize
+ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
+ chat = client.chats.search(
+ account_ids=[
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI",
+ ],
+ cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9",
+ direction="after",
+ inbox="primary",
+ include_muted=True,
+ last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"),
+ last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"),
+ limit=1,
+ query="x",
+ scope="titles",
+ type="single",
+ unread_only=True,
+ )
+ assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+
+ @parametrize
+ def test_raw_response_search(self, client: BeeperDesktop) -> None:
+ response = client.chats.with_raw_response.search()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = response.parse()
+ assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+
+ @parametrize
+ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
+ with client.chats.with_streaming_response.search() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = response.parse()
+ assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncChats:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ )
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ message_text="messageText",
+ title="title",
+ )
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.with_raw_response.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = await response.parse()
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.with_streaming_response.create(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ participant_ids=["string"],
+ type="single",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = await response.parse()
+ assert_matches_type(ChatCreateResponse, chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(Chat, chat, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ max_participant_count=50,
+ )
+ assert_matches_type(Chat, chat, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.with_raw_response.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = await response.parse()
+ assert_matches_type(Chat, chat, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.with_streaming_response.retrieve(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = await response.parse()
+ assert_matches_type(Chat, chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ await async_client.chats.with_raw_response.retrieve(
+ chat_id="",
+ )
+
+ @parametrize
+ async def test_method_archive(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ @parametrize
+ async def test_method_archive_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ archived=True,
+ )
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ @parametrize
+ async def test_raw_response_archive(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.with_raw_response.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = await response.parse()
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_archive(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.with_streaming_response.archive(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = await response.parse()
+ assert_matches_type(BaseResponse, chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_archive(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ await async_client.chats.with_raw_response.archive(
+ chat_id="",
+ )
+
+ @parametrize
+ async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.search()
+ assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+
+ @parametrize
+ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.search(
+ account_ids=[
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI",
+ ],
+ cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9",
+ direction="after",
+ inbox="primary",
+ include_muted=True,
+ last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"),
+ last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"),
+ limit=1,
+ query="x",
+ scope="titles",
+ type="single",
+ unread_only=True,
+ )
+ assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+
+ @parametrize
+ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.with_raw_response.search()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = await response.parse()
+ assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.with_streaming_response.search() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = await response.parse()
+ assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py
new file mode 100644
index 0000000..d5de032
--- /dev/null
+++ b/tests/api_resources/test_client.py
@@ -0,0 +1,222 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types import (
+ OpenResponse,
+ SearchResponse,
+ DownloadAssetResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestClient:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_download_asset(self, client: BeeperDesktop) -> None:
+ client_ = client.download_asset(
+ url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
+ )
+ assert_matches_type(DownloadAssetResponse, client_, path=["response"])
+
+ @parametrize
+ def test_raw_response_download_asset(self, client: BeeperDesktop) -> None:
+ response = client.with_raw_response.download_asset(
+ url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client_ = response.parse()
+ assert_matches_type(DownloadAssetResponse, client_, path=["response"])
+
+ @parametrize
+ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None:
+ with client.with_streaming_response.download_asset(
+ url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client_ = response.parse()
+ assert_matches_type(DownloadAssetResponse, client_, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_open(self, client: BeeperDesktop) -> None:
+ client_ = client.open()
+ assert_matches_type(OpenResponse, client_, path=["response"])
+
+ @parametrize
+ def test_method_open_with_all_params(self, client: BeeperDesktop) -> None:
+ client_ = client.open(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ draft_attachment_path="draftAttachmentPath",
+ draft_text="draftText",
+ message_id="messageID",
+ )
+ assert_matches_type(OpenResponse, client_, path=["response"])
+
+ @parametrize
+ def test_raw_response_open(self, client: BeeperDesktop) -> None:
+ response = client.with_raw_response.open()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client_ = response.parse()
+ assert_matches_type(OpenResponse, client_, path=["response"])
+
+ @parametrize
+ def test_streaming_response_open(self, client: BeeperDesktop) -> None:
+ with client.with_streaming_response.open() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client_ = response.parse()
+ assert_matches_type(OpenResponse, client_, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_search(self, client: BeeperDesktop) -> None:
+ client_ = client.search(
+ query="x",
+ )
+ assert_matches_type(SearchResponse, client_, path=["response"])
+
+ @parametrize
+ def test_raw_response_search(self, client: BeeperDesktop) -> None:
+ response = client.with_raw_response.search(
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client_ = response.parse()
+ assert_matches_type(SearchResponse, client_, path=["response"])
+
+ @parametrize
+ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
+ with client.with_streaming_response.search(
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client_ = response.parse()
+ assert_matches_type(SearchResponse, client_, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncClient:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_download_asset(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.download_asset(
+ url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
+ )
+ assert_matches_type(DownloadAssetResponse, client, path=["response"])
+
+ @parametrize
+ async def test_raw_response_download_asset(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.with_raw_response.download_asset(
+ url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client = await response.parse()
+ assert_matches_type(DownloadAssetResponse, client, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_download_asset(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.with_streaming_response.download_asset(
+ url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client = await response.parse()
+ assert_matches_type(DownloadAssetResponse, client, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.open()
+ assert_matches_type(OpenResponse, client, path=["response"])
+
+ @parametrize
+ async def test_method_open_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.open(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ draft_attachment_path="draftAttachmentPath",
+ draft_text="draftText",
+ message_id="messageID",
+ )
+ assert_matches_type(OpenResponse, client, path=["response"])
+
+ @parametrize
+ async def test_raw_response_open(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.with_raw_response.open()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client = await response.parse()
+ assert_matches_type(OpenResponse, client, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_open(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.with_streaming_response.open() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client = await response.parse()
+ assert_matches_type(OpenResponse, client, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.search(
+ query="x",
+ )
+ assert_matches_type(SearchResponse, client, path=["response"])
+
+ @parametrize
+ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.with_raw_response.search(
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client = await response.parse()
+ assert_matches_type(SearchResponse, client, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.with_streaming_response.search(
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client = await response.parse()
+ assert_matches_type(SearchResponse, client, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py
new file mode 100644
index 0000000..6308d1f
--- /dev/null
+++ b/tests/api_resources/test_contacts.py
@@ -0,0 +1,92 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types import ContactSearchResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestContacts:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_search(self, client: BeeperDesktop) -> None:
+ contact = client.contacts.search(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+ assert_matches_type(ContactSearchResponse, contact, path=["response"])
+
+ @parametrize
+ def test_raw_response_search(self, client: BeeperDesktop) -> None:
+ response = client.contacts.with_raw_response.search(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ contact = response.parse()
+ assert_matches_type(ContactSearchResponse, contact, path=["response"])
+
+ @parametrize
+ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
+ with client.contacts.with_streaming_response.search(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ contact = response.parse()
+ assert_matches_type(ContactSearchResponse, contact, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncContacts:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
+ contact = await async_client.contacts.search(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+ assert_matches_type(ContactSearchResponse, contact, path=["response"])
+
+ @parametrize
+ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.contacts.with_raw_response.search(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ contact = await response.parse()
+ assert_matches_type(ContactSearchResponse, contact, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.contacts.with_streaming_response.search(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ contact = await response.parse()
+ assert_matches_type(ContactSearchResponse, contact, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
new file mode 100644
index 0000000..eb5fc6e
--- /dev/null
+++ b/tests/api_resources/test_messages.py
@@ -0,0 +1,203 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types import MessageSendResponse
+from beeper_desktop_api._utils import parse_datetime
+from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
+from beeper_desktop_api.types.shared import Message
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestMessages:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_search(self, client: BeeperDesktop) -> None:
+ message = client.messages.search()
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
+ message = client.messages.search(
+ account_ids=[
+ "whatsapp",
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU",
+ ],
+ chat_ids=["!NCdzlIaMjZUmvmvyHU:beeper.com", "1231073"],
+ chat_type="group",
+ cursor="1725489123456|c29tZUltc2dQYWdl",
+ date_after=parse_datetime("2025-08-01T00:00:00Z"),
+ date_before=parse_datetime("2025-08-31T23:59:59Z"),
+ direction="before",
+ exclude_low_priority=True,
+ include_muted=True,
+ limit=20,
+ media_types=["any"],
+ query="dinner",
+ sender="me",
+ )
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ def test_raw_response_search(self, client: BeeperDesktop) -> None:
+ response = client.messages.with_raw_response.search()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ message = response.parse()
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
+ with client.messages.with_streaming_response.search() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ message = response.parse()
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_send(self, client: BeeperDesktop) -> None:
+ message = client.messages.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ @parametrize
+ def test_method_send_with_all_params(self, client: BeeperDesktop) -> None:
+ message = client.messages.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reply_to_message_id="replyToMessageID",
+ text="text",
+ )
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ @parametrize
+ def test_raw_response_send(self, client: BeeperDesktop) -> None:
+ response = client.messages.with_raw_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ message = response.parse()
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ @parametrize
+ def test_streaming_response_send(self, client: BeeperDesktop) -> None:
+ with client.messages.with_streaming_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ message = response.parse()
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncMessages:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
+ message = await async_client.messages.search()
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ message = await async_client.messages.search(
+ account_ids=[
+ "whatsapp",
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU",
+ ],
+ chat_ids=["!NCdzlIaMjZUmvmvyHU:beeper.com", "1231073"],
+ chat_type="group",
+ cursor="1725489123456|c29tZUltc2dQYWdl",
+ date_after=parse_datetime("2025-08-01T00:00:00Z"),
+ date_before=parse_datetime("2025-08-31T23:59:59Z"),
+ direction="before",
+ exclude_low_priority=True,
+ include_muted=True,
+ limit=20,
+ media_types=["any"],
+ query="dinner",
+ sender="me",
+ )
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.messages.with_raw_response.search()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ message = await response.parse()
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.messages.with_streaming_response.search() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ message = await response.parse()
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_send(self, async_client: AsyncBeeperDesktop) -> None:
+ message = await async_client.messages.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ @parametrize
+ async def test_method_send_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ message = await async_client.messages.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ reply_to_message_id="replyToMessageID",
+ text="text",
+ )
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ @parametrize
+ async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.messages.with_raw_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ message = await response.parse()
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.messages.with_streaming_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ message = await response.parse()
+ assert_matches_type(MessageSendResponse, message, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/test_client.py b/tests/test_client.py
index 1450af1..c9e3e9e 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -747,20 +747,20 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str
@mock.patch("beeper_desktop_api._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: BeeperDesktop) -> None:
- respx_mock.get("/oauth/userinfo").mock(side_effect=httpx.TimeoutException("Test timeout error"))
+ respx_mock.get("/v1/accounts").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- client.token.with_streaming_response.info().__enter__()
+ client.accounts.with_streaming_response.list().__enter__()
assert _get_open_connections(self.client) == 0
@mock.patch("beeper_desktop_api._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: BeeperDesktop) -> None:
- respx_mock.get("/oauth/userinfo").mock(return_value=httpx.Response(500))
+ respx_mock.get("/v1/accounts").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- client.token.with_streaming_response.info().__enter__()
+ client.accounts.with_streaming_response.list().__enter__()
assert _get_open_connections(self.client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@@ -787,9 +787,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
return httpx.Response(200)
- respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler)
+ respx_mock.get("/v1/accounts").mock(side_effect=retry_handler)
- response = client.token.with_raw_response.info()
+ response = client.accounts.with_raw_response.list()
assert response.retries_taken == failures_before_success
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
@@ -811,9 +811,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
return httpx.Response(200)
- respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler)
+ respx_mock.get("/v1/accounts").mock(side_effect=retry_handler)
- response = client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": Omit()})
+ response = client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": Omit()})
assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
@@ -834,9 +834,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
return httpx.Response(200)
- respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler)
+ respx_mock.get("/v1/accounts").mock(side_effect=retry_handler)
- response = client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": "42"})
+ response = client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": "42"})
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
@@ -1584,10 +1584,10 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte
async def test_retrying_timeout_errors_doesnt_leak(
self, respx_mock: MockRouter, async_client: AsyncBeeperDesktop
) -> None:
- respx_mock.get("/oauth/userinfo").mock(side_effect=httpx.TimeoutException("Test timeout error"))
+ respx_mock.get("/v1/accounts").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- await async_client.token.with_streaming_response.info().__aenter__()
+ await async_client.accounts.with_streaming_response.list().__aenter__()
assert _get_open_connections(self.client) == 0
@@ -1596,10 +1596,10 @@ async def test_retrying_timeout_errors_doesnt_leak(
async def test_retrying_status_errors_doesnt_leak(
self, respx_mock: MockRouter, async_client: AsyncBeeperDesktop
) -> None:
- respx_mock.get("/oauth/userinfo").mock(return_value=httpx.Response(500))
+ respx_mock.get("/v1/accounts").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- await async_client.token.with_streaming_response.info().__aenter__()
+ await async_client.accounts.with_streaming_response.list().__aenter__()
assert _get_open_connections(self.client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@@ -1627,9 +1627,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
return httpx.Response(200)
- respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler)
+ respx_mock.get("/v1/accounts").mock(side_effect=retry_handler)
- response = await client.token.with_raw_response.info()
+ response = await client.accounts.with_raw_response.list()
assert response.retries_taken == failures_before_success
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
@@ -1652,9 +1652,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
return httpx.Response(200)
- respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler)
+ respx_mock.get("/v1/accounts").mock(side_effect=retry_handler)
- response = await client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": Omit()})
+ response = await client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": Omit()})
assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
@@ -1676,9 +1676,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
return httpx.Response(200)
- respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler)
+ respx_mock.get("/v1/accounts").mock(side_effect=retry_handler)
- response = await client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": "42"})
+ response = await client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": "42"})
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
From 1ea87ff08b4b50541e3c26bef6f4bd581af6324c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:02:04 +0000
Subject: [PATCH 03/20] feat(api): manual updates
---
.stats.yml | 4 +-
api.md | 4 +-
src/beeper_desktop_api/resources/accounts.py | 4 +-
.../resources/chats/chats.py | 133 +++++++++++++++++-
src/beeper_desktop_api/resources/messages.py | 130 ++++++++++++++++-
src/beeper_desktop_api/types/__init__.py | 3 +
.../types/chat_list_params.py | 30 ++++
.../types/chat_list_response.py | 13 ++
.../types/message_list_params.py | 27 ++++
tests/api_resources/test_chats.py | 79 +++++++++++
tests/api_resources/test_messages.py | 86 ++++++++++-
11 files changed, 505 insertions(+), 8 deletions(-)
create mode 100644 src/beeper_desktop_api/types/chat_list_params.py
create mode 100644 src/beeper_desktop_api/types/chat_list_response.py
create mode 100644 src/beeper_desktop_api/types/message_list_params.py
diff --git a/.stats.yml b/.stats.yml
index 8526f3e..e531b74 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 14
+configured_endpoints: 16
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
-config_hash: f83b2b6eb86f2dd68101065998479cb2
+config_hash: be3f3b31e322be0f4de6a23e32ab004c
diff --git a/api.md b/api.md
index cfe2dc3..dbf9b16 100644
--- a/api.md
+++ b/api.md
@@ -47,13 +47,14 @@ Methods:
Types:
```python
-from beeper_desktop_api.types import Chat, ChatCreateResponse
+from beeper_desktop_api.types import Chat, ChatCreateResponse, ChatListResponse
```
Methods:
- client.chats.create(\*\*params) -> ChatCreateResponse
- client.chats.retrieve(chat_id, \*\*params) -> Chat
+- client.chats.list(\*\*params) -> SyncCursor[ChatListResponse]
- client.chats.archive(chat_id, \*\*params) -> BaseResponse
- client.chats.search(\*\*params) -> SyncCursor[Chat]
@@ -74,6 +75,7 @@ from beeper_desktop_api.types import MessageSendResponse
Methods:
+- client.messages.list(\*\*params) -> SyncCursor[Message]
- client.messages.search(\*\*params) -> SyncCursor[Message]
- client.messages.send(\*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/resources/accounts.py b/src/beeper_desktop_api/resources/accounts.py
index 49a5df2..1210fce 100644
--- a/src/beeper_desktop_api/resources/accounts.py
+++ b/src/beeper_desktop_api/resources/accounts.py
@@ -20,7 +20,7 @@
class AccountsResource(SyncAPIResource):
- """Accounts operations"""
+ """Manage connected chat accounts"""
@cached_property
def with_raw_response(self) -> AccountsResourceWithRawResponse:
@@ -65,7 +65,7 @@ def list(
class AsyncAccountsResource(AsyncAPIResource):
- """Accounts operations"""
+ """Manage connected chat accounts"""
@cached_property
def with_raw_response(self) -> AsyncAccountsResourceWithRawResponse:
diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py
index b5ec602..93587e7 100644
--- a/src/beeper_desktop_api/resources/chats/chats.py
+++ b/src/beeper_desktop_api/resources/chats/chats.py
@@ -8,7 +8,7 @@
import httpx
-from ...types import chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params
+from ...types import chat_list_params, chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params
from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
@@ -30,6 +30,7 @@
from ...pagination import SyncCursor, AsyncCursor
from ...types.chat import Chat
from ..._base_client import AsyncPaginator, make_request_options
+from ...types.chat_list_response import ChatListResponse
from ...types.chat_create_response import ChatCreateResponse
from ...types.shared.base_response import BaseResponse
@@ -166,6 +167,65 @@ def retrieve(
cast_to=Chat,
)
+ def list(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ limit: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncCursor[ChatListResponse]:
+ """List all chats sorted by last activity (most recent first).
+
+ Combines all
+ accounts into a single paginated list.
+
+ Args:
+ account_ids: Limit to specific account IDs. If omitted, fetches from all accounts.
+
+ cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction
+ to navigate results.
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+
+ limit: Maximum number of chats to return (1–200). Defaults to 50.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/chats",
+ page=SyncCursor[ChatListResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "cursor": cursor,
+ "direction": direction,
+ "limit": limit,
+ },
+ chat_list_params.ChatListParams,
+ ),
+ ),
+ model=ChatListResponse,
+ )
+
def archive(
self,
chat_id: str,
@@ -436,6 +496,65 @@ async def retrieve(
cast_to=Chat,
)
+ def list(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ limit: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[ChatListResponse, AsyncCursor[ChatListResponse]]:
+ """List all chats sorted by last activity (most recent first).
+
+ Combines all
+ accounts into a single paginated list.
+
+ Args:
+ account_ids: Limit to specific account IDs. If omitted, fetches from all accounts.
+
+ cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction
+ to navigate results.
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+
+ limit: Maximum number of chats to return (1–200). Defaults to 50.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/chats",
+ page=AsyncCursor[ChatListResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "cursor": cursor,
+ "direction": direction,
+ "limit": limit,
+ },
+ chat_list_params.ChatListParams,
+ ),
+ ),
+ model=ChatListResponse,
+ )
+
async def archive(
self,
chat_id: str,
@@ -586,6 +705,9 @@ def __init__(self, chats: ChatsResource) -> None:
self.retrieve = to_raw_response_wrapper(
chats.retrieve,
)
+ self.list = to_raw_response_wrapper(
+ chats.list,
+ )
self.archive = to_raw_response_wrapper(
chats.archive,
)
@@ -609,6 +731,9 @@ def __init__(self, chats: AsyncChatsResource) -> None:
self.retrieve = async_to_raw_response_wrapper(
chats.retrieve,
)
+ self.list = async_to_raw_response_wrapper(
+ chats.list,
+ )
self.archive = async_to_raw_response_wrapper(
chats.archive,
)
@@ -632,6 +757,9 @@ def __init__(self, chats: ChatsResource) -> None:
self.retrieve = to_streamed_response_wrapper(
chats.retrieve,
)
+ self.list = to_streamed_response_wrapper(
+ chats.list,
+ )
self.archive = to_streamed_response_wrapper(
chats.archive,
)
@@ -655,6 +783,9 @@ def __init__(self, chats: AsyncChatsResource) -> None:
self.retrieve = async_to_streamed_response_wrapper(
chats.retrieve,
)
+ self.list = async_to_streamed_response_wrapper(
+ chats.list,
+ )
self.archive = async_to_streamed_response_wrapper(
chats.archive,
)
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index ea1ea25..485a86b 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -8,7 +8,7 @@
import httpx
-from ..types import message_send_params, message_search_params
+from ..types import message_list_params, message_send_params, message_search_params
from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from .._utils import maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -49,6 +49,64 @@ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse:
"""
return MessagesResourceWithStreamingResponse(self)
+ def list(
+ self,
+ *,
+ chat_id: str,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ limit: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncCursor[Message]:
+ """List all messages in a chat with cursor-based pagination.
+
+ Sorted by timestamp.
+
+ Args:
+ chat_id: The chat ID to list messages from
+
+ cursor: Message cursor for pagination. Use with direction to navigate results.
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older messages,
+ 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
+ provided.
+
+ limit: Maximum number of messages to return (1–500). Defaults to 50.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/messages",
+ page=SyncCursor[Message],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "chat_id": chat_id,
+ "cursor": cursor,
+ "direction": direction,
+ "limit": limit,
+ },
+ message_list_params.MessageListParams,
+ ),
+ ),
+ model=Message,
+ )
+
def search(
self,
*,
@@ -223,6 +281,64 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse:
"""
return AsyncMessagesResourceWithStreamingResponse(self)
+ def list(
+ self,
+ *,
+ chat_id: str,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ limit: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Message, AsyncCursor[Message]]:
+ """List all messages in a chat with cursor-based pagination.
+
+ Sorted by timestamp.
+
+ Args:
+ chat_id: The chat ID to list messages from
+
+ cursor: Message cursor for pagination. Use with direction to navigate results.
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older messages,
+ 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
+ provided.
+
+ limit: Maximum number of messages to return (1–500). Defaults to 50.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/messages",
+ page=AsyncCursor[Message],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "chat_id": chat_id,
+ "cursor": cursor,
+ "direction": direction,
+ "limit": limit,
+ },
+ message_list_params.MessageListParams,
+ ),
+ ),
+ model=Message,
+ )
+
def search(
self,
*,
@@ -379,6 +495,9 @@ class MessagesResourceWithRawResponse:
def __init__(self, messages: MessagesResource) -> None:
self._messages = messages
+ self.list = to_raw_response_wrapper(
+ messages.list,
+ )
self.search = to_raw_response_wrapper(
messages.search,
)
@@ -391,6 +510,9 @@ class AsyncMessagesResourceWithRawResponse:
def __init__(self, messages: AsyncMessagesResource) -> None:
self._messages = messages
+ self.list = async_to_raw_response_wrapper(
+ messages.list,
+ )
self.search = async_to_raw_response_wrapper(
messages.search,
)
@@ -403,6 +525,9 @@ class MessagesResourceWithStreamingResponse:
def __init__(self, messages: MessagesResource) -> None:
self._messages = messages
+ self.list = to_streamed_response_wrapper(
+ messages.list,
+ )
self.search = to_streamed_response_wrapper(
messages.search,
)
@@ -415,6 +540,9 @@ class AsyncMessagesResourceWithStreamingResponse:
def __init__(self, messages: AsyncMessagesResource) -> None:
self._messages = messages
+ self.list = async_to_streamed_response_wrapper(
+ messages.list,
+ )
self.search = async_to_streamed_response_wrapper(
messages.search,
)
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index 5bede4c..ffab91e 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -15,10 +15,13 @@
from .user_info import UserInfo as UserInfo
from .open_response import OpenResponse as OpenResponse
from .search_response import SearchResponse as SearchResponse
+from .chat_list_params import ChatListParams as ChatListParams
from .chat_create_params import ChatCreateParams as ChatCreateParams
+from .chat_list_response import ChatListResponse as ChatListResponse
from .chat_search_params import ChatSearchParams as ChatSearchParams
from .client_open_params import ClientOpenParams as ClientOpenParams
from .chat_archive_params import ChatArchiveParams as ChatArchiveParams
+from .message_list_params import MessageListParams as MessageListParams
from .message_send_params import MessageSendParams as MessageSendParams
from .chat_create_response import ChatCreateResponse as ChatCreateResponse
from .chat_retrieve_params import ChatRetrieveParams as ChatRetrieveParams
diff --git a/src/beeper_desktop_api/types/chat_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py
new file mode 100644
index 0000000..d8e1784
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_list_params.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["ChatListParams"]
+
+
+class ChatListParams(TypedDict, total=False):
+ account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")]
+ """Limit to specific account IDs. If omitted, fetches from all accounts."""
+
+ cursor: str
+ """Timestamp cursor (milliseconds since epoch) for pagination.
+
+ Use with direction to navigate results.
+ """
+
+ direction: Literal["after", "before"]
+ """
+ Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+ """
+
+ limit: int
+ """Maximum number of chats to return (1–200). Defaults to 50."""
diff --git a/src/beeper_desktop_api/types/chat_list_response.py b/src/beeper_desktop_api/types/chat_list_response.py
new file mode 100644
index 0000000..80e3885
--- /dev/null
+++ b/src/beeper_desktop_api/types/chat_list_response.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .chat import Chat
+from .shared.message import Message
+
+__all__ = ["ChatListResponse"]
+
+
+class ChatListResponse(Chat):
+ preview: Optional[Message] = None
+ """Last message preview for this chat, if available."""
diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py
new file mode 100644
index 0000000..ca56fab
--- /dev/null
+++ b/src/beeper_desktop_api/types/message_list_params.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["MessageListParams"]
+
+
+class MessageListParams(TypedDict, total=False):
+ chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]]
+ """The chat ID to list messages from"""
+
+ cursor: str
+ """Message cursor for pagination. Use with direction to navigate results."""
+
+ direction: Literal["after", "before"]
+ """
+ Pagination direction used with 'cursor': 'before' fetches older messages,
+ 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
+ provided.
+ """
+
+ limit: int
+ """Maximum number of messages to return (1–500). Defaults to 50."""
diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py
index 3eba100..3009cc9 100644
--- a/tests/api_resources/test_chats.py
+++ b/tests/api_resources/test_chats.py
@@ -11,6 +11,7 @@
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
from beeper_desktop_api.types import (
Chat,
+ ChatListResponse,
ChatCreateResponse,
)
from beeper_desktop_api._utils import parse_datetime
@@ -117,6 +118,45 @@ def test_path_params_retrieve(self, client: BeeperDesktop) -> None:
chat_id="",
)
+ @parametrize
+ def test_method_list(self, client: BeeperDesktop) -> None:
+ chat = client.chats.list()
+ assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+
+ @parametrize
+ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
+ chat = client.chats.list(
+ account_ids=[
+ "whatsapp",
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU",
+ ],
+ cursor="1725489123456",
+ direction="before",
+ limit=1,
+ )
+ assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: BeeperDesktop) -> None:
+ response = client.chats.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = response.parse()
+ assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
+ with client.chats.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = response.parse()
+ assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
def test_method_archive(self, client: BeeperDesktop) -> None:
chat = client.chats.archive(
@@ -309,6 +349,45 @@ async def test_path_params_retrieve(self, async_client: AsyncBeeperDesktop) -> N
chat_id="",
)
+ @parametrize
+ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.list()
+ assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ chat = await async_client.chats.list(
+ account_ids=[
+ "whatsapp",
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU",
+ ],
+ cursor="1725489123456",
+ direction="before",
+ limit=1,
+ )
+ assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.chats.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ chat = await response.parse()
+ assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.chats.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ chat = await response.parse()
+ assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
async def test_method_archive(self, async_client: AsyncBeeperDesktop) -> None:
chat = await async_client.chats.archive(
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index eb5fc6e..85ebebb 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -9,7 +9,9 @@
from tests.utils import assert_matches_type
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
-from beeper_desktop_api.types import MessageSendResponse
+from beeper_desktop_api.types import (
+ MessageSendResponse,
+)
from beeper_desktop_api._utils import parse_datetime
from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
from beeper_desktop_api.types.shared import Message
@@ -20,6 +22,47 @@
class TestMessages:
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+ @parametrize
+ def test_method_list(self, client: BeeperDesktop) -> None:
+ message = client.messages.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
+ message = client.messages.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ cursor="821744079",
+ direction="before",
+ limit=1,
+ )
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: BeeperDesktop) -> None:
+ response = client.messages.with_raw_response.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ message = response.parse()
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
+ with client.messages.with_streaming_response.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ message = response.parse()
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
def test_method_search(self, client: BeeperDesktop) -> None:
message = client.messages.search()
@@ -114,6 +157,47 @@ class TestAsyncMessages:
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
)
+ @parametrize
+ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
+ message = await async_client.messages.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ message = await async_client.messages.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ cursor="821744079",
+ direction="before",
+ limit=1,
+ )
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.messages.with_raw_response.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ message = await response.parse()
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.messages.with_streaming_response.list(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ message = await response.parse()
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.search()
From 88bce73dfef13b6a1cdef0749dc3078af97255e4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:07:31 +0000
Subject: [PATCH 04/20] feat(api): manual updates
---
.stats.yml | 2 +-
api.md | 20 +--
src/beeper_desktop_api/_client.py | 61 ++++++--
src/beeper_desktop_api/resources/__init__.py | 14 --
src/beeper_desktop_api/resources/token.py | 139 ------------------
src/beeper_desktop_api/types/__init__.py | 2 +-
...ser_info.py => get_token_info_response.py} | 4 +-
tests/api_resources/test_client.py | 51 +++++++
tests/api_resources/test_token.py | 74 ----------
9 files changed, 114 insertions(+), 253 deletions(-)
delete mode 100644 src/beeper_desktop_api/resources/token.py
rename src/beeper_desktop_api/types/{user_info.py => get_token_info_response.py} (89%)
delete mode 100644 tests/api_resources/test_token.py
diff --git a/.stats.yml b/.stats.yml
index e531b74..3cb504d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 16
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
-config_hash: be3f3b31e322be0f4de6a23e32ab004c
+config_hash: 00db138e547960c0d9c47754c2f59051
diff --git a/api.md b/api.md
index dbf9b16..502d7e8 100644
--- a/api.md
+++ b/api.md
@@ -9,12 +9,18 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, R
Types:
```python
-from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse
+from beeper_desktop_api.types import (
+ DownloadAssetResponse,
+ GetTokenInfoResponse,
+ OpenResponse,
+ SearchResponse,
+)
```
Methods:
- client.download_asset(\*\*params) -> DownloadAssetResponse
+- client.get_token_info() -> GetTokenInfoResponse
- client.open(\*\*params) -> OpenResponse
- client.search(\*\*params) -> SearchResponse
@@ -78,15 +84,3 @@ Methods:
- client.messages.list(\*\*params) -> SyncCursor[Message]
- client.messages.search(\*\*params) -> SyncCursor[Message]
- client.messages.send(\*\*params) -> MessageSendResponse
-
-# Token
-
-Types:
-
-```python
-from beeper_desktop_api.types import UserInfo
-```
-
-Methods:
-
-- client.token.info() -> UserInfo
diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py
index 7a1a10e..01dd758 100644
--- a/src/beeper_desktop_api/_client.py
+++ b/src/beeper_desktop_api/_client.py
@@ -37,7 +37,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .resources import token, accounts, contacts, messages
+from .resources import accounts, contacts, messages
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import APIStatusError, BeeperDesktopError
from ._base_client import (
@@ -50,6 +50,7 @@
from .types.open_response import OpenResponse
from .types.search_response import SearchResponse
from .types.download_asset_response import DownloadAssetResponse
+from .types.get_token_info_response import GetTokenInfoResponse
__all__ = [
"Timeout",
@@ -68,7 +69,6 @@ class BeeperDesktop(SyncAPIClient):
contacts: contacts.ContactsResource
chats: chats.ChatsResource
messages: messages.MessagesResource
- token: token.TokenResource
with_raw_response: BeeperDesktopWithRawResponse
with_streaming_response: BeeperDesktopWithStreamedResponse
@@ -130,7 +130,6 @@ def __init__(
self.contacts = contacts.ContactsResource(self)
self.chats = chats.ChatsResource(self)
self.messages = messages.MessagesResource(self)
- self.token = token.TokenResource(self)
self.with_raw_response = BeeperDesktopWithRawResponse(self)
self.with_streaming_response = BeeperDesktopWithStreamedResponse(self)
@@ -240,6 +239,25 @@ def download_asset(
cast_to=DownloadAssetResponse,
)
+ def get_token_info(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> GetTokenInfoResponse:
+ """Returns information about the authenticated user/token"""
+ return self.get(
+ "/oauth/userinfo",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=GetTokenInfoResponse,
+ )
+
def open(
self,
*,
@@ -371,7 +389,6 @@ class AsyncBeeperDesktop(AsyncAPIClient):
contacts: contacts.AsyncContactsResource
chats: chats.AsyncChatsResource
messages: messages.AsyncMessagesResource
- token: token.AsyncTokenResource
with_raw_response: AsyncBeeperDesktopWithRawResponse
with_streaming_response: AsyncBeeperDesktopWithStreamedResponse
@@ -433,7 +450,6 @@ def __init__(
self.contacts = contacts.AsyncContactsResource(self)
self.chats = chats.AsyncChatsResource(self)
self.messages = messages.AsyncMessagesResource(self)
- self.token = token.AsyncTokenResource(self)
self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self)
self.with_streaming_response = AsyncBeeperDesktopWithStreamedResponse(self)
@@ -543,6 +559,25 @@ async def download_asset(
cast_to=DownloadAssetResponse,
)
+ async def get_token_info(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> GetTokenInfoResponse:
+ """Returns information about the authenticated user/token"""
+ return await self.get(
+ "/oauth/userinfo",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=GetTokenInfoResponse,
+ )
+
async def open(
self,
*,
@@ -675,11 +710,13 @@ def __init__(self, client: BeeperDesktop) -> None:
self.contacts = contacts.ContactsResourceWithRawResponse(client.contacts)
self.chats = chats.ChatsResourceWithRawResponse(client.chats)
self.messages = messages.MessagesResourceWithRawResponse(client.messages)
- self.token = token.TokenResourceWithRawResponse(client.token)
self.download_asset = to_raw_response_wrapper(
client.download_asset,
)
+ self.get_token_info = to_raw_response_wrapper(
+ client.get_token_info,
+ )
self.open = to_raw_response_wrapper(
client.open,
)
@@ -694,11 +731,13 @@ def __init__(self, client: AsyncBeeperDesktop) -> None:
self.contacts = contacts.AsyncContactsResourceWithRawResponse(client.contacts)
self.chats = chats.AsyncChatsResourceWithRawResponse(client.chats)
self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages)
- self.token = token.AsyncTokenResourceWithRawResponse(client.token)
self.download_asset = async_to_raw_response_wrapper(
client.download_asset,
)
+ self.get_token_info = async_to_raw_response_wrapper(
+ client.get_token_info,
+ )
self.open = async_to_raw_response_wrapper(
client.open,
)
@@ -713,11 +752,13 @@ def __init__(self, client: BeeperDesktop) -> None:
self.contacts = contacts.ContactsResourceWithStreamingResponse(client.contacts)
self.chats = chats.ChatsResourceWithStreamingResponse(client.chats)
self.messages = messages.MessagesResourceWithStreamingResponse(client.messages)
- self.token = token.TokenResourceWithStreamingResponse(client.token)
self.download_asset = to_streamed_response_wrapper(
client.download_asset,
)
+ self.get_token_info = to_streamed_response_wrapper(
+ client.get_token_info,
+ )
self.open = to_streamed_response_wrapper(
client.open,
)
@@ -732,11 +773,13 @@ def __init__(self, client: AsyncBeeperDesktop) -> None:
self.contacts = contacts.AsyncContactsResourceWithStreamingResponse(client.contacts)
self.chats = chats.AsyncChatsResourceWithStreamingResponse(client.chats)
self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages)
- self.token = token.AsyncTokenResourceWithStreamingResponse(client.token)
self.download_asset = async_to_streamed_response_wrapper(
client.download_asset,
)
+ self.get_token_info = async_to_streamed_response_wrapper(
+ client.get_token_info,
+ )
self.open = async_to_streamed_response_wrapper(
client.open,
)
diff --git a/src/beeper_desktop_api/resources/__init__.py b/src/beeper_desktop_api/resources/__init__.py
index 24ab242..ebf006b 100644
--- a/src/beeper_desktop_api/resources/__init__.py
+++ b/src/beeper_desktop_api/resources/__init__.py
@@ -8,14 +8,6 @@
ChatsResourceWithStreamingResponse,
AsyncChatsResourceWithStreamingResponse,
)
-from .token import (
- TokenResource,
- AsyncTokenResource,
- TokenResourceWithRawResponse,
- AsyncTokenResourceWithRawResponse,
- TokenResourceWithStreamingResponse,
- AsyncTokenResourceWithStreamingResponse,
-)
from .accounts import (
AccountsResource,
AsyncAccountsResource,
@@ -66,10 +58,4 @@
"AsyncMessagesResourceWithRawResponse",
"MessagesResourceWithStreamingResponse",
"AsyncMessagesResourceWithStreamingResponse",
- "TokenResource",
- "AsyncTokenResource",
- "TokenResourceWithRawResponse",
- "AsyncTokenResourceWithRawResponse",
- "TokenResourceWithStreamingResponse",
- "AsyncTokenResourceWithStreamingResponse",
]
diff --git a/src/beeper_desktop_api/resources/token.py b/src/beeper_desktop_api/resources/token.py
deleted file mode 100644
index 5648872..0000000
--- a/src/beeper_desktop_api/resources/token.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import httpx
-
-from .._types import Body, Query, Headers, NotGiven, not_given
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from .._base_client import make_request_options
-from ..types.user_info import UserInfo
-
-__all__ = ["TokenResource", "AsyncTokenResource"]
-
-
-class TokenResource(SyncAPIResource):
- """Operations related to the current access token"""
-
- @cached_property
- def with_raw_response(self) -> TokenResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
- """
- return TokenResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> TokenResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
- """
- return TokenResourceWithStreamingResponse(self)
-
- def info(
- self,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UserInfo:
- """Returns information about the authenticated user/token"""
- return self._get(
- "/oauth/userinfo",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=UserInfo,
- )
-
-
-class AsyncTokenResource(AsyncAPIResource):
- """Operations related to the current access token"""
-
- @cached_property
- def with_raw_response(self) -> AsyncTokenResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
- """
- return AsyncTokenResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncTokenResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
- """
- return AsyncTokenResourceWithStreamingResponse(self)
-
- async def info(
- self,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UserInfo:
- """Returns information about the authenticated user/token"""
- return await self._get(
- "/oauth/userinfo",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=UserInfo,
- )
-
-
-class TokenResourceWithRawResponse:
- def __init__(self, token: TokenResource) -> None:
- self._token = token
-
- self.info = to_raw_response_wrapper(
- token.info,
- )
-
-
-class AsyncTokenResourceWithRawResponse:
- def __init__(self, token: AsyncTokenResource) -> None:
- self._token = token
-
- self.info = async_to_raw_response_wrapper(
- token.info,
- )
-
-
-class TokenResourceWithStreamingResponse:
- def __init__(self, token: TokenResource) -> None:
- self._token = token
-
- self.info = to_streamed_response_wrapper(
- token.info,
- )
-
-
-class AsyncTokenResourceWithStreamingResponse:
- def __init__(self, token: AsyncTokenResource) -> None:
- self._token = token
-
- self.info = async_to_streamed_response_wrapper(
- token.info,
- )
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index ffab91e..d778ad9 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -12,7 +12,6 @@
BaseResponse as BaseResponse,
)
from .account import Account as Account
-from .user_info import UserInfo as UserInfo
from .open_response import OpenResponse as OpenResponse
from .search_response import SearchResponse as SearchResponse
from .chat_list_params import ChatListParams as ChatListParams
@@ -32,4 +31,5 @@
from .message_send_response import MessageSendResponse as MessageSendResponse
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
+from .get_token_info_response import GetTokenInfoResponse as GetTokenInfoResponse
from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
diff --git a/src/beeper_desktop_api/types/user_info.py b/src/beeper_desktop_api/types/get_token_info_response.py
similarity index 89%
rename from src/beeper_desktop_api/types/user_info.py
rename to src/beeper_desktop_api/types/get_token_info_response.py
index d023e31..5dcf865 100644
--- a/src/beeper_desktop_api/types/user_info.py
+++ b/src/beeper_desktop_api/types/get_token_info_response.py
@@ -5,10 +5,10 @@
from .._models import BaseModel
-__all__ = ["UserInfo"]
+__all__ = ["GetTokenInfoResponse"]
-class UserInfo(BaseModel):
+class GetTokenInfoResponse(BaseModel):
iat: float
"""Issued at timestamp (Unix epoch seconds)"""
diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py
index d5de032..45f7fd0 100644
--- a/tests/api_resources/test_client.py
+++ b/tests/api_resources/test_client.py
@@ -12,6 +12,7 @@
from beeper_desktop_api.types import (
OpenResponse,
SearchResponse,
+ GetTokenInfoResponse,
DownloadAssetResponse,
)
@@ -52,6 +53,31 @@ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_get_token_info(self, client: BeeperDesktop) -> None:
+ client_ = client.get_token_info()
+ assert_matches_type(GetTokenInfoResponse, client_, path=["response"])
+
+ @parametrize
+ def test_raw_response_get_token_info(self, client: BeeperDesktop) -> None:
+ response = client.with_raw_response.get_token_info()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client_ = response.parse()
+ assert_matches_type(GetTokenInfoResponse, client_, path=["response"])
+
+ @parametrize
+ def test_streaming_response_get_token_info(self, client: BeeperDesktop) -> None:
+ with client.with_streaming_response.get_token_info() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client_ = response.parse()
+ assert_matches_type(GetTokenInfoResponse, client_, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
def test_method_open(self, client: BeeperDesktop) -> None:
client_ = client.open()
@@ -155,6 +181,31 @@ async def test_streaming_response_download_asset(self, async_client: AsyncBeeper
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_get_token_info(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.get_token_info()
+ assert_matches_type(GetTokenInfoResponse, client, path=["response"])
+
+ @parametrize
+ async def test_raw_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.with_raw_response.get_token_info()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ client = await response.parse()
+ assert_matches_type(GetTokenInfoResponse, client, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.with_streaming_response.get_token_info() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ client = await response.parse()
+ assert_matches_type(GetTokenInfoResponse, client, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None:
client = await async_client.open()
diff --git a/tests/api_resources/test_token.py b/tests/api_resources/test_token.py
deleted file mode 100644
index 538aa77..0000000
--- a/tests/api_resources/test_token.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from tests.utils import assert_matches_type
-from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
-from beeper_desktop_api.types import UserInfo
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestToken:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- def test_method_info(self, client: BeeperDesktop) -> None:
- token = client.token.info()
- assert_matches_type(UserInfo, token, path=["response"])
-
- @parametrize
- def test_raw_response_info(self, client: BeeperDesktop) -> None:
- response = client.token.with_raw_response.info()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- token = response.parse()
- assert_matches_type(UserInfo, token, path=["response"])
-
- @parametrize
- def test_streaming_response_info(self, client: BeeperDesktop) -> None:
- with client.token.with_streaming_response.info() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- token = response.parse()
- assert_matches_type(UserInfo, token, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
-
-class TestAsyncToken:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
-
- @parametrize
- async def test_method_info(self, async_client: AsyncBeeperDesktop) -> None:
- token = await async_client.token.info()
- assert_matches_type(UserInfo, token, path=["response"])
-
- @parametrize
- async def test_raw_response_info(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.token.with_raw_response.info()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- token = await response.parse()
- assert_matches_type(UserInfo, token, path=["response"])
-
- @parametrize
- async def test_streaming_response_info(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.token.with_streaming_response.info() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- token = await response.parse()
- assert_matches_type(UserInfo, token, path=["response"])
-
- assert cast(Any, response.is_closed) is True
From 7aa256c4678bff37841bc4ec35670fc19fc563a7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:13:33 +0000
Subject: [PATCH 05/20] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 3cb504d..dffcc04 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 16
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
-config_hash: 00db138e547960c0d9c47754c2f59051
+config_hash: 382b53633aa9cc48d7b7f44ecf5e3e8c
From 0151017ee47e53f613a9b55dd3460c4aece0a91d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:15:27 +0000
Subject: [PATCH 06/20] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index dffcc04..b4e4371 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 16
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
-config_hash: 382b53633aa9cc48d7b7f44ecf5e3e8c
+config_hash: 58f19d979ad9a375e32b814503ce3e86
From 635746eb468dffce959dfc8bbb93e56859ee6df9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:17:37 +0000
Subject: [PATCH 07/20] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index b4e4371..5b8f55b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 16
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
-config_hash: 58f19d979ad9a375e32b814503ce3e86
+config_hash: a9434fa7b77fc01af6e667f3717eb768
From 7c655fb94ba070083173c15a501be7a0f119a38b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:22:36 +0000
Subject: [PATCH 08/20] feat(api): manual updates
---
.stats.yml | 8 +--
api.md | 12 +---
src/beeper_desktop_api/_client.py | 59 ++-----------------
src/beeper_desktop_api/types/__init__.py | 1 -
.../types/get_token_info_response.py | 31 ----------
tests/api_resources/test_client.py | 51 ----------------
6 files changed, 11 insertions(+), 151 deletions(-)
delete mode 100644 src/beeper_desktop_api/types/get_token_info_response.py
diff --git a/.stats.yml b/.stats.yml
index 5b8f55b..d64bab5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 16
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml
-openapi_spec_hash: ba834200758376aaea47b2a276f64c1b
-config_hash: a9434fa7b77fc01af6e667f3717eb768
+configured_endpoints: 15
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml
+openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef
+config_hash: d48fc12c89d2d812adf19d0508306f4a
diff --git a/api.md b/api.md
index 502d7e8..529a147 100644
--- a/api.md
+++ b/api.md
@@ -9,19 +9,13 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, R
Types:
```python
-from beeper_desktop_api.types import (
- DownloadAssetResponse,
- GetTokenInfoResponse,
- OpenResponse,
- SearchResponse,
-)
+from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse
```
Methods:
-- client.download_asset(\*\*params) -> DownloadAssetResponse
-- client.get_token_info() -> GetTokenInfoResponse
-- client.open(\*\*params) -> OpenResponse
+- client.download_asset(\*\*params) -> DownloadAssetResponse
+- client.open(\*\*params) -> OpenResponse
- client.search(\*\*params) -> SearchResponse
# Accounts
diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py
index 01dd758..f8ba71d 100644
--- a/src/beeper_desktop_api/_client.py
+++ b/src/beeper_desktop_api/_client.py
@@ -50,7 +50,6 @@
from .types.open_response import OpenResponse
from .types.search_response import SearchResponse
from .types.download_asset_response import DownloadAssetResponse
-from .types.get_token_info_response import GetTokenInfoResponse
__all__ = [
"Timeout",
@@ -231,7 +230,7 @@ def download_asset(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self.post(
- "/v1/app/download-asset",
+ "/v1/download-asset",
body=maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -239,25 +238,6 @@ def download_asset(
cast_to=DownloadAssetResponse,
)
- def get_token_info(
- self,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> GetTokenInfoResponse:
- """Returns information about the authenticated user/token"""
- return self.get(
- "/oauth/userinfo",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=GetTokenInfoResponse,
- )
-
def open(
self,
*,
@@ -295,7 +275,7 @@ def open(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self.post(
- "/v1/app/open",
+ "/v1/open",
body=maybe_transform(
{
"chat_id": chat_id,
@@ -551,7 +531,7 @@ async def download_asset(
timeout: Override the client-level default timeout for this request, in seconds
"""
return await self.post(
- "/v1/app/download-asset",
+ "/v1/download-asset",
body=await async_maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -559,25 +539,6 @@ async def download_asset(
cast_to=DownloadAssetResponse,
)
- async def get_token_info(
- self,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> GetTokenInfoResponse:
- """Returns information about the authenticated user/token"""
- return await self.get(
- "/oauth/userinfo",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=GetTokenInfoResponse,
- )
-
async def open(
self,
*,
@@ -615,7 +576,7 @@ async def open(
timeout: Override the client-level default timeout for this request, in seconds
"""
return await self.post(
- "/v1/app/open",
+ "/v1/open",
body=await async_maybe_transform(
{
"chat_id": chat_id,
@@ -714,9 +675,6 @@ def __init__(self, client: BeeperDesktop) -> None:
self.download_asset = to_raw_response_wrapper(
client.download_asset,
)
- self.get_token_info = to_raw_response_wrapper(
- client.get_token_info,
- )
self.open = to_raw_response_wrapper(
client.open,
)
@@ -735,9 +693,6 @@ def __init__(self, client: AsyncBeeperDesktop) -> None:
self.download_asset = async_to_raw_response_wrapper(
client.download_asset,
)
- self.get_token_info = async_to_raw_response_wrapper(
- client.get_token_info,
- )
self.open = async_to_raw_response_wrapper(
client.open,
)
@@ -756,9 +711,6 @@ def __init__(self, client: BeeperDesktop) -> None:
self.download_asset = to_streamed_response_wrapper(
client.download_asset,
)
- self.get_token_info = to_streamed_response_wrapper(
- client.get_token_info,
- )
self.open = to_streamed_response_wrapper(
client.open,
)
@@ -777,9 +729,6 @@ def __init__(self, client: AsyncBeeperDesktop) -> None:
self.download_asset = async_to_streamed_response_wrapper(
client.download_asset,
)
- self.get_token_info = async_to_streamed_response_wrapper(
- client.get_token_info,
- )
self.open = async_to_streamed_response_wrapper(
client.open,
)
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index d778ad9..e577cbf 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -31,5 +31,4 @@
from .message_send_response import MessageSendResponse as MessageSendResponse
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
-from .get_token_info_response import GetTokenInfoResponse as GetTokenInfoResponse
from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
diff --git a/src/beeper_desktop_api/types/get_token_info_response.py b/src/beeper_desktop_api/types/get_token_info_response.py
deleted file mode 100644
index 5dcf865..0000000
--- a/src/beeper_desktop_api/types/get_token_info_response.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-from typing_extensions import Literal
-
-from .._models import BaseModel
-
-__all__ = ["GetTokenInfoResponse"]
-
-
-class GetTokenInfoResponse(BaseModel):
- iat: float
- """Issued at timestamp (Unix epoch seconds)"""
-
- scope: str
- """Granted scopes"""
-
- sub: str
- """Subject identifier (token ID)"""
-
- token_use: Literal["access"]
- """Token type"""
-
- aud: Optional[str] = None
- """Audience (client ID)"""
-
- client_id: Optional[str] = None
- """Client identifier"""
-
- exp: Optional[float] = None
- """Expiration timestamp (Unix epoch seconds)"""
diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py
index 45f7fd0..d5de032 100644
--- a/tests/api_resources/test_client.py
+++ b/tests/api_resources/test_client.py
@@ -12,7 +12,6 @@
from beeper_desktop_api.types import (
OpenResponse,
SearchResponse,
- GetTokenInfoResponse,
DownloadAssetResponse,
)
@@ -53,31 +52,6 @@ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None:
assert cast(Any, response.is_closed) is True
- @parametrize
- def test_method_get_token_info(self, client: BeeperDesktop) -> None:
- client_ = client.get_token_info()
- assert_matches_type(GetTokenInfoResponse, client_, path=["response"])
-
- @parametrize
- def test_raw_response_get_token_info(self, client: BeeperDesktop) -> None:
- response = client.with_raw_response.get_token_info()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- client_ = response.parse()
- assert_matches_type(GetTokenInfoResponse, client_, path=["response"])
-
- @parametrize
- def test_streaming_response_get_token_info(self, client: BeeperDesktop) -> None:
- with client.with_streaming_response.get_token_info() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- client_ = response.parse()
- assert_matches_type(GetTokenInfoResponse, client_, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
@parametrize
def test_method_open(self, client: BeeperDesktop) -> None:
client_ = client.open()
@@ -181,31 +155,6 @@ async def test_streaming_response_download_asset(self, async_client: AsyncBeeper
assert cast(Any, response.is_closed) is True
- @parametrize
- async def test_method_get_token_info(self, async_client: AsyncBeeperDesktop) -> None:
- client = await async_client.get_token_info()
- assert_matches_type(GetTokenInfoResponse, client, path=["response"])
-
- @parametrize
- async def test_raw_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.with_raw_response.get_token_info()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- client = await response.parse()
- assert_matches_type(GetTokenInfoResponse, client, path=["response"])
-
- @parametrize
- async def test_streaming_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.with_streaming_response.get_token_info() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- client = await response.parse()
- assert_matches_type(GetTokenInfoResponse, client, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
@parametrize
async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None:
client = await async_client.open()
From 6d8c6f207ddb0a4795ab5d4ed24ea8ebc3ab359a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:27:46 +0000
Subject: [PATCH 09/20] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index d64bab5..2621f51 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml
openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef
-config_hash: d48fc12c89d2d812adf19d0508306f4a
+config_hash: b43f460701263c30aba16a32385b20ed
From 58799fa0735e1f030be5afe194fd979a60b5480f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:32:17 +0000
Subject: [PATCH 10/20] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 2621f51..103ad16 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml
openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef
-config_hash: b43f460701263c30aba16a32385b20ed
+config_hash: 738402ade5ac9528c8ef1677aa1d70f7
From c9f3b2d3a7fb7e2ce3b30de215497079fff3aca9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 15:24:15 +0000
Subject: [PATCH 11/20] feat(api): manual updates
---
.stats.yml | 2 +-
README.md | 79 -------------------
api.md | 4 +-
src/beeper_desktop_api/pagination.py | 67 +++++++++++++++-
src/beeper_desktop_api/resources/messages.py | 19 +++--
src/beeper_desktop_api/types/__init__.py | 1 +
.../types/message_search_response.py | 34 ++++++++
tests/api_resources/test_messages.py | 17 ++--
8 files changed, 121 insertions(+), 102 deletions(-)
create mode 100644 src/beeper_desktop_api/types/message_search_response.py
diff --git a/.stats.yml b/.stats.yml
index 103ad16..134452d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml
openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef
-config_hash: 738402ade5ac9528c8ef1677aa1d70f7
+config_hash: 4fb2010b528ce4358300ddd10e750265
diff --git a/README.md b/README.md
index 9f0e7ba..0f8fafb 100644
--- a/README.md
+++ b/README.md
@@ -118,85 +118,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
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`.
-## Pagination
-
-List methods in the Beeper Desktop API are paginated.
-
-This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
-
-```python
-from beeper_desktop_api import BeeperDesktop
-
-client = BeeperDesktop()
-
-all_messages = []
-# Automatically fetches more pages as needed.
-for message in client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
-):
- # Do something with message here
- all_messages.append(message)
-print(all_messages)
-```
-
-Or, asynchronously:
-
-```python
-import asyncio
-from beeper_desktop_api import AsyncBeeperDesktop
-
-client = AsyncBeeperDesktop()
-
-
-async def main() -> None:
- all_messages = []
- # Iterate through items across all pages, issuing requests as needed.
- async for message in client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
- ):
- all_messages.append(message)
- print(all_messages)
-
-
-asyncio.run(main())
-```
-
-Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
-
-```python
-first_page = await client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
-)
-if first_page.has_next_page():
- print(f"will fetch next page using these details: {first_page.next_page_info()}")
- next_page = await first_page.get_next_page()
- print(f"number of items we just fetched: {len(next_page.items)}")
-
-# Remove `await` for non-async usage.
-```
-
-Or just work directly with the returned data:
-
-```python
-first_page = await client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
-)
-
-print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..."
-for message in first_page.items:
- print(message.id)
-
-# Remove `await` for non-async usage.
-```
-
## Nested params
Nested parameters are dictionaries, typed using `TypedDict`, for example:
diff --git a/api.md b/api.md
index 529a147..e862d8e 100644
--- a/api.md
+++ b/api.md
@@ -70,11 +70,11 @@ Methods:
Types:
```python
-from beeper_desktop_api.types import MessageSendResponse
+from beeper_desktop_api.types import MessageSearchResponse, MessageSendResponse
```
Methods:
- client.messages.list(\*\*params) -> SyncCursor[Message]
-- client.messages.search(\*\*params) -> SyncCursor[Message]
+- client.messages.search(\*\*params) -> MessageSearchResponse
- client.messages.send(\*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py
index 4606312..806a7a0 100644
--- a/src/beeper_desktop_api/pagination.py
+++ b/src/beeper_desktop_api/pagination.py
@@ -1,13 +1,14 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Generic, TypeVar, Optional
+from typing import Dict, List, Generic, TypeVar, Optional
from typing_extensions import override
from pydantic import Field as FieldInfo
+from .types.chat import Chat
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["SyncCursor", "AsyncCursor"]
+__all__ = ["SyncCursor", "AsyncCursor", "SyncCursorWithChats", "AsyncCursorWithChats"]
_T = TypeVar("_T")
@@ -70,3 +71,65 @@ def next_page_info(self) -> Optional[PageInfo]:
return None
return PageInfo(params={"cursor": oldest_cursor})
+
+
+class SyncCursorWithChats(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ chats: Optional[Dict[str, Chat]] = None
+ has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
+ oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
+ newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ oldest_cursor = self.oldest_cursor
+ if not oldest_cursor:
+ return None
+
+ return PageInfo(params={"cursor": oldest_cursor})
+
+
+class AsyncCursorWithChats(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ chats: Optional[Dict[str, Chat]] = None
+ has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
+ oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
+ newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ oldest_cursor = self.oldest_cursor
+ if not oldest_cursor:
+ return None
+
+ return PageInfo(params={"cursor": oldest_cursor})
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index 485a86b..d7d40ef 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -23,6 +23,7 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.shared.message import Message
from ..types.message_send_response import MessageSendResponse
+from ..types.message_search_response import MessageSearchResponse
__all__ = ["MessagesResource", "AsyncMessagesResource"]
@@ -129,7 +130,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursor[Message]:
+ ) -> MessageSearchResponse:
"""
Search messages across chats using Beeper's message index
@@ -179,9 +180,8 @@ def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get_api_list(
+ return self._get(
"/v1/messages/search",
- page=SyncCursor[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -206,7 +206,7 @@ def search(
message_search_params.MessageSearchParams,
),
),
- model=Message,
+ cast_to=MessageSearchResponse,
)
def send(
@@ -339,7 +339,7 @@ def list(
model=Message,
)
- def search(
+ async def search(
self,
*,
account_ids: SequenceNotStr[str] | Omit = omit,
@@ -361,7 +361,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Message, AsyncCursor[Message]]:
+ ) -> MessageSearchResponse:
"""
Search messages across chats using Beeper's message index
@@ -411,15 +411,14 @@ def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get_api_list(
+ return await self._get(
"/v1/messages/search",
- page=AsyncCursor[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform(
+ query=await async_maybe_transform(
{
"account_ids": account_ids,
"chat_ids": chat_ids,
@@ -438,7 +437,7 @@ def search(
message_search_params.MessageSearchParams,
),
),
- model=Message,
+ cast_to=MessageSearchResponse,
)
async def send(
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index e577cbf..83e9ef1 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -31,4 +31,5 @@
from .message_send_response import MessageSendResponse as MessageSendResponse
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
+from .message_search_response import MessageSearchResponse as MessageSearchResponse
from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
diff --git a/src/beeper_desktop_api/types/message_search_response.py b/src/beeper_desktop_api/types/message_search_response.py
new file mode 100644
index 0000000..51f3d6f
--- /dev/null
+++ b/src/beeper_desktop_api/types/message_search_response.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Optional
+
+from pydantic import Field as FieldInfo
+
+from .chat import Chat
+from .._models import BaseModel
+from .shared.message import Message
+
+__all__ = ["MessageSearchResponse"]
+
+
+class MessageSearchResponse(BaseModel):
+ chats: Dict[str, Chat]
+ """Map of chatID -> chat details for chats referenced in items."""
+
+ has_more: bool = FieldInfo(alias="hasMore")
+ """True if additional results can be fetched using the provided cursors."""
+
+ items: List[Message]
+ """Messages matching the query and filters."""
+
+ newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
+ """Cursor for fetching newer results (use with direction='after').
+
+ Opaque string; do not inspect.
+ """
+
+ oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
+ """Cursor for fetching older results (use with direction='before').
+
+ Opaque string; do not inspect.
+ """
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index 85ebebb..7853ea0 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -11,6 +11,7 @@
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
from beeper_desktop_api.types import (
MessageSendResponse,
+ MessageSearchResponse,
)
from beeper_desktop_api._utils import parse_datetime
from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
@@ -66,7 +67,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
@parametrize
def test_method_search(self, client: BeeperDesktop) -> None:
message = client.messages.search()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
@parametrize
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:
query="dinner",
sender="me",
)
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
@parametrize
def test_raw_response_search(self, client: BeeperDesktop) -> None:
@@ -98,7 +99,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
@parametrize
def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@@ -107,7 +108,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -201,7 +202,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
@parametrize
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.search()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
@parametrize
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
query="dinner",
sender="me",
)
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
@parametrize
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
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
@parametrize
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)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(MessageSearchResponse, message, path=["response"])
assert cast(Any, response.is_closed) is True
From 48b4b7f01064d016b84e954f9aa9f327863cc1d3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 17:35:45 +0000
Subject: [PATCH 12/20] feat(api): manual updates
---
.stats.yml | 6 +-
README.md | 79 +++++++++++++++++++
api.md | 4 +-
src/beeper_desktop_api/pagination.py | 67 +---------------
src/beeper_desktop_api/resources/messages.py | 19 ++---
src/beeper_desktop_api/types/__init__.py | 1 -
.../types/message_search_response.py | 34 --------
tests/api_resources/test_messages.py | 17 ++--
8 files changed, 104 insertions(+), 123 deletions(-)
delete mode 100644 src/beeper_desktop_api/types/message_search_response.py
diff --git a/.stats.yml b/.stats.yml
index 134452d..8831e71 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml
-openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef
-config_hash: 4fb2010b528ce4358300ddd10e750265
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml
+openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765
+config_hash: 738402ade5ac9528c8ef1677aa1d70f7
diff --git a/README.md b/README.md
index 0f8fafb..9f0e7ba 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,85 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
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`.
+## Pagination
+
+List methods in the Beeper Desktop API are paginated.
+
+This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
+
+```python
+from beeper_desktop_api import BeeperDesktop
+
+client = BeeperDesktop()
+
+all_messages = []
+# Automatically fetches more pages as needed.
+for message in client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+):
+ # Do something with message here
+ all_messages.append(message)
+print(all_messages)
+```
+
+Or, asynchronously:
+
+```python
+import asyncio
+from beeper_desktop_api import AsyncBeeperDesktop
+
+client = AsyncBeeperDesktop()
+
+
+async def main() -> None:
+ all_messages = []
+ # Iterate through items across all pages, issuing requests as needed.
+ async for message in client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+ ):
+ all_messages.append(message)
+ print(all_messages)
+
+
+asyncio.run(main())
+```
+
+Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
+
+```python
+first_page = await client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+)
+if first_page.has_next_page():
+ print(f"will fetch next page using these details: {first_page.next_page_info()}")
+ next_page = await first_page.get_next_page()
+ print(f"number of items we just fetched: {len(next_page.items)}")
+
+# Remove `await` for non-async usage.
+```
+
+Or just work directly with the returned data:
+
+```python
+first_page = await client.messages.search(
+ account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
+ limit=10,
+ query="deployment",
+)
+
+print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..."
+for message in first_page.items:
+ print(message.id)
+
+# Remove `await` for non-async usage.
+```
+
## Nested params
Nested parameters are dictionaries, typed using `TypedDict`, for example:
diff --git a/api.md b/api.md
index e862d8e..529a147 100644
--- a/api.md
+++ b/api.md
@@ -70,11 +70,11 @@ Methods:
Types:
```python
-from beeper_desktop_api.types import MessageSearchResponse, MessageSendResponse
+from beeper_desktop_api.types import MessageSendResponse
```
Methods:
- client.messages.list(\*\*params) -> SyncCursor[Message]
-- client.messages.search(\*\*params) -> MessageSearchResponse
+- client.messages.search(\*\*params) -> SyncCursor[Message]
- client.messages.send(\*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py
index 806a7a0..4606312 100644
--- a/src/beeper_desktop_api/pagination.py
+++ b/src/beeper_desktop_api/pagination.py
@@ -1,14 +1,13 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, List, Generic, TypeVar, Optional
+from typing import List, Generic, TypeVar, Optional
from typing_extensions import override
from pydantic import Field as FieldInfo
-from .types.chat import Chat
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["SyncCursor", "AsyncCursor", "SyncCursorWithChats", "AsyncCursorWithChats"]
+__all__ = ["SyncCursor", "AsyncCursor"]
_T = TypeVar("_T")
@@ -71,65 +70,3 @@ def next_page_info(self) -> Optional[PageInfo]:
return None
return PageInfo(params={"cursor": oldest_cursor})
-
-
-class SyncCursorWithChats(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
- items: List[_T]
- chats: Optional[Dict[str, Chat]] = None
- has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
- oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
- newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
-
- @override
- def _get_page_items(self) -> List[_T]:
- items = self.items
- if not items:
- return []
- return items
-
- @override
- def has_next_page(self) -> bool:
- has_more = self.has_more
- if has_more is not None and has_more is False:
- return False
-
- return super().has_next_page()
-
- @override
- def next_page_info(self) -> Optional[PageInfo]:
- oldest_cursor = self.oldest_cursor
- if not oldest_cursor:
- return None
-
- return PageInfo(params={"cursor": oldest_cursor})
-
-
-class AsyncCursorWithChats(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
- items: List[_T]
- chats: Optional[Dict[str, Chat]] = None
- has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
- oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
- newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
-
- @override
- def _get_page_items(self) -> List[_T]:
- items = self.items
- if not items:
- return []
- return items
-
- @override
- def has_next_page(self) -> bool:
- has_more = self.has_more
- if has_more is not None and has_more is False:
- return False
-
- return super().has_next_page()
-
- @override
- def next_page_info(self) -> Optional[PageInfo]:
- oldest_cursor = self.oldest_cursor
- if not oldest_cursor:
- return None
-
- return PageInfo(params={"cursor": oldest_cursor})
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index d7d40ef..485a86b 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -23,7 +23,6 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.shared.message import Message
from ..types.message_send_response import MessageSendResponse
-from ..types.message_search_response import MessageSearchResponse
__all__ = ["MessagesResource", "AsyncMessagesResource"]
@@ -130,7 +129,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> MessageSearchResponse:
+ ) -> SyncCursor[Message]:
"""
Search messages across chats using Beeper's message index
@@ -180,8 +179,9 @@ def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get(
+ return self._get_api_list(
"/v1/messages/search",
+ page=SyncCursor[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -206,7 +206,7 @@ def search(
message_search_params.MessageSearchParams,
),
),
- cast_to=MessageSearchResponse,
+ model=Message,
)
def send(
@@ -339,7 +339,7 @@ def list(
model=Message,
)
- async def search(
+ def search(
self,
*,
account_ids: SequenceNotStr[str] | Omit = omit,
@@ -361,7 +361,7 @@ async def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> MessageSearchResponse:
+ ) -> AsyncPaginator[Message, AsyncCursor[Message]]:
"""
Search messages across chats using Beeper's message index
@@ -411,14 +411,15 @@ async def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return await self._get(
+ return self._get_api_list(
"/v1/messages/search",
+ page=AsyncCursor[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform(
+ query=maybe_transform(
{
"account_ids": account_ids,
"chat_ids": chat_ids,
@@ -437,7 +438,7 @@ async def search(
message_search_params.MessageSearchParams,
),
),
- cast_to=MessageSearchResponse,
+ model=Message,
)
async def send(
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index 83e9ef1..e577cbf 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -31,5 +31,4 @@
from .message_send_response import MessageSendResponse as MessageSendResponse
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
-from .message_search_response import MessageSearchResponse as MessageSearchResponse
from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
diff --git a/src/beeper_desktop_api/types/message_search_response.py b/src/beeper_desktop_api/types/message_search_response.py
deleted file mode 100644
index 51f3d6f..0000000
--- a/src/beeper_desktop_api/types/message_search_response.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Dict, List, Optional
-
-from pydantic import Field as FieldInfo
-
-from .chat import Chat
-from .._models import BaseModel
-from .shared.message import Message
-
-__all__ = ["MessageSearchResponse"]
-
-
-class MessageSearchResponse(BaseModel):
- chats: Dict[str, Chat]
- """Map of chatID -> chat details for chats referenced in items."""
-
- has_more: bool = FieldInfo(alias="hasMore")
- """True if additional results can be fetched using the provided cursors."""
-
- items: List[Message]
- """Messages matching the query and filters."""
-
- newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
- """Cursor for fetching newer results (use with direction='after').
-
- Opaque string; do not inspect.
- """
-
- oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
- """Cursor for fetching older results (use with direction='before').
-
- Opaque string; do not inspect.
- """
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index 7853ea0..85ebebb 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -11,7 +11,6 @@
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
from beeper_desktop_api.types import (
MessageSendResponse,
- MessageSearchResponse,
)
from beeper_desktop_api._utils import parse_datetime
from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
@@ -67,7 +66,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
@parametrize
def test_method_search(self, client: BeeperDesktop) -> None:
message = client.messages.search()
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
@parametrize
def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
@@ -90,7 +89,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
query="dinner",
sender="me",
)
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
@parametrize
def test_raw_response_search(self, client: BeeperDesktop) -> None:
@@ -99,7 +98,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
@parametrize
def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@@ -108,7 +107,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(SyncCursor[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -202,7 +201,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
@parametrize
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.search()
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
@parametrize
async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -225,7 +224,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk
query="dinner",
sender="me",
)
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
@parametrize
async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -234,7 +233,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
@parametrize
async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -243,7 +242,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(MessageSearchResponse, message, path=["response"])
+ assert_matches_type(AsyncCursor[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
From 2443524a37ad578bf8cb479e25c0b06b505547d9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:19:53 +0000
Subject: [PATCH 13/20] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 8831e71..baf1a99 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml
openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765
-config_hash: 738402ade5ac9528c8ef1677aa1d70f7
+config_hash: fc42f6a9efd6f34ca68f1c4328272acf
From d5cb6c2ee132bc3d558552df145082396c80521c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 21:17:25 +0000
Subject: [PATCH 14/20] feat(api): remove limit from list routes
---
.stats.yml | 6 +-
api.md | 8 +--
src/beeper_desktop_api/pagination.py | 66 ++++++++++++++++++-
.../resources/chats/chats.py | 26 +++-----
src/beeper_desktop_api/resources/messages.py | 30 ++++-----
.../types/chat_list_params.py | 3 -
.../types/message_list_params.py | 5 +-
tests/api_resources/test_chats.py | 36 +++++-----
tests/api_resources/test_messages.py | 36 +++++-----
9 files changed, 125 insertions(+), 91 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index baf1a99..c2693cc 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml
-openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765
-config_hash: fc42f6a9efd6f34ca68f1c4328272acf
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-a3fb0de6dd98f8a51d73e3fdf51de6143f2e8e764048246392624a56b4a3a481.yml
+openapi_spec_hash: 50e1001c340cb0bd3436b6329240769b
+config_hash: 2e31d02f28a11ef29eb747bcf559786a
diff --git a/api.md b/api.md
index 529a147..dd074eb 100644
--- a/api.md
+++ b/api.md
@@ -54,9 +54,9 @@ Methods:
- client.chats.create(\*\*params) -> ChatCreateResponse
- client.chats.retrieve(chat_id, \*\*params) -> Chat
-- client.chats.list(\*\*params) -> SyncCursor[ChatListResponse]
+- client.chats.list(\*\*params) -> SyncCursorList[ChatListResponse]
- client.chats.archive(chat_id, \*\*params) -> BaseResponse
-- client.chats.search(\*\*params) -> SyncCursor[Chat]
+- client.chats.search(\*\*params) -> SyncCursorSearch[Chat]
## Reminders
@@ -75,6 +75,6 @@ from beeper_desktop_api.types import MessageSendResponse
Methods:
-- client.messages.list(\*\*params) -> SyncCursor[Message]
-- client.messages.search(\*\*params) -> SyncCursor[Message]
+- client.messages.list(\*\*params) -> SyncCursorList[Message]
+- client.messages.search(\*\*params) -> SyncCursorSearch[Message]
- client.messages.send(\*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py
index 4606312..ee568dc 100644
--- a/src/beeper_desktop_api/pagination.py
+++ b/src/beeper_desktop_api/pagination.py
@@ -7,12 +7,12 @@
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["SyncCursor", "AsyncCursor"]
+__all__ = ["SyncCursorSearch", "AsyncCursorSearch", "SyncCursorList", "AsyncCursorList"]
_T = TypeVar("_T")
-class SyncCursor(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+class SyncCursorSearch(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
@@ -42,7 +42,67 @@ def next_page_info(self) -> Optional[PageInfo]:
return PageInfo(params={"cursor": oldest_cursor})
-class AsyncCursor(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+class AsyncCursorSearch(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
+ oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
+ newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ oldest_cursor = self.oldest_cursor
+ if not oldest_cursor:
+ return None
+
+ return PageInfo(params={"cursor": oldest_cursor})
+
+
+class SyncCursorList(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
+ oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
+ newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ oldest_cursor = self.oldest_cursor
+ if not oldest_cursor:
+ return None
+
+ return PageInfo(params={"cursor": oldest_cursor})
+
+
+class AsyncCursorList(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py
index 93587e7..7b8d06c 100644
--- a/src/beeper_desktop_api/resources/chats/chats.py
+++ b/src/beeper_desktop_api/resources/chats/chats.py
@@ -27,7 +27,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ...pagination import SyncCursor, AsyncCursor
+from ...pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
from ...types.chat import Chat
from ..._base_client import AsyncPaginator, make_request_options
from ...types.chat_list_response import ChatListResponse
@@ -173,14 +173,13 @@ def list(
account_ids: SequenceNotStr[str] | Omit = omit,
cursor: str | Omit = omit,
direction: Literal["after", "before"] | Omit = omit,
- limit: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursor[ChatListResponse]:
+ ) -> SyncCursorList[ChatListResponse]:
"""List all chats sorted by last activity (most recent first).
Combines all
@@ -195,8 +194,6 @@ def list(
direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
fetches newer results. Defaults to 'before' when only 'cursor' is provided.
- limit: Maximum number of chats to return (1–200). Defaults to 50.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -207,7 +204,7 @@ def list(
"""
return self._get_api_list(
"/v1/chats",
- page=SyncCursor[ChatListResponse],
+ page=SyncCursorList[ChatListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -218,7 +215,6 @@ def list(
"account_ids": account_ids,
"cursor": cursor,
"direction": direction,
- "limit": limit,
},
chat_list_params.ChatListParams,
),
@@ -289,7 +285,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursor[Chat]:
+ ) -> SyncCursorSearch[Chat]:
"""
Search chats by title/network or participants using Beeper Desktop's renderer
algorithm.
@@ -338,7 +334,7 @@ def search(
"""
return self._get_api_list(
"/v1/chats/search",
- page=SyncCursor[Chat],
+ page=SyncCursorSearch[Chat],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -502,14 +498,13 @@ def list(
account_ids: SequenceNotStr[str] | Omit = omit,
cursor: str | Omit = omit,
direction: Literal["after", "before"] | Omit = omit,
- limit: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[ChatListResponse, AsyncCursor[ChatListResponse]]:
+ ) -> AsyncPaginator[ChatListResponse, AsyncCursorList[ChatListResponse]]:
"""List all chats sorted by last activity (most recent first).
Combines all
@@ -524,8 +519,6 @@ def list(
direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
fetches newer results. Defaults to 'before' when only 'cursor' is provided.
- limit: Maximum number of chats to return (1–200). Defaults to 50.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -536,7 +529,7 @@ def list(
"""
return self._get_api_list(
"/v1/chats",
- page=AsyncCursor[ChatListResponse],
+ page=AsyncCursorList[ChatListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -547,7 +540,6 @@ def list(
"account_ids": account_ids,
"cursor": cursor,
"direction": direction,
- "limit": limit,
},
chat_list_params.ChatListParams,
),
@@ -618,7 +610,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Chat, AsyncCursor[Chat]]:
+ ) -> AsyncPaginator[Chat, AsyncCursorSearch[Chat]]:
"""
Search chats by title/network or participants using Beeper Desktop's renderer
algorithm.
@@ -667,7 +659,7 @@ def search(
"""
return self._get_api_list(
"/v1/chats/search",
- page=AsyncCursor[Chat],
+ page=AsyncCursorSearch[Chat],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index 485a86b..3732473 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -19,7 +19,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncCursor, AsyncCursor
+from ..pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
from .._base_client import AsyncPaginator, make_request_options
from ..types.shared.message import Message
from ..types.message_send_response import MessageSendResponse
@@ -55,20 +55,19 @@ def list(
chat_id: str,
cursor: str | Omit = omit,
direction: Literal["after", "before"] | Omit = omit,
- limit: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursor[Message]:
+ ) -> SyncCursorList[Message]:
"""List all messages in a chat with cursor-based pagination.
Sorted by timestamp.
Args:
- chat_id: The chat ID to list messages from
+ chat_id: Chat ID to list messages from
cursor: Message cursor for pagination. Use with direction to navigate results.
@@ -76,8 +75,6 @@ def list(
'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
provided.
- limit: Maximum number of messages to return (1–500). Defaults to 50.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -88,7 +85,7 @@ def list(
"""
return self._get_api_list(
"/v1/messages",
- page=SyncCursor[Message],
+ page=SyncCursorList[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -99,7 +96,6 @@ def list(
"chat_id": chat_id,
"cursor": cursor,
"direction": direction,
- "limit": limit,
},
message_list_params.MessageListParams,
),
@@ -129,7 +125,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursor[Message]:
+ ) -> SyncCursorSearch[Message]:
"""
Search messages across chats using Beeper's message index
@@ -181,7 +177,7 @@ def search(
"""
return self._get_api_list(
"/v1/messages/search",
- page=SyncCursor[Message],
+ page=SyncCursorSearch[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -287,20 +283,19 @@ def list(
chat_id: str,
cursor: str | Omit = omit,
direction: Literal["after", "before"] | Omit = omit,
- limit: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Message, AsyncCursor[Message]]:
+ ) -> AsyncPaginator[Message, AsyncCursorList[Message]]:
"""List all messages in a chat with cursor-based pagination.
Sorted by timestamp.
Args:
- chat_id: The chat ID to list messages from
+ chat_id: Chat ID to list messages from
cursor: Message cursor for pagination. Use with direction to navigate results.
@@ -308,8 +303,6 @@ def list(
'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
provided.
- limit: Maximum number of messages to return (1–500). Defaults to 50.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -320,7 +313,7 @@ def list(
"""
return self._get_api_list(
"/v1/messages",
- page=AsyncCursor[Message],
+ page=AsyncCursorList[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -331,7 +324,6 @@ def list(
"chat_id": chat_id,
"cursor": cursor,
"direction": direction,
- "limit": limit,
},
message_list_params.MessageListParams,
),
@@ -361,7 +353,7 @@ def search(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Message, AsyncCursor[Message]]:
+ ) -> AsyncPaginator[Message, AsyncCursorSearch[Message]]:
"""
Search messages across chats using Beeper's message index
@@ -413,7 +405,7 @@ def search(
"""
return self._get_api_list(
"/v1/messages/search",
- page=AsyncCursor[Message],
+ page=AsyncCursorSearch[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
diff --git a/src/beeper_desktop_api/types/chat_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py
index d8e1784..e1d10b2 100644
--- a/src/beeper_desktop_api/types/chat_list_params.py
+++ b/src/beeper_desktop_api/types/chat_list_params.py
@@ -25,6 +25,3 @@ class ChatListParams(TypedDict, total=False):
Pagination direction used with 'cursor': 'before' fetches older results, 'after'
fetches newer results. Defaults to 'before' when only 'cursor' is provided.
"""
-
- limit: int
- """Maximum number of chats to return (1–200). Defaults to 50."""
diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py
index ca56fab..2dd8438 100644
--- a/src/beeper_desktop_api/types/message_list_params.py
+++ b/src/beeper_desktop_api/types/message_list_params.py
@@ -11,7 +11,7 @@
class MessageListParams(TypedDict, total=False):
chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]]
- """The chat ID to list messages from"""
+ """Chat ID to list messages from"""
cursor: str
"""Message cursor for pagination. Use with direction to navigate results."""
@@ -22,6 +22,3 @@ class MessageListParams(TypedDict, total=False):
'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
provided.
"""
-
- limit: int
- """Maximum number of messages to return (1–500). Defaults to 50."""
diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py
index 3009cc9..352bc2f 100644
--- a/tests/api_resources/test_chats.py
+++ b/tests/api_resources/test_chats.py
@@ -15,7 +15,7 @@
ChatCreateResponse,
)
from beeper_desktop_api._utils import parse_datetime
-from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
+from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
from beeper_desktop_api.types.shared import BaseResponse
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -121,7 +121,7 @@ def test_path_params_retrieve(self, client: BeeperDesktop) -> None:
@parametrize
def test_method_list(self, client: BeeperDesktop) -> None:
chat = client.chats.list()
- assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
@@ -133,9 +133,8 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
],
cursor="1725489123456",
direction="before",
- limit=1,
)
- assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"])
@parametrize
def test_raw_response_list(self, client: BeeperDesktop) -> None:
@@ -144,7 +143,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = response.parse()
- assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"])
@parametrize
def test_streaming_response_list(self, client: BeeperDesktop) -> None:
@@ -153,7 +152,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = response.parse()
- assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -206,7 +205,7 @@ def test_path_params_archive(self, client: BeeperDesktop) -> None:
@parametrize
def test_method_search(self, client: BeeperDesktop) -> None:
chat = client.chats.search()
- assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
@parametrize
def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
@@ -227,7 +226,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
type="single",
unread_only=True,
)
- assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
@parametrize
def test_raw_response_search(self, client: BeeperDesktop) -> None:
@@ -236,7 +235,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = response.parse()
- assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
@parametrize
def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@@ -245,7 +244,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = response.parse()
- assert_matches_type(SyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -352,7 +351,7 @@ async def test_path_params_retrieve(self, async_client: AsyncBeeperDesktop) -> N
@parametrize
async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
chat = await async_client.chats.list()
- assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -364,9 +363,8 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto
],
cursor="1725489123456",
direction="before",
- limit=1,
)
- assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -375,7 +373,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = await response.parse()
- assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -384,7 +382,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = await response.parse()
- assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"])
+ assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -437,7 +435,7 @@ async def test_path_params_archive(self, async_client: AsyncBeeperDesktop) -> No
@parametrize
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
chat = await async_client.chats.search()
- assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
@parametrize
async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -458,7 +456,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk
type="single",
unread_only=True,
)
- assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
@parametrize
async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -467,7 +465,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = await response.parse()
- assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
@parametrize
async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -476,6 +474,6 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
chat = await response.parse()
- assert_matches_type(AsyncCursor[Chat], chat, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index 85ebebb..3f36468 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -13,7 +13,7 @@
MessageSendResponse,
)
from beeper_desktop_api._utils import parse_datetime
-from beeper_desktop_api.pagination import SyncCursor, AsyncCursor
+from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
from beeper_desktop_api.types.shared import Message
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -27,7 +27,7 @@ def test_method_list(self, client: BeeperDesktop) -> None:
message = client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
)
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
@@ -35,9 +35,8 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
cursor="821744079",
direction="before",
- limit=1,
)
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
@parametrize
def test_raw_response_list(self, client: BeeperDesktop) -> None:
@@ -48,7 +47,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
@parametrize
def test_streaming_response_list(self, client: BeeperDesktop) -> None:
@@ -59,14 +58,14 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
@parametrize
def test_method_search(self, client: BeeperDesktop) -> None:
message = client.messages.search()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorSearch[Message], message, path=["response"])
@parametrize
def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
@@ -89,7 +88,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
query="dinner",
sender="me",
)
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorSearch[Message], message, path=["response"])
@parametrize
def test_raw_response_search(self, client: BeeperDesktop) -> None:
@@ -98,7 +97,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorSearch[Message], message, path=["response"])
@parametrize
def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@@ -107,7 +106,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursor[Message], message, path=["response"])
+ assert_matches_type(SyncCursorSearch[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -162,7 +161,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
)
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -170,9 +169,8 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
cursor="821744079",
direction="before",
- limit=1,
)
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -183,7 +181,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -194,14 +192,14 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
@parametrize
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.search()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Message], message, path=["response"])
@parametrize
async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -224,7 +222,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk
query="dinner",
sender="me",
)
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Message], message, path=["response"])
@parametrize
async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -233,7 +231,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Message], message, path=["response"])
@parametrize
async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
@@ -242,7 +240,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursor[Message], message, path=["response"])
+ assert_matches_type(AsyncCursorSearch[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
From dce712498ff2678222fd203118e7bb91f13ccfc5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:00:13 +0000
Subject: [PATCH 15/20] feat(api): manual updates
---
.stats.yml | 4 +-
api.md | 4 +-
src/beeper_desktop_api/resources/messages.py | 31 ++++++-------
src/beeper_desktop_api/types/__init__.py | 1 +
.../types/message_list_response.py | 21 +++++++++
.../types/message_search_params.py | 6 +--
.../types/message_send_params.py | 4 +-
tests/api_resources/test_messages.py | 43 +++++++------------
8 files changed, 59 insertions(+), 55 deletions(-)
create mode 100644 src/beeper_desktop_api/types/message_list_response.py
diff --git a/.stats.yml b/.stats.yml
index c2693cc..7d02041 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-a3fb0de6dd98f8a51d73e3fdf51de6143f2e8e764048246392624a56b4a3a481.yml
-openapi_spec_hash: 50e1001c340cb0bd3436b6329240769b
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-100e7052e74644026f594642a424e04ab306d44e6c73a1f4761cf8a7d7ee0d8f.yml
+openapi_spec_hash: 3437145a74c032f2319a235bf40baa88
config_hash: 2e31d02f28a11ef29eb747bcf559786a
diff --git a/api.md b/api.md
index dd074eb..d2a744f 100644
--- a/api.md
+++ b/api.md
@@ -70,11 +70,11 @@ Methods:
Types:
```python
-from beeper_desktop_api.types import MessageSendResponse
+from beeper_desktop_api.types import MessageListResponse, MessageSendResponse
```
Methods:
-- client.messages.list(\*\*params) -> SyncCursorList[Message]
+- client.messages.list(\*\*params) -> MessageListResponse
- client.messages.search(\*\*params) -> SyncCursorSearch[Message]
- client.messages.send(\*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index 3732473..1a3bc64 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -19,9 +19,10 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
+from ..pagination import SyncCursorSearch, AsyncCursorSearch
from .._base_client import AsyncPaginator, make_request_options
from ..types.shared.message import Message
+from ..types.message_list_response import MessageListResponse
from ..types.message_send_response import MessageSendResponse
__all__ = ["MessagesResource", "AsyncMessagesResource"]
@@ -61,7 +62,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursorList[Message]:
+ ) -> MessageListResponse:
"""List all messages in a chat with cursor-based pagination.
Sorted by timestamp.
@@ -83,9 +84,8 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get_api_list(
+ return self._get(
"/v1/messages",
- page=SyncCursorList[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -100,7 +100,7 @@ def list(
message_list_params.MessageListParams,
),
),
- model=Message,
+ cast_to=MessageListResponse,
)
def search(
@@ -153,8 +153,7 @@ def search(
include_muted: Include messages in chats marked as Muted by the user, which are usually less
important. Default: true. Set to false if the user wants a more refined search.
- limit: Maximum number of messages to return (1–500). Defaults to 20. The current
- implementation caps each page at 20 items even if a higher limit is requested.
+ limit: Maximum number of messages to return.
media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact
types like ['video', 'image']. Omit for no media filtering.
@@ -208,7 +207,7 @@ def search(
def send(
self,
*,
- chat_id: str,
+ chat_id: str | Omit = omit,
reply_to_message_id: str | Omit = omit,
text: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -277,7 +276,7 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse:
"""
return AsyncMessagesResourceWithStreamingResponse(self)
- def list(
+ async def list(
self,
*,
chat_id: str,
@@ -289,7 +288,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Message, AsyncCursorList[Message]]:
+ ) -> MessageListResponse:
"""List all messages in a chat with cursor-based pagination.
Sorted by timestamp.
@@ -311,15 +310,14 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get_api_list(
+ return await self._get(
"/v1/messages",
- page=AsyncCursorList[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform(
+ query=await async_maybe_transform(
{
"chat_id": chat_id,
"cursor": cursor,
@@ -328,7 +326,7 @@ def list(
message_list_params.MessageListParams,
),
),
- model=Message,
+ cast_to=MessageListResponse,
)
def search(
@@ -381,8 +379,7 @@ def search(
include_muted: Include messages in chats marked as Muted by the user, which are usually less
important. Default: true. Set to false if the user wants a more refined search.
- limit: Maximum number of messages to return (1–500). Defaults to 20. The current
- implementation caps each page at 20 items even if a higher limit is requested.
+ limit: Maximum number of messages to return.
media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact
types like ['video', 'image']. Omit for no media filtering.
@@ -436,7 +433,7 @@ def search(
async def send(
self,
*,
- chat_id: str,
+ chat_id: str | Omit = omit,
reply_to_message_id: str | Omit = omit,
text: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index e577cbf..28eab5d 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -27,6 +27,7 @@
from .client_search_params import ClientSearchParams as ClientSearchParams
from .account_list_response import AccountListResponse as AccountListResponse
from .contact_search_params import ContactSearchParams as ContactSearchParams
+from .message_list_response import MessageListResponse as MessageListResponse
from .message_search_params import MessageSearchParams as MessageSearchParams
from .message_send_response import MessageSendResponse as MessageSendResponse
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
diff --git a/src/beeper_desktop_api/types/message_list_response.py b/src/beeper_desktop_api/types/message_list_response.py
new file mode 100644
index 0000000..a66746f
--- /dev/null
+++ b/src/beeper_desktop_api/types/message_list_response.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .shared.message import Message
+
+__all__ = ["MessageListResponse"]
+
+
+class MessageListResponse(BaseModel):
+ has_more: bool = FieldInfo(alias="hasMore")
+ """True if additional results can be fetched."""
+
+ items: List[Message]
+ """Messages from the chat, sorted by timestamp.
+
+ Use message.sortKey as cursor for pagination.
+ """
diff --git a/src/beeper_desktop_api/types/message_search_params.py b/src/beeper_desktop_api/types/message_search_params.py
index 650775f..93fbd63 100644
--- a/src/beeper_desktop_api/types/message_search_params.py
+++ b/src/beeper_desktop_api/types/message_search_params.py
@@ -56,11 +56,7 @@ class MessageSearchParams(TypedDict, total=False):
"""
limit: int
- """Maximum number of messages to return (1–500).
-
- Defaults to 20. The current implementation caps each page at 20 items even if a
- higher limit is requested.
- """
+ """Maximum number of messages to return."""
media_types: Annotated[List[Literal["any", "video", "image", "link", "file"]], PropertyInfo(alias="mediaTypes")]
"""Filter messages by media types.
diff --git a/src/beeper_desktop_api/types/message_send_params.py b/src/beeper_desktop_api/types/message_send_params.py
index 8b05d6a..b165b27 100644
--- a/src/beeper_desktop_api/types/message_send_params.py
+++ b/src/beeper_desktop_api/types/message_send_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing_extensions import Required, Annotated, TypedDict
+from typing_extensions import Annotated, TypedDict
from .._utils import PropertyInfo
@@ -10,7 +10,7 @@
class MessageSendParams(TypedDict, total=False):
- chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]]
+ chat_id: Annotated[str, PropertyInfo(alias="chatID")]
"""Unique identifier of the chat."""
reply_to_message_id: Annotated[str, PropertyInfo(alias="replyToMessageID")]
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index 3f36468..302e146 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -10,10 +10,11 @@
from tests.utils import assert_matches_type
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
from beeper_desktop_api.types import (
+ MessageListResponse,
MessageSendResponse,
)
from beeper_desktop_api._utils import parse_datetime
-from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
+from beeper_desktop_api.pagination import SyncCursorSearch, AsyncCursorSearch
from beeper_desktop_api.types.shared import Message
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -27,7 +28,7 @@ def test_method_list(self, client: BeeperDesktop) -> None:
message = client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
)
- assert_matches_type(SyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
@@ -36,7 +37,7 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
cursor="821744079",
direction="before",
)
- assert_matches_type(SyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
@parametrize
def test_raw_response_list(self, client: BeeperDesktop) -> None:
@@ -47,7 +48,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
@parametrize
def test_streaming_response_list(self, client: BeeperDesktop) -> None:
@@ -58,7 +59,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(SyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -112,9 +113,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@parametrize
def test_method_send(self, client: BeeperDesktop) -> None:
- message = client.messages.send(
- chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- )
+ message = client.messages.send()
assert_matches_type(MessageSendResponse, message, path=["response"])
@parametrize
@@ -128,9 +127,7 @@ def test_method_send_with_all_params(self, client: BeeperDesktop) -> None:
@parametrize
def test_raw_response_send(self, client: BeeperDesktop) -> None:
- response = client.messages.with_raw_response.send(
- chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- )
+ response = client.messages.with_raw_response.send()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -139,9 +136,7 @@ def test_raw_response_send(self, client: BeeperDesktop) -> None:
@parametrize
def test_streaming_response_send(self, client: BeeperDesktop) -> None:
- with client.messages.with_streaming_response.send(
- chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- ) as response:
+ with client.messages.with_streaming_response.send() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -161,7 +156,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
)
- assert_matches_type(AsyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -170,7 +165,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto
cursor="821744079",
direction="before",
)
- assert_matches_type(AsyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -181,7 +176,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -192,7 +187,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(AsyncCursorList[Message], message, path=["response"])
+ assert_matches_type(MessageListResponse, message, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -246,9 +241,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
@parametrize
async def test_method_send(self, async_client: AsyncBeeperDesktop) -> None:
- message = await async_client.messages.send(
- chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- )
+ message = await async_client.messages.send()
assert_matches_type(MessageSendResponse, message, path=["response"])
@parametrize
@@ -262,9 +255,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncBeeperDeskto
@parametrize
async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.messages.with_raw_response.send(
- chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- )
+ response = await async_client.messages.with_raw_response.send()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -273,9 +264,7 @@ async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None
@parametrize
async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.messages.with_streaming_response.send(
- chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- ) as response:
+ async with async_client.messages.with_streaming_response.send() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
From 0d38dfa50d797ff879df6d5c633bbcb43c3a98fd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:05:05 +0000
Subject: [PATCH 16/20] chore: update SDK settings
---
.stats.yml | 2 +-
README.md | 11 ++++-------
2 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 7d02041..96af950 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-100e7052e74644026f594642a424e04ab306d44e6c73a1f4761cf8a7d7ee0d8f.yml
openapi_spec_hash: 3437145a74c032f2319a235bf40baa88
-config_hash: 2e31d02f28a11ef29eb747bcf559786a
+config_hash: 36b26d0d29548d4aa575fc337915ad42
diff --git a/README.md b/README.md
index 9f0e7ba..e20984e 100644
--- a/README.md
+++ b/README.md
@@ -14,13 +14,10 @@ The REST API documentation can be found on [developers.beeper.com](https://devel
## Installation
```sh
-# install from the production repo
-pip install git+ssh://git@github.com/beeper/desktop-api-python.git
+# install from PyPI
+pip install beeper_desktop_api
```
-> [!NOTE]
-> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install beeper_desktop_api`
-
## Usage
The full API of this library can be found in [api.md](api.md).
@@ -81,8 +78,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv
You can enable this by installing `aiohttp`:
```sh
-# install from the production repo
-pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/desktop-api-python.git'
+# install from PyPI
+pip install beeper_desktop_api[aiohttp]
```
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
From 0fcd71f9951498d349fb816b42dc21347f3ab5dc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Oct 2025 21:09:25 +0000
Subject: [PATCH 17/20] feat(api): manual updates
---
.stats.yml | 6 +-
README.md | 11 +--
api.md | 8 +--
src/beeper_desktop_api/pagination.py | 35 +++++----
src/beeper_desktop_api/resources/contacts.py | 28 +++-----
src/beeper_desktop_api/resources/messages.py | 49 +++++++------
src/beeper_desktop_api/types/__init__.py | 1 -
.../types/contact_search_params.py | 7 +-
.../types/message_list_params.py | 7 +-
.../types/message_list_response.py | 21 ------
.../types/message_send_params.py | 3 -
tests/api_resources/test_contacts.py | 16 +++++
tests/api_resources/test_messages.py | 71 ++++++++++++++-----
13 files changed, 147 insertions(+), 116 deletions(-)
delete mode 100644 src/beeper_desktop_api/types/message_list_response.py
diff --git a/.stats.yml b/.stats.yml
index 96af950..d065147 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-100e7052e74644026f594642a424e04ab306d44e6c73a1f4761cf8a7d7ee0d8f.yml
-openapi_spec_hash: 3437145a74c032f2319a235bf40baa88
-config_hash: 36b26d0d29548d4aa575fc337915ad42
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-f48e33c7d90ed6418a852f8d4d951d07b09f4f3f939feb395dc2aa03f522d81e.yml
+openapi_spec_hash: c516120ecf51bb8425b3b9ed76c6423a
+config_hash: c5ac9bd5889d27aa168f06d6d0fef0b3
diff --git a/README.md b/README.md
index e20984e..9f0e7ba 100644
--- a/README.md
+++ b/README.md
@@ -14,10 +14,13 @@ The REST API documentation can be found on [developers.beeper.com](https://devel
## Installation
```sh
-# install from PyPI
-pip install beeper_desktop_api
+# install from the production repo
+pip install git+ssh://git@github.com/beeper/desktop-api-python.git
```
+> [!NOTE]
+> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install beeper_desktop_api`
+
## Usage
The full API of this library can be found in [api.md](api.md).
@@ -78,8 +81,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv
You can enable this by installing `aiohttp`:
```sh
-# install from PyPI
-pip install beeper_desktop_api[aiohttp]
+# install from the production repo
+pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/desktop-api-python.git'
```
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
diff --git a/api.md b/api.md
index d2a744f..4fbc194 100644
--- a/api.md
+++ b/api.md
@@ -40,7 +40,7 @@ from beeper_desktop_api.types import ContactSearchResponse
Methods:
-- client.contacts.search(\*\*params) -> ContactSearchResponse
+- client.contacts.search(account_id, \*\*params) -> ContactSearchResponse
# Chats
@@ -70,11 +70,11 @@ Methods:
Types:
```python
-from beeper_desktop_api.types import MessageListResponse, MessageSendResponse
+from beeper_desktop_api.types import MessageSendResponse
```
Methods:
-- client.messages.list(\*\*params) -> MessageListResponse
+- client.messages.list(chat_id, \*\*params) -> SyncCursorList[Message]
- client.messages.search(\*\*params) -> SyncCursorSearch[Message]
-- client.messages.send(\*\*params) -> MessageSendResponse
+- client.messages.send(chat_id, \*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py
index ee568dc..7fd745f 100644
--- a/src/beeper_desktop_api/pagination.py
+++ b/src/beeper_desktop_api/pagination.py
@@ -1,7 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Generic, TypeVar, Optional
-from typing_extensions import override
+from typing import Any, List, Generic, TypeVar, Optional, cast
+from typing_extensions import Protocol, override, runtime_checkable
from pydantic import Field as FieldInfo
@@ -12,6 +12,11 @@
_T = TypeVar("_T")
+@runtime_checkable
+class CursorListItem(Protocol):
+ sort_key: Optional[str]
+
+
class SyncCursorSearch(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
@@ -75,8 +80,6 @@ def next_page_info(self) -> Optional[PageInfo]:
class SyncCursorList(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
- oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
- newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
@override
def _get_page_items(self) -> List[_T]:
@@ -95,18 +98,21 @@ def has_next_page(self) -> bool:
@override
def next_page_info(self) -> Optional[PageInfo]:
- oldest_cursor = self.oldest_cursor
- if not oldest_cursor:
+ items = self.items
+ if not items:
return None
- return PageInfo(params={"cursor": oldest_cursor})
+ item = cast(Any, items[-1])
+ if not isinstance(item, CursorListItem) or item.sort_key is None:
+ # TODO emit warning log
+ return None
+
+ return PageInfo(params={"cursor": item.sort_key})
class AsyncCursorList(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None)
- oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None)
- newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None)
@override
def _get_page_items(self) -> List[_T]:
@@ -125,8 +131,13 @@ def has_next_page(self) -> bool:
@override
def next_page_info(self) -> Optional[PageInfo]:
- oldest_cursor = self.oldest_cursor
- if not oldest_cursor:
+ items = self.items
+ if not items:
return None
- return PageInfo(params={"cursor": oldest_cursor})
+ item = cast(Any, items[-1])
+ if not isinstance(item, CursorListItem) or item.sort_key is None:
+ # TODO emit warning log
+ return None
+
+ return PageInfo(params={"cursor": item.sort_key})
diff --git a/src/beeper_desktop_api/resources/contacts.py b/src/beeper_desktop_api/resources/contacts.py
index db84950..50fc4f9 100644
--- a/src/beeper_desktop_api/resources/contacts.py
+++ b/src/beeper_desktop_api/resources/contacts.py
@@ -45,8 +45,8 @@ def with_streaming_response(self) -> ContactsResourceWithStreamingResponse:
def search(
self,
- *,
account_id: str,
+ *,
query: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -72,20 +72,16 @@ def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not account_id:
+ raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}")
return self._get(
- "/v1/contacts/search",
+ f"/v1/accounts/{account_id}/contacts/search",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform(
- {
- "account_id": account_id,
- "query": query,
- },
- contact_search_params.ContactSearchParams,
- ),
+ query=maybe_transform({"query": query}, contact_search_params.ContactSearchParams),
),
cast_to=ContactSearchResponse,
)
@@ -115,8 +111,8 @@ def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse:
async def search(
self,
- *,
account_id: str,
+ *,
query: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -142,20 +138,16 @@ async def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not account_id:
+ raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}")
return await self._get(
- "/v1/contacts/search",
+ f"/v1/accounts/{account_id}/contacts/search",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform(
- {
- "account_id": account_id,
- "query": query,
- },
- contact_search_params.ContactSearchParams,
- ),
+ query=await async_maybe_transform({"query": query}, contact_search_params.ContactSearchParams),
),
cast_to=ContactSearchResponse,
)
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index 1a3bc64..e77b8f1 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -19,10 +19,9 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncCursorSearch, AsyncCursorSearch
+from ..pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
from .._base_client import AsyncPaginator, make_request_options
from ..types.shared.message import Message
-from ..types.message_list_response import MessageListResponse
from ..types.message_send_response import MessageSendResponse
__all__ = ["MessagesResource", "AsyncMessagesResource"]
@@ -52,8 +51,8 @@ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse:
def list(
self,
- *,
chat_id: str,
+ *,
cursor: str | Omit = omit,
direction: Literal["after", "before"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -62,7 +61,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> MessageListResponse:
+ ) -> SyncCursorList[Message]:
"""List all messages in a chat with cursor-based pagination.
Sorted by timestamp.
@@ -84,8 +83,11 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get(
- "/v1/messages",
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return self._get_api_list(
+ f"/v1/chats/{chat_id}/messages",
+ page=SyncCursorList[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -93,14 +95,13 @@ def list(
timeout=timeout,
query=maybe_transform(
{
- "chat_id": chat_id,
"cursor": cursor,
"direction": direction,
},
message_list_params.MessageListParams,
),
),
- cast_to=MessageListResponse,
+ model=Message,
)
def search(
@@ -206,8 +207,8 @@ def search(
def send(
self,
+ chat_id: str,
*,
- chat_id: str | Omit = omit,
reply_to_message_id: str | Omit = omit,
text: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -237,11 +238,12 @@ def send(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
return self._post(
- "/v1/messages",
+ f"/v1/chats/{chat_id}/messages",
body=maybe_transform(
{
- "chat_id": chat_id,
"reply_to_message_id": reply_to_message_id,
"text": text,
},
@@ -276,10 +278,10 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse:
"""
return AsyncMessagesResourceWithStreamingResponse(self)
- async def list(
+ def list(
self,
- *,
chat_id: str,
+ *,
cursor: str | Omit = omit,
direction: Literal["after", "before"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -288,7 +290,7 @@ async def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> MessageListResponse:
+ ) -> AsyncPaginator[Message, AsyncCursorList[Message]]:
"""List all messages in a chat with cursor-based pagination.
Sorted by timestamp.
@@ -310,23 +312,25 @@ async def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return await self._get(
- "/v1/messages",
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
+ return self._get_api_list(
+ f"/v1/chats/{chat_id}/messages",
+ page=AsyncCursorList[Message],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform(
+ query=maybe_transform(
{
- "chat_id": chat_id,
"cursor": cursor,
"direction": direction,
},
message_list_params.MessageListParams,
),
),
- cast_to=MessageListResponse,
+ model=Message,
)
def search(
@@ -432,8 +436,8 @@ def search(
async def send(
self,
+ chat_id: str,
*,
- chat_id: str | Omit = omit,
reply_to_message_id: str | Omit = omit,
text: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -463,11 +467,12 @@ async def send(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not chat_id:
+ raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
return await self._post(
- "/v1/messages",
+ f"/v1/chats/{chat_id}/messages",
body=await async_maybe_transform(
{
- "chat_id": chat_id,
"reply_to_message_id": reply_to_message_id,
"text": text,
},
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index 28eab5d..e577cbf 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -27,7 +27,6 @@
from .client_search_params import ClientSearchParams as ClientSearchParams
from .account_list_response import AccountListResponse as AccountListResponse
from .contact_search_params import ContactSearchParams as ContactSearchParams
-from .message_list_response import MessageListResponse as MessageListResponse
from .message_search_params import MessageSearchParams as MessageSearchParams
from .message_send_response import MessageSendResponse as MessageSendResponse
from .contact_search_response import ContactSearchResponse as ContactSearchResponse
diff --git a/src/beeper_desktop_api/types/contact_search_params.py b/src/beeper_desktop_api/types/contact_search_params.py
index 53d052f..f9063e0 100644
--- a/src/beeper_desktop_api/types/contact_search_params.py
+++ b/src/beeper_desktop_api/types/contact_search_params.py
@@ -2,16 +2,11 @@
from __future__ import annotations
-from typing_extensions import Required, Annotated, TypedDict
-
-from .._utils import PropertyInfo
+from typing_extensions import Required, TypedDict
__all__ = ["ContactSearchParams"]
class ContactSearchParams(TypedDict, total=False):
- account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]]
- """Account ID this resource belongs to."""
-
query: Required[str]
"""Text to search users by. Network-specific behavior."""
diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py
index 2dd8438..d4e343a 100644
--- a/src/beeper_desktop_api/types/message_list_params.py
+++ b/src/beeper_desktop_api/types/message_list_params.py
@@ -2,17 +2,12 @@
from __future__ import annotations
-from typing_extensions import Literal, Required, Annotated, TypedDict
-
-from .._utils import PropertyInfo
+from typing_extensions import Literal, TypedDict
__all__ = ["MessageListParams"]
class MessageListParams(TypedDict, total=False):
- chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]]
- """Chat ID to list messages from"""
-
cursor: str
"""Message cursor for pagination. Use with direction to navigate results."""
diff --git a/src/beeper_desktop_api/types/message_list_response.py b/src/beeper_desktop_api/types/message_list_response.py
deleted file mode 100644
index a66746f..0000000
--- a/src/beeper_desktop_api/types/message_list_response.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import List
-
-from pydantic import Field as FieldInfo
-
-from .._models import BaseModel
-from .shared.message import Message
-
-__all__ = ["MessageListResponse"]
-
-
-class MessageListResponse(BaseModel):
- has_more: bool = FieldInfo(alias="hasMore")
- """True if additional results can be fetched."""
-
- items: List[Message]
- """Messages from the chat, sorted by timestamp.
-
- Use message.sortKey as cursor for pagination.
- """
diff --git a/src/beeper_desktop_api/types/message_send_params.py b/src/beeper_desktop_api/types/message_send_params.py
index b165b27..840e745 100644
--- a/src/beeper_desktop_api/types/message_send_params.py
+++ b/src/beeper_desktop_api/types/message_send_params.py
@@ -10,9 +10,6 @@
class MessageSendParams(TypedDict, total=False):
- chat_id: Annotated[str, PropertyInfo(alias="chatID")]
- """Unique identifier of the chat."""
-
reply_to_message_id: Annotated[str, PropertyInfo(alias="replyToMessageID")]
"""Provide a message ID to send this as a reply to an existing message"""
diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py
index 6308d1f..158b961 100644
--- a/tests/api_resources/test_contacts.py
+++ b/tests/api_resources/test_contacts.py
@@ -51,6 +51,14 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_search(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"):
+ client.contacts.with_raw_response.search(
+ account_id="",
+ query="x",
+ )
+
class TestAsyncContacts:
parametrize = pytest.mark.parametrize(
@@ -90,3 +98,11 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
assert_matches_type(ContactSearchResponse, contact, path=["response"])
assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_search(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"):
+ await async_client.contacts.with_raw_response.search(
+ account_id="",
+ query="x",
+ )
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index 302e146..dd93537 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -10,11 +10,10 @@
from tests.utils import assert_matches_type
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
from beeper_desktop_api.types import (
- MessageListResponse,
MessageSendResponse,
)
from beeper_desktop_api._utils import parse_datetime
-from beeper_desktop_api.pagination import SyncCursorSearch, AsyncCursorSearch
+from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
from beeper_desktop_api.types.shared import Message
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -28,7 +27,7 @@ def test_method_list(self, client: BeeperDesktop) -> None:
message = client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
)
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
@@ -37,7 +36,7 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
cursor="821744079",
direction="before",
)
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
@parametrize
def test_raw_response_list(self, client: BeeperDesktop) -> None:
@@ -48,7 +47,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
@parametrize
def test_streaming_response_list(self, client: BeeperDesktop) -> None:
@@ -59,10 +58,17 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = response.parse()
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(SyncCursorList[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_list(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ client.messages.with_raw_response.list(
+ chat_id="",
+ )
+
@parametrize
def test_method_search(self, client: BeeperDesktop) -> None:
message = client.messages.search()
@@ -113,7 +119,9 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None:
@parametrize
def test_method_send(self, client: BeeperDesktop) -> None:
- message = client.messages.send()
+ message = client.messages.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
assert_matches_type(MessageSendResponse, message, path=["response"])
@parametrize
@@ -127,7 +135,9 @@ def test_method_send_with_all_params(self, client: BeeperDesktop) -> None:
@parametrize
def test_raw_response_send(self, client: BeeperDesktop) -> None:
- response = client.messages.with_raw_response.send()
+ response = client.messages.with_raw_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -136,7 +146,9 @@ def test_raw_response_send(self, client: BeeperDesktop) -> None:
@parametrize
def test_streaming_response_send(self, client: BeeperDesktop) -> None:
- with client.messages.with_streaming_response.send() as response:
+ with client.messages.with_streaming_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -145,6 +157,13 @@ def test_streaming_response_send(self, client: BeeperDesktop) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_send(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ client.messages.with_raw_response.send(
+ chat_id="",
+ )
+
class TestAsyncMessages:
parametrize = pytest.mark.parametrize(
@@ -156,7 +175,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
)
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
@@ -165,7 +184,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto
cursor="821744079",
direction="before",
)
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -176,7 +195,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None:
@@ -187,10 +206,17 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
message = await response.parse()
- assert_matches_type(MessageListResponse, message, path=["response"])
+ assert_matches_type(AsyncCursorList[Message], message, path=["response"])
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ await async_client.messages.with_raw_response.list(
+ chat_id="",
+ )
+
@parametrize
async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.search()
@@ -241,7 +267,9 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop)
@parametrize
async def test_method_send(self, async_client: AsyncBeeperDesktop) -> None:
- message = await async_client.messages.send()
+ message = await async_client.messages.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
assert_matches_type(MessageSendResponse, message, path=["response"])
@parametrize
@@ -255,7 +283,9 @@ async def test_method_send_with_all_params(self, async_client: AsyncBeeperDeskto
@parametrize
async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.messages.with_raw_response.send()
+ response = await async_client.messages.with_raw_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -264,7 +294,9 @@ async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None
@parametrize
async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.messages.with_streaming_response.send() as response:
+ async with async_client.messages.with_streaming_response.send(
+ chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -272,3 +304,10 @@ async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -
assert_matches_type(MessageSendResponse, message, path=["response"])
assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_send(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"):
+ await async_client.messages.with_raw_response.send(
+ chat_id="",
+ )
From 86218ff03f8a0cd42050b0c3babdf78178fda3da Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Oct 2025 22:25:34 +0000
Subject: [PATCH 18/20] feat(api): manual updates
---
.stats.yml | 6 +-
README.md | 100 +----
api.md | 14 +-
src/beeper_desktop_api/_client.py | 62 +--
src/beeper_desktop_api/resources/__init__.py | 28 +-
.../resources/chats/chats.py | 231 +---------
.../resources/chats/reminders.py | 12 +-
src/beeper_desktop_api/resources/contacts.py | 189 ---------
src/beeper_desktop_api/resources/messages.py | 22 +-
src/beeper_desktop_api/resources/search.py | 397 ++++++++++++++++++
src/beeper_desktop_api/types/__init__.py | 10 +-
.../types/chat_list_params.py | 5 +-
..._open_params.py => client_focus_params.py} | 4 +-
.../{open_response.py => focus_response.py} | 4 +-
.../types/message_list_params.py | 7 +-
...earch_params.py => search_chats_params.py} | 15 +-
...ch_params.py => search_contacts_params.py} | 4 +-
...esponse.py => search_contacts_response.py} | 4 +-
.../types/shared/message.py | 11 +-
tests/api_resources/test_chats.py | 99 +----
tests/api_resources/test_client.py | 50 +--
tests/api_resources/test_contacts.py | 108 -----
tests/api_resources/test_messages.py | 4 +-
tests/api_resources/test_search.py | 202 +++++++++
24 files changed, 737 insertions(+), 851 deletions(-)
delete mode 100644 src/beeper_desktop_api/resources/contacts.py
create mode 100644 src/beeper_desktop_api/resources/search.py
rename src/beeper_desktop_api/types/{client_open_params.py => client_focus_params.py} (91%)
rename src/beeper_desktop_api/types/{open_response.py => focus_response.py} (76%)
rename src/beeper_desktop_api/types/{chat_search_params.py => search_chats_params.py} (87%)
rename src/beeper_desktop_api/types/{contact_search_params.py => search_contacts_params.py} (75%)
rename src/beeper_desktop_api/types/{contact_search_response.py => search_contacts_response.py} (71%)
delete mode 100644 tests/api_resources/test_contacts.py
create mode 100644 tests/api_resources/test_search.py
diff --git a/.stats.yml b/.stats.yml
index d065147..831b150 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-f48e33c7d90ed6418a852f8d4d951d07b09f4f3f939feb395dc2aa03f522d81e.yml
-openapi_spec_hash: c516120ecf51bb8425b3b9ed76c6423a
-config_hash: c5ac9bd5889d27aa168f06d6d0fef0b3
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-f48bb509412536c41fdfa537894cecd4af486099d95fe79369f2ef239fa94a75.yml
+openapi_spec_hash: f8b886fdfdc5ee3d51d2cd05daee3bab
+config_hash: 6f12c5e4c662e1f315b95a70389b1549
diff --git a/README.md b/README.md
index 9f0e7ba..f06fa5b 100644
--- a/README.md
+++ b/README.md
@@ -33,12 +33,7 @@ client = BeeperDesktop(
access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted
)
-page = client.chats.search(
- include_muted=True,
- limit=3,
- type="single",
-)
-print(page.items)
+accounts = client.accounts.list()
```
While you can provide a `access_token` keyword argument,
@@ -61,12 +56,7 @@ client = AsyncBeeperDesktop(
async def main() -> None:
- page = await client.chats.search(
- include_muted=True,
- limit=3,
- type="single",
- )
- print(page.items)
+ accounts = await client.accounts.list()
asyncio.run(main())
@@ -98,12 +88,7 @@ async def main() -> None:
access_token="My Access Token",
http_client=DefaultAioHttpClient(),
) as client:
- page = await client.chats.search(
- include_muted=True,
- limit=3,
- type="single",
- )
- print(page.items)
+ accounts = await client.accounts.list()
asyncio.run(main())
@@ -118,85 +103,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
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`.
-## Pagination
-
-List methods in the Beeper Desktop API are paginated.
-
-This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
-
-```python
-from beeper_desktop_api import BeeperDesktop
-
-client = BeeperDesktop()
-
-all_messages = []
-# Automatically fetches more pages as needed.
-for message in client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
-):
- # Do something with message here
- all_messages.append(message)
-print(all_messages)
-```
-
-Or, asynchronously:
-
-```python
-import asyncio
-from beeper_desktop_api import AsyncBeeperDesktop
-
-client = AsyncBeeperDesktop()
-
-
-async def main() -> None:
- all_messages = []
- # Iterate through items across all pages, issuing requests as needed.
- async for message in client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
- ):
- all_messages.append(message)
- print(all_messages)
-
-
-asyncio.run(main())
-```
-
-Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
-
-```python
-first_page = await client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
-)
-if first_page.has_next_page():
- print(f"will fetch next page using these details: {first_page.next_page_info()}")
- next_page = await first_page.get_next_page()
- print(f"number of items we just fetched: {len(next_page.items)}")
-
-# Remove `await` for non-async usage.
-```
-
-Or just work directly with the returned data:
-
-```python
-first_page = await client.messages.search(
- account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"],
- limit=10,
- query="deployment",
-)
-
-print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..."
-for message in first_page.items:
- print(message.id)
-
-# Remove `await` for non-async usage.
-```
-
## Nested params
Nested parameters are dictionaries, typed using `TypedDict`, for example:
diff --git a/api.md b/api.md
index 4fbc194..119d6f4 100644
--- a/api.md
+++ b/api.md
@@ -9,13 +9,13 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, R
Types:
```python
-from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse
+from beeper_desktop_api.types import DownloadAssetResponse, FocusResponse, SearchResponse
```
Methods:
- client.download_asset(\*\*params) -> DownloadAssetResponse
-- client.open(\*\*params) -> OpenResponse
+- client.focus(\*\*params) -> FocusResponse
- client.search(\*\*params) -> SearchResponse
# Accounts
@@ -30,17 +30,18 @@ Methods:
- client.accounts.list() -> AccountListResponse
-# Contacts
+# Search
Types:
```python
-from beeper_desktop_api.types import ContactSearchResponse
+from beeper_desktop_api.types import SearchContactsResponse
```
Methods:
-- client.contacts.search(account_id, \*\*params) -> ContactSearchResponse
+- client.search.chats(\*\*params) -> SyncCursorSearch[Chat]
+- client.search.contacts(account_id, \*\*params) -> SearchContactsResponse
# Chats
@@ -56,7 +57,6 @@ Methods:
- client.chats.retrieve(chat_id, \*\*params) -> Chat
- client.chats.list(\*\*params) -> SyncCursorList[ChatListResponse]
- client.chats.archive(chat_id, \*\*params) -> BaseResponse
-- client.chats.search(\*\*params) -> SyncCursorSearch[Chat]
## Reminders
@@ -76,5 +76,5 @@ from beeper_desktop_api.types import MessageSendResponse
Methods:
- client.messages.list(chat_id, \*\*params) -> SyncCursorList[Message]
-- client.messages.search(\*\*params) -> SyncCursorSearch[Message]
+- client.messages.search(\*\*params) -> SyncCursorSearch[Message]
- client.messages.send(chat_id, \*\*params) -> MessageSendResponse
diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py
index f8ba71d..82c9d75 100644
--- a/src/beeper_desktop_api/_client.py
+++ b/src/beeper_desktop_api/_client.py
@@ -10,7 +10,7 @@
from . import _exceptions
from ._qs import Querystring
-from .types import client_open_params, client_search_params, client_download_asset_params
+from .types import client_focus_params, client_search_params, client_download_asset_params
from ._types import (
Body,
Omit,
@@ -37,7 +37,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .resources import accounts, contacts, messages
+from .resources import search, accounts, messages
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import APIStatusError, BeeperDesktopError
from ._base_client import (
@@ -47,7 +47,7 @@
make_request_options,
)
from .resources.chats import chats
-from .types.open_response import OpenResponse
+from .types.focus_response import FocusResponse
from .types.search_response import SearchResponse
from .types.download_asset_response import DownloadAssetResponse
@@ -65,7 +65,7 @@
class BeeperDesktop(SyncAPIClient):
accounts: accounts.AccountsResource
- contacts: contacts.ContactsResource
+ search: search.SearchResource
chats: chats.ChatsResource
messages: messages.MessagesResource
with_raw_response: BeeperDesktopWithRawResponse
@@ -126,7 +126,7 @@ def __init__(
)
self.accounts = accounts.AccountsResource(self)
- self.contacts = contacts.ContactsResource(self)
+ self.search = search.SearchResource(self)
self.chats = chats.ChatsResource(self)
self.messages = messages.MessagesResource(self)
self.with_raw_response = BeeperDesktopWithRawResponse(self)
@@ -238,7 +238,7 @@ def download_asset(
cast_to=DownloadAssetResponse,
)
- def open(
+ def focus(
self,
*,
chat_id: str | Omit = omit,
@@ -251,9 +251,9 @@ def open(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> OpenResponse:
+ ) -> FocusResponse:
"""
- Open Beeper Desktop and optionally navigate to a specific chat, message, or
+ Focus Beeper Desktop and optionally navigate to a specific chat, message, or
pre-fill draft text and attachment.
Args:
@@ -275,7 +275,7 @@ def open(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self.post(
- "/v1/open",
+ "/v1/focus",
body=maybe_transform(
{
"chat_id": chat_id,
@@ -283,12 +283,12 @@ def open(
"draft_text": draft_text,
"message_id": message_id,
},
- client_open_params.ClientOpenParams,
+ client_focus_params.ClientFocusParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=OpenResponse,
+ cast_to=FocusResponse,
)
def search(
@@ -366,7 +366,7 @@ def _make_status_error(
class AsyncBeeperDesktop(AsyncAPIClient):
accounts: accounts.AsyncAccountsResource
- contacts: contacts.AsyncContactsResource
+ search: search.AsyncSearchResource
chats: chats.AsyncChatsResource
messages: messages.AsyncMessagesResource
with_raw_response: AsyncBeeperDesktopWithRawResponse
@@ -427,7 +427,7 @@ def __init__(
)
self.accounts = accounts.AsyncAccountsResource(self)
- self.contacts = contacts.AsyncContactsResource(self)
+ self.search = search.AsyncSearchResource(self)
self.chats = chats.AsyncChatsResource(self)
self.messages = messages.AsyncMessagesResource(self)
self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self)
@@ -539,7 +539,7 @@ async def download_asset(
cast_to=DownloadAssetResponse,
)
- async def open(
+ async def focus(
self,
*,
chat_id: str | Omit = omit,
@@ -552,9 +552,9 @@ async def open(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> OpenResponse:
+ ) -> FocusResponse:
"""
- Open Beeper Desktop and optionally navigate to a specific chat, message, or
+ Focus Beeper Desktop and optionally navigate to a specific chat, message, or
pre-fill draft text and attachment.
Args:
@@ -576,7 +576,7 @@ async def open(
timeout: Override the client-level default timeout for this request, in seconds
"""
return await self.post(
- "/v1/open",
+ "/v1/focus",
body=await async_maybe_transform(
{
"chat_id": chat_id,
@@ -584,12 +584,12 @@ async def open(
"draft_text": draft_text,
"message_id": message_id,
},
- client_open_params.ClientOpenParams,
+ client_focus_params.ClientFocusParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=OpenResponse,
+ cast_to=FocusResponse,
)
async def search(
@@ -668,15 +668,15 @@ def _make_status_error(
class BeeperDesktopWithRawResponse:
def __init__(self, client: BeeperDesktop) -> None:
self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts)
- self.contacts = contacts.ContactsResourceWithRawResponse(client.contacts)
+ self.search = search.SearchResourceWithRawResponse(client.search)
self.chats = chats.ChatsResourceWithRawResponse(client.chats)
self.messages = messages.MessagesResourceWithRawResponse(client.messages)
self.download_asset = to_raw_response_wrapper(
client.download_asset,
)
- self.open = to_raw_response_wrapper(
- client.open,
+ self.focus = to_raw_response_wrapper(
+ client.focus,
)
self.search = to_raw_response_wrapper(
client.search,
@@ -686,15 +686,15 @@ def __init__(self, client: BeeperDesktop) -> None:
class AsyncBeeperDesktopWithRawResponse:
def __init__(self, client: AsyncBeeperDesktop) -> None:
self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts)
- self.contacts = contacts.AsyncContactsResourceWithRawResponse(client.contacts)
+ self.search = search.AsyncSearchResourceWithRawResponse(client.search)
self.chats = chats.AsyncChatsResourceWithRawResponse(client.chats)
self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages)
self.download_asset = async_to_raw_response_wrapper(
client.download_asset,
)
- self.open = async_to_raw_response_wrapper(
- client.open,
+ self.focus = async_to_raw_response_wrapper(
+ client.focus,
)
self.search = async_to_raw_response_wrapper(
client.search,
@@ -704,15 +704,15 @@ def __init__(self, client: AsyncBeeperDesktop) -> None:
class BeeperDesktopWithStreamedResponse:
def __init__(self, client: BeeperDesktop) -> None:
self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts)
- self.contacts = contacts.ContactsResourceWithStreamingResponse(client.contacts)
+ self.search = search.SearchResourceWithStreamingResponse(client.search)
self.chats = chats.ChatsResourceWithStreamingResponse(client.chats)
self.messages = messages.MessagesResourceWithStreamingResponse(client.messages)
self.download_asset = to_streamed_response_wrapper(
client.download_asset,
)
- self.open = to_streamed_response_wrapper(
- client.open,
+ self.focus = to_streamed_response_wrapper(
+ client.focus,
)
self.search = to_streamed_response_wrapper(
client.search,
@@ -722,15 +722,15 @@ def __init__(self, client: BeeperDesktop) -> None:
class AsyncBeeperDesktopWithStreamedResponse:
def __init__(self, client: AsyncBeeperDesktop) -> None:
self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts)
- self.contacts = contacts.AsyncContactsResourceWithStreamingResponse(client.contacts)
+ self.search = search.AsyncSearchResourceWithStreamingResponse(client.search)
self.chats = chats.AsyncChatsResourceWithStreamingResponse(client.chats)
self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages)
self.download_asset = async_to_streamed_response_wrapper(
client.download_asset,
)
- self.open = async_to_streamed_response_wrapper(
- client.open,
+ self.focus = async_to_streamed_response_wrapper(
+ client.focus,
)
self.search = async_to_streamed_response_wrapper(
client.search,
diff --git a/src/beeper_desktop_api/resources/__init__.py b/src/beeper_desktop_api/resources/__init__.py
index ebf006b..eedc9bc 100644
--- a/src/beeper_desktop_api/resources/__init__.py
+++ b/src/beeper_desktop_api/resources/__init__.py
@@ -8,6 +8,14 @@
ChatsResourceWithStreamingResponse,
AsyncChatsResourceWithStreamingResponse,
)
+from .search import (
+ SearchResource,
+ AsyncSearchResource,
+ SearchResourceWithRawResponse,
+ AsyncSearchResourceWithRawResponse,
+ SearchResourceWithStreamingResponse,
+ AsyncSearchResourceWithStreamingResponse,
+)
from .accounts import (
AccountsResource,
AsyncAccountsResource,
@@ -16,14 +24,6 @@
AccountsResourceWithStreamingResponse,
AsyncAccountsResourceWithStreamingResponse,
)
-from .contacts import (
- ContactsResource,
- AsyncContactsResource,
- ContactsResourceWithRawResponse,
- AsyncContactsResourceWithRawResponse,
- ContactsResourceWithStreamingResponse,
- AsyncContactsResourceWithStreamingResponse,
-)
from .messages import (
MessagesResource,
AsyncMessagesResource,
@@ -40,12 +40,12 @@
"AsyncAccountsResourceWithRawResponse",
"AccountsResourceWithStreamingResponse",
"AsyncAccountsResourceWithStreamingResponse",
- "ContactsResource",
- "AsyncContactsResource",
- "ContactsResourceWithRawResponse",
- "AsyncContactsResourceWithRawResponse",
- "ContactsResourceWithStreamingResponse",
- "AsyncContactsResourceWithStreamingResponse",
+ "SearchResource",
+ "AsyncSearchResource",
+ "SearchResourceWithRawResponse",
+ "AsyncSearchResourceWithRawResponse",
+ "SearchResourceWithStreamingResponse",
+ "AsyncSearchResourceWithStreamingResponse",
"ChatsResource",
"AsyncChatsResource",
"ChatsResourceWithRawResponse",
diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py
index 7b8d06c..7752636 100644
--- a/src/beeper_desktop_api/resources/chats/chats.py
+++ b/src/beeper_desktop_api/resources/chats/chats.py
@@ -2,13 +2,12 @@
from __future__ import annotations
-from typing import Union, Optional
-from datetime import datetime
+from typing import Optional
from typing_extensions import Literal
import httpx
-from ...types import chat_list_params, chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params
+from ...types import chat_list_params, chat_create_params, chat_archive_params, chat_retrieve_params
from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
@@ -27,7 +26,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ...pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
+from ...pagination import SyncCursorList, AsyncCursorList
from ...types.chat import Chat
from ..._base_client import AsyncPaginator, make_request_options
from ...types.chat_list_response import ChatListResponse
@@ -137,8 +136,7 @@ def retrieve(
Retrieve chat details including metadata, participants, and latest message
Args:
- chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats.
- Participants are limited by 'maxParticipantCount'.
+ chat_id: Unique identifier of the chat.
max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500.
Defaults to 20.
@@ -188,8 +186,7 @@ def list(
Args:
account_ids: Limit to specific account IDs. If omitted, fetches from all accounts.
- cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction
- to navigate results.
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
fetches newer results. Defaults to 'before' when only 'cursor' is provided.
@@ -240,8 +237,7 @@ def archive(
archived=false to move back to inbox
Args:
- chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and
- local chat ID)
+ chat_id: Unique identifier of the chat.
archived: True to archive, false to unarchive
@@ -264,103 +260,6 @@ def archive(
cast_to=BaseResponse,
)
- def search(
- self,
- *,
- account_ids: SequenceNotStr[str] | Omit = omit,
- cursor: str | Omit = omit,
- direction: Literal["after", "before"] | Omit = omit,
- inbox: Literal["primary", "low-priority", "archive"] | Omit = omit,
- include_muted: Optional[bool] | Omit = omit,
- last_activity_after: Union[str, datetime] | Omit = omit,
- last_activity_before: Union[str, datetime] | Omit = omit,
- limit: int | Omit = omit,
- query: str | Omit = omit,
- scope: Literal["titles", "participants"] | Omit = omit,
- type: Literal["single", "group", "any"] | Omit = omit,
- unread_only: Optional[bool] | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursorSearch[Chat]:
- """
- Search chats by title/network or participants using Beeper Desktop's renderer
- algorithm.
-
- Args:
- account_ids: Provide an array of account IDs to filter chats from specific messaging accounts
- only
-
- cursor: Pagination cursor from previous response. Use with direction to navigate results
-
- direction: Pagination direction: "after" for newer page, "before" for older page. Defaults
- to "before" when only cursor is provided.
-
- inbox: Filter by inbox type: "primary" (non-archived, non-low-priority),
- "low-priority", or "archive". If not specified, shows all chats.
-
- include_muted: Include chats marked as Muted by the user, which are usually less important.
- Default: true. Set to false if the user wants a more refined search.
-
- last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after
- this time
-
- last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before
- this time
-
- limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50
-
- query: Literal token search (non-semantic). Use single words users type (e.g.,
- "dinner"). When multiple words provided, ALL must match. Case-insensitive.
-
- scope: Search scope: 'titles' matches title + network; 'participants' matches
- participant names.
-
- type: Specify the type of chats to retrieve: use "single" for direct messages, "group"
- for group chats, or "any" to get all types
-
- unread_only: Set to true to only retrieve chats that have unread messages
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return self._get_api_list(
- "/v1/chats/search",
- page=SyncCursorSearch[Chat],
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "account_ids": account_ids,
- "cursor": cursor,
- "direction": direction,
- "inbox": inbox,
- "include_muted": include_muted,
- "last_activity_after": last_activity_after,
- "last_activity_before": last_activity_before,
- "limit": limit,
- "query": query,
- "scope": scope,
- "type": type,
- "unread_only": unread_only,
- },
- chat_search_params.ChatSearchParams,
- ),
- ),
- model=Chat,
- )
-
class AsyncChatsResource(AsyncAPIResource):
"""Chats operations"""
@@ -462,8 +361,7 @@ async def retrieve(
Retrieve chat details including metadata, participants, and latest message
Args:
- chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats.
- Participants are limited by 'maxParticipantCount'.
+ chat_id: Unique identifier of the chat.
max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500.
Defaults to 20.
@@ -513,8 +411,7 @@ def list(
Args:
account_ids: Limit to specific account IDs. If omitted, fetches from all accounts.
- cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction
- to navigate results.
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
fetches newer results. Defaults to 'before' when only 'cursor' is provided.
@@ -565,8 +462,7 @@ async def archive(
archived=false to move back to inbox
Args:
- chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and
- local chat ID)
+ chat_id: Unique identifier of the chat.
archived: True to archive, false to unarchive
@@ -589,103 +485,6 @@ async def archive(
cast_to=BaseResponse,
)
- def search(
- self,
- *,
- account_ids: SequenceNotStr[str] | Omit = omit,
- cursor: str | Omit = omit,
- direction: Literal["after", "before"] | Omit = omit,
- inbox: Literal["primary", "low-priority", "archive"] | Omit = omit,
- include_muted: Optional[bool] | Omit = omit,
- last_activity_after: Union[str, datetime] | Omit = omit,
- last_activity_before: Union[str, datetime] | Omit = omit,
- limit: int | Omit = omit,
- query: str | Omit = omit,
- scope: Literal["titles", "participants"] | Omit = omit,
- type: Literal["single", "group", "any"] | Omit = omit,
- unread_only: Optional[bool] | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Chat, AsyncCursorSearch[Chat]]:
- """
- Search chats by title/network or participants using Beeper Desktop's renderer
- algorithm.
-
- Args:
- account_ids: Provide an array of account IDs to filter chats from specific messaging accounts
- only
-
- cursor: Pagination cursor from previous response. Use with direction to navigate results
-
- direction: Pagination direction: "after" for newer page, "before" for older page. Defaults
- to "before" when only cursor is provided.
-
- inbox: Filter by inbox type: "primary" (non-archived, non-low-priority),
- "low-priority", or "archive". If not specified, shows all chats.
-
- include_muted: Include chats marked as Muted by the user, which are usually less important.
- Default: true. Set to false if the user wants a more refined search.
-
- last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after
- this time
-
- last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before
- this time
-
- limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50
-
- query: Literal token search (non-semantic). Use single words users type (e.g.,
- "dinner"). When multiple words provided, ALL must match. Case-insensitive.
-
- scope: Search scope: 'titles' matches title + network; 'participants' matches
- participant names.
-
- type: Specify the type of chats to retrieve: use "single" for direct messages, "group"
- for group chats, or "any" to get all types
-
- unread_only: Set to true to only retrieve chats that have unread messages
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return self._get_api_list(
- "/v1/chats/search",
- page=AsyncCursorSearch[Chat],
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "account_ids": account_ids,
- "cursor": cursor,
- "direction": direction,
- "inbox": inbox,
- "include_muted": include_muted,
- "last_activity_after": last_activity_after,
- "last_activity_before": last_activity_before,
- "limit": limit,
- "query": query,
- "scope": scope,
- "type": type,
- "unread_only": unread_only,
- },
- chat_search_params.ChatSearchParams,
- ),
- ),
- model=Chat,
- )
-
class ChatsResourceWithRawResponse:
def __init__(self, chats: ChatsResource) -> None:
@@ -703,9 +502,6 @@ def __init__(self, chats: ChatsResource) -> None:
self.archive = to_raw_response_wrapper(
chats.archive,
)
- self.search = to_raw_response_wrapper(
- chats.search,
- )
@cached_property
def reminders(self) -> RemindersResourceWithRawResponse:
@@ -729,9 +525,6 @@ def __init__(self, chats: AsyncChatsResource) -> None:
self.archive = async_to_raw_response_wrapper(
chats.archive,
)
- self.search = async_to_raw_response_wrapper(
- chats.search,
- )
@cached_property
def reminders(self) -> AsyncRemindersResourceWithRawResponse:
@@ -755,9 +548,6 @@ def __init__(self, chats: ChatsResource) -> None:
self.archive = to_streamed_response_wrapper(
chats.archive,
)
- self.search = to_streamed_response_wrapper(
- chats.search,
- )
@cached_property
def reminders(self) -> RemindersResourceWithStreamingResponse:
@@ -781,9 +571,6 @@ def __init__(self, chats: AsyncChatsResource) -> None:
self.archive = async_to_streamed_response_wrapper(
chats.archive,
)
- self.search = async_to_streamed_response_wrapper(
- chats.search,
- )
@cached_property
def reminders(self) -> AsyncRemindersResourceWithStreamingResponse:
diff --git a/src/beeper_desktop_api/resources/chats/reminders.py b/src/beeper_desktop_api/resources/chats/reminders.py
index e9da3b4..bf628ae 100644
--- a/src/beeper_desktop_api/resources/chats/reminders.py
+++ b/src/beeper_desktop_api/resources/chats/reminders.py
@@ -59,8 +59,7 @@ def create(
Set a reminder for a chat at a specific time
Args:
- chat_id: The identifier of the chat to set reminder for (accepts both chatID and local
- chat ID)
+ chat_id: Unique identifier of the chat.
reminder: Reminder configuration
@@ -98,8 +97,7 @@ def delete(
Clear an existing reminder from a chat
Args:
- chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local
- chat ID)
+ chat_id: Unique identifier of the chat.
extra_headers: Send extra headers
@@ -158,8 +156,7 @@ async def create(
Set a reminder for a chat at a specific time
Args:
- chat_id: The identifier of the chat to set reminder for (accepts both chatID and local
- chat ID)
+ chat_id: Unique identifier of the chat.
reminder: Reminder configuration
@@ -197,8 +194,7 @@ async def delete(
Clear an existing reminder from a chat
Args:
- chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local
- chat ID)
+ chat_id: Unique identifier of the chat.
extra_headers: Send extra headers
diff --git a/src/beeper_desktop_api/resources/contacts.py b/src/beeper_desktop_api/resources/contacts.py
deleted file mode 100644
index 50fc4f9..0000000
--- a/src/beeper_desktop_api/resources/contacts.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import httpx
-
-from ..types import contact_search_params
-from .._types import Body, Query, Headers, NotGiven, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from .._base_client import make_request_options
-from ..types.contact_search_response import ContactSearchResponse
-
-__all__ = ["ContactsResource", "AsyncContactsResource"]
-
-
-class ContactsResource(SyncAPIResource):
- """Contacts operations"""
-
- @cached_property
- def with_raw_response(self) -> ContactsResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
- """
- return ContactsResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> ContactsResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
- """
- return ContactsResourceWithStreamingResponse(self)
-
- def search(
- self,
- account_id: str,
- *,
- query: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ContactSearchResponse:
- """
- Search contacts across on a specific account using the network's search API.
- Only use for creating new chats.
-
- Args:
- account_id: Account ID this resource belongs to.
-
- query: Text to search users by. Network-specific behavior.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not account_id:
- raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}")
- return self._get(
- f"/v1/accounts/{account_id}/contacts/search",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform({"query": query}, contact_search_params.ContactSearchParams),
- ),
- cast_to=ContactSearchResponse,
- )
-
-
-class AsyncContactsResource(AsyncAPIResource):
- """Contacts operations"""
-
- @cached_property
- def with_raw_response(self) -> AsyncContactsResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
- """
- return AsyncContactsResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
- """
- return AsyncContactsResourceWithStreamingResponse(self)
-
- async def search(
- self,
- account_id: str,
- *,
- query: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ContactSearchResponse:
- """
- Search contacts across on a specific account using the network's search API.
- Only use for creating new chats.
-
- Args:
- account_id: Account ID this resource belongs to.
-
- query: Text to search users by. Network-specific behavior.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not account_id:
- raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}")
- return await self._get(
- f"/v1/accounts/{account_id}/contacts/search",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform({"query": query}, contact_search_params.ContactSearchParams),
- ),
- cast_to=ContactSearchResponse,
- )
-
-
-class ContactsResourceWithRawResponse:
- def __init__(self, contacts: ContactsResource) -> None:
- self._contacts = contacts
-
- self.search = to_raw_response_wrapper(
- contacts.search,
- )
-
-
-class AsyncContactsResourceWithRawResponse:
- def __init__(self, contacts: AsyncContactsResource) -> None:
- self._contacts = contacts
-
- self.search = async_to_raw_response_wrapper(
- contacts.search,
- )
-
-
-class ContactsResourceWithStreamingResponse:
- def __init__(self, contacts: ContactsResource) -> None:
- self._contacts = contacts
-
- self.search = to_streamed_response_wrapper(
- contacts.search,
- )
-
-
-class AsyncContactsResourceWithStreamingResponse:
- def __init__(self, contacts: AsyncContactsResource) -> None:
- self._contacts = contacts
-
- self.search = async_to_streamed_response_wrapper(
- contacts.search,
- )
diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py
index e77b8f1..ad07df3 100644
--- a/src/beeper_desktop_api/resources/messages.py
+++ b/src/beeper_desktop_api/resources/messages.py
@@ -67,13 +67,12 @@ def list(
Sorted by timestamp.
Args:
- chat_id: Chat ID to list messages from
+ chat_id: Unique identifier of the chat.
- cursor: Message cursor for pagination. Use with direction to navigate results.
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
- direction: Pagination direction used with 'cursor': 'before' fetches older messages,
- 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
- provided.
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
extra_headers: Send extra headers
@@ -176,7 +175,7 @@ def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/v1/messages/search",
+ "/v1/search/messages",
page=SyncCursorSearch[Message],
options=make_request_options(
extra_headers=extra_headers,
@@ -296,13 +295,12 @@ def list(
Sorted by timestamp.
Args:
- chat_id: Chat ID to list messages from
+ chat_id: Unique identifier of the chat.
- cursor: Message cursor for pagination. Use with direction to navigate results.
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
- direction: Pagination direction used with 'cursor': 'before' fetches older messages,
- 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
- provided.
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
extra_headers: Send extra headers
@@ -405,7 +403,7 @@ def search(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/v1/messages/search",
+ "/v1/search/messages",
page=AsyncCursorSearch[Message],
options=make_request_options(
extra_headers=extra_headers,
diff --git a/src/beeper_desktop_api/resources/search.py b/src/beeper_desktop_api/resources/search.py
new file mode 100644
index 0000000..0653cfa
--- /dev/null
+++ b/src/beeper_desktop_api/resources/search.py
@@ -0,0 +1,397 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import search_chats_params, search_contacts_params
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from .._utils import maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncCursorSearch, AsyncCursorSearch
+from ..types.chat import Chat
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.search_contacts_response import SearchContactsResponse
+
+__all__ = ["SearchResource", "AsyncSearchResource"]
+
+
+class SearchResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> SearchResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return SearchResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> SearchResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return SearchResourceWithStreamingResponse(self)
+
+ def chats(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ inbox: Literal["primary", "low-priority", "archive"] | Omit = omit,
+ include_muted: Optional[bool] | Omit = omit,
+ last_activity_after: Union[str, datetime] | Omit = omit,
+ last_activity_before: Union[str, datetime] | Omit = omit,
+ limit: int | Omit = omit,
+ query: str | Omit = omit,
+ scope: Literal["titles", "participants"] | Omit = omit,
+ type: Literal["single", "group", "any"] | Omit = omit,
+ unread_only: Optional[bool] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncCursorSearch[Chat]:
+ """
+ Search chats by title/network or participants using Beeper Desktop's renderer
+ algorithm.
+
+ Args:
+ account_ids: Provide an array of account IDs to filter chats from specific messaging accounts
+ only
+
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+
+ inbox: Filter by inbox type: "primary" (non-archived, non-low-priority),
+ "low-priority", or "archive". If not specified, shows all chats.
+
+ include_muted: Include chats marked as Muted by the user, which are usually less important.
+ Default: true. Set to false if the user wants a more refined search.
+
+ last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after
+ this time
+
+ last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before
+ this time
+
+ limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50
+
+ query: Literal token search (non-semantic). Use single words users type (e.g.,
+ "dinner"). When multiple words provided, ALL must match. Case-insensitive.
+
+ scope: Search scope: 'titles' matches title + network; 'participants' matches
+ participant names.
+
+ type: Specify the type of chats to retrieve: use "single" for direct messages, "group"
+ for group chats, or "any" to get all types
+
+ unread_only: Set to true to only retrieve chats that have unread messages
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/search/chats",
+ page=SyncCursorSearch[Chat],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "cursor": cursor,
+ "direction": direction,
+ "inbox": inbox,
+ "include_muted": include_muted,
+ "last_activity_after": last_activity_after,
+ "last_activity_before": last_activity_before,
+ "limit": limit,
+ "query": query,
+ "scope": scope,
+ "type": type,
+ "unread_only": unread_only,
+ },
+ search_chats_params.SearchChatsParams,
+ ),
+ ),
+ model=Chat,
+ )
+
+ def contacts(
+ self,
+ account_id: str,
+ *,
+ query: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SearchContactsResponse:
+ """
+ Search contacts across on a specific account using the network's search API.
+ Only use for creating new chats.
+
+ Args:
+ account_id: Account ID this resource belongs to.
+
+ query: Text to search users by. Network-specific behavior.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not account_id:
+ raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}")
+ return self._get(
+ f"/v1/search/contacts/{account_id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"query": query}, search_contacts_params.SearchContactsParams),
+ ),
+ cast_to=SearchContactsResponse,
+ )
+
+
+class AsyncSearchResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncSearchResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncSearchResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncSearchResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response
+ """
+ return AsyncSearchResourceWithStreamingResponse(self)
+
+ def chats(
+ self,
+ *,
+ account_ids: SequenceNotStr[str] | Omit = omit,
+ cursor: str | Omit = omit,
+ direction: Literal["after", "before"] | Omit = omit,
+ inbox: Literal["primary", "low-priority", "archive"] | Omit = omit,
+ include_muted: Optional[bool] | Omit = omit,
+ last_activity_after: Union[str, datetime] | Omit = omit,
+ last_activity_before: Union[str, datetime] | Omit = omit,
+ limit: int | Omit = omit,
+ query: str | Omit = omit,
+ scope: Literal["titles", "participants"] | Omit = omit,
+ type: Literal["single", "group", "any"] | Omit = omit,
+ unread_only: Optional[bool] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Chat, AsyncCursorSearch[Chat]]:
+ """
+ Search chats by title/network or participants using Beeper Desktop's renderer
+ algorithm.
+
+ Args:
+ account_ids: Provide an array of account IDs to filter chats from specific messaging accounts
+ only
+
+ cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'.
+
+ direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
+
+ inbox: Filter by inbox type: "primary" (non-archived, non-low-priority),
+ "low-priority", or "archive". If not specified, shows all chats.
+
+ include_muted: Include chats marked as Muted by the user, which are usually less important.
+ Default: true. Set to false if the user wants a more refined search.
+
+ last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after
+ this time
+
+ last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before
+ this time
+
+ limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50
+
+ query: Literal token search (non-semantic). Use single words users type (e.g.,
+ "dinner"). When multiple words provided, ALL must match. Case-insensitive.
+
+ scope: Search scope: 'titles' matches title + network; 'participants' matches
+ participant names.
+
+ type: Specify the type of chats to retrieve: use "single" for direct messages, "group"
+ for group chats, or "any" to get all types
+
+ unread_only: Set to true to only retrieve chats that have unread messages
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/search/chats",
+ page=AsyncCursorSearch[Chat],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "account_ids": account_ids,
+ "cursor": cursor,
+ "direction": direction,
+ "inbox": inbox,
+ "include_muted": include_muted,
+ "last_activity_after": last_activity_after,
+ "last_activity_before": last_activity_before,
+ "limit": limit,
+ "query": query,
+ "scope": scope,
+ "type": type,
+ "unread_only": unread_only,
+ },
+ search_chats_params.SearchChatsParams,
+ ),
+ ),
+ model=Chat,
+ )
+
+ async def contacts(
+ self,
+ account_id: str,
+ *,
+ query: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SearchContactsResponse:
+ """
+ Search contacts across on a specific account using the network's search API.
+ Only use for creating new chats.
+
+ Args:
+ account_id: Account ID this resource belongs to.
+
+ query: Text to search users by. Network-specific behavior.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not account_id:
+ raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}")
+ return await self._get(
+ f"/v1/search/contacts/{account_id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"query": query}, search_contacts_params.SearchContactsParams),
+ ),
+ cast_to=SearchContactsResponse,
+ )
+
+
+class SearchResourceWithRawResponse:
+ def __init__(self, search: SearchResource) -> None:
+ self._search = search
+
+ self.chats = to_raw_response_wrapper(
+ search.chats,
+ )
+ self.contacts = to_raw_response_wrapper(
+ search.contacts,
+ )
+
+
+class AsyncSearchResourceWithRawResponse:
+ def __init__(self, search: AsyncSearchResource) -> None:
+ self._search = search
+
+ self.chats = async_to_raw_response_wrapper(
+ search.chats,
+ )
+ self.contacts = async_to_raw_response_wrapper(
+ search.contacts,
+ )
+
+
+class SearchResourceWithStreamingResponse:
+ def __init__(self, search: SearchResource) -> None:
+ self._search = search
+
+ self.chats = to_streamed_response_wrapper(
+ search.chats,
+ )
+ self.contacts = to_streamed_response_wrapper(
+ search.contacts,
+ )
+
+
+class AsyncSearchResourceWithStreamingResponse:
+ def __init__(self, search: AsyncSearchResource) -> None:
+ self._search = search
+
+ self.chats = async_to_streamed_response_wrapper(
+ search.chats,
+ )
+ self.contacts = async_to_streamed_response_wrapper(
+ search.contacts,
+ )
diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py
index e577cbf..6aeb07d 100644
--- a/src/beeper_desktop_api/types/__init__.py
+++ b/src/beeper_desktop_api/types/__init__.py
@@ -12,23 +12,23 @@
BaseResponse as BaseResponse,
)
from .account import Account as Account
-from .open_response import OpenResponse as OpenResponse
+from .focus_response import FocusResponse as FocusResponse
from .search_response import SearchResponse as SearchResponse
from .chat_list_params import ChatListParams as ChatListParams
from .chat_create_params import ChatCreateParams as ChatCreateParams
from .chat_list_response import ChatListResponse as ChatListResponse
-from .chat_search_params import ChatSearchParams as ChatSearchParams
-from .client_open_params import ClientOpenParams as ClientOpenParams
from .chat_archive_params import ChatArchiveParams as ChatArchiveParams
+from .client_focus_params import ClientFocusParams as ClientFocusParams
from .message_list_params import MessageListParams as MessageListParams
from .message_send_params import MessageSendParams as MessageSendParams
+from .search_chats_params import SearchChatsParams as SearchChatsParams
from .chat_create_response import ChatCreateResponse as ChatCreateResponse
from .chat_retrieve_params import ChatRetrieveParams as ChatRetrieveParams
from .client_search_params import ClientSearchParams as ClientSearchParams
from .account_list_response import AccountListResponse as AccountListResponse
-from .contact_search_params import ContactSearchParams as ContactSearchParams
from .message_search_params import MessageSearchParams as MessageSearchParams
from .message_send_response import MessageSendResponse as MessageSendResponse
-from .contact_search_response import ContactSearchResponse as ContactSearchResponse
+from .search_contacts_params import SearchContactsParams as SearchContactsParams
from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse
+from .search_contacts_response import SearchContactsResponse as SearchContactsResponse
from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams
diff --git a/src/beeper_desktop_api/types/chat_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py
index e1d10b2..d216046 100644
--- a/src/beeper_desktop_api/types/chat_list_params.py
+++ b/src/beeper_desktop_api/types/chat_list_params.py
@@ -15,10 +15,7 @@ class ChatListParams(TypedDict, total=False):
"""Limit to specific account IDs. If omitted, fetches from all accounts."""
cursor: str
- """Timestamp cursor (milliseconds since epoch) for pagination.
-
- Use with direction to navigate results.
- """
+ """Opaque pagination cursor; do not inspect. Use together with 'direction'."""
direction: Literal["after", "before"]
"""
diff --git a/src/beeper_desktop_api/types/client_open_params.py b/src/beeper_desktop_api/types/client_focus_params.py
similarity index 91%
rename from src/beeper_desktop_api/types/client_open_params.py
rename to src/beeper_desktop_api/types/client_focus_params.py
index 84dea5f..6359eb2 100644
--- a/src/beeper_desktop_api/types/client_open_params.py
+++ b/src/beeper_desktop_api/types/client_focus_params.py
@@ -6,10 +6,10 @@
from .._utils import PropertyInfo
-__all__ = ["ClientOpenParams"]
+__all__ = ["ClientFocusParams"]
-class ClientOpenParams(TypedDict, total=False):
+class ClientFocusParams(TypedDict, total=False):
chat_id: Annotated[str, PropertyInfo(alias="chatID")]
"""Optional Beeper chat ID (or local chat ID) to focus after opening the app.
diff --git a/src/beeper_desktop_api/types/open_response.py b/src/beeper_desktop_api/types/focus_response.py
similarity index 76%
rename from src/beeper_desktop_api/types/open_response.py
rename to src/beeper_desktop_api/types/focus_response.py
index 970f2ba..28875b1 100644
--- a/src/beeper_desktop_api/types/open_response.py
+++ b/src/beeper_desktop_api/types/focus_response.py
@@ -2,9 +2,9 @@
from .._models import BaseModel
-__all__ = ["OpenResponse"]
+__all__ = ["FocusResponse"]
-class OpenResponse(BaseModel):
+class FocusResponse(BaseModel):
success: bool
"""Whether the app was successfully opened/focused."""
diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py
index d4e343a..e6a04d2 100644
--- a/src/beeper_desktop_api/types/message_list_params.py
+++ b/src/beeper_desktop_api/types/message_list_params.py
@@ -9,11 +9,10 @@
class MessageListParams(TypedDict, total=False):
cursor: str
- """Message cursor for pagination. Use with direction to navigate results."""
+ """Opaque pagination cursor; do not inspect. Use together with 'direction'."""
direction: Literal["after", "before"]
"""
- Pagination direction used with 'cursor': 'before' fetches older messages,
- 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is
- provided.
+ Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
"""
diff --git a/src/beeper_desktop_api/types/chat_search_params.py b/src/beeper_desktop_api/types/search_chats_params.py
similarity index 87%
rename from src/beeper_desktop_api/types/chat_search_params.py
rename to src/beeper_desktop_api/types/search_chats_params.py
index de94b8d..d393720 100644
--- a/src/beeper_desktop_api/types/chat_search_params.py
+++ b/src/beeper_desktop_api/types/search_chats_params.py
@@ -9,10 +9,10 @@
from .._types import SequenceNotStr
from .._utils import PropertyInfo
-__all__ = ["ChatSearchParams"]
+__all__ = ["SearchChatsParams"]
-class ChatSearchParams(TypedDict, total=False):
+class SearchChatsParams(TypedDict, total=False):
account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")]
"""
Provide an array of account IDs to filter chats from specific messaging accounts
@@ -20,15 +20,12 @@ class ChatSearchParams(TypedDict, total=False):
"""
cursor: str
- """Pagination cursor from previous response.
-
- Use with direction to navigate results
- """
+ """Opaque pagination cursor; do not inspect. Use together with 'direction'."""
direction: Literal["after", "before"]
- """Pagination direction: "after" for newer page, "before" for older page.
-
- Defaults to "before" when only cursor is provided.
+ """
+ Pagination direction used with 'cursor': 'before' fetches older results, 'after'
+ fetches newer results. Defaults to 'before' when only 'cursor' is provided.
"""
inbox: Literal["primary", "low-priority", "archive"]
diff --git a/src/beeper_desktop_api/types/contact_search_params.py b/src/beeper_desktop_api/types/search_contacts_params.py
similarity index 75%
rename from src/beeper_desktop_api/types/contact_search_params.py
rename to src/beeper_desktop_api/types/search_contacts_params.py
index f9063e0..3e0352b 100644
--- a/src/beeper_desktop_api/types/contact_search_params.py
+++ b/src/beeper_desktop_api/types/search_contacts_params.py
@@ -4,9 +4,9 @@
from typing_extensions import Required, TypedDict
-__all__ = ["ContactSearchParams"]
+__all__ = ["SearchContactsParams"]
-class ContactSearchParams(TypedDict, total=False):
+class SearchContactsParams(TypedDict, total=False):
query: Required[str]
"""Text to search users by. Network-specific behavior."""
diff --git a/src/beeper_desktop_api/types/contact_search_response.py b/src/beeper_desktop_api/types/search_contacts_response.py
similarity index 71%
rename from src/beeper_desktop_api/types/contact_search_response.py
rename to src/beeper_desktop_api/types/search_contacts_response.py
index 71c609e..1bbf6db 100644
--- a/src/beeper_desktop_api/types/contact_search_response.py
+++ b/src/beeper_desktop_api/types/search_contacts_response.py
@@ -5,8 +5,8 @@
from .._models import BaseModel
from .shared.user import User
-__all__ = ["ContactSearchResponse"]
+__all__ = ["SearchContactsResponse"]
-class ContactSearchResponse(BaseModel):
+class SearchContactsResponse(BaseModel):
items: List[User]
diff --git a/src/beeper_desktop_api/types/shared/message.py b/src/beeper_desktop_api/types/shared/message.py
index b9d70ff..ff2ca3a 100644
--- a/src/beeper_desktop_api/types/shared/message.py
+++ b/src/beeper_desktop_api/types/shared/message.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Union, Optional
+from typing import List, Optional
from datetime import datetime
from pydantic import Field as FieldInfo
@@ -14,21 +14,18 @@
class Message(BaseModel):
id: str
- """Stable message ID for cursor pagination."""
+ """Message ID."""
account_id: str = FieldInfo(alias="accountID")
"""Beeper account ID the message belongs to."""
chat_id: str = FieldInfo(alias="chatID")
- """Beeper chat/thread/room ID."""
-
- message_id: str = FieldInfo(alias="messageID")
- """Stable message ID (same as id)."""
+ """Unique identifier of the chat."""
sender_id: str = FieldInfo(alias="senderID")
"""Sender user ID."""
- sort_key: Union[str, float] = FieldInfo(alias="sortKey")
+ sort_key: str = FieldInfo(alias="sortKey")
"""A unique key used to sort messages"""
timestamp: datetime
diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py
index 352bc2f..84cfdb1 100644
--- a/tests/api_resources/test_chats.py
+++ b/tests/api_resources/test_chats.py
@@ -14,8 +14,7 @@
ChatListResponse,
ChatCreateResponse,
)
-from beeper_desktop_api._utils import parse_datetime
-from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch
+from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList
from beeper_desktop_api.types.shared import BaseResponse
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -131,7 +130,7 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
"local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
"local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU",
],
- cursor="1725489123456",
+ cursor="1725489123456|c29tZUltc2dQYWdl",
direction="before",
)
assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"])
@@ -202,52 +201,6 @@ def test_path_params_archive(self, client: BeeperDesktop) -> None:
chat_id="",
)
- @parametrize
- def test_method_search(self, client: BeeperDesktop) -> None:
- chat = client.chats.search()
- assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
-
- @parametrize
- def test_method_search_with_all_params(self, client: BeeperDesktop) -> None:
- chat = client.chats.search(
- account_ids=[
- "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI",
- ],
- cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9",
- direction="after",
- inbox="primary",
- include_muted=True,
- last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"),
- last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"),
- limit=1,
- query="x",
- scope="titles",
- type="single",
- unread_only=True,
- )
- assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
-
- @parametrize
- def test_raw_response_search(self, client: BeeperDesktop) -> None:
- response = client.chats.with_raw_response.search()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- chat = response.parse()
- assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
-
- @parametrize
- def test_streaming_response_search(self, client: BeeperDesktop) -> None:
- with client.chats.with_streaming_response.search() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- chat = response.parse()
- assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
class TestAsyncChats:
parametrize = pytest.mark.parametrize(
@@ -361,7 +314,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto
"local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
"local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU",
],
- cursor="1725489123456",
+ cursor="1725489123456|c29tZUltc2dQYWdl",
direction="before",
)
assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"])
@@ -431,49 +384,3 @@ async def test_path_params_archive(self, async_client: AsyncBeeperDesktop) -> No
await async_client.chats.with_raw_response.archive(
chat_id="",
)
-
- @parametrize
- async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
- chat = await async_client.chats.search()
- assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
-
- @parametrize
- async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
- chat = await async_client.chats.search(
- account_ids=[
- "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI",
- ],
- cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9",
- direction="after",
- inbox="primary",
- include_muted=True,
- last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"),
- last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"),
- limit=1,
- query="x",
- scope="titles",
- type="single",
- unread_only=True,
- )
- assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
-
- @parametrize
- async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.chats.with_raw_response.search()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- chat = await response.parse()
- assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
-
- @parametrize
- async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.chats.with_streaming_response.search() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- chat = await response.parse()
- assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"])
-
- assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py
index d5de032..54e150e 100644
--- a/tests/api_resources/test_client.py
+++ b/tests/api_resources/test_client.py
@@ -10,7 +10,7 @@
from tests.utils import assert_matches_type
from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
from beeper_desktop_api.types import (
- OpenResponse,
+ FocusResponse,
SearchResponse,
DownloadAssetResponse,
)
@@ -53,37 +53,37 @@ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None:
assert cast(Any, response.is_closed) is True
@parametrize
- def test_method_open(self, client: BeeperDesktop) -> None:
- client_ = client.open()
- assert_matches_type(OpenResponse, client_, path=["response"])
+ def test_method_focus(self, client: BeeperDesktop) -> None:
+ client_ = client.focus()
+ assert_matches_type(FocusResponse, client_, path=["response"])
@parametrize
- def test_method_open_with_all_params(self, client: BeeperDesktop) -> None:
- client_ = client.open(
+ def test_method_focus_with_all_params(self, client: BeeperDesktop) -> None:
+ client_ = client.focus(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
draft_attachment_path="draftAttachmentPath",
draft_text="draftText",
message_id="messageID",
)
- assert_matches_type(OpenResponse, client_, path=["response"])
+ assert_matches_type(FocusResponse, client_, path=["response"])
@parametrize
- def test_raw_response_open(self, client: BeeperDesktop) -> None:
- response = client.with_raw_response.open()
+ def test_raw_response_focus(self, client: BeeperDesktop) -> None:
+ response = client.with_raw_response.focus()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
client_ = response.parse()
- assert_matches_type(OpenResponse, client_, path=["response"])
+ assert_matches_type(FocusResponse, client_, path=["response"])
@parametrize
- def test_streaming_response_open(self, client: BeeperDesktop) -> None:
- with client.with_streaming_response.open() as response:
+ def test_streaming_response_focus(self, client: BeeperDesktop) -> None:
+ with client.with_streaming_response.focus() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
client_ = response.parse()
- assert_matches_type(OpenResponse, client_, path=["response"])
+ assert_matches_type(FocusResponse, client_, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -156,37 +156,37 @@ async def test_streaming_response_download_asset(self, async_client: AsyncBeeper
assert cast(Any, response.is_closed) is True
@parametrize
- async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None:
- client = await async_client.open()
- assert_matches_type(OpenResponse, client, path=["response"])
+ async def test_method_focus(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.focus()
+ assert_matches_type(FocusResponse, client, path=["response"])
@parametrize
- async def test_method_open_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
- client = await async_client.open(
+ async def test_method_focus_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ client = await async_client.focus(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
draft_attachment_path="draftAttachmentPath",
draft_text="draftText",
message_id="messageID",
)
- assert_matches_type(OpenResponse, client, path=["response"])
+ assert_matches_type(FocusResponse, client, path=["response"])
@parametrize
- async def test_raw_response_open(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.with_raw_response.open()
+ async def test_raw_response_focus(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.with_raw_response.focus()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
client = await response.parse()
- assert_matches_type(OpenResponse, client, path=["response"])
+ assert_matches_type(FocusResponse, client, path=["response"])
@parametrize
- async def test_streaming_response_open(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.with_streaming_response.open() as response:
+ async def test_streaming_response_focus(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.with_streaming_response.focus() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
client = await response.parse()
- assert_matches_type(OpenResponse, client, path=["response"])
+ assert_matches_type(FocusResponse, client, path=["response"])
assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py
deleted file mode 100644
index 158b961..0000000
--- a/tests/api_resources/test_contacts.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from tests.utils import assert_matches_type
-from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
-from beeper_desktop_api.types import ContactSearchResponse
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestContacts:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- def test_method_search(self, client: BeeperDesktop) -> None:
- contact = client.contacts.search(
- account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- query="x",
- )
- assert_matches_type(ContactSearchResponse, contact, path=["response"])
-
- @parametrize
- def test_raw_response_search(self, client: BeeperDesktop) -> None:
- response = client.contacts.with_raw_response.search(
- account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- query="x",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- contact = response.parse()
- assert_matches_type(ContactSearchResponse, contact, path=["response"])
-
- @parametrize
- def test_streaming_response_search(self, client: BeeperDesktop) -> None:
- with client.contacts.with_streaming_response.search(
- account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- query="x",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- contact = response.parse()
- assert_matches_type(ContactSearchResponse, contact, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_path_params_search(self, client: BeeperDesktop) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"):
- client.contacts.with_raw_response.search(
- account_id="",
- query="x",
- )
-
-
-class TestAsyncContacts:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
-
- @parametrize
- async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None:
- contact = await async_client.contacts.search(
- account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- query="x",
- )
- assert_matches_type(ContactSearchResponse, contact, path=["response"])
-
- @parametrize
- async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None:
- response = await async_client.contacts.with_raw_response.search(
- account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- query="x",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- contact = await response.parse()
- assert_matches_type(ContactSearchResponse, contact, path=["response"])
-
- @parametrize
- async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None:
- async with async_client.contacts.with_streaming_response.search(
- account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
- query="x",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- contact = await response.parse()
- assert_matches_type(ContactSearchResponse, contact, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_search(self, async_client: AsyncBeeperDesktop) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"):
- await async_client.contacts.with_raw_response.search(
- account_id="",
- query="x",
- )
diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py
index dd93537..d64cf44 100644
--- a/tests/api_resources/test_messages.py
+++ b/tests/api_resources/test_messages.py
@@ -33,7 +33,7 @@ def test_method_list(self, client: BeeperDesktop) -> None:
def test_method_list_with_all_params(self, client: BeeperDesktop) -> None:
message = client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- cursor="821744079",
+ cursor="1725489123456|c29tZUltc2dQYWdl",
direction="before",
)
assert_matches_type(SyncCursorList[Message], message, path=["response"])
@@ -181,7 +181,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
message = await async_client.messages.list(
chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com",
- cursor="821744079",
+ cursor="1725489123456|c29tZUltc2dQYWdl",
direction="before",
)
assert_matches_type(AsyncCursorList[Message], message, path=["response"])
diff --git a/tests/api_resources/test_search.py b/tests/api_resources/test_search.py
new file mode 100644
index 0000000..1c70e2d
--- /dev/null
+++ b/tests/api_resources/test_search.py
@@ -0,0 +1,202 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop
+from beeper_desktop_api.types import Chat, SearchContactsResponse
+from beeper_desktop_api._utils import parse_datetime
+from beeper_desktop_api.pagination import SyncCursorSearch, AsyncCursorSearch
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestSearch:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_chats(self, client: BeeperDesktop) -> None:
+ search = client.search.chats()
+ assert_matches_type(SyncCursorSearch[Chat], search, path=["response"])
+
+ @parametrize
+ def test_method_chats_with_all_params(self, client: BeeperDesktop) -> None:
+ search = client.search.chats(
+ account_ids=[
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI",
+ ],
+ cursor="1725489123456|c29tZUltc2dQYWdl",
+ direction="before",
+ inbox="primary",
+ include_muted=True,
+ last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"),
+ last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"),
+ limit=1,
+ query="x",
+ scope="titles",
+ type="single",
+ unread_only=True,
+ )
+ assert_matches_type(SyncCursorSearch[Chat], search, path=["response"])
+
+ @parametrize
+ def test_raw_response_chats(self, client: BeeperDesktop) -> None:
+ response = client.search.with_raw_response.chats()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = response.parse()
+ assert_matches_type(SyncCursorSearch[Chat], search, path=["response"])
+
+ @parametrize
+ def test_streaming_response_chats(self, client: BeeperDesktop) -> None:
+ with client.search.with_streaming_response.chats() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = response.parse()
+ assert_matches_type(SyncCursorSearch[Chat], search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_contacts(self, client: BeeperDesktop) -> None:
+ search = client.search.contacts(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+ assert_matches_type(SearchContactsResponse, search, path=["response"])
+
+ @parametrize
+ def test_raw_response_contacts(self, client: BeeperDesktop) -> None:
+ response = client.search.with_raw_response.contacts(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = response.parse()
+ assert_matches_type(SearchContactsResponse, search, path=["response"])
+
+ @parametrize
+ def test_streaming_response_contacts(self, client: BeeperDesktop) -> None:
+ with client.search.with_streaming_response.contacts(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = response.parse()
+ assert_matches_type(SearchContactsResponse, search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_contacts(self, client: BeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"):
+ client.search.with_raw_response.contacts(
+ account_id="",
+ query="x",
+ )
+
+
+class TestAsyncSearch:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_chats(self, async_client: AsyncBeeperDesktop) -> None:
+ search = await async_client.search.chats()
+ assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"])
+
+ @parametrize
+ async def test_method_chats_with_all_params(self, async_client: AsyncBeeperDesktop) -> None:
+ search = await async_client.search.chats(
+ account_ids=[
+ "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI",
+ ],
+ cursor="1725489123456|c29tZUltc2dQYWdl",
+ direction="before",
+ inbox="primary",
+ include_muted=True,
+ last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"),
+ last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"),
+ limit=1,
+ query="x",
+ scope="titles",
+ type="single",
+ unread_only=True,
+ )
+ assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"])
+
+ @parametrize
+ async def test_raw_response_chats(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.search.with_raw_response.chats()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = await response.parse()
+ assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_chats(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.search.with_streaming_response.chats() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = await response.parse()
+ assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_contacts(self, async_client: AsyncBeeperDesktop) -> None:
+ search = await async_client.search.contacts(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+ assert_matches_type(SearchContactsResponse, search, path=["response"])
+
+ @parametrize
+ async def test_raw_response_contacts(self, async_client: AsyncBeeperDesktop) -> None:
+ response = await async_client.search.with_raw_response.contacts(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = await response.parse()
+ assert_matches_type(SearchContactsResponse, search, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_contacts(self, async_client: AsyncBeeperDesktop) -> None:
+ async with async_client.search.with_streaming_response.contacts(
+ account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc",
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = await response.parse()
+ assert_matches_type(SearchContactsResponse, search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_contacts(self, async_client: AsyncBeeperDesktop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"):
+ await async_client.search.with_raw_response.contacts(
+ account_id="",
+ query="x",
+ )
From 5e058450070fedbd9730bd6ec57fa392974d09e1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 11 Oct 2025 02:14:01 +0000
Subject: [PATCH 19/20] chore(internal): detect missing future annotations with
ruff
---
pyproject.toml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
index d3a4a85..4da43df 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -224,6 +224,8 @@ select = [
"B",
# remove unused imports
"F401",
+ # check for missing future annotations
+ "FA102",
# bare except statements
"E722",
# unused arguments
@@ -246,6 +248,8 @@ unfixable = [
"T203",
]
+extend-safe-fixes = ["FA102"]
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"
From 7f1a2a5e46fff43555450aa8d69a89a7d7b19d1a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 11 Oct 2025 02:14:17 +0000
Subject: [PATCH 20/20] release: 0.2.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 27 +++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/beeper_desktop_api/_version.py | 2 +-
4 files changed, 30 insertions(+), 3 deletions(-)
create mode 100644 CHANGELOG.md
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 1332969..10f3091 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.0.1"
+ ".": "0.2.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..b45f837
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,27 @@
+# Changelog
+
+## 0.2.0 (2025-10-11)
+
+Full Changelog: [v0.0.1...v0.2.0](https://github.com/beeper/desktop-api-python/compare/v0.0.1...v0.2.0)
+
+### Features
+
+* **api:** manual updates ([86218ff](https://github.com/beeper/desktop-api-python/commit/86218ff03f8a0cd42050b0c3babdf78178fda3da))
+* **api:** manual updates ([0fcd71f](https://github.com/beeper/desktop-api-python/commit/0fcd71f9951498d349fb816b42dc21347f3ab5dc))
+* **api:** manual updates ([dce7124](https://github.com/beeper/desktop-api-python/commit/dce712498ff2678222fd203118e7bb91f13ccfc5))
+* **api:** manual updates ([48b4b7f](https://github.com/beeper/desktop-api-python/commit/48b4b7f01064d016b84e954f9aa9f327863cc1d3))
+* **api:** manual updates ([c9f3b2d](https://github.com/beeper/desktop-api-python/commit/c9f3b2d3a7fb7e2ce3b30de215497079fff3aca9))
+* **api:** manual updates ([7c655fb](https://github.com/beeper/desktop-api-python/commit/7c655fb94ba070083173c15a501be7a0f119a38b))
+* **api:** manual updates ([88bce73](https://github.com/beeper/desktop-api-python/commit/88bce73dfef13b6a1cdef0749dc3078af97255e4))
+* **api:** manual updates ([1ea87ff](https://github.com/beeper/desktop-api-python/commit/1ea87ff08b4b50541e3c26bef6f4bd581af6324c))
+* **api:** manual updates ([b1ba1c0](https://github.com/beeper/desktop-api-python/commit/b1ba1c0584b99ab402f7c1643c13c19881baa600))
+* **api:** manual updates ([545ed69](https://github.com/beeper/desktop-api-python/commit/545ed69d7251f47a309f2f46ee4f3b8e4cf1cc60))
+* **api:** remove limit from list routes ([d5cb6c2](https://github.com/beeper/desktop-api-python/commit/d5cb6c2ee132bc3d558552df145082396c80521c))
+
+
+### Chores
+
+* configure new SDK language ([d0b2ca6](https://github.com/beeper/desktop-api-python/commit/d0b2ca6bd2e9331cd42fe5143e0f94861502f11f))
+* configure new SDK language ([d11b464](https://github.com/beeper/desktop-api-python/commit/d11b4641e572db6ceb07bb4b9d47b97beadd9253))
+* **internal:** detect missing future annotations with ruff ([5e05845](https://github.com/beeper/desktop-api-python/commit/5e058450070fedbd9730bd6ec57fa392974d09e1))
+* update SDK settings ([0d38dfa](https://github.com/beeper/desktop-api-python/commit/0d38dfa50d797ff879df6d5c633bbcb43c3a98fd))
diff --git a/pyproject.toml b/pyproject.toml
index 4da43df..d4f0594 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "beeper_desktop_api"
-version = "0.0.1"
+version = "0.2.0"
description = "The official Python library for the beeperdesktop API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/beeper_desktop_api/_version.py b/src/beeper_desktop_api/_version.py
index 3ba6273..49311d4 100644
--- a/src/beeper_desktop_api/_version.py
+++ b/src/beeper_desktop_api/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "beeper_desktop_api"
-__version__ = "0.0.1" # x-release-please-version
+__version__ = "0.2.0" # x-release-please-version