From fcf96d3c4f3bdbec2cd1b88745cdbbc48e864be2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:41:54 +0000 Subject: [PATCH 01/36] chore(internal): add request options to SSE classes --- src/beeper_desktop_api/_response.py | 3 +++ src/beeper_desktop_api/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/beeper_desktop_api/_response.py b/src/beeper_desktop_api/_response.py index 5d155b7..a7f1bf9 100644 --- a/src/beeper_desktop_api/_response.py +++ b/src/beeper_desktop_api/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/beeper_desktop_api/_streaming.py b/src/beeper_desktop_api/_streaming.py index 55409b8..be797cc 100644 --- a/src/beeper_desktop_api/_streaming.py +++ b/src/beeper_desktop_api/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import BeeperDesktop, AsyncBeeperDesktop + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: BeeperDesktop, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncBeeperDesktop, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 2420dd3d3de95350f142acaf7fb923fd292af59e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:53:34 +0000 Subject: [PATCH 02/36] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 5cbca10..aa68ad7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -984,6 +984,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1916,6 +1918,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From a54d51a23c31d38e124dc263f748f6ada2f2409c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:38:28 +0000 Subject: [PATCH 03/36] chore: configure new SDK language --- .stats.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 56c368e..eea9217 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-4acef56b00be513f305543096fdd407e6947f0a5ad268ab2e627ff30b37a75db.yml openapi_spec_hash: e876d796b6c25f18577f6be3944bf7d9 -config_hash: 659111d4e28efa599b5f800619ed79c2 +config_hash: 66617ffb2c7b6ef016e9704e766e7f65 diff --git a/README.md b/README.md index b42ad75..cb71054 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1tY3AiXSwiZW52Ijp7IkJFRVBFUl9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4ifX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1hcGktbWNwIl0sImVudiI6eyJCRUVQRVJfQUNDRVNTX1RPS0VOIjoiTXkgQWNjZXNzIFRva2VuIn19) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-api-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 770a8e2a6fc4d96dae58b3b787d55072faf63e34 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:39:39 +0000 Subject: [PATCH 04/36] feat(api): api update --- .stats.yml | 6 +++--- README.md | 18 ++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index eea9217..6e96390 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-4acef56b00be513f305543096fdd407e6947f0a5ad268ab2e627ff30b37a75db.yml -openapi_spec_hash: e876d796b6c25f18577f6be3944bf7d9 -config_hash: 66617ffb2c7b6ef016e9704e766e7f65 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml +openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a +config_hash: 659111d4e28efa599b5f800619ed79c2 diff --git a/README.md b/README.md index cb71054..15ac23a 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1hcGktbWNwIl0sImVudiI6eyJCRUVQRVJfQUNDRVNTX1RPS0VOIjoiTXkgQWNjZXNzIFRva2VuIn19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-api-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1tY3AiXSwiZW52Ijp7IkJFRVBFUl9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4ifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) > Note: You may need to set environment variables in your MCP client. @@ -35,12 +35,9 @@ pip install git+ssh://git@github.com/beeper/desktop-api-python.git The full API of this library can be found in [api.md](api.md). ```python -import os from beeper_desktop_api import BeeperDesktop -client = BeeperDesktop( - access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted -) +client = BeeperDesktop() page = client.chats.search( include_muted=True, @@ -60,13 +57,10 @@ so that your Access Token is not stored in source control. Simply import `AsyncBeeperDesktop` instead of `BeeperDesktop` and use `await` with each API call: ```python -import os import asyncio from beeper_desktop_api import AsyncBeeperDesktop -client = AsyncBeeperDesktop( - access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted -) +client = AsyncBeeperDesktop() async def main() -> None: @@ -97,7 +91,6 @@ pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/deskt Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python -import os import asyncio from beeper_desktop_api import DefaultAioHttpClient from beeper_desktop_api import AsyncBeeperDesktop @@ -105,9 +98,6 @@ from beeper_desktop_api import AsyncBeeperDesktop async def main() -> None: async with AsyncBeeperDesktop( - access_token=os.environ.get( - "BEEPER_ACCESS_TOKEN" - ), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: page = await client.chats.search( From 8b9d76c76fe4e3ae99d85429f20d9b782bea2520 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:41:55 +0000 Subject: [PATCH 05/36] chore: configure new SDK language --- .stats.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6e96390..ea5e4be 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 659111d4e28efa599b5f800619ed79c2 +config_hash: 66617ffb2c7b6ef016e9704e766e7f65 diff --git a/README.md b/README.md index 15ac23a..40c528f 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1tY3AiXSwiZW52Ijp7IkJFRVBFUl9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4ifX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1hcGktbWNwIl0sImVudiI6eyJCRUVQRVJfQUNDRVNTX1RPS0VOIjoiTXkgQWNjZXNzIFRva2VuIn19) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-api-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 266e1afc38d192d9412a4d295727fdc16d74235d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:58:36 +0000 Subject: [PATCH 06/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ea5e4be..7c03a30 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 66617ffb2c7b6ef016e9704e766e7f65 +config_hash: 2f5c2448fc8eec47bb412de39beb09dc From 5df69bf22554340ee0fd0c694fb755c80907ee22 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:59:54 +0000 Subject: [PATCH 07/36] 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 7c03a30..004aab8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 2f5c2448fc8eec47bb412de39beb09dc +config_hash: aa49273410d42fb96c5515dbce1f182f diff --git a/README.md b/README.md index 40c528f..b82d06c 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,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). @@ -84,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 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 1ad2ddfe678d2a495d69e45d7a1a8f0856af4211 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:25:14 +0000 Subject: [PATCH 08/36] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index aa68ad7..a687487 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -984,8 +984,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1918,8 +1924,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From a6b8aac8430c698cd1a73bab2cd257c9cf553df6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:47:58 +0000 Subject: [PATCH 09/36] chore(internal): codegen related update --- src/beeper_desktop_api/_client.py | 6 ++++++ src/beeper_desktop_api/resources/info.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py index 2dc0bf9..2f1306e 100644 --- a/src/beeper_desktop_api/_client.py +++ b/src/beeper_desktop_api/_client.py @@ -154,6 +154,7 @@ def assets(self) -> AssetsResource: @cached_property def info(self) -> InfoResource: + """Control the Beeper Desktop application""" from .resources.info import InfoResource return InfoResource(self) @@ -448,6 +449,7 @@ def assets(self) -> AsyncAssetsResource: @cached_property def info(self) -> AsyncInfoResource: + """Control the Beeper Desktop application""" from .resources.info import AsyncInfoResource return AsyncInfoResource(self) @@ -700,6 +702,7 @@ def assets(self) -> assets.AssetsResourceWithRawResponse: @cached_property def info(self) -> info.InfoResourceWithRawResponse: + """Control the Beeper Desktop application""" from .resources.info import InfoResourceWithRawResponse return InfoResourceWithRawResponse(self._client.info) @@ -748,6 +751,7 @@ def assets(self) -> assets.AsyncAssetsResourceWithRawResponse: @cached_property def info(self) -> info.AsyncInfoResourceWithRawResponse: + """Control the Beeper Desktop application""" from .resources.info import AsyncInfoResourceWithRawResponse return AsyncInfoResourceWithRawResponse(self._client.info) @@ -796,6 +800,7 @@ def assets(self) -> assets.AssetsResourceWithStreamingResponse: @cached_property def info(self) -> info.InfoResourceWithStreamingResponse: + """Control the Beeper Desktop application""" from .resources.info import InfoResourceWithStreamingResponse return InfoResourceWithStreamingResponse(self._client.info) @@ -844,6 +849,7 @@ def assets(self) -> assets.AsyncAssetsResourceWithStreamingResponse: @cached_property def info(self) -> info.AsyncInfoResourceWithStreamingResponse: + """Control the Beeper Desktop application""" from .resources.info import AsyncInfoResourceWithStreamingResponse return AsyncInfoResourceWithStreamingResponse(self._client.info) diff --git a/src/beeper_desktop_api/resources/info.py b/src/beeper_desktop_api/resources/info.py index 43a98bf..9b6bc94 100644 --- a/src/beeper_desktop_api/resources/info.py +++ b/src/beeper_desktop_api/resources/info.py @@ -20,6 +20,8 @@ class InfoResource(SyncAPIResource): + """Control the Beeper Desktop application""" + @cached_property def with_raw_response(self) -> InfoResourceWithRawResponse: """ @@ -63,6 +65,8 @@ def retrieve( class AsyncInfoResource(AsyncAPIResource): + """Control the Beeper Desktop application""" + @cached_property def with_raw_response(self) -> AsyncInfoResourceWithRawResponse: """ From 352dc26df496dac34b88c8d410a3d5761fad7cde Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:02:00 +0000 Subject: [PATCH 10/36] chore(test): do not count install time for mock server timeout --- scripts/mock | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/mock b/scripts/mock index 0b28f6e..bcf3b39 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - # Wait for server to come online + # Wait for server to come online (max 30s) echo -n "Waiting for server" + attempts=0 while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi echo -n "." sleep 0.1 done From 3f5692eb199bd02db1359e8131c8774eee7fabcf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:08:09 +0000 Subject: [PATCH 11/36] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e7de04..99e51ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,14 +61,18 @@ jobs: run: rye build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/beeper-desktop-api-python' + if: |- + github.repository == 'stainless-sdks/beeper-desktop-api-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/beeper-desktop-api-python' + if: |- + github.repository == 'stainless-sdks/beeper-desktop-api-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From f9883db325ccb453c60affa4218f2b257c4d41d8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:08:50 +0000 Subject: [PATCH 12/36] chore: update placeholder string --- tests/api_resources/test_assets.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/test_assets.py b/tests/api_resources/test_assets.py index 64927ac..16d9ffa 100644 --- a/tests/api_resources/test_assets.py +++ b/tests/api_resources/test_assets.py @@ -86,14 +86,14 @@ def test_streaming_response_serve(self, client: BeeperDesktop) -> None: @parametrize def test_method_upload(self, client: BeeperDesktop) -> None: asset = client.assets.upload( - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(AssetUploadResponse, asset, path=["response"]) @parametrize def test_method_upload_with_all_params(self, client: BeeperDesktop) -> None: asset = client.assets.upload( - file=b"raw file contents", + file=b"Example data", file_name="fileName", mime_type="mimeType", ) @@ -102,7 +102,7 @@ def test_method_upload_with_all_params(self, client: BeeperDesktop) -> None: @parametrize def test_raw_response_upload(self, client: BeeperDesktop) -> None: response = client.assets.with_raw_response.upload( - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -113,7 +113,7 @@ def test_raw_response_upload(self, client: BeeperDesktop) -> None: @parametrize def test_streaming_response_upload(self, client: BeeperDesktop) -> None: with client.assets.with_streaming_response.upload( - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -234,14 +234,14 @@ async def test_streaming_response_serve(self, async_client: AsyncBeeperDesktop) @parametrize async def test_method_upload(self, async_client: AsyncBeeperDesktop) -> None: asset = await async_client.assets.upload( - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(AssetUploadResponse, asset, path=["response"]) @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: asset = await async_client.assets.upload( - file=b"raw file contents", + file=b"Example data", file_name="fileName", mime_type="mimeType", ) @@ -250,7 +250,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncBeeperDesk @parametrize async def test_raw_response_upload(self, async_client: AsyncBeeperDesktop) -> None: response = await async_client.assets.with_raw_response.upload( - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -261,7 +261,7 @@ async def test_raw_response_upload(self, async_client: AsyncBeeperDesktop) -> No @parametrize async def test_streaming_response_upload(self, async_client: AsyncBeeperDesktop) -> None: async with async_client.assets.with_streaming_response.upload( - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 0d6781765c31c396a4cf0b0e89b61dd816645941 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:45:30 +0000 Subject: [PATCH 13/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 004aab8..06ba3c3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: aa49273410d42fb96c5515dbce1f182f +config_hash: bfb432c69dc0a8d273043a3cdd87ffe1 From c84dca576d56b83e314ab798749607e70aea7223 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:22:00 +0000 Subject: [PATCH 14/36] feat(api): manual updates --- .stats.yml | 2 +- README.md | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 06ba3c3..5dbc3d6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: bfb432c69dc0a8d273043a3cdd87ffe1 +config_hash: 6fc4359a793fc3fc9ac01712b5ef8c0d diff --git a/README.md b/README.md index b82d06c..7972750 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@ The Beeper Desktop Python library provides convenient access to the Beeper Deskt application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). +It is generated with [Stainless](https://www.stainless.com/). + ## MCP Server Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1hcGktbWNwIl0sImVudiI6eyJCRUVQRVJfQUNDRVNTX1RPS0VOIjoiTXkgQWNjZXNzIFRva2VuIn19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-api-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1tY3AiXSwiZW52Ijp7IkJFRVBFUl9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4ifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) > Note: You may need to set environment variables in your MCP client. @@ -23,10 +25,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). @@ -81,8 +86,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()`: From bbfc0badc6259db8d6d448ef828cf9ac484d1667 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:36:12 +0000 Subject: [PATCH 15/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 5dbc3d6..2b39be6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 6fc4359a793fc3fc9ac01712b5ef8c0d +config_hash: ca148af6be59ec54295b2c5f852a38d1 From 8b9fe85df1911bc10a65b5c965e5465c4041e065 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:15:09 +0000 Subject: [PATCH 16/36] fix(pydantic): do not pass `by_alias` unless set --- src/beeper_desktop_api/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/beeper_desktop_api/_compat.py b/src/beeper_desktop_api/_compat.py index 786ff42..e6690a4 100644 --- a/src/beeper_desktop_api/_compat.py +++ b/src/beeper_desktop_api/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From 922d90aeb6d75306490a359d26ebbf71a6e340b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:33:27 +0000 Subject: [PATCH 17/36] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 089b317..3f8161a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", From 311a998617de99c1defaa4c54f1b8a308d1bfaf3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:38:25 +0000 Subject: [PATCH 18/36] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99e51ac..afb122d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From 900c955edf1d5f8cf7aa9c7d8a7859e5b61ae379 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:08:03 +0000 Subject: [PATCH 19/36] fix: sanitize endpoint path params --- src/beeper_desktop_api/_utils/__init__.py | 1 + src/beeper_desktop_api/_utils/_path.py | 127 ++++++++++++++++++ .../resources/accounts/contacts.py | 10 +- .../resources/chats/chats.py | 10 +- .../resources/chats/messages/reactions.py | 18 ++- .../resources/chats/reminders.py | 10 +- src/beeper_desktop_api/resources/messages.py | 14 +- tests/test_utils/test_path.py | 89 ++++++++++++ 8 files changed, 252 insertions(+), 27 deletions(-) create mode 100644 src/beeper_desktop_api/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/beeper_desktop_api/_utils/__init__.py b/src/beeper_desktop_api/_utils/__init__.py index dc64e29..10cb66d 100644 --- a/src/beeper_desktop_api/_utils/__init__.py +++ b/src/beeper_desktop_api/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/beeper_desktop_api/_utils/_path.py b/src/beeper_desktop_api/_utils/_path.py new file mode 100644 index 0000000..4d6e1e4 --- /dev/null +++ b/src/beeper_desktop_api/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/beeper_desktop_api/resources/accounts/contacts.py b/src/beeper_desktop_api/resources/accounts/contacts.py index 02749f1..ba704bb 100644 --- a/src/beeper_desktop_api/resources/accounts/contacts.py +++ b/src/beeper_desktop_api/resources/accounts/contacts.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -88,7 +88,7 @@ def list( if not account_id: raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") return self._get_api_list( - f"/v1/accounts/{account_id}/contacts/list", + path_template("/v1/accounts/{account_id}/contacts/list", account_id=account_id), page=SyncCursorSearch[User], options=make_request_options( extra_headers=extra_headers, @@ -140,7 +140,7 @@ def search( 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", + path_template("/v1/accounts/{account_id}/contacts", account_id=account_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -215,7 +215,7 @@ def list( if not account_id: raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") return self._get_api_list( - f"/v1/accounts/{account_id}/contacts/list", + path_template("/v1/accounts/{account_id}/contacts/list", account_id=account_id), page=AsyncCursorSearch[User], options=make_request_options( extra_headers=extra_headers, @@ -267,7 +267,7 @@ async def search( 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", + path_template("/v1/accounts/{account_id}/contacts", account_id=account_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py index 6a3cdb0..b72d252 100644 --- a/src/beeper_desktop_api/resources/chats/chats.py +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -10,7 +10,7 @@ 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, NoneType, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from .reminders import ( RemindersResource, @@ -180,7 +180,7 @@ def retrieve( 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}", + path_template("/v1/chats/{chat_id}", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -281,7 +281,7 @@ def archive( raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/v1/chats/{chat_id}/archive", + path_template("/v1/chats/{chat_id}/archive", chat_id=chat_id), 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 @@ -523,7 +523,7 @@ async def retrieve( 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}", + path_template("/v1/chats/{chat_id}", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -624,7 +624,7 @@ async def archive( raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/v1/chats/{chat_id}/archive", + path_template("/v1/chats/{chat_id}/archive", chat_id=chat_id), 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 diff --git a/src/beeper_desktop_api/resources/chats/messages/reactions.py b/src/beeper_desktop_api/resources/chats/messages/reactions.py index d9e610d..13a855d 100644 --- a/src/beeper_desktop_api/resources/chats/messages/reactions.py +++ b/src/beeper_desktop_api/resources/chats/messages/reactions.py @@ -5,7 +5,7 @@ import httpx from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform +from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -78,7 +78,9 @@ def delete( if not message_id: raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") return self._delete( - f"/v1/chats/{chat_id}/messages/{message_id}/reactions", + path_template( + "/v1/chats/{chat_id}/messages/{message_id}/reactions", chat_id=chat_id, message_id=message_id + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -126,7 +128,9 @@ def add( if not message_id: raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") return self._post( - f"/v1/chats/{chat_id}/messages/{message_id}/reactions", + path_template( + "/v1/chats/{chat_id}/messages/{message_id}/reactions", chat_id=chat_id, message_id=message_id + ), body=maybe_transform( { "reaction_key": reaction_key, @@ -197,7 +201,9 @@ async def delete( if not message_id: raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") return await self._delete( - f"/v1/chats/{chat_id}/messages/{message_id}/reactions", + path_template( + "/v1/chats/{chat_id}/messages/{message_id}/reactions", chat_id=chat_id, message_id=message_id + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -247,7 +253,9 @@ async def add( if not message_id: raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") return await self._post( - f"/v1/chats/{chat_id}/messages/{message_id}/reactions", + path_template( + "/v1/chats/{chat_id}/messages/{message_id}/reactions", chat_id=chat_id, message_id=message_id + ), body=await async_maybe_transform( { "reaction_key": reaction_key, diff --git a/src/beeper_desktop_api/resources/chats/reminders.py b/src/beeper_desktop_api/resources/chats/reminders.py index 2096903..32a169b 100644 --- a/src/beeper_desktop_api/resources/chats/reminders.py +++ b/src/beeper_desktop_api/resources/chats/reminders.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Query, Headers, NoneType, NotGiven, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -74,7 +74,7 @@ def create( raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/v1/chats/{chat_id}/reminders", + path_template("/v1/chats/{chat_id}/reminders", chat_id=chat_id), 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 @@ -111,7 +111,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/v1/chats/{chat_id}/reminders", + path_template("/v1/chats/{chat_id}/reminders", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -173,7 +173,7 @@ async def create( raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/v1/chats/{chat_id}/reminders", + path_template("/v1/chats/{chat_id}/reminders", chat_id=chat_id), 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 @@ -210,7 +210,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/v1/chats/{chat_id}/reminders", + path_template("/v1/chats/{chat_id}/reminders", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index b97c7a0..af2178e 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -10,7 +10,7 @@ from ..types import message_list_params, message_send_params, message_search_params, message_update_params from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -86,7 +86,7 @@ def update( if not message_id: raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") return self._put( - f"/v1/chats/{chat_id}/messages/{message_id}", + path_template("/v1/chats/{chat_id}/messages/{message_id}", chat_id=chat_id, message_id=message_id), body=maybe_transform({"text": text}, message_update_params.MessageUpdateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -130,7 +130,7 @@ def list( 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", + path_template("/v1/chats/{chat_id}/messages", chat_id=chat_id), page=SyncCursorSortKey[Message], options=make_request_options( extra_headers=extra_headers, @@ -288,7 +288,7 @@ def send( 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}/messages", + path_template("/v1/chats/{chat_id}/messages", chat_id=chat_id), body=maybe_transform( { "attachment": attachment, @@ -362,7 +362,7 @@ async def update( if not message_id: raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") return await self._put( - f"/v1/chats/{chat_id}/messages/{message_id}", + path_template("/v1/chats/{chat_id}/messages/{message_id}", chat_id=chat_id, message_id=message_id), body=await async_maybe_transform({"text": text}, message_update_params.MessageUpdateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -406,7 +406,7 @@ def list( 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", + path_template("/v1/chats/{chat_id}/messages", chat_id=chat_id), page=AsyncCursorSortKey[Message], options=make_request_options( extra_headers=extra_headers, @@ -564,7 +564,7 @@ async def send( 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}/messages", + path_template("/v1/chats/{chat_id}/messages", chat_id=chat_id), body=await async_maybe_transform( { "attachment": attachment, diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 0000000..d429db8 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from beeper_desktop_api._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From ef99778f642f49a26aad1d65c59df9f9cfa766e9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:09:41 +0000 Subject: [PATCH 20/36] refactor(tests): switch from prism to steady --- CONTRIBUTING.md | 2 +- scripts/mock | 26 +++++++++++++------------- scripts/test | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08c3ec2..f303ab9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ $ pip install ./path-to-wheel-file.whl ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/scripts/mock b/scripts/mock index bcf3b39..00b490b 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.3 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index dbeda2d..d0fe9be 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi From 68f14afb85f168eb61a91398165b1d8222fbeea4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:11:00 +0000 Subject: [PATCH 21/36] chore(tests): bump steady to v0.19.4 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 00b490b..f310477 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.3 -- steady --version + npm exec --package=@stdy/cli@0.19.4 -- steady --version - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index d0fe9be..0c2bfad 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 9229d32b59f8494f49677dbf508d2fedd21cd8b4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:18:23 +0000 Subject: [PATCH 22/36] chore(tests): bump steady to v0.19.5 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index f310477..54fc791 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.4 -- steady --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 0c2bfad..4153738 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 3dfb3799f6ebbde6400db092864361e3b6a6e07f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:09:33 +0000 Subject: [PATCH 23/36] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb18..3824f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From 166b069fbd3034b27d16baf50ef96c61a46996d9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:16:24 +0000 Subject: [PATCH 24/36] chore(tests): bump steady to v0.19.6 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 54fc791..0f82c95 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.5 -- steady --version + npm exec --package=@stdy/cli@0.19.6 -- steady --version - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 4153738..4f9eef9 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 1fe013eacfb814508b88eefa3b2ee3bf51618edc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:05:07 +0000 Subject: [PATCH 25/36] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afb122d..1ca0ca2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -38,7 +38,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From 31a8e58a09bd798b9930c195da2be239d50a2e77 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:05:53 +0000 Subject: [PATCH 26/36] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 0f82c95..3732f8e 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 4f9eef9..e642cea 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From b954521c09743ea77dc1fe3f49e62cadb9cb6b1f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:03 +0000 Subject: [PATCH 27/36] 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 2b39be6..60bb453 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: ca148af6be59ec54295b2c5f852a38d1 +config_hash: e342a96262eaf44c54e8bbb93cc8d7a7 diff --git a/README.md b/README.md index 7972750..b0c6f12 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,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). @@ -86,8 +83,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 fc973f48f6dce9712ce7da0c588ed9fee2e6348e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:37 +0000 Subject: [PATCH 28/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 60bb453..16d5bba 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: e342a96262eaf44c54e8bbb93cc8d7a7 +config_hash: f99f904573839260bdb6d428bad17613 From daf5390c2b456877ee06b154d9567af708bb0057 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:26:20 +0000 Subject: [PATCH 29/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 16d5bba..2c47924 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: f99f904573839260bdb6d428bad17613 +config_hash: 7d85c0b454fc78a59db6474c5c4d73c6 From de85c3aef481f44350bab667ed81e155573ace81 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:09:32 +0000 Subject: [PATCH 30/36] feat(internal): implement indices array format for query and form serialization --- scripts/mock | 4 ++-- scripts/test | 2 +- src/beeper_desktop_api/_qs.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 3732f8e..58e4628 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index e642cea..3970464 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 diff --git a/src/beeper_desktop_api/_qs.py b/src/beeper_desktop_api/_qs.py index ada6fd3..de8c99b 100644 --- a/src/beeper_desktop_api/_qs.py +++ b/src/beeper_desktop_api/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From d2cf119042313a4b82f4a395a43c175874ceca1e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:17:51 +0000 Subject: [PATCH 31/36] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 58e4628..5ea72a2 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 3970464..3fdac80 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 0def55c17787849b27d1e8c21cb6cd129069e220 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:22:29 +0000 Subject: [PATCH 32/36] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 5ea72a2..7c58865 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 3fdac80..87cdeac 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 9e86464960e28472bc3a4f137c5d2c025f2acc16 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:10:53 +0000 Subject: [PATCH 33/36] fix(client): preserve hardcoded query params when merging with user params --- src/beeper_desktop_api/_base_client.py | 4 +++ tests/test_client.py | 48 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/beeper_desktop_api/_base_client.py b/src/beeper_desktop_api/_base_client.py index 25424b1..4e62b4b 100644 --- a/src/beeper_desktop_api/_base_client.py +++ b/src/beeper_desktop_api/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index a687487..0e4b49b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -443,6 +443,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: BeeperDesktop) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: BeeperDesktop) -> None: request = client._build_request( FinalRequestOptions( @@ -1366,6 +1390,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncBeeperDesktop) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: BeeperDesktop) -> None: request = client._build_request( FinalRequestOptions( From 69f6d11ecb0959d1a5eb90c41c76542a1ea5826f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:30:02 +0000 Subject: [PATCH 34/36] fix: ensure file data are only sent as 1 parameter --- src/beeper_desktop_api/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/beeper_desktop_api/_utils/_utils.py b/src/beeper_desktop_api/_utils/_utils.py index eec7f4a..63b8cd6 100644 --- a/src/beeper_desktop_api/_utils/_utils.py +++ b/src/beeper_desktop_api/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 497fb79..7c7c5dd 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From af70fc9fab45036721b4be634bb4444964c70d1e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:31:50 +0000 Subject: [PATCH 35/36] feat(api): add network, bridge fields to accounts --- .stats.yml | 6 +- README.md | 34 +++++--- .../resources/chats/chats.py | 86 +------------------ src/beeper_desktop_api/types/account.py | 23 ++++- .../types/chat_create_params.py | 83 ++++++++++-------- tests/api_resources/test_chats.py | 74 +++++++--------- 6 files changed, 127 insertions(+), 179 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2c47924..229f6b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml -openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 7d85c0b454fc78a59db6474c5c4d73c6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml +openapi_spec_hash: d6c0a1776048dab04f6c5625c9893c9c +config_hash: 39ed0717b5f415499aaace2468346e1a diff --git a/README.md b/README.md index b0c6f12..c0c9be9 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ The Beeper Desktop Python library provides convenient access to the Beeper Deskt application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). -It is generated with [Stainless](https://www.stainless.com/). - ## MCP Server Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. @@ -25,18 +23,24 @@ 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). ```python +import os from beeper_desktop_api import BeeperDesktop -client = BeeperDesktop() +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, @@ -56,10 +60,13 @@ so that your Access Token is not stored in source control. Simply import `AsyncBeeperDesktop` instead of `BeeperDesktop` and use `await` with each API call: ```python +import os import asyncio from beeper_desktop_api import AsyncBeeperDesktop -client = AsyncBeeperDesktop() +client = AsyncBeeperDesktop( + access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted +) async def main() -> None: @@ -83,13 +90,14 @@ 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()`: ```python +import os import asyncio from beeper_desktop_api import DefaultAioHttpClient from beeper_desktop_api import AsyncBeeperDesktop @@ -97,6 +105,9 @@ from beeper_desktop_api import AsyncBeeperDesktop async def main() -> None: async with AsyncBeeperDesktop( + access_token=os.environ.get( + "BEEPER_ACCESS_TOKEN" + ), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: page = await client.chats.search( @@ -207,11 +218,10 @@ from beeper_desktop_api import BeeperDesktop client = BeeperDesktop() -chat = client.chats.create( - account_id="accountID", - user={}, +client.chats.reminders.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, ) -print(chat.user) ``` ## File uploads diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py index b72d252..2a6a92c 100644 --- a/src/beeper_desktop_api/resources/chats/chats.py +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -79,14 +79,7 @@ def with_streaming_response(self) -> ChatsResourceWithStreamingResponse: def create( self, *, - account_id: str, - allow_invite: bool | Omit = omit, - message_text: str | Omit = omit, - mode: Literal["create", "start"] | Omit = omit, - participant_ids: SequenceNotStr[str] | Omit = omit, - title: str | Omit = omit, - type: Literal["single", "group"] | Omit = omit, - user: chat_create_params.User | Omit = omit, + params: chat_create_params.Params | 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, @@ -99,26 +92,6 @@ def create( user data (mode='start'). Args: - account_id: Account to create or start the chat on. - - allow_invite: Whether invite-based DM creation is allowed when required by the platform. Used - for mode='start'. - - message_text: Optional first message content if the platform requires it to create the chat. - - mode: Operation mode. Defaults to 'create' when omitted. - - participant_ids: Required when mode='create'. User IDs to include in the new chat. - - title: Optional title for group chats when mode='create'; ignored for single chats on - most platforms. - - type: Required when mode='create'. 'single' requires exactly one participantID; - 'group' supports multiple participants and optional title. - - user: Required when mode='start'. Merged user-like contact payload used to resolve the - best identifier. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -129,19 +102,7 @@ def create( """ return self._post( "/v1/chats", - body=maybe_transform( - { - "account_id": account_id, - "allow_invite": allow_invite, - "message_text": message_text, - "mode": mode, - "participant_ids": participant_ids, - "title": title, - "type": type, - "user": user, - }, - chat_create_params.ChatCreateParams, - ), + body=maybe_transform(params, chat_create_params.ChatCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -422,14 +383,7 @@ def with_streaming_response(self) -> AsyncChatsResourceWithStreamingResponse: async def create( self, *, - account_id: str, - allow_invite: bool | Omit = omit, - message_text: str | Omit = omit, - mode: Literal["create", "start"] | Omit = omit, - participant_ids: SequenceNotStr[str] | Omit = omit, - title: str | Omit = omit, - type: Literal["single", "group"] | Omit = omit, - user: chat_create_params.User | Omit = omit, + params: chat_create_params.Params | 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, @@ -442,26 +396,6 @@ async def create( user data (mode='start'). Args: - account_id: Account to create or start the chat on. - - allow_invite: Whether invite-based DM creation is allowed when required by the platform. Used - for mode='start'. - - message_text: Optional first message content if the platform requires it to create the chat. - - mode: Operation mode. Defaults to 'create' when omitted. - - participant_ids: Required when mode='create'. User IDs to include in the new chat. - - title: Optional title for group chats when mode='create'; ignored for single chats on - most platforms. - - type: Required when mode='create'. 'single' requires exactly one participantID; - 'group' supports multiple participants and optional title. - - user: Required when mode='start'. Merged user-like contact payload used to resolve the - best identifier. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -472,19 +406,7 @@ async def create( """ return await self._post( "/v1/chats", - body=await async_maybe_transform( - { - "account_id": account_id, - "allow_invite": allow_invite, - "message_text": message_text, - "mode": mode, - "participant_ids": participant_ids, - "title": title, - "type": type, - "user": user, - }, - chat_create_params.ChatCreateParams, - ), + body=await async_maybe_transform(params, chat_create_params.ChatCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/beeper_desktop_api/types/account.py b/src/beeper_desktop_api/types/account.py index ff00c78..c024419 100644 --- a/src/beeper_desktop_api/types/account.py +++ b/src/beeper_desktop_api/types/account.py @@ -1,11 +1,26 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing_extensions import Literal + from pydantic import Field as FieldInfo from .._models import BaseModel from .shared.user import User -__all__ = ["Account"] +__all__ = ["Account", "Bridge"] + + +class Bridge(BaseModel): + """Bridge metadata for the account. Available from Beeper Desktop v.4.2.719+.""" + + id: str + """Bridge instance identifier.""" + + provider: Literal["cloud", "self-hosted", "local", "platform-sdk"] + """Bridge provider for the account.""" + + type: str + """Bridge type.""" class Account(BaseModel): @@ -14,5 +29,11 @@ class Account(BaseModel): account_id: str = FieldInfo(alias="accountID") """Chat account added to Beeper. Use this to route account-scoped actions.""" + bridge: Bridge + """Bridge metadata for the account. Available from Beeper Desktop v.4.2.719+.""" + + network: str + """Human-friendly network name for the account.""" + user: User """User the account belongs to.""" diff --git a/src/beeper_desktop_api/types/chat_create_params.py b/src/beeper_desktop_api/types/chat_create_params.py index 93229c1..f755b70 100644 --- a/src/beeper_desktop_api/types/chat_create_params.py +++ b/src/beeper_desktop_api/types/chat_create_params.py @@ -2,18 +2,48 @@ from __future__ import annotations -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import Union +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._types import SequenceNotStr from .._utils import PropertyInfo -__all__ = ["ChatCreateParams", "User"] +__all__ = ["ChatCreateParams", "Params", "ParamsUnionMember0", "ParamsUnionMember0User", "ParamsUnionMember1"] class ChatCreateParams(TypedDict, total=False): + params: Params + + +class ParamsUnionMember0User(TypedDict, total=False): + """Merged user-like contact payload used to resolve the best identifier.""" + + id: str + """Known user ID when available.""" + + email: str + """Email candidate.""" + + full_name: Annotated[str, PropertyInfo(alias="fullName")] + """Display name hint used for ranking only.""" + + phone_number: Annotated[str, PropertyInfo(alias="phoneNumber")] + """Phone number candidate (E.164 preferred).""" + + username: str + """Username/handle candidate.""" + + +class ParamsUnionMember0(TypedDict, total=False): account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]] """Account to create or start the chat on.""" + mode: Required[Literal["start"]] + """Operation mode. Use 'start' to resolve a user/contact and start a direct chat.""" + + user: Required[ParamsUnionMember0User] + """Merged user-like contact payload used to resolve the best identifier.""" + allow_invite: Annotated[bool, PropertyInfo(alias="allowInvite")] """Whether invite-based DM creation is allowed when required by the platform. @@ -23,49 +53,28 @@ class ChatCreateParams(TypedDict, total=False): message_text: Annotated[str, PropertyInfo(alias="messageText")] """Optional first message content if the platform requires it to create the chat.""" - mode: Literal["create", "start"] - """Operation mode. Defaults to 'create' when omitted.""" - - participant_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="participantIDs")] - """Required when mode='create'. User IDs to include in the new chat.""" - title: str - """ - Optional title for group chats when mode='create'; ignored for single chats on - most platforms. - """ +class ParamsUnionMember1(TypedDict, total=False): + account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]] + """Account to create or start the chat on.""" - type: Literal["single", "group"] - """Required when mode='create'. + participant_ids: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="participantIDs")]] + """User IDs to include in the new chat.""" + type: Required[Literal["single", "group"]] + """ 'single' requires exactly one participantID; 'group' supports multiple participants and optional title. """ - user: User - """Required when mode='start'. - - Merged user-like contact payload used to resolve the best identifier. - """ - - -class User(TypedDict, total=False): - """Required when mode='start'. - - Merged user-like contact payload used to resolve the best identifier. - """ - - id: str - """Known user ID when available.""" + message_text: Annotated[str, PropertyInfo(alias="messageText")] + """Optional first message content if the platform requires it to create the chat.""" - email: str - """Email candidate.""" + mode: Literal["create"] + """Operation mode. Defaults to 'create' when omitted.""" - full_name: Annotated[str, PropertyInfo(alias="fullName")] - """Display name hint used for ranking only.""" + title: str + """Optional title for group chats; ignored for single chats on most platforms.""" - phone_number: Annotated[str, PropertyInfo(alias="phoneNumber")] - """Phone number candidate (E.164 preferred).""" - username: str - """Username/handle candidate.""" +Params: TypeAlias = Union[ParamsUnionMember0, ParamsUnionMember1] diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py index b899add..10de8b2 100644 --- a/tests/api_resources/test_chats.py +++ b/tests/api_resources/test_chats.py @@ -25,36 +25,31 @@ class TestChats: @parametrize def test_method_create(self, client: BeeperDesktop) -> None: - chat = client.chats.create( - account_id="accountID", - ) + chat = client.chats.create() 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="accountID", - allow_invite=True, - message_text="messageText", - mode="create", - participant_ids=["string"], - title="title", - type="single", - user={ - "id": "id", - "email": "email", - "full_name": "fullName", - "phone_number": "phoneNumber", - "username": "username", + params={ + "account_id": "accountID", + "mode": "start", + "user": { + "id": "id", + "email": "email", + "full_name": "fullName", + "phone_number": "phoneNumber", + "username": "username", + }, + "allow_invite": True, + "message_text": "messageText", }, ) 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="accountID", - ) + response = client.chats.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -63,9 +58,7 @@ def test_raw_response_create(self, client: BeeperDesktop) -> None: @parametrize def test_streaming_response_create(self, client: BeeperDesktop) -> None: - with client.chats.with_streaming_response.create( - account_id="accountID", - ) as response: + with client.chats.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -257,36 +250,31 @@ class TestAsyncChats: @parametrize async def test_method_create(self, async_client: AsyncBeeperDesktop) -> None: - chat = await async_client.chats.create( - account_id="accountID", - ) + chat = await async_client.chats.create() 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="accountID", - allow_invite=True, - message_text="messageText", - mode="create", - participant_ids=["string"], - title="title", - type="single", - user={ - "id": "id", - "email": "email", - "full_name": "fullName", - "phone_number": "phoneNumber", - "username": "username", + params={ + "account_id": "accountID", + "mode": "start", + "user": { + "id": "id", + "email": "email", + "full_name": "fullName", + "phone_number": "phoneNumber", + "username": "username", + }, + "allow_invite": True, + "message_text": "messageText", }, ) 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="accountID", - ) + response = await async_client.chats.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -295,9 +283,7 @@ async def test_raw_response_create(self, async_client: AsyncBeeperDesktop) -> No @parametrize async def test_streaming_response_create(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.chats.with_streaming_response.create( - account_id="accountID", - ) as response: + async with async_client.chats.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From e7927bcc67bc7d612d24c15d1286967bcba7c205 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:32:12 +0000 Subject: [PATCH 36/36] release: 4.4.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 49 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/beeper_desktop_api/_version.py | 2 +- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 29102ae..934f2cc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.3.0" + ".": "4.4.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ee4c9..f37b826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 4.4.0 (2026-04-12) + +Full Changelog: [v4.3.0...v4.4.0](https://github.com/beeper/desktop-api-python/compare/v4.3.0...v4.4.0) + +### Features + +* **api:** add network, bridge fields to accounts ([af70fc9](https://github.com/beeper/desktop-api-python/commit/af70fc9fab45036721b4be634bb4444964c70d1e)) +* **api:** api update ([770a8e2](https://github.com/beeper/desktop-api-python/commit/770a8e2a6fc4d96dae58b3b787d55072faf63e34)) +* **api:** manual updates ([c84dca5](https://github.com/beeper/desktop-api-python/commit/c84dca576d56b83e314ab798749607e70aea7223)) +* **internal:** implement indices array format for query and form serialization ([de85c3a](https://github.com/beeper/desktop-api-python/commit/de85c3aef481f44350bab667ed81e155573ace81)) + + +### Bug Fixes + +* **client:** preserve hardcoded query params when merging with user params ([9e86464](https://github.com/beeper/desktop-api-python/commit/9e86464960e28472bc3a4f137c5d2c025f2acc16)) +* **deps:** bump minimum typing-extensions version ([922d90a](https://github.com/beeper/desktop-api-python/commit/922d90aeb6d75306490a359d26ebbf71a6e340b8)) +* ensure file data are only sent as 1 parameter ([69f6d11](https://github.com/beeper/desktop-api-python/commit/69f6d11ecb0959d1a5eb90c41c76542a1ea5826f)) +* **pydantic:** do not pass `by_alias` unless set ([8b9fe85](https://github.com/beeper/desktop-api-python/commit/8b9fe85df1911bc10a65b5c965e5465c4041e065)) +* sanitize endpoint path params ([900c955](https://github.com/beeper/desktop-api-python/commit/900c955edf1d5f8cf7aa9c7d8a7859e5b61ae379)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([1fe013e](https://github.com/beeper/desktop-api-python/commit/1fe013eacfb814508b88eefa3b2ee3bf51618edc)) +* **ci:** skip uploading artifacts on stainless-internal branches ([3f5692e](https://github.com/beeper/desktop-api-python/commit/3f5692eb199bd02db1359e8131c8774eee7fabcf)) +* configure new SDK language ([8b9d76c](https://github.com/beeper/desktop-api-python/commit/8b9d76c76fe4e3ae99d85429f20d9b782bea2520)) +* configure new SDK language ([a54d51a](https://github.com/beeper/desktop-api-python/commit/a54d51a23c31d38e124dc263f748f6ada2f2409c)) +* **internal:** add request options to SSE classes ([fcf96d3](https://github.com/beeper/desktop-api-python/commit/fcf96d3c4f3bdbec2cd1b88745cdbbc48e864be2)) +* **internal:** codegen related update ([a6b8aac](https://github.com/beeper/desktop-api-python/commit/a6b8aac8430c698cd1a73bab2cd257c9cf553df6)) +* **internal:** make `test_proxy_environment_variables` more resilient ([2420dd3](https://github.com/beeper/desktop-api-python/commit/2420dd3d3de95350f142acaf7fb923fd292af59e)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([1ad2ddf](https://github.com/beeper/desktop-api-python/commit/1ad2ddfe678d2a495d69e45d7a1a8f0856af4211)) +* **internal:** tweak CI branches ([311a998](https://github.com/beeper/desktop-api-python/commit/311a998617de99c1defaa4c54f1b8a308d1bfaf3)) +* **internal:** update gitignore ([3dfb379](https://github.com/beeper/desktop-api-python/commit/3dfb3799f6ebbde6400db092864361e3b6a6e07f)) +* **test:** do not count install time for mock server timeout ([352dc26](https://github.com/beeper/desktop-api-python/commit/352dc26df496dac34b88c8d410a3d5761fad7cde)) +* **tests:** bump steady to v0.19.4 ([68f14af](https://github.com/beeper/desktop-api-python/commit/68f14afb85f168eb61a91398165b1d8222fbeea4)) +* **tests:** bump steady to v0.19.5 ([9229d32](https://github.com/beeper/desktop-api-python/commit/9229d32b59f8494f49677dbf508d2fedd21cd8b4)) +* **tests:** bump steady to v0.19.6 ([166b069](https://github.com/beeper/desktop-api-python/commit/166b069fbd3034b27d16baf50ef96c61a46996d9)) +* **tests:** bump steady to v0.19.7 ([31a8e58](https://github.com/beeper/desktop-api-python/commit/31a8e58a09bd798b9930c195da2be239d50a2e77)) +* **tests:** bump steady to v0.20.1 ([d2cf119](https://github.com/beeper/desktop-api-python/commit/d2cf119042313a4b82f4a395a43c175874ceca1e)) +* **tests:** bump steady to v0.20.2 ([0def55c](https://github.com/beeper/desktop-api-python/commit/0def55c17787849b27d1e8c21cb6cd129069e220)) +* update placeholder string ([f9883db](https://github.com/beeper/desktop-api-python/commit/f9883db325ccb453c60affa4218f2b257c4d41d8)) +* update SDK settings ([b954521](https://github.com/beeper/desktop-api-python/commit/b954521c09743ea77dc1fe3f49e62cadb9cb6b1f)) +* update SDK settings ([5df69bf](https://github.com/beeper/desktop-api-python/commit/5df69bf22554340ee0fd0c694fb755c80907ee22)) + + +### Refactors + +* **tests:** switch from prism to steady ([ef99778](https://github.com/beeper/desktop-api-python/commit/ef99778f642f49a26aad1d65c59df9f9cfa766e9)) + ## 4.3.0 (2026-02-20) Full Changelog: [v4.2.0...v4.3.0](https://github.com/beeper/desktop-api-python/compare/v4.2.0...v4.3.0) diff --git a/pyproject.toml b/pyproject.toml index 3f8161a..4e6b6f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "beeper_desktop_api" -version = "4.3.0" +version = "4.4.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 1bc95e4..d6b05f3 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__ = "4.3.0" # x-release-please-version +__version__ = "4.4.0" # x-release-please-version