From 545ed69d7251f47a309f2f46ee4f3b8e4cf1cc60 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:58:21 +0000 Subject: [PATCH 01/20] feat(api): manual updates --- .github/workflows/publish-pypi.yml | 31 +++++++++++ .github/workflows/release-doctor.yml | 21 ++++++++ .release-please-manifest.json | 3 ++ .stats.yml | 2 +- CONTRIBUTING.md | 4 +- README.md | 14 ++--- bin/check-release-environment | 21 ++++++++ pyproject.toml | 6 +-- release-please-config.json | 66 +++++++++++++++++++++++ src/beeper_desktop_api/_version.py | 2 +- src/beeper_desktop_api/resources/token.py | 8 +-- 11 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..c22364e --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/beeper/desktop-api-python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.BEEPER_DESKTOP_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..c6b3e44 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'beeper/desktop-api-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v4 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.BEEPER_DESKTOP_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..1332969 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index abfa216..ab027c2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 1 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: def03aa92de3408ec65438763617f5c7 +config_hash: f83b2b6eb86f2dd68101065998479cb2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7899f17..81bc94a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/stainless-sdks/beeper-desktop-api-python.git +$ pip install git+ssh://git@github.com/beeper/desktop-api-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/beeper-desktop-api-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/beeper/desktop-api-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index c634600..51fb670 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ The REST API documentation can be found on [developers.beeper.com](https://devel ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/beeper-desktop-api-python.git +# install from the production repo +pip install git+ssh://git@github.com/beeper/desktop-api-python.git ``` > [!NOTE] @@ -73,8 +73,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from this staging repo -pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/stainless-sdks/beeper-desktop-api-python.git' +# install from the production repo +pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/desktop-api-python.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: @@ -236,9 +236,9 @@ token = response.parse() # get the object that `token.info()` would have return print(token.sub) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/beeper-desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) object. +These methods return an [`APIResponse`](https://github.com/beeper/desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/beeper-desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/beeper/desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -342,7 +342,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/beeper-desktop-api-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/beeper/desktop-api-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..b845b0f --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index b90ac16..d3a4a85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/beeper-desktop-api-python" -Repository = "https://github.com/stainless-sdks/beeper-desktop-api-python" +Homepage = "https://github.com/beeper/desktop-api-python" +Repository = "https://github.com/beeper/desktop-api-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] @@ -124,7 +124,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/beeper-desktop-api-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/beeper/desktop-api-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..fd672c1 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/beeper_desktop_api/_version.py" + ] +} \ No newline at end of file diff --git a/src/beeper_desktop_api/_version.py b/src/beeper_desktop_api/_version.py index 72a9009..3ba6273 100644 --- a/src/beeper_desktop_api/_version.py +++ b/src/beeper_desktop_api/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "beeper_desktop_api" -__version__ = "0.0.1" +__version__ = "0.0.1" # x-release-please-version diff --git a/src/beeper_desktop_api/resources/token.py b/src/beeper_desktop_api/resources/token.py index fbf0425..5648872 100644 --- a/src/beeper_desktop_api/resources/token.py +++ b/src/beeper_desktop_api/resources/token.py @@ -28,7 +28,7 @@ def with_raw_response(self) -> TokenResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers """ return TokenResourceWithRawResponse(self) @@ -37,7 +37,7 @@ def with_streaming_response(self) -> TokenResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#with_streaming_response + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response """ return TokenResourceWithStreamingResponse(self) @@ -70,7 +70,7 @@ def with_raw_response(self) -> AsyncTokenResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers """ return AsyncTokenResourceWithRawResponse(self) @@ -79,7 +79,7 @@ def with_streaming_response(self) -> AsyncTokenResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/beeper-desktop-api-python#with_streaming_response + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response """ return AsyncTokenResourceWithStreamingResponse(self) From b1ba1c0584b99ab402f7c1643c13c19881baa600 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:58:48 +0000 Subject: [PATCH 02/20] feat(api): manual updates --- .stats.yml | 6 +- README.md | 136 +++- api.md | 61 +- src/beeper_desktop_api/_client.py | 351 ++++++++- src/beeper_desktop_api/resources/__init__.py | 56 ++ src/beeper_desktop_api/resources/accounts.py | 145 ++++ .../resources/chats/__init__.py | 33 + .../resources/chats/chats.py | 668 ++++++++++++++++++ .../resources/chats/reminders.py | 267 +++++++ src/beeper_desktop_api/resources/contacts.py | 197 ++++++ src/beeper_desktop_api/resources/messages.py | 423 +++++++++++ src/beeper_desktop_api/types/__init__.py | 19 + src/beeper_desktop_api/types/account.py | 19 + .../types/account_list_response.py | 10 + src/beeper_desktop_api/types/chat.py | 67 ++ .../types/chat_archive_params.py | 12 + .../types/chat_create_params.py | 30 + .../types/chat_create_response.py | 14 + .../types/chat_retrieve_params.py | 18 + .../types/chat_search_params.py | 81 +++ .../types/chats/__init__.py | 2 + .../types/chats/reminder_create_params.py | 22 + .../types/client_download_asset_params.py | 12 + .../types/client_open_params.py | 26 + .../types/client_search_params.py | 12 + .../types/contact_search_params.py | 17 + .../types/contact_search_response.py | 12 + .../types/download_asset_response.py | 17 + .../types/message_search_params.py | 85 +++ .../types/message_send_params.py | 20 + .../types/message_send_response.py | 15 + src/beeper_desktop_api/types/open_response.py | 10 + .../types/search_response.py | 48 ++ src/beeper_desktop_api/types/shared/error.py | 12 +- tests/api_resources/chats/__init__.py | 1 + tests/api_resources/chats/test_reminders.py | 206 ++++++ tests/api_resources/test_accounts.py | 74 ++ tests/api_resources/test_chats.py | 402 +++++++++++ tests/api_resources/test_client.py | 222 ++++++ tests/api_resources/test_contacts.py | 92 +++ tests/api_resources/test_messages.py | 203 ++++++ tests/test_client.py | 40 +- 42 files changed, 4113 insertions(+), 50 deletions(-) create mode 100644 src/beeper_desktop_api/resources/accounts.py create mode 100644 src/beeper_desktop_api/resources/chats/__init__.py create mode 100644 src/beeper_desktop_api/resources/chats/chats.py create mode 100644 src/beeper_desktop_api/resources/chats/reminders.py create mode 100644 src/beeper_desktop_api/resources/contacts.py create mode 100644 src/beeper_desktop_api/resources/messages.py create mode 100644 src/beeper_desktop_api/types/account.py create mode 100644 src/beeper_desktop_api/types/account_list_response.py create mode 100644 src/beeper_desktop_api/types/chat.py create mode 100644 src/beeper_desktop_api/types/chat_archive_params.py create mode 100644 src/beeper_desktop_api/types/chat_create_params.py create mode 100644 src/beeper_desktop_api/types/chat_create_response.py create mode 100644 src/beeper_desktop_api/types/chat_retrieve_params.py create mode 100644 src/beeper_desktop_api/types/chat_search_params.py create mode 100644 src/beeper_desktop_api/types/chats/reminder_create_params.py create mode 100644 src/beeper_desktop_api/types/client_download_asset_params.py create mode 100644 src/beeper_desktop_api/types/client_open_params.py create mode 100644 src/beeper_desktop_api/types/client_search_params.py create mode 100644 src/beeper_desktop_api/types/contact_search_params.py create mode 100644 src/beeper_desktop_api/types/contact_search_response.py create mode 100644 src/beeper_desktop_api/types/download_asset_response.py create mode 100644 src/beeper_desktop_api/types/message_search_params.py create mode 100644 src/beeper_desktop_api/types/message_send_params.py create mode 100644 src/beeper_desktop_api/types/message_send_response.py create mode 100644 src/beeper_desktop_api/types/open_response.py create mode 100644 src/beeper_desktop_api/types/search_response.py create mode 100644 tests/api_resources/chats/__init__.py create mode 100644 tests/api_resources/chats/test_reminders.py create mode 100644 tests/api_resources/test_accounts.py create mode 100644 tests/api_resources/test_chats.py create mode 100644 tests/api_resources/test_client.py create mode 100644 tests/api_resources/test_contacts.py create mode 100644 tests/api_resources/test_messages.py diff --git a/.stats.yml b/.stats.yml index ab027c2..8526f3e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 1 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml -openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 +configured_endpoints: 14 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml +openapi_spec_hash: ba834200758376aaea47b2a276f64c1b config_hash: f83b2b6eb86f2dd68101065998479cb2 diff --git a/README.md b/README.md index 51fb670..9f0e7ba 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,12 @@ client = BeeperDesktop( access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted ) -user_info = client.token.info() -print(user_info.sub) +page = client.chats.search( + include_muted=True, + limit=3, + type="single", +) +print(page.items) ``` While you can provide a `access_token` keyword argument, @@ -57,8 +61,12 @@ client = AsyncBeeperDesktop( async def main() -> None: - user_info = await client.token.info() - print(user_info.sub) + page = await client.chats.search( + include_muted=True, + limit=3, + type="single", + ) + print(page.items) asyncio.run(main()) @@ -90,8 +98,12 @@ async def main() -> None: access_token="My Access Token", http_client=DefaultAioHttpClient(), ) as client: - user_info = await client.token.info() - print(user_info.sub) + page = await client.chats.search( + include_muted=True, + limit=3, + type="single", + ) + print(page.items) asyncio.run(main()) @@ -106,6 +118,101 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Pagination + +List methods in the Beeper Desktop API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from beeper_desktop_api import BeeperDesktop + +client = BeeperDesktop() + +all_messages = [] +# Automatically fetches more pages as needed. +for message in client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", +): + # Do something with message here + all_messages.append(message) +print(all_messages) +``` + +Or, asynchronously: + +```python +import asyncio +from beeper_desktop_api import AsyncBeeperDesktop + +client = AsyncBeeperDesktop() + + +async def main() -> None: + all_messages = [] + # Iterate through items across all pages, issuing requests as needed. + async for message in client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", + ): + all_messages.append(message) + print(all_messages) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.items)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", +) + +print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..." +for message in first_page.items: + print(message.id) + +# Remove `await` for non-async usage. +``` + +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from beeper_desktop_api import BeeperDesktop + +client = BeeperDesktop() + +base_response = client.chats.reminders.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, +) +print(base_response.reminder) +``` + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `beeper_desktop_api.APIConnectionError` is raised. @@ -122,7 +229,10 @@ from beeper_desktop_api import BeeperDesktop client = BeeperDesktop() try: - client.token.info() + client.messages.send( + chat_id="1229391", + text="Hello! Just checking in on the project status.", + ) except beeper_desktop_api.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -165,7 +275,7 @@ client = BeeperDesktop( ) # Or, configure per-request: -client.with_options(max_retries=5).token.info() +client.with_options(max_retries=5).accounts.list() ``` ### Timeouts @@ -188,7 +298,7 @@ client = BeeperDesktop( ) # Override per-request: -client.with_options(timeout=5.0).token.info() +client.with_options(timeout=5.0).accounts.list() ``` On timeout, an `APITimeoutError` is thrown. @@ -229,11 +339,11 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from beeper_desktop_api import BeeperDesktop client = BeeperDesktop() -response = client.token.with_raw_response.info() +response = client.accounts.with_raw_response.list() print(response.headers.get('X-My-Header')) -token = response.parse() # get the object that `token.info()` would have returned -print(token.sub) +account = response.parse() # get the object that `accounts.list()` would have returned +print(account) ``` These methods return an [`APIResponse`](https://github.com/beeper/desktop-api-python/tree/main/src/beeper_desktop_api/_response.py) object. @@ -247,7 +357,7 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.token.with_streaming_response.info() as response: +with client.accounts.with_streaming_response.list() as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/api.md b/api.md index 83f0189..cfe2dc3 100644 --- a/api.md +++ b/api.md @@ -4,22 +4,79 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, Reaction, User ``` +# BeeperDesktop + +Types: + +```python +from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse +``` + +Methods: + +- client.download_asset(\*\*params) -> DownloadAssetResponse +- client.open(\*\*params) -> OpenResponse +- client.search(\*\*params) -> SearchResponse + # Accounts Types: ```python -from beeper_desktop_api.types import Account +from beeper_desktop_api.types import Account, AccountListResponse ``` +Methods: + +- client.accounts.list() -> AccountListResponse + +# Contacts + +Types: + +```python +from beeper_desktop_api.types import ContactSearchResponse +``` + +Methods: + +- client.contacts.search(\*\*params) -> ContactSearchResponse + # Chats Types: ```python -from beeper_desktop_api.types import Chat +from beeper_desktop_api.types import Chat, ChatCreateResponse +``` + +Methods: + +- client.chats.create(\*\*params) -> ChatCreateResponse +- client.chats.retrieve(chat_id, \*\*params) -> Chat +- client.chats.archive(chat_id, \*\*params) -> BaseResponse +- client.chats.search(\*\*params) -> SyncCursor[Chat] + +## Reminders + +Methods: + +- client.chats.reminders.create(chat_id, \*\*params) -> BaseResponse +- client.chats.reminders.delete(chat_id) -> BaseResponse + +# Messages + +Types: + +```python +from beeper_desktop_api.types import MessageSendResponse ``` +Methods: + +- client.messages.search(\*\*params) -> SyncCursor[Message] +- client.messages.send(\*\*params) -> MessageSendResponse + # Token Types: diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py index 44866d0..7a1a10e 100644 --- a/src/beeper_desktop_api/_client.py +++ b/src/beeper_desktop_api/_client.py @@ -10,25 +10,46 @@ from . import _exceptions from ._qs import Querystring +from .types import client_open_params, client_search_params, client_download_asset_params from ._types import ( + Body, Omit, + Query, + Headers, Timeout, NotGiven, Transport, ProxiesTypes, RequestOptions, + omit, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + maybe_transform, + get_async_library, + async_maybe_transform, +) from ._version import __version__ -from .resources import token +from ._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .resources import token, accounts, contacts, messages from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BeeperDesktopError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, AsyncAPIClient, + make_request_options, ) +from .resources.chats import chats +from .types.open_response import OpenResponse +from .types.search_response import SearchResponse +from .types.download_asset_response import DownloadAssetResponse __all__ = [ "Timeout", @@ -43,6 +64,10 @@ class BeeperDesktop(SyncAPIClient): + accounts: accounts.AccountsResource + contacts: contacts.ContactsResource + chats: chats.ChatsResource + messages: messages.MessagesResource token: token.TokenResource with_raw_response: BeeperDesktopWithRawResponse with_streaming_response: BeeperDesktopWithStreamedResponse @@ -101,6 +126,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self.accounts = accounts.AccountsResource(self) + self.contacts = contacts.ContactsResource(self) + self.chats = chats.ChatsResource(self) + self.messages = messages.MessagesResource(self) self.token = token.TokenResource(self) self.with_raw_response = BeeperDesktopWithRawResponse(self) self.with_streaming_response = BeeperDesktopWithStreamedResponse(self) @@ -176,6 +205,133 @@ def copy( # client.with_options(timeout=10).foo.create(...) with_options = copy + def download_asset( + self, + *, + url: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DownloadAssetResponse: + """ + Download a Matrix asset using its mxc:// or localmxc:// URL and return the local + file URL. + + Args: + url: Matrix content URL (mxc:// or localmxc://) for the asset to download. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self.post( + "/v1/app/download-asset", + body=maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DownloadAssetResponse, + ) + + def open( + self, + *, + chat_id: str | Omit = omit, + draft_attachment_path: str | Omit = omit, + draft_text: str | Omit = omit, + message_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OpenResponse: + """ + Open Beeper Desktop and optionally navigate to a specific chat, message, or + pre-fill draft text and attachment. + + Args: + chat_id: Optional Beeper chat ID (or local chat ID) to focus after opening the app. If + omitted, only opens/focuses the app. + + draft_attachment_path: Optional draft attachment path to populate in the message input field. + + draft_text: Optional draft text to populate in the message input field. + + message_id: Optional message ID. Jumps to that message in the chat when opening. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self.post( + "/v1/app/open", + body=maybe_transform( + { + "chat_id": chat_id, + "draft_attachment_path": draft_attachment_path, + "draft_text": draft_text, + "message_id": message_id, + }, + client_open_params.ClientOpenParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=OpenResponse, + ) + + def search( + self, + *, + query: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SearchResponse: + """ + Returns matching chats, participant name matches in groups, and the first page + of messages in one call. Paginate messages via search-messages. Paginate chats + via search-chats. Uses the same sorting as the chat search in the app. + + Args: + query: User-typed search text. Literal word matching (NOT semantic). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self.get( + "/v1/search", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"query": query}, client_search_params.ClientSearchParams), + ), + cast_to=SearchResponse, + ) + @override def _make_status_error( self, @@ -211,6 +367,10 @@ def _make_status_error( class AsyncBeeperDesktop(AsyncAPIClient): + accounts: accounts.AsyncAccountsResource + contacts: contacts.AsyncContactsResource + chats: chats.AsyncChatsResource + messages: messages.AsyncMessagesResource token: token.AsyncTokenResource with_raw_response: AsyncBeeperDesktopWithRawResponse with_streaming_response: AsyncBeeperDesktopWithStreamedResponse @@ -269,6 +429,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self.accounts = accounts.AsyncAccountsResource(self) + self.contacts = contacts.AsyncContactsResource(self) + self.chats = chats.AsyncChatsResource(self) + self.messages = messages.AsyncMessagesResource(self) self.token = token.AsyncTokenResource(self) self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self) self.with_streaming_response = AsyncBeeperDesktopWithStreamedResponse(self) @@ -344,6 +508,133 @@ def copy( # client.with_options(timeout=10).foo.create(...) with_options = copy + async def download_asset( + self, + *, + url: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DownloadAssetResponse: + """ + Download a Matrix asset using its mxc:// or localmxc:// URL and return the local + file URL. + + Args: + url: Matrix content URL (mxc:// or localmxc://) for the asset to download. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self.post( + "/v1/app/download-asset", + body=await async_maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DownloadAssetResponse, + ) + + async def open( + self, + *, + chat_id: str | Omit = omit, + draft_attachment_path: str | Omit = omit, + draft_text: str | Omit = omit, + message_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OpenResponse: + """ + Open Beeper Desktop and optionally navigate to a specific chat, message, or + pre-fill draft text and attachment. + + Args: + chat_id: Optional Beeper chat ID (or local chat ID) to focus after opening the app. If + omitted, only opens/focuses the app. + + draft_attachment_path: Optional draft attachment path to populate in the message input field. + + draft_text: Optional draft text to populate in the message input field. + + message_id: Optional message ID. Jumps to that message in the chat when opening. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self.post( + "/v1/app/open", + body=await async_maybe_transform( + { + "chat_id": chat_id, + "draft_attachment_path": draft_attachment_path, + "draft_text": draft_text, + "message_id": message_id, + }, + client_open_params.ClientOpenParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=OpenResponse, + ) + + async def search( + self, + *, + query: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SearchResponse: + """ + Returns matching chats, participant name matches in groups, and the first page + of messages in one call. Paginate messages via search-messages. Paginate chats + via search-chats. Uses the same sorting as the chat search in the app. + + Args: + query: User-typed search text. Literal word matching (NOT semantic). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self.get( + "/v1/search", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"query": query}, client_search_params.ClientSearchParams), + ), + cast_to=SearchResponse, + ) + @override def _make_status_error( self, @@ -380,23 +671,79 @@ def _make_status_error( class BeeperDesktopWithRawResponse: def __init__(self, client: BeeperDesktop) -> None: + self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts) + self.contacts = contacts.ContactsResourceWithRawResponse(client.contacts) + self.chats = chats.ChatsResourceWithRawResponse(client.chats) + self.messages = messages.MessagesResourceWithRawResponse(client.messages) self.token = token.TokenResourceWithRawResponse(client.token) + self.download_asset = to_raw_response_wrapper( + client.download_asset, + ) + self.open = to_raw_response_wrapper( + client.open, + ) + self.search = to_raw_response_wrapper( + client.search, + ) + class AsyncBeeperDesktopWithRawResponse: def __init__(self, client: AsyncBeeperDesktop) -> None: + self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts) + self.contacts = contacts.AsyncContactsResourceWithRawResponse(client.contacts) + self.chats = chats.AsyncChatsResourceWithRawResponse(client.chats) + self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages) self.token = token.AsyncTokenResourceWithRawResponse(client.token) + self.download_asset = async_to_raw_response_wrapper( + client.download_asset, + ) + self.open = async_to_raw_response_wrapper( + client.open, + ) + self.search = async_to_raw_response_wrapper( + client.search, + ) + class BeeperDesktopWithStreamedResponse: def __init__(self, client: BeeperDesktop) -> None: + self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts) + self.contacts = contacts.ContactsResourceWithStreamingResponse(client.contacts) + self.chats = chats.ChatsResourceWithStreamingResponse(client.chats) + self.messages = messages.MessagesResourceWithStreamingResponse(client.messages) self.token = token.TokenResourceWithStreamingResponse(client.token) + self.download_asset = to_streamed_response_wrapper( + client.download_asset, + ) + self.open = to_streamed_response_wrapper( + client.open, + ) + self.search = to_streamed_response_wrapper( + client.search, + ) + class AsyncBeeperDesktopWithStreamedResponse: def __init__(self, client: AsyncBeeperDesktop) -> None: + self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts) + self.contacts = contacts.AsyncContactsResourceWithStreamingResponse(client.contacts) + self.chats = chats.AsyncChatsResourceWithStreamingResponse(client.chats) + self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages) self.token = token.AsyncTokenResourceWithStreamingResponse(client.token) + self.download_asset = async_to_streamed_response_wrapper( + client.download_asset, + ) + self.open = async_to_streamed_response_wrapper( + client.open, + ) + self.search = async_to_streamed_response_wrapper( + client.search, + ) + Client = BeeperDesktop diff --git a/src/beeper_desktop_api/resources/__init__.py b/src/beeper_desktop_api/resources/__init__.py index 7c3b25f..24ab242 100644 --- a/src/beeper_desktop_api/resources/__init__.py +++ b/src/beeper_desktop_api/resources/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .chats import ( + ChatsResource, + AsyncChatsResource, + ChatsResourceWithRawResponse, + AsyncChatsResourceWithRawResponse, + ChatsResourceWithStreamingResponse, + AsyncChatsResourceWithStreamingResponse, +) from .token import ( TokenResource, AsyncTokenResource, @@ -8,8 +16,56 @@ TokenResourceWithStreamingResponse, AsyncTokenResourceWithStreamingResponse, ) +from .accounts import ( + AccountsResource, + AsyncAccountsResource, + AccountsResourceWithRawResponse, + AsyncAccountsResourceWithRawResponse, + AccountsResourceWithStreamingResponse, + AsyncAccountsResourceWithStreamingResponse, +) +from .contacts import ( + ContactsResource, + AsyncContactsResource, + ContactsResourceWithRawResponse, + AsyncContactsResourceWithRawResponse, + ContactsResourceWithStreamingResponse, + AsyncContactsResourceWithStreamingResponse, +) +from .messages import ( + MessagesResource, + AsyncMessagesResource, + MessagesResourceWithRawResponse, + AsyncMessagesResourceWithRawResponse, + MessagesResourceWithStreamingResponse, + AsyncMessagesResourceWithStreamingResponse, +) __all__ = [ + "AccountsResource", + "AsyncAccountsResource", + "AccountsResourceWithRawResponse", + "AsyncAccountsResourceWithRawResponse", + "AccountsResourceWithStreamingResponse", + "AsyncAccountsResourceWithStreamingResponse", + "ContactsResource", + "AsyncContactsResource", + "ContactsResourceWithRawResponse", + "AsyncContactsResourceWithRawResponse", + "ContactsResourceWithStreamingResponse", + "AsyncContactsResourceWithStreamingResponse", + "ChatsResource", + "AsyncChatsResource", + "ChatsResourceWithRawResponse", + "AsyncChatsResourceWithRawResponse", + "ChatsResourceWithStreamingResponse", + "AsyncChatsResourceWithStreamingResponse", + "MessagesResource", + "AsyncMessagesResource", + "MessagesResourceWithRawResponse", + "AsyncMessagesResourceWithRawResponse", + "MessagesResourceWithStreamingResponse", + "AsyncMessagesResourceWithStreamingResponse", "TokenResource", "AsyncTokenResource", "TokenResourceWithRawResponse", diff --git a/src/beeper_desktop_api/resources/accounts.py b/src/beeper_desktop_api/resources/accounts.py new file mode 100644 index 0000000..49a5df2 --- /dev/null +++ b/src/beeper_desktop_api/resources/accounts.py @@ -0,0 +1,145 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import Body, Query, Headers, NotGiven, not_given +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.account_list_response import AccountListResponse + +__all__ = ["AccountsResource", "AsyncAccountsResource"] + + +class AccountsResource(SyncAPIResource): + """Accounts operations""" + + @cached_property + def with_raw_response(self) -> AccountsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AccountsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccountsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AccountsResourceWithStreamingResponse(self) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccountListResponse: + """ + Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.) + actively connected to this Beeper Desktop instance + """ + return self._get( + "/v1/accounts", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccountListResponse, + ) + + +class AsyncAccountsResource(AsyncAPIResource): + """Accounts operations""" + + @cached_property + def with_raw_response(self) -> AsyncAccountsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AsyncAccountsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccountsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AsyncAccountsResourceWithStreamingResponse(self) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccountListResponse: + """ + Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.) + actively connected to this Beeper Desktop instance + """ + return await self._get( + "/v1/accounts", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccountListResponse, + ) + + +class AccountsResourceWithRawResponse: + def __init__(self, accounts: AccountsResource) -> None: + self._accounts = accounts + + self.list = to_raw_response_wrapper( + accounts.list, + ) + + +class AsyncAccountsResourceWithRawResponse: + def __init__(self, accounts: AsyncAccountsResource) -> None: + self._accounts = accounts + + self.list = async_to_raw_response_wrapper( + accounts.list, + ) + + +class AccountsResourceWithStreamingResponse: + def __init__(self, accounts: AccountsResource) -> None: + self._accounts = accounts + + self.list = to_streamed_response_wrapper( + accounts.list, + ) + + +class AsyncAccountsResourceWithStreamingResponse: + def __init__(self, accounts: AsyncAccountsResource) -> None: + self._accounts = accounts + + self.list = async_to_streamed_response_wrapper( + accounts.list, + ) diff --git a/src/beeper_desktop_api/resources/chats/__init__.py b/src/beeper_desktop_api/resources/chats/__init__.py new file mode 100644 index 0000000..e26ae7f --- /dev/null +++ b/src/beeper_desktop_api/resources/chats/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .chats import ( + ChatsResource, + AsyncChatsResource, + ChatsResourceWithRawResponse, + AsyncChatsResourceWithRawResponse, + ChatsResourceWithStreamingResponse, + AsyncChatsResourceWithStreamingResponse, +) +from .reminders import ( + RemindersResource, + AsyncRemindersResource, + RemindersResourceWithRawResponse, + AsyncRemindersResourceWithRawResponse, + RemindersResourceWithStreamingResponse, + AsyncRemindersResourceWithStreamingResponse, +) + +__all__ = [ + "RemindersResource", + "AsyncRemindersResource", + "RemindersResourceWithRawResponse", + "AsyncRemindersResourceWithRawResponse", + "RemindersResourceWithStreamingResponse", + "AsyncRemindersResourceWithStreamingResponse", + "ChatsResource", + "AsyncChatsResource", + "ChatsResourceWithRawResponse", + "AsyncChatsResourceWithRawResponse", + "ChatsResourceWithStreamingResponse", + "AsyncChatsResourceWithStreamingResponse", +] diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py new file mode 100644 index 0000000..b5ec602 --- /dev/null +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -0,0 +1,668 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Literal + +import httpx + +from ...types import chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params +from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from .reminders import ( + RemindersResource, + AsyncRemindersResource, + RemindersResourceWithRawResponse, + AsyncRemindersResourceWithRawResponse, + RemindersResourceWithStreamingResponse, + AsyncRemindersResourceWithStreamingResponse, +) +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...pagination import SyncCursor, AsyncCursor +from ...types.chat import Chat +from ..._base_client import AsyncPaginator, make_request_options +from ...types.chat_create_response import ChatCreateResponse +from ...types.shared.base_response import BaseResponse + +__all__ = ["ChatsResource", "AsyncChatsResource"] + + +class ChatsResource(SyncAPIResource): + """Chats operations""" + + @cached_property + def reminders(self) -> RemindersResource: + """Reminders operations""" + return RemindersResource(self._client) + + @cached_property + def with_raw_response(self) -> ChatsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return ChatsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ChatsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return ChatsResourceWithStreamingResponse(self) + + def create( + self, + *, + account_id: str, + participant_ids: SequenceNotStr[str], + type: Literal["single", "group"], + message_text: str | Omit = omit, + title: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatCreateResponse: + """ + Create a single or group chat on a specific account using participant IDs and + optional title. + + Args: + account_id: Account to create the chat on. + + participant_ids: User IDs to include in the new chat. + + type: Chat type to create: 'single' requires exactly one participantID; 'group' + supports multiple participants and optional title. + + message_text: Optional first message content if the platform requires it to create the chat. + + title: Optional title for group chats; ignored for single chats on most platforms. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/chats", + body=maybe_transform( + { + "account_id": account_id, + "participant_ids": participant_ids, + "type": type, + "message_text": message_text, + "title": title, + }, + chat_create_params.ChatCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatCreateResponse, + ) + + def retrieve( + self, + chat_id: str, + *, + max_participant_count: Optional[int] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Chat: + """ + Retrieve chat details including metadata, participants, and latest message + + Args: + chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats. + Participants are limited by 'maxParticipantCount'. + + max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500. + Defaults to 20. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._get( + f"/v1/chats/{chat_id}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"max_participant_count": max_participant_count}, chat_retrieve_params.ChatRetrieveParams + ), + ), + cast_to=Chat, + ) + + def archive( + self, + chat_id: str, + *, + archived: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BaseResponse: + """Archive or unarchive a chat. + + Set archived=true to move to archive, + archived=false to move back to inbox + + Args: + chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and + local chat ID) + + archived: True to archive, false to unarchive + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + f"/v1/chats/{chat_id}/archive", + body=maybe_transform({"archived": archived}, chat_archive_params.ChatArchiveParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BaseResponse, + ) + + def search( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + inbox: Literal["primary", "low-priority", "archive"] | Omit = omit, + include_muted: Optional[bool] | Omit = omit, + last_activity_after: Union[str, datetime] | Omit = omit, + last_activity_before: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + query: str | Omit = omit, + scope: Literal["titles", "participants"] | Omit = omit, + type: Literal["single", "group", "any"] | Omit = omit, + unread_only: Optional[bool] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncCursor[Chat]: + """ + Search chats by title/network or participants using Beeper Desktop's renderer + algorithm. + + Args: + account_ids: Provide an array of account IDs to filter chats from specific messaging accounts + only + + cursor: Pagination cursor from previous response. Use with direction to navigate results + + direction: Pagination direction: "after" for newer page, "before" for older page. Defaults + to "before" when only cursor is provided. + + inbox: Filter by inbox type: "primary" (non-archived, non-low-priority), + "low-priority", or "archive". If not specified, shows all chats. + + include_muted: Include chats marked as Muted by the user, which are usually less important. + Default: true. Set to false if the user wants a more refined search. + + last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after + this time + + last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before + this time + + limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 + + query: Literal token search (non-semantic). Use single words users type (e.g., + "dinner"). When multiple words provided, ALL must match. Case-insensitive. + + scope: Search scope: 'titles' matches title + network; 'participants' matches + participant names. + + type: Specify the type of chats to retrieve: use "single" for direct messages, "group" + for group chats, or "any" to get all types + + unread_only: Set to true to only retrieve chats that have unread messages + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/chats/search", + page=SyncCursor[Chat], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "cursor": cursor, + "direction": direction, + "inbox": inbox, + "include_muted": include_muted, + "last_activity_after": last_activity_after, + "last_activity_before": last_activity_before, + "limit": limit, + "query": query, + "scope": scope, + "type": type, + "unread_only": unread_only, + }, + chat_search_params.ChatSearchParams, + ), + ), + model=Chat, + ) + + +class AsyncChatsResource(AsyncAPIResource): + """Chats operations""" + + @cached_property + def reminders(self) -> AsyncRemindersResource: + """Reminders operations""" + return AsyncRemindersResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncChatsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AsyncChatsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncChatsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AsyncChatsResourceWithStreamingResponse(self) + + async def create( + self, + *, + account_id: str, + participant_ids: SequenceNotStr[str], + type: Literal["single", "group"], + message_text: str | Omit = omit, + title: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatCreateResponse: + """ + Create a single or group chat on a specific account using participant IDs and + optional title. + + Args: + account_id: Account to create the chat on. + + participant_ids: User IDs to include in the new chat. + + type: Chat type to create: 'single' requires exactly one participantID; 'group' + supports multiple participants and optional title. + + message_text: Optional first message content if the platform requires it to create the chat. + + title: Optional title for group chats; ignored for single chats on most platforms. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/chats", + body=await async_maybe_transform( + { + "account_id": account_id, + "participant_ids": participant_ids, + "type": type, + "message_text": message_text, + "title": title, + }, + chat_create_params.ChatCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatCreateResponse, + ) + + async def retrieve( + self, + chat_id: str, + *, + max_participant_count: Optional[int] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Chat: + """ + Retrieve chat details including metadata, participants, and latest message + + Args: + chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats. + Participants are limited by 'maxParticipantCount'. + + max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500. + Defaults to 20. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._get( + f"/v1/chats/{chat_id}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"max_participant_count": max_participant_count}, chat_retrieve_params.ChatRetrieveParams + ), + ), + cast_to=Chat, + ) + + async def archive( + self, + chat_id: str, + *, + archived: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BaseResponse: + """Archive or unarchive a chat. + + Set archived=true to move to archive, + archived=false to move back to inbox + + Args: + chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and + local chat ID) + + archived: True to archive, false to unarchive + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + f"/v1/chats/{chat_id}/archive", + body=await async_maybe_transform({"archived": archived}, chat_archive_params.ChatArchiveParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BaseResponse, + ) + + def search( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + inbox: Literal["primary", "low-priority", "archive"] | Omit = omit, + include_muted: Optional[bool] | Omit = omit, + last_activity_after: Union[str, datetime] | Omit = omit, + last_activity_before: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + query: str | Omit = omit, + scope: Literal["titles", "participants"] | Omit = omit, + type: Literal["single", "group", "any"] | Omit = omit, + unread_only: Optional[bool] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Chat, AsyncCursor[Chat]]: + """ + Search chats by title/network or participants using Beeper Desktop's renderer + algorithm. + + Args: + account_ids: Provide an array of account IDs to filter chats from specific messaging accounts + only + + cursor: Pagination cursor from previous response. Use with direction to navigate results + + direction: Pagination direction: "after" for newer page, "before" for older page. Defaults + to "before" when only cursor is provided. + + inbox: Filter by inbox type: "primary" (non-archived, non-low-priority), + "low-priority", or "archive". If not specified, shows all chats. + + include_muted: Include chats marked as Muted by the user, which are usually less important. + Default: true. Set to false if the user wants a more refined search. + + last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after + this time + + last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before + this time + + limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 + + query: Literal token search (non-semantic). Use single words users type (e.g., + "dinner"). When multiple words provided, ALL must match. Case-insensitive. + + scope: Search scope: 'titles' matches title + network; 'participants' matches + participant names. + + type: Specify the type of chats to retrieve: use "single" for direct messages, "group" + for group chats, or "any" to get all types + + unread_only: Set to true to only retrieve chats that have unread messages + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/chats/search", + page=AsyncCursor[Chat], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "cursor": cursor, + "direction": direction, + "inbox": inbox, + "include_muted": include_muted, + "last_activity_after": last_activity_after, + "last_activity_before": last_activity_before, + "limit": limit, + "query": query, + "scope": scope, + "type": type, + "unread_only": unread_only, + }, + chat_search_params.ChatSearchParams, + ), + ), + model=Chat, + ) + + +class ChatsResourceWithRawResponse: + def __init__(self, chats: ChatsResource) -> None: + self._chats = chats + + self.create = to_raw_response_wrapper( + chats.create, + ) + self.retrieve = to_raw_response_wrapper( + chats.retrieve, + ) + self.archive = to_raw_response_wrapper( + chats.archive, + ) + self.search = to_raw_response_wrapper( + chats.search, + ) + + @cached_property + def reminders(self) -> RemindersResourceWithRawResponse: + """Reminders operations""" + return RemindersResourceWithRawResponse(self._chats.reminders) + + +class AsyncChatsResourceWithRawResponse: + def __init__(self, chats: AsyncChatsResource) -> None: + self._chats = chats + + self.create = async_to_raw_response_wrapper( + chats.create, + ) + self.retrieve = async_to_raw_response_wrapper( + chats.retrieve, + ) + self.archive = async_to_raw_response_wrapper( + chats.archive, + ) + self.search = async_to_raw_response_wrapper( + chats.search, + ) + + @cached_property + def reminders(self) -> AsyncRemindersResourceWithRawResponse: + """Reminders operations""" + return AsyncRemindersResourceWithRawResponse(self._chats.reminders) + + +class ChatsResourceWithStreamingResponse: + def __init__(self, chats: ChatsResource) -> None: + self._chats = chats + + self.create = to_streamed_response_wrapper( + chats.create, + ) + self.retrieve = to_streamed_response_wrapper( + chats.retrieve, + ) + self.archive = to_streamed_response_wrapper( + chats.archive, + ) + self.search = to_streamed_response_wrapper( + chats.search, + ) + + @cached_property + def reminders(self) -> RemindersResourceWithStreamingResponse: + """Reminders operations""" + return RemindersResourceWithStreamingResponse(self._chats.reminders) + + +class AsyncChatsResourceWithStreamingResponse: + def __init__(self, chats: AsyncChatsResource) -> None: + self._chats = chats + + self.create = async_to_streamed_response_wrapper( + chats.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + chats.retrieve, + ) + self.archive = async_to_streamed_response_wrapper( + chats.archive, + ) + self.search = async_to_streamed_response_wrapper( + chats.search, + ) + + @cached_property + def reminders(self) -> AsyncRemindersResourceWithStreamingResponse: + """Reminders operations""" + return AsyncRemindersResourceWithStreamingResponse(self._chats.reminders) diff --git a/src/beeper_desktop_api/resources/chats/reminders.py b/src/beeper_desktop_api/resources/chats/reminders.py new file mode 100644 index 0000000..e9da3b4 --- /dev/null +++ b/src/beeper_desktop_api/resources/chats/reminders.py @@ -0,0 +1,267 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.chats import reminder_create_params +from ..._base_client import make_request_options +from ...types.shared.base_response import BaseResponse + +__all__ = ["RemindersResource", "AsyncRemindersResource"] + + +class RemindersResource(SyncAPIResource): + """Reminders operations""" + + @cached_property + def with_raw_response(self) -> RemindersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return RemindersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RemindersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return RemindersResourceWithStreamingResponse(self) + + def create( + self, + chat_id: str, + *, + reminder: reminder_create_params.Reminder, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BaseResponse: + """ + Set a reminder for a chat at a specific time + + Args: + chat_id: The identifier of the chat to set reminder for (accepts both chatID and local + chat ID) + + reminder: Reminder configuration + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + f"/v1/chats/{chat_id}/reminders", + body=maybe_transform({"reminder": reminder}, reminder_create_params.ReminderCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BaseResponse, + ) + + def delete( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BaseResponse: + """ + Clear an existing reminder from a chat + + Args: + chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local + chat ID) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._delete( + f"/v1/chats/{chat_id}/reminders", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BaseResponse, + ) + + +class AsyncRemindersResource(AsyncAPIResource): + """Reminders operations""" + + @cached_property + def with_raw_response(self) -> AsyncRemindersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AsyncRemindersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRemindersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AsyncRemindersResourceWithStreamingResponse(self) + + async def create( + self, + chat_id: str, + *, + reminder: reminder_create_params.Reminder, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BaseResponse: + """ + Set a reminder for a chat at a specific time + + Args: + chat_id: The identifier of the chat to set reminder for (accepts both chatID and local + chat ID) + + reminder: Reminder configuration + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + f"/v1/chats/{chat_id}/reminders", + body=await async_maybe_transform({"reminder": reminder}, reminder_create_params.ReminderCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BaseResponse, + ) + + async def delete( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BaseResponse: + """ + Clear an existing reminder from a chat + + Args: + chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local + chat ID) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._delete( + f"/v1/chats/{chat_id}/reminders", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BaseResponse, + ) + + +class RemindersResourceWithRawResponse: + def __init__(self, reminders: RemindersResource) -> None: + self._reminders = reminders + + self.create = to_raw_response_wrapper( + reminders.create, + ) + self.delete = to_raw_response_wrapper( + reminders.delete, + ) + + +class AsyncRemindersResourceWithRawResponse: + def __init__(self, reminders: AsyncRemindersResource) -> None: + self._reminders = reminders + + self.create = async_to_raw_response_wrapper( + reminders.create, + ) + self.delete = async_to_raw_response_wrapper( + reminders.delete, + ) + + +class RemindersResourceWithStreamingResponse: + def __init__(self, reminders: RemindersResource) -> None: + self._reminders = reminders + + self.create = to_streamed_response_wrapper( + reminders.create, + ) + self.delete = to_streamed_response_wrapper( + reminders.delete, + ) + + +class AsyncRemindersResourceWithStreamingResponse: + def __init__(self, reminders: AsyncRemindersResource) -> None: + self._reminders = reminders + + self.create = async_to_streamed_response_wrapper( + reminders.create, + ) + self.delete = async_to_streamed_response_wrapper( + reminders.delete, + ) diff --git a/src/beeper_desktop_api/resources/contacts.py b/src/beeper_desktop_api/resources/contacts.py new file mode 100644 index 0000000..db84950 --- /dev/null +++ b/src/beeper_desktop_api/resources/contacts.py @@ -0,0 +1,197 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import contact_search_params +from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.contact_search_response import ContactSearchResponse + +__all__ = ["ContactsResource", "AsyncContactsResource"] + + +class ContactsResource(SyncAPIResource): + """Contacts operations""" + + @cached_property + def with_raw_response(self) -> ContactsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return ContactsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ContactsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return ContactsResourceWithStreamingResponse(self) + + def search( + self, + *, + account_id: str, + query: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactSearchResponse: + """ + Search contacts across on a specific account using the network's search API. + Only use for creating new chats. + + Args: + account_id: Account ID this resource belongs to. + + query: Text to search users by. Network-specific behavior. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/v1/contacts/search", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_id": account_id, + "query": query, + }, + contact_search_params.ContactSearchParams, + ), + ), + cast_to=ContactSearchResponse, + ) + + +class AsyncContactsResource(AsyncAPIResource): + """Contacts operations""" + + @cached_property + def with_raw_response(self) -> AsyncContactsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AsyncContactsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AsyncContactsResourceWithStreamingResponse(self) + + async def search( + self, + *, + account_id: str, + query: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactSearchResponse: + """ + Search contacts across on a specific account using the network's search API. + Only use for creating new chats. + + Args: + account_id: Account ID this resource belongs to. + + query: Text to search users by. Network-specific behavior. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/v1/contacts/search", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "account_id": account_id, + "query": query, + }, + contact_search_params.ContactSearchParams, + ), + ), + cast_to=ContactSearchResponse, + ) + + +class ContactsResourceWithRawResponse: + def __init__(self, contacts: ContactsResource) -> None: + self._contacts = contacts + + self.search = to_raw_response_wrapper( + contacts.search, + ) + + +class AsyncContactsResourceWithRawResponse: + def __init__(self, contacts: AsyncContactsResource) -> None: + self._contacts = contacts + + self.search = async_to_raw_response_wrapper( + contacts.search, + ) + + +class ContactsResourceWithStreamingResponse: + def __init__(self, contacts: ContactsResource) -> None: + self._contacts = contacts + + self.search = to_streamed_response_wrapper( + contacts.search, + ) + + +class AsyncContactsResourceWithStreamingResponse: + def __init__(self, contacts: AsyncContactsResource) -> None: + self._contacts = contacts + + self.search = async_to_streamed_response_wrapper( + contacts.search, + ) diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py new file mode 100644 index 0000000..ea1ea25 --- /dev/null +++ b/src/beeper_desktop_api/resources/messages.py @@ -0,0 +1,423 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal + +import httpx + +from ..types import message_send_params, message_search_params +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncCursor, AsyncCursor +from .._base_client import AsyncPaginator, make_request_options +from ..types.shared.message import Message +from ..types.message_send_response import MessageSendResponse + +__all__ = ["MessagesResource", "AsyncMessagesResource"] + + +class MessagesResource(SyncAPIResource): + """Messages operations""" + + @cached_property + def with_raw_response(self) -> MessagesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return MessagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return MessagesResourceWithStreamingResponse(self) + + def search( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + chat_ids: SequenceNotStr[str] | Omit = omit, + chat_type: Literal["group", "single"] | Omit = omit, + cursor: str | Omit = omit, + date_after: Union[str, datetime] | Omit = omit, + date_before: Union[str, datetime] | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + exclude_low_priority: Optional[bool] | Omit = omit, + include_muted: Optional[bool] | Omit = omit, + limit: int | Omit = omit, + media_types: List[Literal["any", "video", "image", "link", "file"]] | Omit = omit, + query: str | Omit = omit, + sender: Union[Literal["me", "others"], str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncCursor[Message]: + """ + Search messages across chats using Beeper's message index + + Args: + account_ids: Limit search to specific account IDs. + + chat_ids: Limit search to specific chat IDs. + + chat_type: Filter by chat type: 'group' for group chats, 'single' for 1:1 chats. + + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. + + date_after: Only include messages with timestamp strictly after this ISO 8601 datetime + (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00'). + + date_before: Only include messages with timestamp strictly before this ISO 8601 datetime + (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00'). + + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + + exclude_low_priority: Exclude messages marked Low Priority by the user. Default: true. Set to false to + include all. + + include_muted: Include messages in chats marked as Muted by the user, which are usually less + important. Default: true. Set to false if the user wants a more refined search. + + limit: Maximum number of messages to return (1–500). Defaults to 20. The current + implementation caps each page at 20 items even if a higher limit is requested. + + media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact + types like ['video', 'image']. Omit for no media filtering. + + query: Literal word search (NOT semantic). Finds messages containing these EXACT words + in any order. Use single words users actually type, not concepts or phrases. + Example: use "dinner" not "dinner plans", use "sick" not "health issues". If + omitted, returns results filtered only by other parameters. + + sender: Filter by sender: 'me' (messages sent by the authenticated user), 'others' + (messages sent by others), or a specific user ID string (user.id). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/messages/search", + page=SyncCursor[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "chat_ids": chat_ids, + "chat_type": chat_type, + "cursor": cursor, + "date_after": date_after, + "date_before": date_before, + "direction": direction, + "exclude_low_priority": exclude_low_priority, + "include_muted": include_muted, + "limit": limit, + "media_types": media_types, + "query": query, + "sender": sender, + }, + message_search_params.MessageSearchParams, + ), + ), + model=Message, + ) + + def send( + self, + *, + chat_id: str, + reply_to_message_id: str | Omit = omit, + text: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageSendResponse: + """Send a text message to a specific chat. + + Supports replying to existing messages. + Returns the sent message ID. + + Args: + chat_id: Unique identifier of the chat. + + reply_to_message_id: Provide a message ID to send this as a reply to an existing message + + text: Text content of the message you want to send. You may use markdown. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/messages", + body=maybe_transform( + { + "chat_id": chat_id, + "reply_to_message_id": reply_to_message_id, + "text": text, + }, + message_send_params.MessageSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageSendResponse, + ) + + +class AsyncMessagesResource(AsyncAPIResource): + """Messages operations""" + + @cached_property + def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AsyncMessagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AsyncMessagesResourceWithStreamingResponse(self) + + def search( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + chat_ids: SequenceNotStr[str] | Omit = omit, + chat_type: Literal["group", "single"] | Omit = omit, + cursor: str | Omit = omit, + date_after: Union[str, datetime] | Omit = omit, + date_before: Union[str, datetime] | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + exclude_low_priority: Optional[bool] | Omit = omit, + include_muted: Optional[bool] | Omit = omit, + limit: int | Omit = omit, + media_types: List[Literal["any", "video", "image", "link", "file"]] | Omit = omit, + query: str | Omit = omit, + sender: Union[Literal["me", "others"], str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Message, AsyncCursor[Message]]: + """ + Search messages across chats using Beeper's message index + + Args: + account_ids: Limit search to specific account IDs. + + chat_ids: Limit search to specific chat IDs. + + chat_type: Filter by chat type: 'group' for group chats, 'single' for 1:1 chats. + + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. + + date_after: Only include messages with timestamp strictly after this ISO 8601 datetime + (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00'). + + date_before: Only include messages with timestamp strictly before this ISO 8601 datetime + (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00'). + + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + + exclude_low_priority: Exclude messages marked Low Priority by the user. Default: true. Set to false to + include all. + + include_muted: Include messages in chats marked as Muted by the user, which are usually less + important. Default: true. Set to false if the user wants a more refined search. + + limit: Maximum number of messages to return (1–500). Defaults to 20. The current + implementation caps each page at 20 items even if a higher limit is requested. + + media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact + types like ['video', 'image']. Omit for no media filtering. + + query: Literal word search (NOT semantic). Finds messages containing these EXACT words + in any order. Use single words users actually type, not concepts or phrases. + Example: use "dinner" not "dinner plans", use "sick" not "health issues". If + omitted, returns results filtered only by other parameters. + + sender: Filter by sender: 'me' (messages sent by the authenticated user), 'others' + (messages sent by others), or a specific user ID string (user.id). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/messages/search", + page=AsyncCursor[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "chat_ids": chat_ids, + "chat_type": chat_type, + "cursor": cursor, + "date_after": date_after, + "date_before": date_before, + "direction": direction, + "exclude_low_priority": exclude_low_priority, + "include_muted": include_muted, + "limit": limit, + "media_types": media_types, + "query": query, + "sender": sender, + }, + message_search_params.MessageSearchParams, + ), + ), + model=Message, + ) + + async def send( + self, + *, + chat_id: str, + reply_to_message_id: str | Omit = omit, + text: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageSendResponse: + """Send a text message to a specific chat. + + Supports replying to existing messages. + Returns the sent message ID. + + Args: + chat_id: Unique identifier of the chat. + + reply_to_message_id: Provide a message ID to send this as a reply to an existing message + + text: Text content of the message you want to send. You may use markdown. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/messages", + body=await async_maybe_transform( + { + "chat_id": chat_id, + "reply_to_message_id": reply_to_message_id, + "text": text, + }, + message_send_params.MessageSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageSendResponse, + ) + + +class MessagesResourceWithRawResponse: + def __init__(self, messages: MessagesResource) -> None: + self._messages = messages + + self.search = to_raw_response_wrapper( + messages.search, + ) + self.send = to_raw_response_wrapper( + messages.send, + ) + + +class AsyncMessagesResourceWithRawResponse: + def __init__(self, messages: AsyncMessagesResource) -> None: + self._messages = messages + + self.search = async_to_raw_response_wrapper( + messages.search, + ) + self.send = async_to_raw_response_wrapper( + messages.send, + ) + + +class MessagesResourceWithStreamingResponse: + def __init__(self, messages: MessagesResource) -> None: + self._messages = messages + + self.search = to_streamed_response_wrapper( + messages.search, + ) + self.send = to_streamed_response_wrapper( + messages.send, + ) + + +class AsyncMessagesResourceWithStreamingResponse: + def __init__(self, messages: AsyncMessagesResource) -> None: + self._messages = messages + + self.search = async_to_streamed_response_wrapper( + messages.search, + ) + self.send = async_to_streamed_response_wrapper( + messages.send, + ) diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index bb86833..5bede4c 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from .chat import Chat as Chat from .shared import ( User as User, Error as Error, @@ -10,4 +11,22 @@ Attachment as Attachment, BaseResponse as BaseResponse, ) +from .account import Account as Account from .user_info import UserInfo as UserInfo +from .open_response import OpenResponse as OpenResponse +from .search_response import SearchResponse as SearchResponse +from .chat_create_params import ChatCreateParams as ChatCreateParams +from .chat_search_params import ChatSearchParams as ChatSearchParams +from .client_open_params import ClientOpenParams as ClientOpenParams +from .chat_archive_params import ChatArchiveParams as ChatArchiveParams +from .message_send_params import MessageSendParams as MessageSendParams +from .chat_create_response import ChatCreateResponse as ChatCreateResponse +from .chat_retrieve_params import ChatRetrieveParams as ChatRetrieveParams +from .client_search_params import ClientSearchParams as ClientSearchParams +from .account_list_response import AccountListResponse as AccountListResponse +from .contact_search_params import ContactSearchParams as ContactSearchParams +from .message_search_params import MessageSearchParams as MessageSearchParams +from .message_send_response import MessageSendResponse as MessageSendResponse +from .contact_search_response import ContactSearchResponse as ContactSearchResponse +from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse +from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams diff --git a/src/beeper_desktop_api/types/account.py b/src/beeper_desktop_api/types/account.py new file mode 100644 index 0000000..97336b7 --- /dev/null +++ b/src/beeper_desktop_api/types/account.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from .._models import BaseModel +from .shared.user import User + +__all__ = ["Account"] + + +class Account(BaseModel): + account_id: str = FieldInfo(alias="accountID") + """Chat account added to Beeper. Use this to route account-scoped actions.""" + + network: str + """Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger').""" + + user: User + """User the account belongs to.""" diff --git a/src/beeper_desktop_api/types/account_list_response.py b/src/beeper_desktop_api/types/account_list_response.py new file mode 100644 index 0000000..8268843 --- /dev/null +++ b/src/beeper_desktop_api/types/account_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .account import Account + +__all__ = ["AccountListResponse"] + +AccountListResponse: TypeAlias = List[Account] diff --git a/src/beeper_desktop_api/types/chat.py b/src/beeper_desktop_api/types/chat.py new file mode 100644 index 0000000..d580426 --- /dev/null +++ b/src/beeper_desktop_api/types/chat.py @@ -0,0 +1,67 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel +from .shared.user import User + +__all__ = ["Chat", "Participants"] + + +class Participants(BaseModel): + has_more: bool = FieldInfo(alias="hasMore") + """True if there are more participants than included in items.""" + + items: List[User] + """Participants returned for this chat (limited by the request; may be a subset).""" + + total: int + """Total number of participants in the chat.""" + + +class Chat(BaseModel): + id: str + """Unique identifier of the chat (room/thread ID, same as id) across Beeper.""" + + account_id: str = FieldInfo(alias="accountID") + """Beeper account ID this chat belongs to.""" + + network: str + """Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger').""" + + participants: Participants + """Chat participants information.""" + + title: str + """Display title of the chat as computed by the client/server.""" + + type: Literal["single", "group"] + """Chat type: 'single' for direct messages, 'group' for group chats.""" + + unread_count: int = FieldInfo(alias="unreadCount") + """Number of unread messages.""" + + is_archived: Optional[bool] = FieldInfo(alias="isArchived", default=None) + """True if chat is archived.""" + + is_muted: Optional[bool] = FieldInfo(alias="isMuted", default=None) + """True if chat notifications are muted.""" + + is_pinned: Optional[bool] = FieldInfo(alias="isPinned", default=None) + """True if chat is pinned.""" + + last_activity: Optional[datetime] = FieldInfo(alias="lastActivity", default=None) + """Timestamp of last activity. + + Chats with more recent activity are often more important. + """ + + last_read_message_sort_key: Union[int, str, None] = FieldInfo(alias="lastReadMessageSortKey", default=None) + """Last read message sortKey (hsOrder). Used to compute 'isUnread'.""" + + local_chat_id: Optional[str] = FieldInfo(alias="localChatID", default=None) + """Local chat ID specific to this Beeper Desktop installation.""" diff --git a/src/beeper_desktop_api/types/chat_archive_params.py b/src/beeper_desktop_api/types/chat_archive_params.py new file mode 100644 index 0000000..38cc168 --- /dev/null +++ b/src/beeper_desktop_api/types/chat_archive_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ChatArchiveParams"] + + +class ChatArchiveParams(TypedDict, total=False): + archived: bool + """True to archive, false to unarchive""" diff --git a/src/beeper_desktop_api/types/chat_create_params.py b/src/beeper_desktop_api/types/chat_create_params.py new file mode 100644 index 0000000..686bfaa --- /dev/null +++ b/src/beeper_desktop_api/types/chat_create_params.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["ChatCreateParams"] + + +class ChatCreateParams(TypedDict, total=False): + account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]] + """Account to create the chat on.""" + + participant_ids: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="participantIDs")]] + """User IDs to include in the new chat.""" + + type: Required[Literal["single", "group"]] + """ + Chat type to create: 'single' requires exactly one participantID; 'group' + supports multiple participants and optional title. + """ + + message_text: Annotated[str, PropertyInfo(alias="messageText")] + """Optional first message content if the platform requires it to create the chat.""" + + title: str + """Optional title for group chats; ignored for single chats on most platforms.""" diff --git a/src/beeper_desktop_api/types/chat_create_response.py b/src/beeper_desktop_api/types/chat_create_response.py new file mode 100644 index 0000000..64b6981 --- /dev/null +++ b/src/beeper_desktop_api/types/chat_create_response.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from .shared.base_response import BaseResponse + +__all__ = ["ChatCreateResponse"] + + +class ChatCreateResponse(BaseResponse): + chat_id: Optional[str] = FieldInfo(alias="chatID", default=None) + """Newly created chat if available.""" diff --git a/src/beeper_desktop_api/types/chat_retrieve_params.py b/src/beeper_desktop_api/types/chat_retrieve_params.py new file mode 100644 index 0000000..ea22752 --- /dev/null +++ b/src/beeper_desktop_api/types/chat_retrieve_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ChatRetrieveParams"] + + +class ChatRetrieveParams(TypedDict, total=False): + max_participant_count: Annotated[Optional[int], PropertyInfo(alias="maxParticipantCount")] + """Maximum number of participants to return. + + Use -1 for all; otherwise 0–500. Defaults to 20. + """ diff --git a/src/beeper_desktop_api/types/chat_search_params.py b/src/beeper_desktop_api/types/chat_search_params.py new file mode 100644 index 0000000..de94b8d --- /dev/null +++ b/src/beeper_desktop_api/types/chat_search_params.py @@ -0,0 +1,81 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["ChatSearchParams"] + + +class ChatSearchParams(TypedDict, total=False): + account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")] + """ + Provide an array of account IDs to filter chats from specific messaging accounts + only + """ + + cursor: str + """Pagination cursor from previous response. + + Use with direction to navigate results + """ + + direction: Literal["after", "before"] + """Pagination direction: "after" for newer page, "before" for older page. + + Defaults to "before" when only cursor is provided. + """ + + inbox: Literal["primary", "low-priority", "archive"] + """ + Filter by inbox type: "primary" (non-archived, non-low-priority), + "low-priority", or "archive". If not specified, shows all chats. + """ + + include_muted: Annotated[Optional[bool], PropertyInfo(alias="includeMuted")] + """Include chats marked as Muted by the user, which are usually less important. + + Default: true. Set to false if the user wants a more refined search. + """ + + last_activity_after: Annotated[Union[str, datetime], PropertyInfo(alias="lastActivityAfter", format="iso8601")] + """ + Provide an ISO datetime string to only retrieve chats with last activity after + this time + """ + + last_activity_before: Annotated[Union[str, datetime], PropertyInfo(alias="lastActivityBefore", format="iso8601")] + """ + Provide an ISO datetime string to only retrieve chats with last activity before + this time + """ + + limit: int + """Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50""" + + query: str + """Literal token search (non-semantic). + + Use single words users type (e.g., "dinner"). When multiple words provided, ALL + must match. Case-insensitive. + """ + + scope: Literal["titles", "participants"] + """ + Search scope: 'titles' matches title + network; 'participants' matches + participant names. + """ + + type: Literal["single", "group", "any"] + """ + Specify the type of chats to retrieve: use "single" for direct messages, "group" + for group chats, or "any" to get all types + """ + + unread_only: Annotated[Optional[bool], PropertyInfo(alias="unreadOnly")] + """Set to true to only retrieve chats that have unread messages""" diff --git a/src/beeper_desktop_api/types/chats/__init__.py b/src/beeper_desktop_api/types/chats/__init__.py index f8ee8b1..848b361 100644 --- a/src/beeper_desktop_api/types/chats/__init__.py +++ b/src/beeper_desktop_api/types/chats/__init__.py @@ -1,3 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from __future__ import annotations + +from .reminder_create_params import ReminderCreateParams as ReminderCreateParams diff --git a/src/beeper_desktop_api/types/chats/reminder_create_params.py b/src/beeper_desktop_api/types/chats/reminder_create_params.py new file mode 100644 index 0000000..810263e --- /dev/null +++ b/src/beeper_desktop_api/types/chats/reminder_create_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["ReminderCreateParams", "Reminder"] + + +class ReminderCreateParams(TypedDict, total=False): + reminder: Required[Reminder] + """Reminder configuration""" + + +class Reminder(TypedDict, total=False): + remind_at_ms: Required[Annotated[float, PropertyInfo(alias="remindAtMs")]] + """Unix timestamp in milliseconds when reminder should trigger""" + + dismiss_on_incoming_message: Annotated[bool, PropertyInfo(alias="dismissOnIncomingMessage")] + """Cancel reminder if someone messages in the chat""" diff --git a/src/beeper_desktop_api/types/client_download_asset_params.py b/src/beeper_desktop_api/types/client_download_asset_params.py new file mode 100644 index 0000000..fe824e0 --- /dev/null +++ b/src/beeper_desktop_api/types/client_download_asset_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ClientDownloadAssetParams"] + + +class ClientDownloadAssetParams(TypedDict, total=False): + url: Required[str] + """Matrix content URL (mxc:// or localmxc://) for the asset to download.""" diff --git a/src/beeper_desktop_api/types/client_open_params.py b/src/beeper_desktop_api/types/client_open_params.py new file mode 100644 index 0000000..84dea5f --- /dev/null +++ b/src/beeper_desktop_api/types/client_open_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ClientOpenParams"] + + +class ClientOpenParams(TypedDict, total=False): + chat_id: Annotated[str, PropertyInfo(alias="chatID")] + """Optional Beeper chat ID (or local chat ID) to focus after opening the app. + + If omitted, only opens/focuses the app. + """ + + draft_attachment_path: Annotated[str, PropertyInfo(alias="draftAttachmentPath")] + """Optional draft attachment path to populate in the message input field.""" + + draft_text: Annotated[str, PropertyInfo(alias="draftText")] + """Optional draft text to populate in the message input field.""" + + message_id: Annotated[str, PropertyInfo(alias="messageID")] + """Optional message ID. Jumps to that message in the chat when opening.""" diff --git a/src/beeper_desktop_api/types/client_search_params.py b/src/beeper_desktop_api/types/client_search_params.py new file mode 100644 index 0000000..06d58e4 --- /dev/null +++ b/src/beeper_desktop_api/types/client_search_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ClientSearchParams"] + + +class ClientSearchParams(TypedDict, total=False): + query: Required[str] + """User-typed search text. Literal word matching (NOT semantic).""" diff --git a/src/beeper_desktop_api/types/contact_search_params.py b/src/beeper_desktop_api/types/contact_search_params.py new file mode 100644 index 0000000..53d052f --- /dev/null +++ b/src/beeper_desktop_api/types/contact_search_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ContactSearchParams"] + + +class ContactSearchParams(TypedDict, total=False): + account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]] + """Account ID this resource belongs to.""" + + query: Required[str] + """Text to search users by. Network-specific behavior.""" diff --git a/src/beeper_desktop_api/types/contact_search_response.py b/src/beeper_desktop_api/types/contact_search_response.py new file mode 100644 index 0000000..71c609e --- /dev/null +++ b/src/beeper_desktop_api/types/contact_search_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .shared.user import User + +__all__ = ["ContactSearchResponse"] + + +class ContactSearchResponse(BaseModel): + items: List[User] diff --git a/src/beeper_desktop_api/types/download_asset_response.py b/src/beeper_desktop_api/types/download_asset_response.py new file mode 100644 index 0000000..47bc22e --- /dev/null +++ b/src/beeper_desktop_api/types/download_asset_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["DownloadAssetResponse"] + + +class DownloadAssetResponse(BaseModel): + error: Optional[str] = None + """Error message if the download failed.""" + + src_url: Optional[str] = FieldInfo(alias="srcURL", default=None) + """Local file URL to the downloaded asset.""" diff --git a/src/beeper_desktop_api/types/message_search_params.py b/src/beeper_desktop_api/types/message_search_params.py new file mode 100644 index 0000000..650775f --- /dev/null +++ b/src/beeper_desktop_api/types/message_search_params.py @@ -0,0 +1,85 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["MessageSearchParams"] + + +class MessageSearchParams(TypedDict, total=False): + account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")] + """Limit search to specific account IDs.""" + + chat_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="chatIDs")] + """Limit search to specific chat IDs.""" + + chat_type: Annotated[Literal["group", "single"], PropertyInfo(alias="chatType")] + """Filter by chat type: 'group' for group chats, 'single' for 1:1 chats.""" + + cursor: str + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" + + date_after: Annotated[Union[str, datetime], PropertyInfo(alias="dateAfter", format="iso8601")] + """ + Only include messages with timestamp strictly after this ISO 8601 datetime + (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00'). + """ + + date_before: Annotated[Union[str, datetime], PropertyInfo(alias="dateBefore", format="iso8601")] + """ + Only include messages with timestamp strictly before this ISO 8601 datetime + (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00'). + """ + + direction: Literal["after", "before"] + """ + Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + """ + + exclude_low_priority: Annotated[Optional[bool], PropertyInfo(alias="excludeLowPriority")] + """Exclude messages marked Low Priority by the user. + + Default: true. Set to false to include all. + """ + + include_muted: Annotated[Optional[bool], PropertyInfo(alias="includeMuted")] + """ + Include messages in chats marked as Muted by the user, which are usually less + important. Default: true. Set to false if the user wants a more refined search. + """ + + limit: int + """Maximum number of messages to return (1–500). + + Defaults to 20. The current implementation caps each page at 20 items even if a + higher limit is requested. + """ + + media_types: Annotated[List[Literal["any", "video", "image", "link", "file"]], PropertyInfo(alias="mediaTypes")] + """Filter messages by media types. + + Use ['any'] for any media type, or specify exact types like ['video', 'image']. + Omit for no media filtering. + """ + + query: str + """Literal word search (NOT semantic). + + Finds messages containing these EXACT words in any order. Use single words users + actually type, not concepts or phrases. Example: use "dinner" not "dinner + plans", use "sick" not "health issues". If omitted, returns results filtered + only by other parameters. + """ + + sender: Union[Literal["me", "others"], str] + """ + Filter by sender: 'me' (messages sent by the authenticated user), 'others' + (messages sent by others), or a specific user ID string (user.id). + """ diff --git a/src/beeper_desktop_api/types/message_send_params.py b/src/beeper_desktop_api/types/message_send_params.py new file mode 100644 index 0000000..8b05d6a --- /dev/null +++ b/src/beeper_desktop_api/types/message_send_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["MessageSendParams"] + + +class MessageSendParams(TypedDict, total=False): + chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]] + """Unique identifier of the chat.""" + + reply_to_message_id: Annotated[str, PropertyInfo(alias="replyToMessageID")] + """Provide a message ID to send this as a reply to an existing message""" + + text: str + """Text content of the message you want to send. You may use markdown.""" diff --git a/src/beeper_desktop_api/types/message_send_response.py b/src/beeper_desktop_api/types/message_send_response.py new file mode 100644 index 0000000..05cc535 --- /dev/null +++ b/src/beeper_desktop_api/types/message_send_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from .shared.base_response import BaseResponse + +__all__ = ["MessageSendResponse"] + + +class MessageSendResponse(BaseResponse): + chat_id: str = FieldInfo(alias="chatID") + """Unique identifier of the chat.""" + + pending_message_id: str = FieldInfo(alias="pendingMessageID") + """Pending message ID""" diff --git a/src/beeper_desktop_api/types/open_response.py b/src/beeper_desktop_api/types/open_response.py new file mode 100644 index 0000000..970f2ba --- /dev/null +++ b/src/beeper_desktop_api/types/open_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["OpenResponse"] + + +class OpenResponse(BaseModel): + success: bool + """Whether the app was successfully opened/focused.""" diff --git a/src/beeper_desktop_api/types/search_response.py b/src/beeper_desktop_api/types/search_response.py new file mode 100644 index 0000000..fe5113c --- /dev/null +++ b/src/beeper_desktop_api/types/search_response.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional + +from pydantic import Field as FieldInfo + +from .chat import Chat +from .._models import BaseModel +from .shared.message import Message + +__all__ = ["SearchResponse", "Results", "ResultsMessages"] + + +class ResultsMessages(BaseModel): + chats: Dict[str, Chat] + """Map of chatID -> chat details for chats referenced in items.""" + + has_more: bool = FieldInfo(alias="hasMore") + """True if additional results can be fetched using the provided cursors.""" + + items: List[Message] + """Messages matching the query and filters.""" + + newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) + """Cursor for fetching newer results (use with direction='after'). + + Opaque string; do not inspect. + """ + + oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) + """Cursor for fetching older results (use with direction='before'). + + Opaque string; do not inspect. + """ + + +class Results(BaseModel): + chats: List[Chat] + """Top chat results.""" + + in_groups: List[Chat] + """Top group results by participant matches.""" + + messages: ResultsMessages + + +class SearchResponse(BaseModel): + results: Results diff --git a/src/beeper_desktop_api/types/shared/error.py b/src/beeper_desktop_api/types/shared/error.py index 1f82efd..e5b5a77 100644 --- a/src/beeper_desktop_api/types/shared/error.py +++ b/src/beeper_desktop_api/types/shared/error.py @@ -1,18 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Optional - from ..._models import BaseModel __all__ = ["Error"] class Error(BaseModel): - error: str - """Error message""" - - code: Optional[str] = None - """Error code""" - - details: Optional[Dict[str, str]] = None - """Additional error details""" + error: Error + """Error details""" diff --git a/tests/api_resources/chats/__init__.py b/tests/api_resources/chats/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/chats/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/chats/test_reminders.py b/tests/api_resources/chats/test_reminders.py new file mode 100644 index 0000000..fea1bcb --- /dev/null +++ b/tests/api_resources/chats/test_reminders.py @@ -0,0 +1,206 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types.shared import BaseResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestReminders: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: BeeperDesktop) -> None: + reminder = client.chats.reminders.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, + ) + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: BeeperDesktop) -> None: + reminder = client.chats.reminders.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={ + "remind_at_ms": 0, + "dismiss_on_incoming_message": True, + }, + ) + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: BeeperDesktop) -> None: + response = client.chats.reminders.with_raw_response.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + reminder = response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: BeeperDesktop) -> None: + with client.chats.reminders.with_streaming_response.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + reminder = response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.reminders.with_raw_response.create( + chat_id="", + reminder={"remind_at_ms": 0}, + ) + + @parametrize + def test_method_delete(self, client: BeeperDesktop) -> None: + reminder = client.chats.reminders.delete( + "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: BeeperDesktop) -> None: + response = client.chats.reminders.with_raw_response.delete( + "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + reminder = response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: BeeperDesktop) -> None: + with client.chats.reminders.with_streaming_response.delete( + "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + reminder = response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.reminders.with_raw_response.delete( + "", + ) + + +class TestAsyncReminders: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncBeeperDesktop) -> None: + reminder = await async_client.chats.reminders.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, + ) + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + reminder = await async_client.chats.reminders.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={ + "remind_at_ms": 0, + "dismiss_on_incoming_message": True, + }, + ) + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.reminders.with_raw_response.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + reminder = await response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.reminders.with_streaming_response.create( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reminder={"remind_at_ms": 0}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + reminder = await response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.reminders.with_raw_response.create( + chat_id="", + reminder={"remind_at_ms": 0}, + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBeeperDesktop) -> None: + reminder = await async_client.chats.reminders.delete( + "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.reminders.with_raw_response.delete( + "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + reminder = await response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.reminders.with_streaming_response.delete( + "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + reminder = await response.parse() + assert_matches_type(BaseResponse, reminder, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.reminders.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_accounts.py b/tests/api_resources/test_accounts.py new file mode 100644 index 0000000..46ac702 --- /dev/null +++ b/tests/api_resources/test_accounts.py @@ -0,0 +1,74 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types import AccountListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAccounts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: BeeperDesktop) -> None: + account = client.accounts.list() + assert_matches_type(AccountListResponse, account, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: BeeperDesktop) -> None: + response = client.accounts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(AccountListResponse, account, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: BeeperDesktop) -> None: + with client.accounts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account = response.parse() + assert_matches_type(AccountListResponse, account, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAccounts: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: + account = await async_client.accounts.list() + assert_matches_type(AccountListResponse, account, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.accounts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = await response.parse() + assert_matches_type(AccountListResponse, account, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.accounts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account = await response.parse() + assert_matches_type(AccountListResponse, account, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py new file mode 100644 index 0000000..3eba100 --- /dev/null +++ b/tests/api_resources/test_chats.py @@ -0,0 +1,402 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types import ( + Chat, + ChatCreateResponse, +) +from beeper_desktop_api._utils import parse_datetime +from beeper_desktop_api.pagination import SyncCursor, AsyncCursor +from beeper_desktop_api.types.shared import BaseResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestChats: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: BeeperDesktop) -> None: + chat = client.chats.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + ) + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: BeeperDesktop) -> None: + chat = client.chats.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + message_text="messageText", + title="title", + ) + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: BeeperDesktop) -> None: + response = client.chats.with_raw_response.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: BeeperDesktop) -> None: + with client.chats.with_streaming_response.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: BeeperDesktop) -> None: + chat = client.chats.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(Chat, chat, path=["response"]) + + @parametrize + def test_method_retrieve_with_all_params(self, client: BeeperDesktop) -> None: + chat = client.chats.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + max_participant_count=50, + ) + assert_matches_type(Chat, chat, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: BeeperDesktop) -> None: + response = client.chats.with_raw_response.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(Chat, chat, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: BeeperDesktop) -> None: + with client.chats.with_streaming_response.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(Chat, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.with_raw_response.retrieve( + chat_id="", + ) + + @parametrize + def test_method_archive(self, client: BeeperDesktop) -> None: + chat = client.chats.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(BaseResponse, chat, path=["response"]) + + @parametrize + def test_method_archive_with_all_params(self, client: BeeperDesktop) -> None: + chat = client.chats.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + archived=True, + ) + assert_matches_type(BaseResponse, chat, path=["response"]) + + @parametrize + def test_raw_response_archive(self, client: BeeperDesktop) -> None: + response = client.chats.with_raw_response.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(BaseResponse, chat, path=["response"]) + + @parametrize + def test_streaming_response_archive(self, client: BeeperDesktop) -> None: + with client.chats.with_streaming_response.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(BaseResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_archive(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.with_raw_response.archive( + chat_id="", + ) + + @parametrize + def test_method_search(self, client: BeeperDesktop) -> None: + chat = client.chats.search() + assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + + @parametrize + def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: + chat = client.chats.search( + account_ids=[ + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", + ], + cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9", + direction="after", + inbox="primary", + include_muted=True, + last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"), + last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"), + limit=1, + query="x", + scope="titles", + type="single", + unread_only=True, + ) + assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + + @parametrize + def test_raw_response_search(self, client: BeeperDesktop) -> None: + response = client.chats.with_raw_response.search() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + + @parametrize + def test_streaming_response_search(self, client: BeeperDesktop) -> None: + with client.chats.with_streaming_response.search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncChats: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + ) + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + message_text="messageText", + title="title", + ) + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.with_raw_response.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.with_streaming_response.create( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + participant_ids=["string"], + type="single", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(ChatCreateResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(Chat, chat, path=["response"]) + + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + max_participant_count=50, + ) + assert_matches_type(Chat, chat, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.with_raw_response.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(Chat, chat, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.with_streaming_response.retrieve( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(Chat, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.with_raw_response.retrieve( + chat_id="", + ) + + @parametrize + async def test_method_archive(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(BaseResponse, chat, path=["response"]) + + @parametrize + async def test_method_archive_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + archived=True, + ) + assert_matches_type(BaseResponse, chat, path=["response"]) + + @parametrize + async def test_raw_response_archive(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.with_raw_response.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(BaseResponse, chat, path=["response"]) + + @parametrize + async def test_streaming_response_archive(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.with_streaming_response.archive( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(BaseResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_archive(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.with_raw_response.archive( + chat_id="", + ) + + @parametrize + async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.search() + assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + + @parametrize + async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.search( + account_ids=[ + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", + ], + cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9", + direction="after", + inbox="primary", + include_muted=True, + last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"), + last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"), + limit=1, + query="x", + scope="titles", + type="single", + unread_only=True, + ) + assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + + @parametrize + async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.with_raw_response.search() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + + @parametrize + async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.with_streaming_response.search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py new file mode 100644 index 0000000..d5de032 --- /dev/null +++ b/tests/api_resources/test_client.py @@ -0,0 +1,222 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types import ( + OpenResponse, + SearchResponse, + DownloadAssetResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestClient: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_download_asset(self, client: BeeperDesktop) -> None: + client_ = client.download_asset( + url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", + ) + assert_matches_type(DownloadAssetResponse, client_, path=["response"]) + + @parametrize + def test_raw_response_download_asset(self, client: BeeperDesktop) -> None: + response = client.with_raw_response.download_asset( + url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client_ = response.parse() + assert_matches_type(DownloadAssetResponse, client_, path=["response"]) + + @parametrize + def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None: + with client.with_streaming_response.download_asset( + url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client_ = response.parse() + assert_matches_type(DownloadAssetResponse, client_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_open(self, client: BeeperDesktop) -> None: + client_ = client.open() + assert_matches_type(OpenResponse, client_, path=["response"]) + + @parametrize + def test_method_open_with_all_params(self, client: BeeperDesktop) -> None: + client_ = client.open( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + draft_attachment_path="draftAttachmentPath", + draft_text="draftText", + message_id="messageID", + ) + assert_matches_type(OpenResponse, client_, path=["response"]) + + @parametrize + def test_raw_response_open(self, client: BeeperDesktop) -> None: + response = client.with_raw_response.open() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client_ = response.parse() + assert_matches_type(OpenResponse, client_, path=["response"]) + + @parametrize + def test_streaming_response_open(self, client: BeeperDesktop) -> None: + with client.with_streaming_response.open() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client_ = response.parse() + assert_matches_type(OpenResponse, client_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_search(self, client: BeeperDesktop) -> None: + client_ = client.search( + query="x", + ) + assert_matches_type(SearchResponse, client_, path=["response"]) + + @parametrize + def test_raw_response_search(self, client: BeeperDesktop) -> None: + response = client.with_raw_response.search( + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client_ = response.parse() + assert_matches_type(SearchResponse, client_, path=["response"]) + + @parametrize + def test_streaming_response_search(self, client: BeeperDesktop) -> None: + with client.with_streaming_response.search( + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client_ = response.parse() + assert_matches_type(SearchResponse, client_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncClient: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_download_asset(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.download_asset( + url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", + ) + assert_matches_type(DownloadAssetResponse, client, path=["response"]) + + @parametrize + async def test_raw_response_download_asset(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.with_raw_response.download_asset( + url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client = await response.parse() + assert_matches_type(DownloadAssetResponse, client, path=["response"]) + + @parametrize + async def test_streaming_response_download_asset(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.with_streaming_response.download_asset( + url="mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client = await response.parse() + assert_matches_type(DownloadAssetResponse, client, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.open() + assert_matches_type(OpenResponse, client, path=["response"]) + + @parametrize + async def test_method_open_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.open( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + draft_attachment_path="draftAttachmentPath", + draft_text="draftText", + message_id="messageID", + ) + assert_matches_type(OpenResponse, client, path=["response"]) + + @parametrize + async def test_raw_response_open(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.with_raw_response.open() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client = await response.parse() + assert_matches_type(OpenResponse, client, path=["response"]) + + @parametrize + async def test_streaming_response_open(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.with_streaming_response.open() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client = await response.parse() + assert_matches_type(OpenResponse, client, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.search( + query="x", + ) + assert_matches_type(SearchResponse, client, path=["response"]) + + @parametrize + async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.with_raw_response.search( + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client = await response.parse() + assert_matches_type(SearchResponse, client, path=["response"]) + + @parametrize + async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.with_streaming_response.search( + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client = await response.parse() + assert_matches_type(SearchResponse, client, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py new file mode 100644 index 0000000..6308d1f --- /dev/null +++ b/tests/api_resources/test_contacts.py @@ -0,0 +1,92 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types import ContactSearchResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestContacts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_search(self, client: BeeperDesktop) -> None: + contact = client.contacts.search( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + assert_matches_type(ContactSearchResponse, contact, path=["response"]) + + @parametrize + def test_raw_response_search(self, client: BeeperDesktop) -> None: + response = client.contacts.with_raw_response.search( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(ContactSearchResponse, contact, path=["response"]) + + @parametrize + def test_streaming_response_search(self, client: BeeperDesktop) -> None: + with client.contacts.with_streaming_response.search( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(ContactSearchResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncContacts: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: + contact = await async_client.contacts.search( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + assert_matches_type(ContactSearchResponse, contact, path=["response"]) + + @parametrize + async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.contacts.with_raw_response.search( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(ContactSearchResponse, contact, path=["response"]) + + @parametrize + async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.contacts.with_streaming_response.search( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(ContactSearchResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py new file mode 100644 index 0000000..eb5fc6e --- /dev/null +++ b/tests/api_resources/test_messages.py @@ -0,0 +1,203 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types import MessageSendResponse +from beeper_desktop_api._utils import parse_datetime +from beeper_desktop_api.pagination import SyncCursor, AsyncCursor +from beeper_desktop_api.types.shared import Message + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMessages: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_search(self, client: BeeperDesktop) -> None: + message = client.messages.search() + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + @parametrize + def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: + message = client.messages.search( + account_ids=[ + "whatsapp", + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", + ], + chat_ids=["!NCdzlIaMjZUmvmvyHU:beeper.com", "1231073"], + chat_type="group", + cursor="1725489123456|c29tZUltc2dQYWdl", + date_after=parse_datetime("2025-08-01T00:00:00Z"), + date_before=parse_datetime("2025-08-31T23:59:59Z"), + direction="before", + exclude_low_priority=True, + include_muted=True, + limit=20, + media_types=["any"], + query="dinner", + sender="me", + ) + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + @parametrize + def test_raw_response_search(self, client: BeeperDesktop) -> None: + response = client.messages.with_raw_response.search() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + @parametrize + def test_streaming_response_search(self, client: BeeperDesktop) -> None: + with client.messages.with_streaming_response.search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_send(self, client: BeeperDesktop) -> None: + message = client.messages.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @parametrize + def test_method_send_with_all_params(self, client: BeeperDesktop) -> None: + message = client.messages.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reply_to_message_id="replyToMessageID", + text="text", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @parametrize + def test_raw_response_send(self, client: BeeperDesktop) -> None: + response = client.messages.with_raw_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @parametrize + def test_streaming_response_send(self, client: BeeperDesktop) -> None: + with client.messages.with_streaming_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncMessages: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: + message = await async_client.messages.search() + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + @parametrize + async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + message = await async_client.messages.search( + account_ids=[ + "whatsapp", + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", + ], + chat_ids=["!NCdzlIaMjZUmvmvyHU:beeper.com", "1231073"], + chat_type="group", + cursor="1725489123456|c29tZUltc2dQYWdl", + date_after=parse_datetime("2025-08-01T00:00:00Z"), + date_before=parse_datetime("2025-08-31T23:59:59Z"), + direction="before", + exclude_low_priority=True, + include_muted=True, + limit=20, + media_types=["any"], + query="dinner", + sender="me", + ) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + @parametrize + async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.messages.with_raw_response.search() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + @parametrize + async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.messages.with_streaming_response.search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_send(self, async_client: AsyncBeeperDesktop) -> None: + message = await async_client.messages.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @parametrize + async def test_method_send_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + message = await async_client.messages.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + reply_to_message_id="replyToMessageID", + text="text", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @parametrize + async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.messages.with_raw_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @parametrize + async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.messages.with_streaming_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 1450af1..c9e3e9e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -747,20 +747,20 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("beeper_desktop_api._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: BeeperDesktop) -> None: - respx_mock.get("/oauth/userinfo").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.get("/v1/accounts").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.token.with_streaming_response.info().__enter__() + client.accounts.with_streaming_response.list().__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("beeper_desktop_api._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: BeeperDesktop) -> None: - respx_mock.get("/oauth/userinfo").mock(return_value=httpx.Response(500)) + respx_mock.get("/v1/accounts").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.token.with_streaming_response.info().__enter__() + client.accounts.with_streaming_response.list().__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -787,9 +787,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler) + respx_mock.get("/v1/accounts").mock(side_effect=retry_handler) - response = client.token.with_raw_response.info() + response = client.accounts.with_raw_response.list() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -811,9 +811,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler) + respx_mock.get("/v1/accounts").mock(side_effect=retry_handler) - response = client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -834,9 +834,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler) + respx_mock.get("/v1/accounts").mock(side_effect=retry_handler) - response = client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": "42"}) + response = client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1584,10 +1584,10 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncBeeperDesktop ) -> None: - respx_mock.get("/oauth/userinfo").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.get("/v1/accounts").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.token.with_streaming_response.info().__aenter__() + await async_client.accounts.with_streaming_response.list().__aenter__() assert _get_open_connections(self.client) == 0 @@ -1596,10 +1596,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncBeeperDesktop ) -> None: - respx_mock.get("/oauth/userinfo").mock(return_value=httpx.Response(500)) + respx_mock.get("/v1/accounts").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.token.with_streaming_response.info().__aenter__() + await async_client.accounts.with_streaming_response.list().__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1627,9 +1627,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler) + respx_mock.get("/v1/accounts").mock(side_effect=retry_handler) - response = await client.token.with_raw_response.info() + response = await client.accounts.with_raw_response.list() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1652,9 +1652,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler) + respx_mock.get("/v1/accounts").mock(side_effect=retry_handler) - response = await client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1676,9 +1676,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/oauth/userinfo").mock(side_effect=retry_handler) + respx_mock.get("/v1/accounts").mock(side_effect=retry_handler) - response = await client.token.with_raw_response.info(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.accounts.with_raw_response.list(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 1ea87ff08b4b50541e3c26bef6f4bd581af6324c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:02:04 +0000 Subject: [PATCH 03/20] feat(api): manual updates --- .stats.yml | 4 +- api.md | 4 +- src/beeper_desktop_api/resources/accounts.py | 4 +- .../resources/chats/chats.py | 133 +++++++++++++++++- src/beeper_desktop_api/resources/messages.py | 130 ++++++++++++++++- src/beeper_desktop_api/types/__init__.py | 3 + .../types/chat_list_params.py | 30 ++++ .../types/chat_list_response.py | 13 ++ .../types/message_list_params.py | 27 ++++ tests/api_resources/test_chats.py | 79 +++++++++++ tests/api_resources/test_messages.py | 86 ++++++++++- 11 files changed, 505 insertions(+), 8 deletions(-) create mode 100644 src/beeper_desktop_api/types/chat_list_params.py create mode 100644 src/beeper_desktop_api/types/chat_list_response.py create mode 100644 src/beeper_desktop_api/types/message_list_params.py diff --git a/.stats.yml b/.stats.yml index 8526f3e..e531b74 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 14 +configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: f83b2b6eb86f2dd68101065998479cb2 +config_hash: be3f3b31e322be0f4de6a23e32ab004c diff --git a/api.md b/api.md index cfe2dc3..dbf9b16 100644 --- a/api.md +++ b/api.md @@ -47,13 +47,14 @@ Methods: Types: ```python -from beeper_desktop_api.types import Chat, ChatCreateResponse +from beeper_desktop_api.types import Chat, ChatCreateResponse, ChatListResponse ``` Methods: - client.chats.create(\*\*params) -> ChatCreateResponse - client.chats.retrieve(chat_id, \*\*params) -> Chat +- client.chats.list(\*\*params) -> SyncCursor[ChatListResponse] - client.chats.archive(chat_id, \*\*params) -> BaseResponse - client.chats.search(\*\*params) -> SyncCursor[Chat] @@ -74,6 +75,7 @@ from beeper_desktop_api.types import MessageSendResponse Methods: +- client.messages.list(\*\*params) -> SyncCursor[Message] - client.messages.search(\*\*params) -> SyncCursor[Message] - client.messages.send(\*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/resources/accounts.py b/src/beeper_desktop_api/resources/accounts.py index 49a5df2..1210fce 100644 --- a/src/beeper_desktop_api/resources/accounts.py +++ b/src/beeper_desktop_api/resources/accounts.py @@ -20,7 +20,7 @@ class AccountsResource(SyncAPIResource): - """Accounts operations""" + """Manage connected chat accounts""" @cached_property def with_raw_response(self) -> AccountsResourceWithRawResponse: @@ -65,7 +65,7 @@ def list( class AsyncAccountsResource(AsyncAPIResource): - """Accounts operations""" + """Manage connected chat accounts""" @cached_property def with_raw_response(self) -> AsyncAccountsResourceWithRawResponse: diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py index b5ec602..93587e7 100644 --- a/src/beeper_desktop_api/resources/chats/chats.py +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -8,7 +8,7 @@ import httpx -from ...types import chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params +from ...types import chat_list_params, chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property @@ -30,6 +30,7 @@ from ...pagination import SyncCursor, AsyncCursor from ...types.chat import Chat from ..._base_client import AsyncPaginator, make_request_options +from ...types.chat_list_response import ChatListResponse from ...types.chat_create_response import ChatCreateResponse from ...types.shared.base_response import BaseResponse @@ -166,6 +167,65 @@ def retrieve( cast_to=Chat, ) + def list( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncCursor[ChatListResponse]: + """List all chats sorted by last activity (most recent first). + + Combines all + accounts into a single paginated list. + + Args: + account_ids: Limit to specific account IDs. If omitted, fetches from all accounts. + + cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction + to navigate results. + + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + + limit: Maximum number of chats to return (1–200). Defaults to 50. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/chats", + page=SyncCursor[ChatListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "cursor": cursor, + "direction": direction, + "limit": limit, + }, + chat_list_params.ChatListParams, + ), + ), + model=ChatListResponse, + ) + def archive( self, chat_id: str, @@ -436,6 +496,65 @@ async def retrieve( cast_to=Chat, ) + def list( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ChatListResponse, AsyncCursor[ChatListResponse]]: + """List all chats sorted by last activity (most recent first). + + Combines all + accounts into a single paginated list. + + Args: + account_ids: Limit to specific account IDs. If omitted, fetches from all accounts. + + cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction + to navigate results. + + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + + limit: Maximum number of chats to return (1–200). Defaults to 50. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/chats", + page=AsyncCursor[ChatListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "cursor": cursor, + "direction": direction, + "limit": limit, + }, + chat_list_params.ChatListParams, + ), + ), + model=ChatListResponse, + ) + async def archive( self, chat_id: str, @@ -586,6 +705,9 @@ def __init__(self, chats: ChatsResource) -> None: self.retrieve = to_raw_response_wrapper( chats.retrieve, ) + self.list = to_raw_response_wrapper( + chats.list, + ) self.archive = to_raw_response_wrapper( chats.archive, ) @@ -609,6 +731,9 @@ def __init__(self, chats: AsyncChatsResource) -> None: self.retrieve = async_to_raw_response_wrapper( chats.retrieve, ) + self.list = async_to_raw_response_wrapper( + chats.list, + ) self.archive = async_to_raw_response_wrapper( chats.archive, ) @@ -632,6 +757,9 @@ def __init__(self, chats: ChatsResource) -> None: self.retrieve = to_streamed_response_wrapper( chats.retrieve, ) + self.list = to_streamed_response_wrapper( + chats.list, + ) self.archive = to_streamed_response_wrapper( chats.archive, ) @@ -655,6 +783,9 @@ def __init__(self, chats: AsyncChatsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( chats.retrieve, ) + self.list = async_to_streamed_response_wrapper( + chats.list, + ) self.archive = async_to_streamed_response_wrapper( chats.archive, ) diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index ea1ea25..485a86b 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -8,7 +8,7 @@ import httpx -from ..types import message_send_params, message_search_params +from ..types import message_list_params, message_send_params, message_search_params from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property @@ -49,6 +49,64 @@ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: """ return MessagesResourceWithStreamingResponse(self) + def list( + self, + *, + chat_id: str, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncCursor[Message]: + """List all messages in a chat with cursor-based pagination. + + Sorted by timestamp. + + Args: + chat_id: The chat ID to list messages from + + cursor: Message cursor for pagination. Use with direction to navigate results. + + direction: Pagination direction used with 'cursor': 'before' fetches older messages, + 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is + provided. + + limit: Maximum number of messages to return (1–500). Defaults to 50. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/messages", + page=SyncCursor[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "chat_id": chat_id, + "cursor": cursor, + "direction": direction, + "limit": limit, + }, + message_list_params.MessageListParams, + ), + ), + model=Message, + ) + def search( self, *, @@ -223,6 +281,64 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: """ return AsyncMessagesResourceWithStreamingResponse(self) + def list( + self, + *, + chat_id: str, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Message, AsyncCursor[Message]]: + """List all messages in a chat with cursor-based pagination. + + Sorted by timestamp. + + Args: + chat_id: The chat ID to list messages from + + cursor: Message cursor for pagination. Use with direction to navigate results. + + direction: Pagination direction used with 'cursor': 'before' fetches older messages, + 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is + provided. + + limit: Maximum number of messages to return (1–500). Defaults to 50. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/messages", + page=AsyncCursor[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "chat_id": chat_id, + "cursor": cursor, + "direction": direction, + "limit": limit, + }, + message_list_params.MessageListParams, + ), + ), + model=Message, + ) + def search( self, *, @@ -379,6 +495,9 @@ class MessagesResourceWithRawResponse: def __init__(self, messages: MessagesResource) -> None: self._messages = messages + self.list = to_raw_response_wrapper( + messages.list, + ) self.search = to_raw_response_wrapper( messages.search, ) @@ -391,6 +510,9 @@ class AsyncMessagesResourceWithRawResponse: def __init__(self, messages: AsyncMessagesResource) -> None: self._messages = messages + self.list = async_to_raw_response_wrapper( + messages.list, + ) self.search = async_to_raw_response_wrapper( messages.search, ) @@ -403,6 +525,9 @@ class MessagesResourceWithStreamingResponse: def __init__(self, messages: MessagesResource) -> None: self._messages = messages + self.list = to_streamed_response_wrapper( + messages.list, + ) self.search = to_streamed_response_wrapper( messages.search, ) @@ -415,6 +540,9 @@ class AsyncMessagesResourceWithStreamingResponse: def __init__(self, messages: AsyncMessagesResource) -> None: self._messages = messages + self.list = async_to_streamed_response_wrapper( + messages.list, + ) self.search = async_to_streamed_response_wrapper( messages.search, ) diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index 5bede4c..ffab91e 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -15,10 +15,13 @@ from .user_info import UserInfo as UserInfo from .open_response import OpenResponse as OpenResponse from .search_response import SearchResponse as SearchResponse +from .chat_list_params import ChatListParams as ChatListParams from .chat_create_params import ChatCreateParams as ChatCreateParams +from .chat_list_response import ChatListResponse as ChatListResponse from .chat_search_params import ChatSearchParams as ChatSearchParams from .client_open_params import ClientOpenParams as ClientOpenParams from .chat_archive_params import ChatArchiveParams as ChatArchiveParams +from .message_list_params import MessageListParams as MessageListParams from .message_send_params import MessageSendParams as MessageSendParams from .chat_create_response import ChatCreateResponse as ChatCreateResponse from .chat_retrieve_params import ChatRetrieveParams as ChatRetrieveParams diff --git a/src/beeper_desktop_api/types/chat_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py new file mode 100644 index 0000000..d8e1784 --- /dev/null +++ b/src/beeper_desktop_api/types/chat_list_params.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["ChatListParams"] + + +class ChatListParams(TypedDict, total=False): + account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")] + """Limit to specific account IDs. If omitted, fetches from all accounts.""" + + cursor: str + """Timestamp cursor (milliseconds since epoch) for pagination. + + Use with direction to navigate results. + """ + + direction: Literal["after", "before"] + """ + Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + """ + + limit: int + """Maximum number of chats to return (1–200). Defaults to 50.""" diff --git a/src/beeper_desktop_api/types/chat_list_response.py b/src/beeper_desktop_api/types/chat_list_response.py new file mode 100644 index 0000000..80e3885 --- /dev/null +++ b/src/beeper_desktop_api/types/chat_list_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .chat import Chat +from .shared.message import Message + +__all__ = ["ChatListResponse"] + + +class ChatListResponse(Chat): + preview: Optional[Message] = None + """Last message preview for this chat, if available.""" diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py new file mode 100644 index 0000000..ca56fab --- /dev/null +++ b/src/beeper_desktop_api/types/message_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["MessageListParams"] + + +class MessageListParams(TypedDict, total=False): + chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]] + """The chat ID to list messages from""" + + cursor: str + """Message cursor for pagination. Use with direction to navigate results.""" + + direction: Literal["after", "before"] + """ + Pagination direction used with 'cursor': 'before' fetches older messages, + 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is + provided. + """ + + limit: int + """Maximum number of messages to return (1–500). Defaults to 50.""" diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py index 3eba100..3009cc9 100644 --- a/tests/api_resources/test_chats.py +++ b/tests/api_resources/test_chats.py @@ -11,6 +11,7 @@ from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop from beeper_desktop_api.types import ( Chat, + ChatListResponse, ChatCreateResponse, ) from beeper_desktop_api._utils import parse_datetime @@ -117,6 +118,45 @@ def test_path_params_retrieve(self, client: BeeperDesktop) -> None: chat_id="", ) + @parametrize + def test_method_list(self, client: BeeperDesktop) -> None: + chat = client.chats.list() + assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: + chat = client.chats.list( + account_ids=[ + "whatsapp", + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", + ], + cursor="1725489123456", + direction="before", + limit=1, + ) + assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: BeeperDesktop) -> None: + response = client.chats.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: BeeperDesktop) -> None: + with client.chats.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_archive(self, client: BeeperDesktop) -> None: chat = client.chats.archive( @@ -309,6 +349,45 @@ async def test_path_params_retrieve(self, async_client: AsyncBeeperDesktop) -> N chat_id="", ) + @parametrize + async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.list() + assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.list( + account_ids=[ + "whatsapp", + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", + ], + cursor="1725489123456", + direction="before", + limit=1, + ) + assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.chats.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.chats.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_archive(self, async_client: AsyncBeeperDesktop) -> None: chat = await async_client.chats.archive( diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index eb5fc6e..85ebebb 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -9,7 +9,9 @@ from tests.utils import assert_matches_type from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop -from beeper_desktop_api.types import MessageSendResponse +from beeper_desktop_api.types import ( + MessageSendResponse, +) from beeper_desktop_api._utils import parse_datetime from beeper_desktop_api.pagination import SyncCursor, AsyncCursor from beeper_desktop_api.types.shared import Message @@ -20,6 +22,47 @@ class TestMessages: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + def test_method_list(self, client: BeeperDesktop) -> None: + message = client.messages.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: + message = client.messages.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + cursor="821744079", + direction="before", + limit=1, + ) + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: BeeperDesktop) -> None: + response = client.messages.with_raw_response.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: BeeperDesktop) -> None: + with client.messages.with_streaming_response.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(SyncCursor[Message], message, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_search(self, client: BeeperDesktop) -> None: message = client.messages.search() @@ -114,6 +157,47 @@ class TestAsyncMessages: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) + @parametrize + async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: + message = await async_client.messages.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + message = await async_client.messages.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + cursor="821744079", + direction="before", + limit=1, + ) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.messages.with_raw_response.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.messages.with_streaming_response.list( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(AsyncCursor[Message], message, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.search() From 88bce73dfef13b6a1cdef0749dc3078af97255e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:07:31 +0000 Subject: [PATCH 04/20] feat(api): manual updates --- .stats.yml | 2 +- api.md | 20 +-- src/beeper_desktop_api/_client.py | 61 ++++++-- src/beeper_desktop_api/resources/__init__.py | 14 -- src/beeper_desktop_api/resources/token.py | 139 ------------------ src/beeper_desktop_api/types/__init__.py | 2 +- ...ser_info.py => get_token_info_response.py} | 4 +- tests/api_resources/test_client.py | 51 +++++++ tests/api_resources/test_token.py | 74 ---------- 9 files changed, 114 insertions(+), 253 deletions(-) delete mode 100644 src/beeper_desktop_api/resources/token.py rename src/beeper_desktop_api/types/{user_info.py => get_token_info_response.py} (89%) delete mode 100644 tests/api_resources/test_token.py diff --git a/.stats.yml b/.stats.yml index e531b74..3cb504d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: be3f3b31e322be0f4de6a23e32ab004c +config_hash: 00db138e547960c0d9c47754c2f59051 diff --git a/api.md b/api.md index dbf9b16..502d7e8 100644 --- a/api.md +++ b/api.md @@ -9,12 +9,18 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, R Types: ```python -from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse +from beeper_desktop_api.types import ( + DownloadAssetResponse, + GetTokenInfoResponse, + OpenResponse, + SearchResponse, +) ``` Methods: - client.download_asset(\*\*params) -> DownloadAssetResponse +- client.get_token_info() -> GetTokenInfoResponse - client.open(\*\*params) -> OpenResponse - client.search(\*\*params) -> SearchResponse @@ -78,15 +84,3 @@ Methods: - client.messages.list(\*\*params) -> SyncCursor[Message] - client.messages.search(\*\*params) -> SyncCursor[Message] - client.messages.send(\*\*params) -> MessageSendResponse - -# Token - -Types: - -```python -from beeper_desktop_api.types import UserInfo -``` - -Methods: - -- client.token.info() -> UserInfo diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py index 7a1a10e..01dd758 100644 --- a/src/beeper_desktop_api/_client.py +++ b/src/beeper_desktop_api/_client.py @@ -37,7 +37,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .resources import token, accounts, contacts, messages +from .resources import accounts, contacts, messages from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BeeperDesktopError from ._base_client import ( @@ -50,6 +50,7 @@ from .types.open_response import OpenResponse from .types.search_response import SearchResponse from .types.download_asset_response import DownloadAssetResponse +from .types.get_token_info_response import GetTokenInfoResponse __all__ = [ "Timeout", @@ -68,7 +69,6 @@ class BeeperDesktop(SyncAPIClient): contacts: contacts.ContactsResource chats: chats.ChatsResource messages: messages.MessagesResource - token: token.TokenResource with_raw_response: BeeperDesktopWithRawResponse with_streaming_response: BeeperDesktopWithStreamedResponse @@ -130,7 +130,6 @@ def __init__( self.contacts = contacts.ContactsResource(self) self.chats = chats.ChatsResource(self) self.messages = messages.MessagesResource(self) - self.token = token.TokenResource(self) self.with_raw_response = BeeperDesktopWithRawResponse(self) self.with_streaming_response = BeeperDesktopWithStreamedResponse(self) @@ -240,6 +239,25 @@ def download_asset( cast_to=DownloadAssetResponse, ) + def get_token_info( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GetTokenInfoResponse: + """Returns information about the authenticated user/token""" + return self.get( + "/oauth/userinfo", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GetTokenInfoResponse, + ) + def open( self, *, @@ -371,7 +389,6 @@ class AsyncBeeperDesktop(AsyncAPIClient): contacts: contacts.AsyncContactsResource chats: chats.AsyncChatsResource messages: messages.AsyncMessagesResource - token: token.AsyncTokenResource with_raw_response: AsyncBeeperDesktopWithRawResponse with_streaming_response: AsyncBeeperDesktopWithStreamedResponse @@ -433,7 +450,6 @@ def __init__( self.contacts = contacts.AsyncContactsResource(self) self.chats = chats.AsyncChatsResource(self) self.messages = messages.AsyncMessagesResource(self) - self.token = token.AsyncTokenResource(self) self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self) self.with_streaming_response = AsyncBeeperDesktopWithStreamedResponse(self) @@ -543,6 +559,25 @@ async def download_asset( cast_to=DownloadAssetResponse, ) + async def get_token_info( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GetTokenInfoResponse: + """Returns information about the authenticated user/token""" + return await self.get( + "/oauth/userinfo", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GetTokenInfoResponse, + ) + async def open( self, *, @@ -675,11 +710,13 @@ def __init__(self, client: BeeperDesktop) -> None: self.contacts = contacts.ContactsResourceWithRawResponse(client.contacts) self.chats = chats.ChatsResourceWithRawResponse(client.chats) self.messages = messages.MessagesResourceWithRawResponse(client.messages) - self.token = token.TokenResourceWithRawResponse(client.token) self.download_asset = to_raw_response_wrapper( client.download_asset, ) + self.get_token_info = to_raw_response_wrapper( + client.get_token_info, + ) self.open = to_raw_response_wrapper( client.open, ) @@ -694,11 +731,13 @@ def __init__(self, client: AsyncBeeperDesktop) -> None: self.contacts = contacts.AsyncContactsResourceWithRawResponse(client.contacts) self.chats = chats.AsyncChatsResourceWithRawResponse(client.chats) self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages) - self.token = token.AsyncTokenResourceWithRawResponse(client.token) self.download_asset = async_to_raw_response_wrapper( client.download_asset, ) + self.get_token_info = async_to_raw_response_wrapper( + client.get_token_info, + ) self.open = async_to_raw_response_wrapper( client.open, ) @@ -713,11 +752,13 @@ def __init__(self, client: BeeperDesktop) -> None: self.contacts = contacts.ContactsResourceWithStreamingResponse(client.contacts) self.chats = chats.ChatsResourceWithStreamingResponse(client.chats) self.messages = messages.MessagesResourceWithStreamingResponse(client.messages) - self.token = token.TokenResourceWithStreamingResponse(client.token) self.download_asset = to_streamed_response_wrapper( client.download_asset, ) + self.get_token_info = to_streamed_response_wrapper( + client.get_token_info, + ) self.open = to_streamed_response_wrapper( client.open, ) @@ -732,11 +773,13 @@ def __init__(self, client: AsyncBeeperDesktop) -> None: self.contacts = contacts.AsyncContactsResourceWithStreamingResponse(client.contacts) self.chats = chats.AsyncChatsResourceWithStreamingResponse(client.chats) self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages) - self.token = token.AsyncTokenResourceWithStreamingResponse(client.token) self.download_asset = async_to_streamed_response_wrapper( client.download_asset, ) + self.get_token_info = async_to_streamed_response_wrapper( + client.get_token_info, + ) self.open = async_to_streamed_response_wrapper( client.open, ) diff --git a/src/beeper_desktop_api/resources/__init__.py b/src/beeper_desktop_api/resources/__init__.py index 24ab242..ebf006b 100644 --- a/src/beeper_desktop_api/resources/__init__.py +++ b/src/beeper_desktop_api/resources/__init__.py @@ -8,14 +8,6 @@ ChatsResourceWithStreamingResponse, AsyncChatsResourceWithStreamingResponse, ) -from .token import ( - TokenResource, - AsyncTokenResource, - TokenResourceWithRawResponse, - AsyncTokenResourceWithRawResponse, - TokenResourceWithStreamingResponse, - AsyncTokenResourceWithStreamingResponse, -) from .accounts import ( AccountsResource, AsyncAccountsResource, @@ -66,10 +58,4 @@ "AsyncMessagesResourceWithRawResponse", "MessagesResourceWithStreamingResponse", "AsyncMessagesResourceWithStreamingResponse", - "TokenResource", - "AsyncTokenResource", - "TokenResourceWithRawResponse", - "AsyncTokenResourceWithRawResponse", - "TokenResourceWithStreamingResponse", - "AsyncTokenResourceWithStreamingResponse", ] diff --git a/src/beeper_desktop_api/resources/token.py b/src/beeper_desktop_api/resources/token.py deleted file mode 100644 index 5648872..0000000 --- a/src/beeper_desktop_api/resources/token.py +++ /dev/null @@ -1,139 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from .._types import Body, Query, Headers, NotGiven, not_given -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.user_info import UserInfo - -__all__ = ["TokenResource", "AsyncTokenResource"] - - -class TokenResource(SyncAPIResource): - """Operations related to the current access token""" - - @cached_property - def with_raw_response(self) -> TokenResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers - """ - return TokenResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> TokenResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response - """ - return TokenResourceWithStreamingResponse(self) - - def info( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UserInfo: - """Returns information about the authenticated user/token""" - return self._get( - "/oauth/userinfo", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UserInfo, - ) - - -class AsyncTokenResource(AsyncAPIResource): - """Operations related to the current access token""" - - @cached_property - def with_raw_response(self) -> AsyncTokenResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers - """ - return AsyncTokenResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncTokenResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response - """ - return AsyncTokenResourceWithStreamingResponse(self) - - async def info( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UserInfo: - """Returns information about the authenticated user/token""" - return await self._get( - "/oauth/userinfo", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UserInfo, - ) - - -class TokenResourceWithRawResponse: - def __init__(self, token: TokenResource) -> None: - self._token = token - - self.info = to_raw_response_wrapper( - token.info, - ) - - -class AsyncTokenResourceWithRawResponse: - def __init__(self, token: AsyncTokenResource) -> None: - self._token = token - - self.info = async_to_raw_response_wrapper( - token.info, - ) - - -class TokenResourceWithStreamingResponse: - def __init__(self, token: TokenResource) -> None: - self._token = token - - self.info = to_streamed_response_wrapper( - token.info, - ) - - -class AsyncTokenResourceWithStreamingResponse: - def __init__(self, token: AsyncTokenResource) -> None: - self._token = token - - self.info = async_to_streamed_response_wrapper( - token.info, - ) diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index ffab91e..d778ad9 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -12,7 +12,6 @@ BaseResponse as BaseResponse, ) from .account import Account as Account -from .user_info import UserInfo as UserInfo from .open_response import OpenResponse as OpenResponse from .search_response import SearchResponse as SearchResponse from .chat_list_params import ChatListParams as ChatListParams @@ -32,4 +31,5 @@ from .message_send_response import MessageSendResponse as MessageSendResponse from .contact_search_response import ContactSearchResponse as ContactSearchResponse from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse +from .get_token_info_response import GetTokenInfoResponse as GetTokenInfoResponse from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams diff --git a/src/beeper_desktop_api/types/user_info.py b/src/beeper_desktop_api/types/get_token_info_response.py similarity index 89% rename from src/beeper_desktop_api/types/user_info.py rename to src/beeper_desktop_api/types/get_token_info_response.py index d023e31..5dcf865 100644 --- a/src/beeper_desktop_api/types/user_info.py +++ b/src/beeper_desktop_api/types/get_token_info_response.py @@ -5,10 +5,10 @@ from .._models import BaseModel -__all__ = ["UserInfo"] +__all__ = ["GetTokenInfoResponse"] -class UserInfo(BaseModel): +class GetTokenInfoResponse(BaseModel): iat: float """Issued at timestamp (Unix epoch seconds)""" diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py index d5de032..45f7fd0 100644 --- a/tests/api_resources/test_client.py +++ b/tests/api_resources/test_client.py @@ -12,6 +12,7 @@ from beeper_desktop_api.types import ( OpenResponse, SearchResponse, + GetTokenInfoResponse, DownloadAssetResponse, ) @@ -52,6 +53,31 @@ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_get_token_info(self, client: BeeperDesktop) -> None: + client_ = client.get_token_info() + assert_matches_type(GetTokenInfoResponse, client_, path=["response"]) + + @parametrize + def test_raw_response_get_token_info(self, client: BeeperDesktop) -> None: + response = client.with_raw_response.get_token_info() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client_ = response.parse() + assert_matches_type(GetTokenInfoResponse, client_, path=["response"]) + + @parametrize + def test_streaming_response_get_token_info(self, client: BeeperDesktop) -> None: + with client.with_streaming_response.get_token_info() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client_ = response.parse() + assert_matches_type(GetTokenInfoResponse, client_, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_open(self, client: BeeperDesktop) -> None: client_ = client.open() @@ -155,6 +181,31 @@ async def test_streaming_response_download_asset(self, async_client: AsyncBeeper assert cast(Any, response.is_closed) is True + @parametrize + async def test_method_get_token_info(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.get_token_info() + assert_matches_type(GetTokenInfoResponse, client, path=["response"]) + + @parametrize + async def test_raw_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.with_raw_response.get_token_info() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + client = await response.parse() + assert_matches_type(GetTokenInfoResponse, client, path=["response"]) + + @parametrize + async def test_streaming_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.with_streaming_response.get_token_info() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + client = await response.parse() + assert_matches_type(GetTokenInfoResponse, client, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None: client = await async_client.open() diff --git a/tests/api_resources/test_token.py b/tests/api_resources/test_token.py deleted file mode 100644 index 538aa77..0000000 --- a/tests/api_resources/test_token.py +++ /dev/null @@ -1,74 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop -from beeper_desktop_api.types import UserInfo - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestToken: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_info(self, client: BeeperDesktop) -> None: - token = client.token.info() - assert_matches_type(UserInfo, token, path=["response"]) - - @parametrize - def test_raw_response_info(self, client: BeeperDesktop) -> None: - response = client.token.with_raw_response.info() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - token = response.parse() - assert_matches_type(UserInfo, token, path=["response"]) - - @parametrize - def test_streaming_response_info(self, client: BeeperDesktop) -> None: - with client.token.with_streaming_response.info() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - token = response.parse() - assert_matches_type(UserInfo, token, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncToken: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @parametrize - async def test_method_info(self, async_client: AsyncBeeperDesktop) -> None: - token = await async_client.token.info() - assert_matches_type(UserInfo, token, path=["response"]) - - @parametrize - async def test_raw_response_info(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.token.with_raw_response.info() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - token = await response.parse() - assert_matches_type(UserInfo, token, path=["response"]) - - @parametrize - async def test_streaming_response_info(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.token.with_streaming_response.info() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - token = await response.parse() - assert_matches_type(UserInfo, token, path=["response"]) - - assert cast(Any, response.is_closed) is True From 7aa256c4678bff37841bc4ec35670fc19fc563a7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:13:33 +0000 Subject: [PATCH 05/20] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3cb504d..dffcc04 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: 00db138e547960c0d9c47754c2f59051 +config_hash: 382b53633aa9cc48d7b7f44ecf5e3e8c From 0151017ee47e53f613a9b55dd3460c4aece0a91d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:15:27 +0000 Subject: [PATCH 06/20] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index dffcc04..b4e4371 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: 382b53633aa9cc48d7b7f44ecf5e3e8c +config_hash: 58f19d979ad9a375e32b814503ce3e86 From 635746eb468dffce959dfc8bbb93e56859ee6df9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:17:37 +0000 Subject: [PATCH 07/20] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b4e4371..5b8f55b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: 58f19d979ad9a375e32b814503ce3e86 +config_hash: a9434fa7b77fc01af6e667f3717eb768 From 7c655fb94ba070083173c15a501be7a0f119a38b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:22:36 +0000 Subject: [PATCH 08/20] feat(api): manual updates --- .stats.yml | 8 +-- api.md | 12 +--- src/beeper_desktop_api/_client.py | 59 ++----------------- src/beeper_desktop_api/types/__init__.py | 1 - .../types/get_token_info_response.py | 31 ---------- tests/api_resources/test_client.py | 51 ---------------- 6 files changed, 11 insertions(+), 151 deletions(-) delete mode 100644 src/beeper_desktop_api/types/get_token_info_response.py diff --git a/.stats.yml b/.stats.yml index 5b8f55b..d64bab5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml -openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: a9434fa7b77fc01af6e667f3717eb768 +configured_endpoints: 15 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml +openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef +config_hash: d48fc12c89d2d812adf19d0508306f4a diff --git a/api.md b/api.md index 502d7e8..529a147 100644 --- a/api.md +++ b/api.md @@ -9,19 +9,13 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, R Types: ```python -from beeper_desktop_api.types import ( - DownloadAssetResponse, - GetTokenInfoResponse, - OpenResponse, - SearchResponse, -) +from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse ``` Methods: -- client.download_asset(\*\*params) -> DownloadAssetResponse -- client.get_token_info() -> GetTokenInfoResponse -- client.open(\*\*params) -> OpenResponse +- client.download_asset(\*\*params) -> DownloadAssetResponse +- client.open(\*\*params) -> OpenResponse - client.search(\*\*params) -> SearchResponse # Accounts diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py index 01dd758..f8ba71d 100644 --- a/src/beeper_desktop_api/_client.py +++ b/src/beeper_desktop_api/_client.py @@ -50,7 +50,6 @@ from .types.open_response import OpenResponse from .types.search_response import SearchResponse from .types.download_asset_response import DownloadAssetResponse -from .types.get_token_info_response import GetTokenInfoResponse __all__ = [ "Timeout", @@ -231,7 +230,7 @@ def download_asset( timeout: Override the client-level default timeout for this request, in seconds """ return self.post( - "/v1/app/download-asset", + "/v1/download-asset", body=maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -239,25 +238,6 @@ def download_asset( cast_to=DownloadAssetResponse, ) - def get_token_info( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> GetTokenInfoResponse: - """Returns information about the authenticated user/token""" - return self.get( - "/oauth/userinfo", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=GetTokenInfoResponse, - ) - def open( self, *, @@ -295,7 +275,7 @@ def open( timeout: Override the client-level default timeout for this request, in seconds """ return self.post( - "/v1/app/open", + "/v1/open", body=maybe_transform( { "chat_id": chat_id, @@ -551,7 +531,7 @@ async def download_asset( timeout: Override the client-level default timeout for this request, in seconds """ return await self.post( - "/v1/app/download-asset", + "/v1/download-asset", body=await async_maybe_transform({"url": url}, client_download_asset_params.ClientDownloadAssetParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -559,25 +539,6 @@ async def download_asset( cast_to=DownloadAssetResponse, ) - async def get_token_info( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> GetTokenInfoResponse: - """Returns information about the authenticated user/token""" - return await self.get( - "/oauth/userinfo", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=GetTokenInfoResponse, - ) - async def open( self, *, @@ -615,7 +576,7 @@ async def open( timeout: Override the client-level default timeout for this request, in seconds """ return await self.post( - "/v1/app/open", + "/v1/open", body=await async_maybe_transform( { "chat_id": chat_id, @@ -714,9 +675,6 @@ def __init__(self, client: BeeperDesktop) -> None: self.download_asset = to_raw_response_wrapper( client.download_asset, ) - self.get_token_info = to_raw_response_wrapper( - client.get_token_info, - ) self.open = to_raw_response_wrapper( client.open, ) @@ -735,9 +693,6 @@ def __init__(self, client: AsyncBeeperDesktop) -> None: self.download_asset = async_to_raw_response_wrapper( client.download_asset, ) - self.get_token_info = async_to_raw_response_wrapper( - client.get_token_info, - ) self.open = async_to_raw_response_wrapper( client.open, ) @@ -756,9 +711,6 @@ def __init__(self, client: BeeperDesktop) -> None: self.download_asset = to_streamed_response_wrapper( client.download_asset, ) - self.get_token_info = to_streamed_response_wrapper( - client.get_token_info, - ) self.open = to_streamed_response_wrapper( client.open, ) @@ -777,9 +729,6 @@ def __init__(self, client: AsyncBeeperDesktop) -> None: self.download_asset = async_to_streamed_response_wrapper( client.download_asset, ) - self.get_token_info = async_to_streamed_response_wrapper( - client.get_token_info, - ) self.open = async_to_streamed_response_wrapper( client.open, ) diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index d778ad9..e577cbf 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -31,5 +31,4 @@ from .message_send_response import MessageSendResponse as MessageSendResponse from .contact_search_response import ContactSearchResponse as ContactSearchResponse from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse -from .get_token_info_response import GetTokenInfoResponse as GetTokenInfoResponse from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams diff --git a/src/beeper_desktop_api/types/get_token_info_response.py b/src/beeper_desktop_api/types/get_token_info_response.py deleted file mode 100644 index 5dcf865..0000000 --- a/src/beeper_desktop_api/types/get_token_info_response.py +++ /dev/null @@ -1,31 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["GetTokenInfoResponse"] - - -class GetTokenInfoResponse(BaseModel): - iat: float - """Issued at timestamp (Unix epoch seconds)""" - - scope: str - """Granted scopes""" - - sub: str - """Subject identifier (token ID)""" - - token_use: Literal["access"] - """Token type""" - - aud: Optional[str] = None - """Audience (client ID)""" - - client_id: Optional[str] = None - """Client identifier""" - - exp: Optional[float] = None - """Expiration timestamp (Unix epoch seconds)""" diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py index 45f7fd0..d5de032 100644 --- a/tests/api_resources/test_client.py +++ b/tests/api_resources/test_client.py @@ -12,7 +12,6 @@ from beeper_desktop_api.types import ( OpenResponse, SearchResponse, - GetTokenInfoResponse, DownloadAssetResponse, ) @@ -53,31 +52,6 @@ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None: assert cast(Any, response.is_closed) is True - @parametrize - def test_method_get_token_info(self, client: BeeperDesktop) -> None: - client_ = client.get_token_info() - assert_matches_type(GetTokenInfoResponse, client_, path=["response"]) - - @parametrize - def test_raw_response_get_token_info(self, client: BeeperDesktop) -> None: - response = client.with_raw_response.get_token_info() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - client_ = response.parse() - assert_matches_type(GetTokenInfoResponse, client_, path=["response"]) - - @parametrize - def test_streaming_response_get_token_info(self, client: BeeperDesktop) -> None: - with client.with_streaming_response.get_token_info() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - client_ = response.parse() - assert_matches_type(GetTokenInfoResponse, client_, path=["response"]) - - assert cast(Any, response.is_closed) is True - @parametrize def test_method_open(self, client: BeeperDesktop) -> None: client_ = client.open() @@ -181,31 +155,6 @@ async def test_streaming_response_download_asset(self, async_client: AsyncBeeper assert cast(Any, response.is_closed) is True - @parametrize - async def test_method_get_token_info(self, async_client: AsyncBeeperDesktop) -> None: - client = await async_client.get_token_info() - assert_matches_type(GetTokenInfoResponse, client, path=["response"]) - - @parametrize - async def test_raw_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.with_raw_response.get_token_info() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - client = await response.parse() - assert_matches_type(GetTokenInfoResponse, client, path=["response"]) - - @parametrize - async def test_streaming_response_get_token_info(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.with_streaming_response.get_token_info() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - client = await response.parse() - assert_matches_type(GetTokenInfoResponse, client, path=["response"]) - - assert cast(Any, response.is_closed) is True - @parametrize async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None: client = await async_client.open() From 6d8c6f207ddb0a4795ab5d4ed24ea8ebc3ab359a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:27:46 +0000 Subject: [PATCH 09/20] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d64bab5..2621f51 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef -config_hash: d48fc12c89d2d812adf19d0508306f4a +config_hash: b43f460701263c30aba16a32385b20ed From 58799fa0735e1f030be5afe194fd979a60b5480f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:32:17 +0000 Subject: [PATCH 10/20] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 2621f51..103ad16 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef -config_hash: b43f460701263c30aba16a32385b20ed +config_hash: 738402ade5ac9528c8ef1677aa1d70f7 From c9f3b2d3a7fb7e2ce3b30de215497079fff3aca9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:24:15 +0000 Subject: [PATCH 11/20] feat(api): manual updates --- .stats.yml | 2 +- README.md | 79 ------------------- api.md | 4 +- src/beeper_desktop_api/pagination.py | 67 +++++++++++++++- src/beeper_desktop_api/resources/messages.py | 19 +++-- src/beeper_desktop_api/types/__init__.py | 1 + .../types/message_search_response.py | 34 ++++++++ tests/api_resources/test_messages.py | 17 ++-- 8 files changed, 121 insertions(+), 102 deletions(-) create mode 100644 src/beeper_desktop_api/types/message_search_response.py diff --git a/.stats.yml b/.stats.yml index 103ad16..134452d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef -config_hash: 738402ade5ac9528c8ef1677aa1d70f7 +config_hash: 4fb2010b528ce4358300ddd10e750265 diff --git a/README.md b/README.md index 9f0e7ba..0f8fafb 100644 --- a/README.md +++ b/README.md @@ -118,85 +118,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. -## Pagination - -List methods in the Beeper Desktop API are paginated. - -This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: - -```python -from beeper_desktop_api import BeeperDesktop - -client = BeeperDesktop() - -all_messages = [] -# Automatically fetches more pages as needed. -for message in client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", -): - # Do something with message here - all_messages.append(message) -print(all_messages) -``` - -Or, asynchronously: - -```python -import asyncio -from beeper_desktop_api import AsyncBeeperDesktop - -client = AsyncBeeperDesktop() - - -async def main() -> None: - all_messages = [] - # Iterate through items across all pages, issuing requests as needed. - async for message in client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", - ): - all_messages.append(message) - print(all_messages) - - -asyncio.run(main()) -``` - -Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: - -```python -first_page = await client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", -) -if first_page.has_next_page(): - print(f"will fetch next page using these details: {first_page.next_page_info()}") - next_page = await first_page.get_next_page() - print(f"number of items we just fetched: {len(next_page.items)}") - -# Remove `await` for non-async usage. -``` - -Or just work directly with the returned data: - -```python -first_page = await client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", -) - -print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..." -for message in first_page.items: - print(message.id) - -# Remove `await` for non-async usage. -``` - ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: diff --git a/api.md b/api.md index 529a147..e862d8e 100644 --- a/api.md +++ b/api.md @@ -70,11 +70,11 @@ Methods: Types: ```python -from beeper_desktop_api.types import MessageSendResponse +from beeper_desktop_api.types import MessageSearchResponse, MessageSendResponse ``` Methods: - client.messages.list(\*\*params) -> SyncCursor[Message] -- client.messages.search(\*\*params) -> SyncCursor[Message] +- client.messages.search(\*\*params) -> MessageSearchResponse - client.messages.send(\*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py index 4606312..806a7a0 100644 --- a/src/beeper_desktop_api/pagination.py +++ b/src/beeper_desktop_api/pagination.py @@ -1,13 +1,14 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Generic, TypeVar, Optional +from typing import Dict, List, Generic, TypeVar, Optional from typing_extensions import override from pydantic import Field as FieldInfo +from .types.chat import Chat from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage -__all__ = ["SyncCursor", "AsyncCursor"] +__all__ = ["SyncCursor", "AsyncCursor", "SyncCursorWithChats", "AsyncCursorWithChats"] _T = TypeVar("_T") @@ -70,3 +71,65 @@ def next_page_info(self) -> Optional[PageInfo]: return None return PageInfo(params={"cursor": oldest_cursor}) + + +class SyncCursorWithChats(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + chats: Optional[Dict[str, Chat]] = None + has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) + oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) + newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + oldest_cursor = self.oldest_cursor + if not oldest_cursor: + return None + + return PageInfo(params={"cursor": oldest_cursor}) + + +class AsyncCursorWithChats(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + chats: Optional[Dict[str, Chat]] = None + has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) + oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) + newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + oldest_cursor = self.oldest_cursor + if not oldest_cursor: + return None + + return PageInfo(params={"cursor": oldest_cursor}) diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index 485a86b..d7d40ef 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -23,6 +23,7 @@ from .._base_client import AsyncPaginator, make_request_options from ..types.shared.message import Message from ..types.message_send_response import MessageSendResponse +from ..types.message_search_response import MessageSearchResponse __all__ = ["MessagesResource", "AsyncMessagesResource"] @@ -129,7 +130,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursor[Message]: + ) -> MessageSearchResponse: """ Search messages across chats using Beeper's message index @@ -179,9 +180,8 @@ def search( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get_api_list( + return self._get( "/v1/messages/search", - page=SyncCursor[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -206,7 +206,7 @@ def search( message_search_params.MessageSearchParams, ), ), - model=Message, + cast_to=MessageSearchResponse, ) def send( @@ -339,7 +339,7 @@ def list( model=Message, ) - def search( + async def search( self, *, account_ids: SequenceNotStr[str] | Omit = omit, @@ -361,7 +361,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Message, AsyncCursor[Message]]: + ) -> MessageSearchResponse: """ Search messages across chats using Beeper's message index @@ -411,15 +411,14 @@ def search( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get_api_list( + return await self._get( "/v1/messages/search", - page=AsyncCursor[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform( + query=await async_maybe_transform( { "account_ids": account_ids, "chat_ids": chat_ids, @@ -438,7 +437,7 @@ def search( message_search_params.MessageSearchParams, ), ), - model=Message, + cast_to=MessageSearchResponse, ) async def send( diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index e577cbf..83e9ef1 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -31,4 +31,5 @@ from .message_send_response import MessageSendResponse as MessageSendResponse from .contact_search_response import ContactSearchResponse as ContactSearchResponse from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse +from .message_search_response import MessageSearchResponse as MessageSearchResponse from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams diff --git a/src/beeper_desktop_api/types/message_search_response.py b/src/beeper_desktop_api/types/message_search_response.py new file mode 100644 index 0000000..51f3d6f --- /dev/null +++ b/src/beeper_desktop_api/types/message_search_response.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional + +from pydantic import Field as FieldInfo + +from .chat import Chat +from .._models import BaseModel +from .shared.message import Message + +__all__ = ["MessageSearchResponse"] + + +class MessageSearchResponse(BaseModel): + chats: Dict[str, Chat] + """Map of chatID -> chat details for chats referenced in items.""" + + has_more: bool = FieldInfo(alias="hasMore") + """True if additional results can be fetched using the provided cursors.""" + + items: List[Message] + """Messages matching the query and filters.""" + + newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) + """Cursor for fetching newer results (use with direction='after'). + + Opaque string; do not inspect. + """ + + oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) + """Cursor for fetching older results (use with direction='before'). + + Opaque string; do not inspect. + """ diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 85ebebb..7853ea0 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -11,6 +11,7 @@ from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop from beeper_desktop_api.types import ( MessageSendResponse, + MessageSearchResponse, ) from beeper_desktop_api._utils import parse_datetime from beeper_desktop_api.pagination import SyncCursor, AsyncCursor @@ -66,7 +67,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None: @parametrize def test_method_search(self, client: BeeperDesktop) -> None: message = client.messages.search() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) @parametrize def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: @@ -89,7 +90,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: query="dinner", sender="me", ) - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) @parametrize def test_raw_response_search(self, client: BeeperDesktop) -> None: @@ -98,7 +99,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) @parametrize def test_streaming_response_search(self, client: BeeperDesktop) -> None: @@ -107,7 +108,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) assert cast(Any, response.is_closed) is True @@ -201,7 +202,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) - @parametrize async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.search() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) @parametrize async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -224,7 +225,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk query="dinner", sender="me", ) - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) @parametrize async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -233,7 +234,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) @parametrize async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -242,7 +243,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(MessageSearchResponse, message, path=["response"]) assert cast(Any, response.is_closed) is True From 48b4b7f01064d016b84e954f9aa9f327863cc1d3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:35:45 +0000 Subject: [PATCH 12/20] feat(api): manual updates --- .stats.yml | 6 +- README.md | 79 +++++++++++++++++++ api.md | 4 +- src/beeper_desktop_api/pagination.py | 67 +--------------- src/beeper_desktop_api/resources/messages.py | 19 ++--- src/beeper_desktop_api/types/__init__.py | 1 - .../types/message_search_response.py | 34 -------- tests/api_resources/test_messages.py | 17 ++-- 8 files changed, 104 insertions(+), 123 deletions(-) delete mode 100644 src/beeper_desktop_api/types/message_search_response.py diff --git a/.stats.yml b/.stats.yml index 134452d..8831e71 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml -openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef -config_hash: 4fb2010b528ce4358300ddd10e750265 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml +openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765 +config_hash: 738402ade5ac9528c8ef1677aa1d70f7 diff --git a/README.md b/README.md index 0f8fafb..9f0e7ba 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,85 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Pagination + +List methods in the Beeper Desktop API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from beeper_desktop_api import BeeperDesktop + +client = BeeperDesktop() + +all_messages = [] +# Automatically fetches more pages as needed. +for message in client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", +): + # Do something with message here + all_messages.append(message) +print(all_messages) +``` + +Or, asynchronously: + +```python +import asyncio +from beeper_desktop_api import AsyncBeeperDesktop + +client = AsyncBeeperDesktop() + + +async def main() -> None: + all_messages = [] + # Iterate through items across all pages, issuing requests as needed. + async for message in client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", + ): + all_messages.append(message) + print(all_messages) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.items)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.messages.search( + account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], + limit=10, + query="deployment", +) + +print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..." +for message in first_page.items: + print(message.id) + +# Remove `await` for non-async usage. +``` + ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: diff --git a/api.md b/api.md index e862d8e..529a147 100644 --- a/api.md +++ b/api.md @@ -70,11 +70,11 @@ Methods: Types: ```python -from beeper_desktop_api.types import MessageSearchResponse, MessageSendResponse +from beeper_desktop_api.types import MessageSendResponse ``` Methods: - client.messages.list(\*\*params) -> SyncCursor[Message] -- client.messages.search(\*\*params) -> MessageSearchResponse +- client.messages.search(\*\*params) -> SyncCursor[Message] - client.messages.send(\*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py index 806a7a0..4606312 100644 --- a/src/beeper_desktop_api/pagination.py +++ b/src/beeper_desktop_api/pagination.py @@ -1,14 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Generic, TypeVar, Optional +from typing import List, Generic, TypeVar, Optional from typing_extensions import override from pydantic import Field as FieldInfo -from .types.chat import Chat from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage -__all__ = ["SyncCursor", "AsyncCursor", "SyncCursorWithChats", "AsyncCursorWithChats"] +__all__ = ["SyncCursor", "AsyncCursor"] _T = TypeVar("_T") @@ -71,65 +70,3 @@ def next_page_info(self) -> Optional[PageInfo]: return None return PageInfo(params={"cursor": oldest_cursor}) - - -class SyncCursorWithChats(BaseSyncPage[_T], BasePage[_T], Generic[_T]): - items: List[_T] - chats: Optional[Dict[str, Chat]] = None - has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) - oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) - newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) - - @override - def _get_page_items(self) -> List[_T]: - items = self.items - if not items: - return [] - return items - - @override - def has_next_page(self) -> bool: - has_more = self.has_more - if has_more is not None and has_more is False: - return False - - return super().has_next_page() - - @override - def next_page_info(self) -> Optional[PageInfo]: - oldest_cursor = self.oldest_cursor - if not oldest_cursor: - return None - - return PageInfo(params={"cursor": oldest_cursor}) - - -class AsyncCursorWithChats(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): - items: List[_T] - chats: Optional[Dict[str, Chat]] = None - has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) - oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) - newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) - - @override - def _get_page_items(self) -> List[_T]: - items = self.items - if not items: - return [] - return items - - @override - def has_next_page(self) -> bool: - has_more = self.has_more - if has_more is not None and has_more is False: - return False - - return super().has_next_page() - - @override - def next_page_info(self) -> Optional[PageInfo]: - oldest_cursor = self.oldest_cursor - if not oldest_cursor: - return None - - return PageInfo(params={"cursor": oldest_cursor}) diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index d7d40ef..485a86b 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -23,7 +23,6 @@ from .._base_client import AsyncPaginator, make_request_options from ..types.shared.message import Message from ..types.message_send_response import MessageSendResponse -from ..types.message_search_response import MessageSearchResponse __all__ = ["MessagesResource", "AsyncMessagesResource"] @@ -130,7 +129,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageSearchResponse: + ) -> SyncCursor[Message]: """ Search messages across chats using Beeper's message index @@ -180,8 +179,9 @@ def search( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/v1/messages/search", + page=SyncCursor[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -206,7 +206,7 @@ def search( message_search_params.MessageSearchParams, ), ), - cast_to=MessageSearchResponse, + model=Message, ) def send( @@ -339,7 +339,7 @@ def list( model=Message, ) - async def search( + def search( self, *, account_ids: SequenceNotStr[str] | Omit = omit, @@ -361,7 +361,7 @@ async def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageSearchResponse: + ) -> AsyncPaginator[Message, AsyncCursor[Message]]: """ Search messages across chats using Beeper's message index @@ -411,14 +411,15 @@ async def search( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/v1/messages/search", + page=AsyncCursor[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "account_ids": account_ids, "chat_ids": chat_ids, @@ -437,7 +438,7 @@ async def search( message_search_params.MessageSearchParams, ), ), - cast_to=MessageSearchResponse, + model=Message, ) async def send( diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index 83e9ef1..e577cbf 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -31,5 +31,4 @@ from .message_send_response import MessageSendResponse as MessageSendResponse from .contact_search_response import ContactSearchResponse as ContactSearchResponse from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse -from .message_search_response import MessageSearchResponse as MessageSearchResponse from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams diff --git a/src/beeper_desktop_api/types/message_search_response.py b/src/beeper_desktop_api/types/message_search_response.py deleted file mode 100644 index 51f3d6f..0000000 --- a/src/beeper_desktop_api/types/message_search_response.py +++ /dev/null @@ -1,34 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional - -from pydantic import Field as FieldInfo - -from .chat import Chat -from .._models import BaseModel -from .shared.message import Message - -__all__ = ["MessageSearchResponse"] - - -class MessageSearchResponse(BaseModel): - chats: Dict[str, Chat] - """Map of chatID -> chat details for chats referenced in items.""" - - has_more: bool = FieldInfo(alias="hasMore") - """True if additional results can be fetched using the provided cursors.""" - - items: List[Message] - """Messages matching the query and filters.""" - - newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) - """Cursor for fetching newer results (use with direction='after'). - - Opaque string; do not inspect. - """ - - oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) - """Cursor for fetching older results (use with direction='before'). - - Opaque string; do not inspect. - """ diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 7853ea0..85ebebb 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -11,7 +11,6 @@ from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop from beeper_desktop_api.types import ( MessageSendResponse, - MessageSearchResponse, ) from beeper_desktop_api._utils import parse_datetime from beeper_desktop_api.pagination import SyncCursor, AsyncCursor @@ -67,7 +66,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None: @parametrize def test_method_search(self, client: BeeperDesktop) -> None: message = client.messages.search() - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(SyncCursor[Message], message, path=["response"]) @parametrize def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: @@ -90,7 +89,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: query="dinner", sender="me", ) - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(SyncCursor[Message], message, path=["response"]) @parametrize def test_raw_response_search(self, client: BeeperDesktop) -> None: @@ -99,7 +98,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(SyncCursor[Message], message, path=["response"]) @parametrize def test_streaming_response_search(self, client: BeeperDesktop) -> None: @@ -108,7 +107,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(SyncCursor[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True @@ -202,7 +201,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) - @parametrize async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.search() - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) @parametrize async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -225,7 +224,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk query="dinner", sender="me", ) - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) @parametrize async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -234,7 +233,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) @parametrize async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -243,7 +242,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(MessageSearchResponse, message, path=["response"]) + assert_matches_type(AsyncCursor[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True From 2443524a37ad578bf8cb479e25c0b06b505547d9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:19:53 +0000 Subject: [PATCH 13/20] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8831e71..baf1a99 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765 -config_hash: 738402ade5ac9528c8ef1677aa1d70f7 +config_hash: fc42f6a9efd6f34ca68f1c4328272acf From d5cb6c2ee132bc3d558552df145082396c80521c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:17:25 +0000 Subject: [PATCH 14/20] feat(api): remove limit from list routes --- .stats.yml | 6 +- api.md | 8 +-- src/beeper_desktop_api/pagination.py | 66 ++++++++++++++++++- .../resources/chats/chats.py | 26 +++----- src/beeper_desktop_api/resources/messages.py | 30 ++++----- .../types/chat_list_params.py | 3 - .../types/message_list_params.py | 5 +- tests/api_resources/test_chats.py | 36 +++++----- tests/api_resources/test_messages.py | 36 +++++----- 9 files changed, 125 insertions(+), 91 deletions(-) diff --git a/.stats.yml b/.stats.yml index baf1a99..c2693cc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml -openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765 -config_hash: fc42f6a9efd6f34ca68f1c4328272acf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-a3fb0de6dd98f8a51d73e3fdf51de6143f2e8e764048246392624a56b4a3a481.yml +openapi_spec_hash: 50e1001c340cb0bd3436b6329240769b +config_hash: 2e31d02f28a11ef29eb747bcf559786a diff --git a/api.md b/api.md index 529a147..dd074eb 100644 --- a/api.md +++ b/api.md @@ -54,9 +54,9 @@ Methods: - client.chats.create(\*\*params) -> ChatCreateResponse - client.chats.retrieve(chat_id, \*\*params) -> Chat -- client.chats.list(\*\*params) -> SyncCursor[ChatListResponse] +- client.chats.list(\*\*params) -> SyncCursorList[ChatListResponse] - client.chats.archive(chat_id, \*\*params) -> BaseResponse -- client.chats.search(\*\*params) -> SyncCursor[Chat] +- client.chats.search(\*\*params) -> SyncCursorSearch[Chat] ## Reminders @@ -75,6 +75,6 @@ from beeper_desktop_api.types import MessageSendResponse Methods: -- client.messages.list(\*\*params) -> SyncCursor[Message] -- client.messages.search(\*\*params) -> SyncCursor[Message] +- client.messages.list(\*\*params) -> SyncCursorList[Message] +- client.messages.search(\*\*params) -> SyncCursorSearch[Message] - client.messages.send(\*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py index 4606312..ee568dc 100644 --- a/src/beeper_desktop_api/pagination.py +++ b/src/beeper_desktop_api/pagination.py @@ -7,12 +7,12 @@ from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage -__all__ = ["SyncCursor", "AsyncCursor"] +__all__ = ["SyncCursorSearch", "AsyncCursorSearch", "SyncCursorList", "AsyncCursorList"] _T = TypeVar("_T") -class SyncCursor(BaseSyncPage[_T], BasePage[_T], Generic[_T]): +class SyncCursorSearch(BaseSyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) @@ -42,7 +42,67 @@ def next_page_info(self) -> Optional[PageInfo]: return PageInfo(params={"cursor": oldest_cursor}) -class AsyncCursor(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): +class AsyncCursorSearch(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) + oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) + newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + oldest_cursor = self.oldest_cursor + if not oldest_cursor: + return None + + return PageInfo(params={"cursor": oldest_cursor}) + + +class SyncCursorList(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) + oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) + newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + oldest_cursor = self.oldest_cursor + if not oldest_cursor: + return None + + return PageInfo(params={"cursor": oldest_cursor}) + + +class AsyncCursorList(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py index 93587e7..7b8d06c 100644 --- a/src/beeper_desktop_api/resources/chats/chats.py +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -27,7 +27,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...pagination import SyncCursor, AsyncCursor +from ...pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch from ...types.chat import Chat from ..._base_client import AsyncPaginator, make_request_options from ...types.chat_list_response import ChatListResponse @@ -173,14 +173,13 @@ def list( account_ids: SequenceNotStr[str] | Omit = omit, cursor: str | Omit = omit, direction: Literal["after", "before"] | Omit = omit, - limit: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursor[ChatListResponse]: + ) -> SyncCursorList[ChatListResponse]: """List all chats sorted by last activity (most recent first). Combines all @@ -195,8 +194,6 @@ def list( direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. - limit: Maximum number of chats to return (1–200). Defaults to 50. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -207,7 +204,7 @@ def list( """ return self._get_api_list( "/v1/chats", - page=SyncCursor[ChatListResponse], + page=SyncCursorList[ChatListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -218,7 +215,6 @@ def list( "account_ids": account_ids, "cursor": cursor, "direction": direction, - "limit": limit, }, chat_list_params.ChatListParams, ), @@ -289,7 +285,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursor[Chat]: + ) -> SyncCursorSearch[Chat]: """ Search chats by title/network or participants using Beeper Desktop's renderer algorithm. @@ -338,7 +334,7 @@ def search( """ return self._get_api_list( "/v1/chats/search", - page=SyncCursor[Chat], + page=SyncCursorSearch[Chat], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -502,14 +498,13 @@ def list( account_ids: SequenceNotStr[str] | Omit = omit, cursor: str | Omit = omit, direction: Literal["after", "before"] | Omit = omit, - limit: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[ChatListResponse, AsyncCursor[ChatListResponse]]: + ) -> AsyncPaginator[ChatListResponse, AsyncCursorList[ChatListResponse]]: """List all chats sorted by last activity (most recent first). Combines all @@ -524,8 +519,6 @@ def list( direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. - limit: Maximum number of chats to return (1–200). Defaults to 50. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -536,7 +529,7 @@ def list( """ return self._get_api_list( "/v1/chats", - page=AsyncCursor[ChatListResponse], + page=AsyncCursorList[ChatListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -547,7 +540,6 @@ def list( "account_ids": account_ids, "cursor": cursor, "direction": direction, - "limit": limit, }, chat_list_params.ChatListParams, ), @@ -618,7 +610,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Chat, AsyncCursor[Chat]]: + ) -> AsyncPaginator[Chat, AsyncCursorSearch[Chat]]: """ Search chats by title/network or participants using Beeper Desktop's renderer algorithm. @@ -667,7 +659,7 @@ def search( """ return self._get_api_list( "/v1/chats/search", - page=AsyncCursor[Chat], + page=AsyncCursorSearch[Chat], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index 485a86b..3732473 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -19,7 +19,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..pagination import SyncCursor, AsyncCursor +from ..pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch from .._base_client import AsyncPaginator, make_request_options from ..types.shared.message import Message from ..types.message_send_response import MessageSendResponse @@ -55,20 +55,19 @@ def list( chat_id: str, cursor: str | Omit = omit, direction: Literal["after", "before"] | Omit = omit, - limit: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursor[Message]: + ) -> SyncCursorList[Message]: """List all messages in a chat with cursor-based pagination. Sorted by timestamp. Args: - chat_id: The chat ID to list messages from + chat_id: Chat ID to list messages from cursor: Message cursor for pagination. Use with direction to navigate results. @@ -76,8 +75,6 @@ def list( 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is provided. - limit: Maximum number of messages to return (1–500). Defaults to 50. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -88,7 +85,7 @@ def list( """ return self._get_api_list( "/v1/messages", - page=SyncCursor[Message], + page=SyncCursorList[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -99,7 +96,6 @@ def list( "chat_id": chat_id, "cursor": cursor, "direction": direction, - "limit": limit, }, message_list_params.MessageListParams, ), @@ -129,7 +125,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursor[Message]: + ) -> SyncCursorSearch[Message]: """ Search messages across chats using Beeper's message index @@ -181,7 +177,7 @@ def search( """ return self._get_api_list( "/v1/messages/search", - page=SyncCursor[Message], + page=SyncCursorSearch[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -287,20 +283,19 @@ def list( chat_id: str, cursor: str | Omit = omit, direction: Literal["after", "before"] | Omit = omit, - limit: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Message, AsyncCursor[Message]]: + ) -> AsyncPaginator[Message, AsyncCursorList[Message]]: """List all messages in a chat with cursor-based pagination. Sorted by timestamp. Args: - chat_id: The chat ID to list messages from + chat_id: Chat ID to list messages from cursor: Message cursor for pagination. Use with direction to navigate results. @@ -308,8 +303,6 @@ def list( 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is provided. - limit: Maximum number of messages to return (1–500). Defaults to 50. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -320,7 +313,7 @@ def list( """ return self._get_api_list( "/v1/messages", - page=AsyncCursor[Message], + page=AsyncCursorList[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -331,7 +324,6 @@ def list( "chat_id": chat_id, "cursor": cursor, "direction": direction, - "limit": limit, }, message_list_params.MessageListParams, ), @@ -361,7 +353,7 @@ def search( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Message, AsyncCursor[Message]]: + ) -> AsyncPaginator[Message, AsyncCursorSearch[Message]]: """ Search messages across chats using Beeper's message index @@ -413,7 +405,7 @@ def search( """ return self._get_api_list( "/v1/messages/search", - page=AsyncCursor[Message], + page=AsyncCursorSearch[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/beeper_desktop_api/types/chat_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py index d8e1784..e1d10b2 100644 --- a/src/beeper_desktop_api/types/chat_list_params.py +++ b/src/beeper_desktop_api/types/chat_list_params.py @@ -25,6 +25,3 @@ class ChatListParams(TypedDict, total=False): Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. """ - - limit: int - """Maximum number of chats to return (1–200). Defaults to 50.""" diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py index ca56fab..2dd8438 100644 --- a/src/beeper_desktop_api/types/message_list_params.py +++ b/src/beeper_desktop_api/types/message_list_params.py @@ -11,7 +11,7 @@ class MessageListParams(TypedDict, total=False): chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]] - """The chat ID to list messages from""" + """Chat ID to list messages from""" cursor: str """Message cursor for pagination. Use with direction to navigate results.""" @@ -22,6 +22,3 @@ class MessageListParams(TypedDict, total=False): 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is provided. """ - - limit: int - """Maximum number of messages to return (1–500). Defaults to 50.""" diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py index 3009cc9..352bc2f 100644 --- a/tests/api_resources/test_chats.py +++ b/tests/api_resources/test_chats.py @@ -15,7 +15,7 @@ ChatCreateResponse, ) from beeper_desktop_api._utils import parse_datetime -from beeper_desktop_api.pagination import SyncCursor, AsyncCursor +from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch from beeper_desktop_api.types.shared import BaseResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -121,7 +121,7 @@ def test_path_params_retrieve(self, client: BeeperDesktop) -> None: @parametrize def test_method_list(self, client: BeeperDesktop) -> None: chat = client.chats.list() - assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: @@ -133,9 +133,8 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: ], cursor="1725489123456", direction="before", - limit=1, ) - assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"]) @parametrize def test_raw_response_list(self, client: BeeperDesktop) -> None: @@ -144,7 +143,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"]) @parametrize def test_streaming_response_list(self, client: BeeperDesktop) -> None: @@ -153,7 +152,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(SyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"]) assert cast(Any, response.is_closed) is True @@ -206,7 +205,7 @@ def test_path_params_archive(self, client: BeeperDesktop) -> None: @parametrize def test_method_search(self, client: BeeperDesktop) -> None: chat = client.chats.search() - assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) @parametrize def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: @@ -227,7 +226,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: type="single", unread_only=True, ) - assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) @parametrize def test_raw_response_search(self, client: BeeperDesktop) -> None: @@ -236,7 +235,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) @parametrize def test_streaming_response_search(self, client: BeeperDesktop) -> None: @@ -245,7 +244,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(SyncCursor[Chat], chat, path=["response"]) + assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) assert cast(Any, response.is_closed) is True @@ -352,7 +351,7 @@ async def test_path_params_retrieve(self, async_client: AsyncBeeperDesktop) -> N @parametrize async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: chat = await async_client.chats.list() - assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -364,9 +363,8 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto ], cursor="1725489123456", direction="before", - limit=1, ) - assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -375,7 +373,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -384,7 +382,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(AsyncCursor[ChatListResponse], chat, path=["response"]) + assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"]) assert cast(Any, response.is_closed) is True @@ -437,7 +435,7 @@ async def test_path_params_archive(self, async_client: AsyncBeeperDesktop) -> No @parametrize async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: chat = await async_client.chats.search() - assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) @parametrize async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -458,7 +456,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk type="single", unread_only=True, ) - assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) @parametrize async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -467,7 +465,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) @parametrize async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -476,6 +474,6 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(AsyncCursor[Chat], chat, path=["response"]) + assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 85ebebb..3f36468 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -13,7 +13,7 @@ MessageSendResponse, ) from beeper_desktop_api._utils import parse_datetime -from beeper_desktop_api.pagination import SyncCursor, AsyncCursor +from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch from beeper_desktop_api.types.shared import Message base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -27,7 +27,7 @@ def test_method_list(self, client: BeeperDesktop) -> None: message = client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", ) - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: @@ -35,9 +35,8 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", cursor="821744079", direction="before", - limit=1, ) - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) @parametrize def test_raw_response_list(self, client: BeeperDesktop) -> None: @@ -48,7 +47,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) @parametrize def test_streaming_response_list(self, client: BeeperDesktop) -> None: @@ -59,14 +58,14 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_method_search(self, client: BeeperDesktop) -> None: message = client.messages.search() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorSearch[Message], message, path=["response"]) @parametrize def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: @@ -89,7 +88,7 @@ def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: query="dinner", sender="me", ) - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorSearch[Message], message, path=["response"]) @parametrize def test_raw_response_search(self, client: BeeperDesktop) -> None: @@ -98,7 +97,7 @@ def test_raw_response_search(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorSearch[Message], message, path=["response"]) @parametrize def test_streaming_response_search(self, client: BeeperDesktop) -> None: @@ -107,7 +106,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursor[Message], message, path=["response"]) + assert_matches_type(SyncCursorSearch[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True @@ -162,7 +161,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", ) - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -170,9 +169,8 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", cursor="821744079", direction="before", - limit=1, ) - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -183,7 +181,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -194,14 +192,14 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.search() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorSearch[Message], message, path=["response"]) @parametrize async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -224,7 +222,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesk query="dinner", sender="me", ) - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorSearch[Message], message, path=["response"]) @parametrize async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -233,7 +231,7 @@ async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorSearch[Message], message, path=["response"]) @parametrize async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: @@ -242,7 +240,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursor[Message], message, path=["response"]) + assert_matches_type(AsyncCursorSearch[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True From dce712498ff2678222fd203118e7bb91f13ccfc5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:00:13 +0000 Subject: [PATCH 15/20] feat(api): manual updates --- .stats.yml | 4 +- api.md | 4 +- src/beeper_desktop_api/resources/messages.py | 31 ++++++------- src/beeper_desktop_api/types/__init__.py | 1 + .../types/message_list_response.py | 21 +++++++++ .../types/message_search_params.py | 6 +-- .../types/message_send_params.py | 4 +- tests/api_resources/test_messages.py | 43 +++++++------------ 8 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 src/beeper_desktop_api/types/message_list_response.py diff --git a/.stats.yml b/.stats.yml index c2693cc..7d02041 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-a3fb0de6dd98f8a51d73e3fdf51de6143f2e8e764048246392624a56b4a3a481.yml -openapi_spec_hash: 50e1001c340cb0bd3436b6329240769b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-100e7052e74644026f594642a424e04ab306d44e6c73a1f4761cf8a7d7ee0d8f.yml +openapi_spec_hash: 3437145a74c032f2319a235bf40baa88 config_hash: 2e31d02f28a11ef29eb747bcf559786a diff --git a/api.md b/api.md index dd074eb..d2a744f 100644 --- a/api.md +++ b/api.md @@ -70,11 +70,11 @@ Methods: Types: ```python -from beeper_desktop_api.types import MessageSendResponse +from beeper_desktop_api.types import MessageListResponse, MessageSendResponse ``` Methods: -- client.messages.list(\*\*params) -> SyncCursorList[Message] +- client.messages.list(\*\*params) -> MessageListResponse - client.messages.search(\*\*params) -> SyncCursorSearch[Message] - client.messages.send(\*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index 3732473..1a3bc64 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -19,9 +19,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch +from ..pagination import SyncCursorSearch, AsyncCursorSearch from .._base_client import AsyncPaginator, make_request_options from ..types.shared.message import Message +from ..types.message_list_response import MessageListResponse from ..types.message_send_response import MessageSendResponse __all__ = ["MessagesResource", "AsyncMessagesResource"] @@ -61,7 +62,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursorList[Message]: + ) -> MessageListResponse: """List all messages in a chat with cursor-based pagination. Sorted by timestamp. @@ -83,9 +84,8 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get_api_list( + return self._get( "/v1/messages", - page=SyncCursorList[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -100,7 +100,7 @@ def list( message_list_params.MessageListParams, ), ), - model=Message, + cast_to=MessageListResponse, ) def search( @@ -153,8 +153,7 @@ def search( include_muted: Include messages in chats marked as Muted by the user, which are usually less important. Default: true. Set to false if the user wants a more refined search. - limit: Maximum number of messages to return (1–500). Defaults to 20. The current - implementation caps each page at 20 items even if a higher limit is requested. + limit: Maximum number of messages to return. media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact types like ['video', 'image']. Omit for no media filtering. @@ -208,7 +207,7 @@ def search( def send( self, *, - chat_id: str, + chat_id: str | Omit = omit, reply_to_message_id: str | Omit = omit, text: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -277,7 +276,7 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: """ return AsyncMessagesResourceWithStreamingResponse(self) - def list( + async def list( self, *, chat_id: str, @@ -289,7 +288,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Message, AsyncCursorList[Message]]: + ) -> MessageListResponse: """List all messages in a chat with cursor-based pagination. Sorted by timestamp. @@ -311,15 +310,14 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get_api_list( + return await self._get( "/v1/messages", - page=AsyncCursorList[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform( + query=await async_maybe_transform( { "chat_id": chat_id, "cursor": cursor, @@ -328,7 +326,7 @@ def list( message_list_params.MessageListParams, ), ), - model=Message, + cast_to=MessageListResponse, ) def search( @@ -381,8 +379,7 @@ def search( include_muted: Include messages in chats marked as Muted by the user, which are usually less important. Default: true. Set to false if the user wants a more refined search. - limit: Maximum number of messages to return (1–500). Defaults to 20. The current - implementation caps each page at 20 items even if a higher limit is requested. + limit: Maximum number of messages to return. media_types: Filter messages by media types. Use ['any'] for any media type, or specify exact types like ['video', 'image']. Omit for no media filtering. @@ -436,7 +433,7 @@ def search( async def send( self, *, - chat_id: str, + chat_id: str | Omit = omit, reply_to_message_id: str | Omit = omit, text: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index e577cbf..28eab5d 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -27,6 +27,7 @@ from .client_search_params import ClientSearchParams as ClientSearchParams from .account_list_response import AccountListResponse as AccountListResponse from .contact_search_params import ContactSearchParams as ContactSearchParams +from .message_list_response import MessageListResponse as MessageListResponse from .message_search_params import MessageSearchParams as MessageSearchParams from .message_send_response import MessageSendResponse as MessageSendResponse from .contact_search_response import ContactSearchResponse as ContactSearchResponse diff --git a/src/beeper_desktop_api/types/message_list_response.py b/src/beeper_desktop_api/types/message_list_response.py new file mode 100644 index 0000000..a66746f --- /dev/null +++ b/src/beeper_desktop_api/types/message_list_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from pydantic import Field as FieldInfo + +from .._models import BaseModel +from .shared.message import Message + +__all__ = ["MessageListResponse"] + + +class MessageListResponse(BaseModel): + has_more: bool = FieldInfo(alias="hasMore") + """True if additional results can be fetched.""" + + items: List[Message] + """Messages from the chat, sorted by timestamp. + + Use message.sortKey as cursor for pagination. + """ diff --git a/src/beeper_desktop_api/types/message_search_params.py b/src/beeper_desktop_api/types/message_search_params.py index 650775f..93fbd63 100644 --- a/src/beeper_desktop_api/types/message_search_params.py +++ b/src/beeper_desktop_api/types/message_search_params.py @@ -56,11 +56,7 @@ class MessageSearchParams(TypedDict, total=False): """ limit: int - """Maximum number of messages to return (1–500). - - Defaults to 20. The current implementation caps each page at 20 items even if a - higher limit is requested. - """ + """Maximum number of messages to return.""" media_types: Annotated[List[Literal["any", "video", "image", "link", "file"]], PropertyInfo(alias="mediaTypes")] """Filter messages by media types. diff --git a/src/beeper_desktop_api/types/message_send_params.py b/src/beeper_desktop_api/types/message_send_params.py index 8b05d6a..b165b27 100644 --- a/src/beeper_desktop_api/types/message_send_params.py +++ b/src/beeper_desktop_api/types/message_send_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Annotated, TypedDict from .._utils import PropertyInfo @@ -10,7 +10,7 @@ class MessageSendParams(TypedDict, total=False): - chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]] + chat_id: Annotated[str, PropertyInfo(alias="chatID")] """Unique identifier of the chat.""" reply_to_message_id: Annotated[str, PropertyInfo(alias="replyToMessageID")] diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 3f36468..302e146 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -10,10 +10,11 @@ from tests.utils import assert_matches_type from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop from beeper_desktop_api.types import ( + MessageListResponse, MessageSendResponse, ) from beeper_desktop_api._utils import parse_datetime -from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch +from beeper_desktop_api.pagination import SyncCursorSearch, AsyncCursorSearch from beeper_desktop_api.types.shared import Message base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -27,7 +28,7 @@ def test_method_list(self, client: BeeperDesktop) -> None: message = client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", ) - assert_matches_type(SyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: @@ -36,7 +37,7 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: cursor="821744079", direction="before", ) - assert_matches_type(SyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) @parametrize def test_raw_response_list(self, client: BeeperDesktop) -> None: @@ -47,7 +48,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) @parametrize def test_streaming_response_list(self, client: BeeperDesktop) -> None: @@ -58,7 +59,7 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(SyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) assert cast(Any, response.is_closed) is True @@ -112,9 +113,7 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: @parametrize def test_method_send(self, client: BeeperDesktop) -> None: - message = client.messages.send( - chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - ) + message = client.messages.send() assert_matches_type(MessageSendResponse, message, path=["response"]) @parametrize @@ -128,9 +127,7 @@ def test_method_send_with_all_params(self, client: BeeperDesktop) -> None: @parametrize def test_raw_response_send(self, client: BeeperDesktop) -> None: - response = client.messages.with_raw_response.send( - chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - ) + response = client.messages.with_raw_response.send() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -139,9 +136,7 @@ def test_raw_response_send(self, client: BeeperDesktop) -> None: @parametrize def test_streaming_response_send(self, client: BeeperDesktop) -> None: - with client.messages.with_streaming_response.send( - chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - ) as response: + with client.messages.with_streaming_response.send() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -161,7 +156,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", ) - assert_matches_type(AsyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -170,7 +165,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto cursor="821744079", direction="before", ) - assert_matches_type(AsyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -181,7 +176,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -192,7 +187,7 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(AsyncCursorList[Message], message, path=["response"]) + assert_matches_type(MessageListResponse, message, path=["response"]) assert cast(Any, response.is_closed) is True @@ -246,9 +241,7 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) @parametrize async def test_method_send(self, async_client: AsyncBeeperDesktop) -> None: - message = await async_client.messages.send( - chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - ) + message = await async_client.messages.send() assert_matches_type(MessageSendResponse, message, path=["response"]) @parametrize @@ -262,9 +255,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncBeeperDeskto @parametrize async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.messages.with_raw_response.send( - chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - ) + response = await async_client.messages.with_raw_response.send() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -273,9 +264,7 @@ async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None @parametrize async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.messages.with_streaming_response.send( - chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - ) as response: + async with async_client.messages.with_streaming_response.send() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 0d38dfa50d797ff879df6d5c633bbcb43c3a98fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:05:05 +0000 Subject: [PATCH 16/20] chore: update SDK settings --- .stats.yml | 2 +- README.md | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7d02041..96af950 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-100e7052e74644026f594642a424e04ab306d44e6c73a1f4761cf8a7d7ee0d8f.yml openapi_spec_hash: 3437145a74c032f2319a235bf40baa88 -config_hash: 2e31d02f28a11ef29eb747bcf559786a +config_hash: 36b26d0d29548d4aa575fc337915ad42 diff --git a/README.md b/README.md index 9f0e7ba..e20984e 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,10 @@ The REST API documentation can be found on [developers.beeper.com](https://devel ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/beeper/desktop-api-python.git +# install from PyPI +pip install beeper_desktop_api ``` -> [!NOTE] -> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install beeper_desktop_api` - ## Usage The full API of this library can be found in [api.md](api.md). @@ -81,8 +78,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from the production repo -pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/desktop-api-python.git' +# install from PyPI +pip install beeper_desktop_api[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From 0fcd71f9951498d349fb816b42dc21347f3ab5dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:09:25 +0000 Subject: [PATCH 17/20] feat(api): manual updates --- .stats.yml | 6 +- README.md | 11 +-- api.md | 8 +-- src/beeper_desktop_api/pagination.py | 35 +++++---- src/beeper_desktop_api/resources/contacts.py | 28 +++----- src/beeper_desktop_api/resources/messages.py | 49 +++++++------ src/beeper_desktop_api/types/__init__.py | 1 - .../types/contact_search_params.py | 7 +- .../types/message_list_params.py | 7 +- .../types/message_list_response.py | 21 ------ .../types/message_send_params.py | 3 - tests/api_resources/test_contacts.py | 16 +++++ tests/api_resources/test_messages.py | 71 ++++++++++++++----- 13 files changed, 147 insertions(+), 116 deletions(-) delete mode 100644 src/beeper_desktop_api/types/message_list_response.py diff --git a/.stats.yml b/.stats.yml index 96af950..d065147 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-100e7052e74644026f594642a424e04ab306d44e6c73a1f4761cf8a7d7ee0d8f.yml -openapi_spec_hash: 3437145a74c032f2319a235bf40baa88 -config_hash: 36b26d0d29548d4aa575fc337915ad42 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-f48e33c7d90ed6418a852f8d4d951d07b09f4f3f939feb395dc2aa03f522d81e.yml +openapi_spec_hash: c516120ecf51bb8425b3b9ed76c6423a +config_hash: c5ac9bd5889d27aa168f06d6d0fef0b3 diff --git a/README.md b/README.md index e20984e..9f0e7ba 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,13 @@ The REST API documentation can be found on [developers.beeper.com](https://devel ## Installation ```sh -# install from PyPI -pip install beeper_desktop_api +# install from the production repo +pip install git+ssh://git@github.com/beeper/desktop-api-python.git ``` +> [!NOTE] +> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install beeper_desktop_api` + ## Usage The full API of this library can be found in [api.md](api.md). @@ -78,8 +81,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from PyPI -pip install beeper_desktop_api[aiohttp] +# install from the production repo +pip install 'beeper_desktop_api[aiohttp] @ git+ssh://git@github.com/beeper/desktop-api-python.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/api.md b/api.md index d2a744f..4fbc194 100644 --- a/api.md +++ b/api.md @@ -40,7 +40,7 @@ from beeper_desktop_api.types import ContactSearchResponse Methods: -- client.contacts.search(\*\*params) -> ContactSearchResponse +- client.contacts.search(account_id, \*\*params) -> ContactSearchResponse # Chats @@ -70,11 +70,11 @@ Methods: Types: ```python -from beeper_desktop_api.types import MessageListResponse, MessageSendResponse +from beeper_desktop_api.types import MessageSendResponse ``` Methods: -- client.messages.list(\*\*params) -> MessageListResponse +- client.messages.list(chat_id, \*\*params) -> SyncCursorList[Message] - client.messages.search(\*\*params) -> SyncCursorSearch[Message] -- client.messages.send(\*\*params) -> MessageSendResponse +- client.messages.send(chat_id, \*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py index ee568dc..7fd745f 100644 --- a/src/beeper_desktop_api/pagination.py +++ b/src/beeper_desktop_api/pagination.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Generic, TypeVar, Optional -from typing_extensions import override +from typing import Any, List, Generic, TypeVar, Optional, cast +from typing_extensions import Protocol, override, runtime_checkable from pydantic import Field as FieldInfo @@ -12,6 +12,11 @@ _T = TypeVar("_T") +@runtime_checkable +class CursorListItem(Protocol): + sort_key: Optional[str] + + class SyncCursorSearch(BaseSyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) @@ -75,8 +80,6 @@ def next_page_info(self) -> Optional[PageInfo]: class SyncCursorList(BaseSyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) - oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) - newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) @override def _get_page_items(self) -> List[_T]: @@ -95,18 +98,21 @@ def has_next_page(self) -> bool: @override def next_page_info(self) -> Optional[PageInfo]: - oldest_cursor = self.oldest_cursor - if not oldest_cursor: + items = self.items + if not items: return None - return PageInfo(params={"cursor": oldest_cursor}) + item = cast(Any, items[-1]) + if not isinstance(item, CursorListItem) or item.sort_key is None: + # TODO emit warning log + return None + + return PageInfo(params={"cursor": item.sort_key}) class AsyncCursorList(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] has_more: Optional[bool] = FieldInfo(alias="hasMore", default=None) - oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) - newest_cursor: Optional[str] = FieldInfo(alias="newestCursor", default=None) @override def _get_page_items(self) -> List[_T]: @@ -125,8 +131,13 @@ def has_next_page(self) -> bool: @override def next_page_info(self) -> Optional[PageInfo]: - oldest_cursor = self.oldest_cursor - if not oldest_cursor: + items = self.items + if not items: return None - return PageInfo(params={"cursor": oldest_cursor}) + item = cast(Any, items[-1]) + if not isinstance(item, CursorListItem) or item.sort_key is None: + # TODO emit warning log + return None + + return PageInfo(params={"cursor": item.sort_key}) diff --git a/src/beeper_desktop_api/resources/contacts.py b/src/beeper_desktop_api/resources/contacts.py index db84950..50fc4f9 100644 --- a/src/beeper_desktop_api/resources/contacts.py +++ b/src/beeper_desktop_api/resources/contacts.py @@ -45,8 +45,8 @@ def with_streaming_response(self) -> ContactsResourceWithStreamingResponse: def search( self, - *, account_id: str, + *, query: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -72,20 +72,16 @@ def search( timeout: Override the client-level default timeout for this request, in seconds """ + if not account_id: + raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") return self._get( - "/v1/contacts/search", + f"/v1/accounts/{account_id}/contacts/search", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform( - { - "account_id": account_id, - "query": query, - }, - contact_search_params.ContactSearchParams, - ), + query=maybe_transform({"query": query}, contact_search_params.ContactSearchParams), ), cast_to=ContactSearchResponse, ) @@ -115,8 +111,8 @@ def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse: async def search( self, - *, account_id: str, + *, query: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -142,20 +138,16 @@ async def search( timeout: Override the client-level default timeout for this request, in seconds """ + if not account_id: + raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") return await self._get( - "/v1/contacts/search", + f"/v1/accounts/{account_id}/contacts/search", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( - { - "account_id": account_id, - "query": query, - }, - contact_search_params.ContactSearchParams, - ), + query=await async_maybe_transform({"query": query}, contact_search_params.ContactSearchParams), ), cast_to=ContactSearchResponse, ) diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index 1a3bc64..e77b8f1 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -19,10 +19,9 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..pagination import SyncCursorSearch, AsyncCursorSearch +from ..pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch from .._base_client import AsyncPaginator, make_request_options from ..types.shared.message import Message -from ..types.message_list_response import MessageListResponse from ..types.message_send_response import MessageSendResponse __all__ = ["MessagesResource", "AsyncMessagesResource"] @@ -52,8 +51,8 @@ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: def list( self, - *, chat_id: str, + *, cursor: str | Omit = omit, direction: Literal["after", "before"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -62,7 +61,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageListResponse: + ) -> SyncCursorList[Message]: """List all messages in a chat with cursor-based pagination. Sorted by timestamp. @@ -84,8 +83,11 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( - "/v1/messages", + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._get_api_list( + f"/v1/chats/{chat_id}/messages", + page=SyncCursorList[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -93,14 +95,13 @@ def list( timeout=timeout, query=maybe_transform( { - "chat_id": chat_id, "cursor": cursor, "direction": direction, }, message_list_params.MessageListParams, ), ), - cast_to=MessageListResponse, + model=Message, ) def search( @@ -206,8 +207,8 @@ def search( def send( self, + chat_id: str, *, - chat_id: str | Omit = omit, reply_to_message_id: str | Omit = omit, text: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -237,11 +238,12 @@ def send( timeout: Override the client-level default timeout for this request, in seconds """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") return self._post( - "/v1/messages", + f"/v1/chats/{chat_id}/messages", body=maybe_transform( { - "chat_id": chat_id, "reply_to_message_id": reply_to_message_id, "text": text, }, @@ -276,10 +278,10 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: """ return AsyncMessagesResourceWithStreamingResponse(self) - async def list( + def list( self, - *, chat_id: str, + *, cursor: str | Omit = omit, direction: Literal["after", "before"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -288,7 +290,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageListResponse: + ) -> AsyncPaginator[Message, AsyncCursorList[Message]]: """List all messages in a chat with cursor-based pagination. Sorted by timestamp. @@ -310,23 +312,25 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( - "/v1/messages", + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._get_api_list( + f"/v1/chats/{chat_id}/messages", + page=AsyncCursorList[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { - "chat_id": chat_id, "cursor": cursor, "direction": direction, }, message_list_params.MessageListParams, ), ), - cast_to=MessageListResponse, + model=Message, ) def search( @@ -432,8 +436,8 @@ def search( async def send( self, + chat_id: str, *, - chat_id: str | Omit = omit, reply_to_message_id: str | Omit = omit, text: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -463,11 +467,12 @@ async def send( timeout: Override the client-level default timeout for this request, in seconds """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") return await self._post( - "/v1/messages", + f"/v1/chats/{chat_id}/messages", body=await async_maybe_transform( { - "chat_id": chat_id, "reply_to_message_id": reply_to_message_id, "text": text, }, diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index 28eab5d..e577cbf 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -27,7 +27,6 @@ from .client_search_params import ClientSearchParams as ClientSearchParams from .account_list_response import AccountListResponse as AccountListResponse from .contact_search_params import ContactSearchParams as ContactSearchParams -from .message_list_response import MessageListResponse as MessageListResponse from .message_search_params import MessageSearchParams as MessageSearchParams from .message_send_response import MessageSendResponse as MessageSendResponse from .contact_search_response import ContactSearchResponse as ContactSearchResponse diff --git a/src/beeper_desktop_api/types/contact_search_params.py b/src/beeper_desktop_api/types/contact_search_params.py index 53d052f..f9063e0 100644 --- a/src/beeper_desktop_api/types/contact_search_params.py +++ b/src/beeper_desktop_api/types/contact_search_params.py @@ -2,16 +2,11 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo +from typing_extensions import Required, TypedDict __all__ = ["ContactSearchParams"] class ContactSearchParams(TypedDict, total=False): - account_id: Required[Annotated[str, PropertyInfo(alias="accountID")]] - """Account ID this resource belongs to.""" - query: Required[str] """Text to search users by. Network-specific behavior.""" diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py index 2dd8438..d4e343a 100644 --- a/src/beeper_desktop_api/types/message_list_params.py +++ b/src/beeper_desktop_api/types/message_list_params.py @@ -2,17 +2,12 @@ from __future__ import annotations -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._utils import PropertyInfo +from typing_extensions import Literal, TypedDict __all__ = ["MessageListParams"] class MessageListParams(TypedDict, total=False): - chat_id: Required[Annotated[str, PropertyInfo(alias="chatID")]] - """Chat ID to list messages from""" - cursor: str """Message cursor for pagination. Use with direction to navigate results.""" diff --git a/src/beeper_desktop_api/types/message_list_response.py b/src/beeper_desktop_api/types/message_list_response.py deleted file mode 100644 index a66746f..0000000 --- a/src/beeper_desktop_api/types/message_list_response.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .shared.message import Message - -__all__ = ["MessageListResponse"] - - -class MessageListResponse(BaseModel): - has_more: bool = FieldInfo(alias="hasMore") - """True if additional results can be fetched.""" - - items: List[Message] - """Messages from the chat, sorted by timestamp. - - Use message.sortKey as cursor for pagination. - """ diff --git a/src/beeper_desktop_api/types/message_send_params.py b/src/beeper_desktop_api/types/message_send_params.py index b165b27..840e745 100644 --- a/src/beeper_desktop_api/types/message_send_params.py +++ b/src/beeper_desktop_api/types/message_send_params.py @@ -10,9 +10,6 @@ class MessageSendParams(TypedDict, total=False): - chat_id: Annotated[str, PropertyInfo(alias="chatID")] - """Unique identifier of the chat.""" - reply_to_message_id: Annotated[str, PropertyInfo(alias="replyToMessageID")] """Provide a message ID to send this as a reply to an existing message""" diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py index 6308d1f..158b961 100644 --- a/tests/api_resources/test_contacts.py +++ b/tests/api_resources/test_contacts.py @@ -51,6 +51,14 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_path_params_search(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"): + client.contacts.with_raw_response.search( + account_id="", + query="x", + ) + class TestAsyncContacts: parametrize = pytest.mark.parametrize( @@ -90,3 +98,11 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) assert_matches_type(ContactSearchResponse, contact, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_search(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"): + await async_client.contacts.with_raw_response.search( + account_id="", + query="x", + ) diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 302e146..dd93537 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -10,11 +10,10 @@ from tests.utils import assert_matches_type from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop from beeper_desktop_api.types import ( - MessageListResponse, MessageSendResponse, ) from beeper_desktop_api._utils import parse_datetime -from beeper_desktop_api.pagination import SyncCursorSearch, AsyncCursorSearch +from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch from beeper_desktop_api.types.shared import Message base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -28,7 +27,7 @@ def test_method_list(self, client: BeeperDesktop) -> None: message = client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: @@ -37,7 +36,7 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: cursor="821744079", direction="before", ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) @parametrize def test_raw_response_list(self, client: BeeperDesktop) -> None: @@ -48,7 +47,7 @@ def test_raw_response_list(self, client: BeeperDesktop) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) @parametrize def test_streaming_response_list(self, client: BeeperDesktop) -> None: @@ -59,10 +58,17 @@ def test_streaming_response_list(self, client: BeeperDesktop) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncCursorList[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True + @parametrize + def test_path_params_list(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.messages.with_raw_response.list( + chat_id="", + ) + @parametrize def test_method_search(self, client: BeeperDesktop) -> None: message = client.messages.search() @@ -113,7 +119,9 @@ def test_streaming_response_search(self, client: BeeperDesktop) -> None: @parametrize def test_method_send(self, client: BeeperDesktop) -> None: - message = client.messages.send() + message = client.messages.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) assert_matches_type(MessageSendResponse, message, path=["response"]) @parametrize @@ -127,7 +135,9 @@ def test_method_send_with_all_params(self, client: BeeperDesktop) -> None: @parametrize def test_raw_response_send(self, client: BeeperDesktop) -> None: - response = client.messages.with_raw_response.send() + response = client.messages.with_raw_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -136,7 +146,9 @@ def test_raw_response_send(self, client: BeeperDesktop) -> None: @parametrize def test_streaming_response_send(self, client: BeeperDesktop) -> None: - with client.messages.with_streaming_response.send() as response: + with client.messages.with_streaming_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -145,6 +157,13 @@ def test_streaming_response_send(self, client: BeeperDesktop) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_path_params_send(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.messages.with_raw_response.send( + chat_id="", + ) + class TestAsyncMessages: parametrize = pytest.mark.parametrize( @@ -156,7 +175,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: @@ -165,7 +184,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto cursor="821744079", direction="before", ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -176,7 +195,7 @@ async def test_raw_response_list(self, async_client: AsyncBeeperDesktop) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) -> None: @@ -187,10 +206,17 @@ async def test_streaming_response_list(self, async_client: AsyncBeeperDesktop) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncCursorList[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True + @parametrize + async def test_path_params_list(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.messages.with_raw_response.list( + chat_id="", + ) + @parametrize async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.search() @@ -241,7 +267,9 @@ async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) @parametrize async def test_method_send(self, async_client: AsyncBeeperDesktop) -> None: - message = await async_client.messages.send() + message = await async_client.messages.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) assert_matches_type(MessageSendResponse, message, path=["response"]) @parametrize @@ -255,7 +283,9 @@ async def test_method_send_with_all_params(self, async_client: AsyncBeeperDeskto @parametrize async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.messages.with_raw_response.send() + response = await async_client.messages.with_raw_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -264,7 +294,9 @@ async def test_raw_response_send(self, async_client: AsyncBeeperDesktop) -> None @parametrize async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.messages.with_streaming_response.send() as response: + async with async_client.messages.with_streaming_response.send( + chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -272,3 +304,10 @@ async def test_streaming_response_send(self, async_client: AsyncBeeperDesktop) - assert_matches_type(MessageSendResponse, message, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_send(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.messages.with_raw_response.send( + chat_id="", + ) From 86218ff03f8a0cd42050b0c3babdf78178fda3da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:25:34 +0000 Subject: [PATCH 18/20] feat(api): manual updates --- .stats.yml | 6 +- README.md | 100 +---- api.md | 14 +- src/beeper_desktop_api/_client.py | 62 +-- src/beeper_desktop_api/resources/__init__.py | 28 +- .../resources/chats/chats.py | 231 +--------- .../resources/chats/reminders.py | 12 +- src/beeper_desktop_api/resources/contacts.py | 189 --------- src/beeper_desktop_api/resources/messages.py | 22 +- src/beeper_desktop_api/resources/search.py | 397 ++++++++++++++++++ src/beeper_desktop_api/types/__init__.py | 10 +- .../types/chat_list_params.py | 5 +- ..._open_params.py => client_focus_params.py} | 4 +- .../{open_response.py => focus_response.py} | 4 +- .../types/message_list_params.py | 7 +- ...earch_params.py => search_chats_params.py} | 15 +- ...ch_params.py => search_contacts_params.py} | 4 +- ...esponse.py => search_contacts_response.py} | 4 +- .../types/shared/message.py | 11 +- tests/api_resources/test_chats.py | 99 +---- tests/api_resources/test_client.py | 50 +-- tests/api_resources/test_contacts.py | 108 ----- tests/api_resources/test_messages.py | 4 +- tests/api_resources/test_search.py | 202 +++++++++ 24 files changed, 737 insertions(+), 851 deletions(-) delete mode 100644 src/beeper_desktop_api/resources/contacts.py create mode 100644 src/beeper_desktop_api/resources/search.py rename src/beeper_desktop_api/types/{client_open_params.py => client_focus_params.py} (91%) rename src/beeper_desktop_api/types/{open_response.py => focus_response.py} (76%) rename src/beeper_desktop_api/types/{chat_search_params.py => search_chats_params.py} (87%) rename src/beeper_desktop_api/types/{contact_search_params.py => search_contacts_params.py} (75%) rename src/beeper_desktop_api/types/{contact_search_response.py => search_contacts_response.py} (71%) delete mode 100644 tests/api_resources/test_contacts.py create mode 100644 tests/api_resources/test_search.py diff --git a/.stats.yml b/.stats.yml index d065147..831b150 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-f48e33c7d90ed6418a852f8d4d951d07b09f4f3f939feb395dc2aa03f522d81e.yml -openapi_spec_hash: c516120ecf51bb8425b3b9ed76c6423a -config_hash: c5ac9bd5889d27aa168f06d6d0fef0b3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-f48bb509412536c41fdfa537894cecd4af486099d95fe79369f2ef239fa94a75.yml +openapi_spec_hash: f8b886fdfdc5ee3d51d2cd05daee3bab +config_hash: 6f12c5e4c662e1f315b95a70389b1549 diff --git a/README.md b/README.md index 9f0e7ba..f06fa5b 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,7 @@ client = BeeperDesktop( access_token=os.environ.get("BEEPER_ACCESS_TOKEN"), # This is the default and can be omitted ) -page = client.chats.search( - include_muted=True, - limit=3, - type="single", -) -print(page.items) +accounts = client.accounts.list() ``` While you can provide a `access_token` keyword argument, @@ -61,12 +56,7 @@ client = AsyncBeeperDesktop( async def main() -> None: - page = await client.chats.search( - include_muted=True, - limit=3, - type="single", - ) - print(page.items) + accounts = await client.accounts.list() asyncio.run(main()) @@ -98,12 +88,7 @@ async def main() -> None: access_token="My Access Token", http_client=DefaultAioHttpClient(), ) as client: - page = await client.chats.search( - include_muted=True, - limit=3, - type="single", - ) - print(page.items) + accounts = await client.accounts.list() asyncio.run(main()) @@ -118,85 +103,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. -## Pagination - -List methods in the Beeper Desktop API are paginated. - -This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: - -```python -from beeper_desktop_api import BeeperDesktop - -client = BeeperDesktop() - -all_messages = [] -# Automatically fetches more pages as needed. -for message in client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", -): - # Do something with message here - all_messages.append(message) -print(all_messages) -``` - -Or, asynchronously: - -```python -import asyncio -from beeper_desktop_api import AsyncBeeperDesktop - -client = AsyncBeeperDesktop() - - -async def main() -> None: - all_messages = [] - # Iterate through items across all pages, issuing requests as needed. - async for message in client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", - ): - all_messages.append(message) - print(all_messages) - - -asyncio.run(main()) -``` - -Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: - -```python -first_page = await client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", -) -if first_page.has_next_page(): - print(f"will fetch next page using these details: {first_page.next_page_info()}") - next_page = await first_page.get_next_page() - print(f"number of items we just fetched: {len(next_page.items)}") - -# Remove `await` for non-async usage. -``` - -Or just work directly with the returned data: - -```python -first_page = await client.messages.search( - account_ids=["local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"], - limit=10, - query="deployment", -) - -print(f"next page cursor: {first_page.oldest_cursor}") # => "next page cursor: ..." -for message in first_page.items: - print(message.id) - -# Remove `await` for non-async usage. -``` - ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: diff --git a/api.md b/api.md index 4fbc194..119d6f4 100644 --- a/api.md +++ b/api.md @@ -9,13 +9,13 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, R Types: ```python -from beeper_desktop_api.types import DownloadAssetResponse, OpenResponse, SearchResponse +from beeper_desktop_api.types import DownloadAssetResponse, FocusResponse, SearchResponse ``` Methods: - client.download_asset(\*\*params) -> DownloadAssetResponse -- client.open(\*\*params) -> OpenResponse +- client.focus(\*\*params) -> FocusResponse - client.search(\*\*params) -> SearchResponse # Accounts @@ -30,17 +30,18 @@ Methods: - client.accounts.list() -> AccountListResponse -# Contacts +# Search Types: ```python -from beeper_desktop_api.types import ContactSearchResponse +from beeper_desktop_api.types import SearchContactsResponse ``` Methods: -- client.contacts.search(account_id, \*\*params) -> ContactSearchResponse +- client.search.chats(\*\*params) -> SyncCursorSearch[Chat] +- client.search.contacts(account_id, \*\*params) -> SearchContactsResponse # Chats @@ -56,7 +57,6 @@ Methods: - client.chats.retrieve(chat_id, \*\*params) -> Chat - client.chats.list(\*\*params) -> SyncCursorList[ChatListResponse] - client.chats.archive(chat_id, \*\*params) -> BaseResponse -- client.chats.search(\*\*params) -> SyncCursorSearch[Chat] ## Reminders @@ -76,5 +76,5 @@ from beeper_desktop_api.types import MessageSendResponse Methods: - client.messages.list(chat_id, \*\*params) -> SyncCursorList[Message] -- client.messages.search(\*\*params) -> SyncCursorSearch[Message] +- client.messages.search(\*\*params) -> SyncCursorSearch[Message] - client.messages.send(chat_id, \*\*params) -> MessageSendResponse diff --git a/src/beeper_desktop_api/_client.py b/src/beeper_desktop_api/_client.py index f8ba71d..82c9d75 100644 --- a/src/beeper_desktop_api/_client.py +++ b/src/beeper_desktop_api/_client.py @@ -10,7 +10,7 @@ from . import _exceptions from ._qs import Querystring -from .types import client_open_params, client_search_params, client_download_asset_params +from .types import client_focus_params, client_search_params, client_download_asset_params from ._types import ( Body, Omit, @@ -37,7 +37,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .resources import accounts, contacts, messages +from .resources import search, accounts, messages from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BeeperDesktopError from ._base_client import ( @@ -47,7 +47,7 @@ make_request_options, ) from .resources.chats import chats -from .types.open_response import OpenResponse +from .types.focus_response import FocusResponse from .types.search_response import SearchResponse from .types.download_asset_response import DownloadAssetResponse @@ -65,7 +65,7 @@ class BeeperDesktop(SyncAPIClient): accounts: accounts.AccountsResource - contacts: contacts.ContactsResource + search: search.SearchResource chats: chats.ChatsResource messages: messages.MessagesResource with_raw_response: BeeperDesktopWithRawResponse @@ -126,7 +126,7 @@ def __init__( ) self.accounts = accounts.AccountsResource(self) - self.contacts = contacts.ContactsResource(self) + self.search = search.SearchResource(self) self.chats = chats.ChatsResource(self) self.messages = messages.MessagesResource(self) self.with_raw_response = BeeperDesktopWithRawResponse(self) @@ -238,7 +238,7 @@ def download_asset( cast_to=DownloadAssetResponse, ) - def open( + def focus( self, *, chat_id: str | Omit = omit, @@ -251,9 +251,9 @@ def open( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> OpenResponse: + ) -> FocusResponse: """ - Open Beeper Desktop and optionally navigate to a specific chat, message, or + Focus Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. Args: @@ -275,7 +275,7 @@ def open( timeout: Override the client-level default timeout for this request, in seconds """ return self.post( - "/v1/open", + "/v1/focus", body=maybe_transform( { "chat_id": chat_id, @@ -283,12 +283,12 @@ def open( "draft_text": draft_text, "message_id": message_id, }, - client_open_params.ClientOpenParams, + client_focus_params.ClientFocusParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=OpenResponse, + cast_to=FocusResponse, ) def search( @@ -366,7 +366,7 @@ def _make_status_error( class AsyncBeeperDesktop(AsyncAPIClient): accounts: accounts.AsyncAccountsResource - contacts: contacts.AsyncContactsResource + search: search.AsyncSearchResource chats: chats.AsyncChatsResource messages: messages.AsyncMessagesResource with_raw_response: AsyncBeeperDesktopWithRawResponse @@ -427,7 +427,7 @@ def __init__( ) self.accounts = accounts.AsyncAccountsResource(self) - self.contacts = contacts.AsyncContactsResource(self) + self.search = search.AsyncSearchResource(self) self.chats = chats.AsyncChatsResource(self) self.messages = messages.AsyncMessagesResource(self) self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self) @@ -539,7 +539,7 @@ async def download_asset( cast_to=DownloadAssetResponse, ) - async def open( + async def focus( self, *, chat_id: str | Omit = omit, @@ -552,9 +552,9 @@ async def open( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> OpenResponse: + ) -> FocusResponse: """ - Open Beeper Desktop and optionally navigate to a specific chat, message, or + Focus Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. Args: @@ -576,7 +576,7 @@ async def open( timeout: Override the client-level default timeout for this request, in seconds """ return await self.post( - "/v1/open", + "/v1/focus", body=await async_maybe_transform( { "chat_id": chat_id, @@ -584,12 +584,12 @@ async def open( "draft_text": draft_text, "message_id": message_id, }, - client_open_params.ClientOpenParams, + client_focus_params.ClientFocusParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=OpenResponse, + cast_to=FocusResponse, ) async def search( @@ -668,15 +668,15 @@ def _make_status_error( class BeeperDesktopWithRawResponse: def __init__(self, client: BeeperDesktop) -> None: self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts) - self.contacts = contacts.ContactsResourceWithRawResponse(client.contacts) + self.search = search.SearchResourceWithRawResponse(client.search) self.chats = chats.ChatsResourceWithRawResponse(client.chats) self.messages = messages.MessagesResourceWithRawResponse(client.messages) self.download_asset = to_raw_response_wrapper( client.download_asset, ) - self.open = to_raw_response_wrapper( - client.open, + self.focus = to_raw_response_wrapper( + client.focus, ) self.search = to_raw_response_wrapper( client.search, @@ -686,15 +686,15 @@ def __init__(self, client: BeeperDesktop) -> None: class AsyncBeeperDesktopWithRawResponse: def __init__(self, client: AsyncBeeperDesktop) -> None: self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts) - self.contacts = contacts.AsyncContactsResourceWithRawResponse(client.contacts) + self.search = search.AsyncSearchResourceWithRawResponse(client.search) self.chats = chats.AsyncChatsResourceWithRawResponse(client.chats) self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages) self.download_asset = async_to_raw_response_wrapper( client.download_asset, ) - self.open = async_to_raw_response_wrapper( - client.open, + self.focus = async_to_raw_response_wrapper( + client.focus, ) self.search = async_to_raw_response_wrapper( client.search, @@ -704,15 +704,15 @@ def __init__(self, client: AsyncBeeperDesktop) -> None: class BeeperDesktopWithStreamedResponse: def __init__(self, client: BeeperDesktop) -> None: self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts) - self.contacts = contacts.ContactsResourceWithStreamingResponse(client.contacts) + self.search = search.SearchResourceWithStreamingResponse(client.search) self.chats = chats.ChatsResourceWithStreamingResponse(client.chats) self.messages = messages.MessagesResourceWithStreamingResponse(client.messages) self.download_asset = to_streamed_response_wrapper( client.download_asset, ) - self.open = to_streamed_response_wrapper( - client.open, + self.focus = to_streamed_response_wrapper( + client.focus, ) self.search = to_streamed_response_wrapper( client.search, @@ -722,15 +722,15 @@ def __init__(self, client: BeeperDesktop) -> None: class AsyncBeeperDesktopWithStreamedResponse: def __init__(self, client: AsyncBeeperDesktop) -> None: self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts) - self.contacts = contacts.AsyncContactsResourceWithStreamingResponse(client.contacts) + self.search = search.AsyncSearchResourceWithStreamingResponse(client.search) self.chats = chats.AsyncChatsResourceWithStreamingResponse(client.chats) self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages) self.download_asset = async_to_streamed_response_wrapper( client.download_asset, ) - self.open = async_to_streamed_response_wrapper( - client.open, + self.focus = async_to_streamed_response_wrapper( + client.focus, ) self.search = async_to_streamed_response_wrapper( client.search, diff --git a/src/beeper_desktop_api/resources/__init__.py b/src/beeper_desktop_api/resources/__init__.py index ebf006b..eedc9bc 100644 --- a/src/beeper_desktop_api/resources/__init__.py +++ b/src/beeper_desktop_api/resources/__init__.py @@ -8,6 +8,14 @@ ChatsResourceWithStreamingResponse, AsyncChatsResourceWithStreamingResponse, ) +from .search import ( + SearchResource, + AsyncSearchResource, + SearchResourceWithRawResponse, + AsyncSearchResourceWithRawResponse, + SearchResourceWithStreamingResponse, + AsyncSearchResourceWithStreamingResponse, +) from .accounts import ( AccountsResource, AsyncAccountsResource, @@ -16,14 +24,6 @@ AccountsResourceWithStreamingResponse, AsyncAccountsResourceWithStreamingResponse, ) -from .contacts import ( - ContactsResource, - AsyncContactsResource, - ContactsResourceWithRawResponse, - AsyncContactsResourceWithRawResponse, - ContactsResourceWithStreamingResponse, - AsyncContactsResourceWithStreamingResponse, -) from .messages import ( MessagesResource, AsyncMessagesResource, @@ -40,12 +40,12 @@ "AsyncAccountsResourceWithRawResponse", "AccountsResourceWithStreamingResponse", "AsyncAccountsResourceWithStreamingResponse", - "ContactsResource", - "AsyncContactsResource", - "ContactsResourceWithRawResponse", - "AsyncContactsResourceWithRawResponse", - "ContactsResourceWithStreamingResponse", - "AsyncContactsResourceWithStreamingResponse", + "SearchResource", + "AsyncSearchResource", + "SearchResourceWithRawResponse", + "AsyncSearchResourceWithRawResponse", + "SearchResourceWithStreamingResponse", + "AsyncSearchResourceWithStreamingResponse", "ChatsResource", "AsyncChatsResource", "ChatsResourceWithRawResponse", diff --git a/src/beeper_desktop_api/resources/chats/chats.py b/src/beeper_desktop_api/resources/chats/chats.py index 7b8d06c..7752636 100644 --- a/src/beeper_desktop_api/resources/chats/chats.py +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -2,13 +2,12 @@ from __future__ import annotations -from typing import Union, Optional -from datetime import datetime +from typing import Optional from typing_extensions import Literal import httpx -from ...types import chat_list_params, chat_create_params, chat_search_params, chat_archive_params, chat_retrieve_params +from ...types import chat_list_params, chat_create_params, chat_archive_params, chat_retrieve_params from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property @@ -27,7 +26,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch +from ...pagination import SyncCursorList, AsyncCursorList from ...types.chat import Chat from ..._base_client import AsyncPaginator, make_request_options from ...types.chat_list_response import ChatListResponse @@ -137,8 +136,7 @@ def retrieve( Retrieve chat details including metadata, participants, and latest message Args: - chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats. - Participants are limited by 'maxParticipantCount'. + chat_id: Unique identifier of the chat. max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to 20. @@ -188,8 +186,7 @@ def list( Args: account_ids: Limit to specific account IDs. If omitted, fetches from all accounts. - cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction - to navigate results. + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. @@ -240,8 +237,7 @@ def archive( archived=false to move back to inbox Args: - chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and - local chat ID) + chat_id: Unique identifier of the chat. archived: True to archive, false to unarchive @@ -264,103 +260,6 @@ def archive( cast_to=BaseResponse, ) - def search( - self, - *, - account_ids: SequenceNotStr[str] | Omit = omit, - cursor: str | Omit = omit, - direction: Literal["after", "before"] | Omit = omit, - inbox: Literal["primary", "low-priority", "archive"] | Omit = omit, - include_muted: Optional[bool] | Omit = omit, - last_activity_after: Union[str, datetime] | Omit = omit, - last_activity_before: Union[str, datetime] | Omit = omit, - limit: int | Omit = omit, - query: str | Omit = omit, - scope: Literal["titles", "participants"] | Omit = omit, - type: Literal["single", "group", "any"] | Omit = omit, - unread_only: Optional[bool] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncCursorSearch[Chat]: - """ - Search chats by title/network or participants using Beeper Desktop's renderer - algorithm. - - Args: - account_ids: Provide an array of account IDs to filter chats from specific messaging accounts - only - - cursor: Pagination cursor from previous response. Use with direction to navigate results - - direction: Pagination direction: "after" for newer page, "before" for older page. Defaults - to "before" when only cursor is provided. - - inbox: Filter by inbox type: "primary" (non-archived, non-low-priority), - "low-priority", or "archive". If not specified, shows all chats. - - include_muted: Include chats marked as Muted by the user, which are usually less important. - Default: true. Set to false if the user wants a more refined search. - - last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after - this time - - last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before - this time - - limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 - - query: Literal token search (non-semantic). Use single words users type (e.g., - "dinner"). When multiple words provided, ALL must match. Case-insensitive. - - scope: Search scope: 'titles' matches title + network; 'participants' matches - participant names. - - type: Specify the type of chats to retrieve: use "single" for direct messages, "group" - for group chats, or "any" to get all types - - unread_only: Set to true to only retrieve chats that have unread messages - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get_api_list( - "/v1/chats/search", - page=SyncCursorSearch[Chat], - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "account_ids": account_ids, - "cursor": cursor, - "direction": direction, - "inbox": inbox, - "include_muted": include_muted, - "last_activity_after": last_activity_after, - "last_activity_before": last_activity_before, - "limit": limit, - "query": query, - "scope": scope, - "type": type, - "unread_only": unread_only, - }, - chat_search_params.ChatSearchParams, - ), - ), - model=Chat, - ) - class AsyncChatsResource(AsyncAPIResource): """Chats operations""" @@ -462,8 +361,7 @@ async def retrieve( Retrieve chat details including metadata, participants, and latest message Args: - chat_id: Unique identifier of the chat to retrieve. Not available for iMessage chats. - Participants are limited by 'maxParticipantCount'. + chat_id: Unique identifier of the chat. max_participant_count: Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to 20. @@ -513,8 +411,7 @@ def list( Args: account_ids: Limit to specific account IDs. If omitted, fetches from all accounts. - cursor: Timestamp cursor (milliseconds since epoch) for pagination. Use with direction - to navigate results. + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. @@ -565,8 +462,7 @@ async def archive( archived=false to move back to inbox Args: - chat_id: The identifier of the chat to archive or unarchive (accepts both chatID and - local chat ID) + chat_id: Unique identifier of the chat. archived: True to archive, false to unarchive @@ -589,103 +485,6 @@ async def archive( cast_to=BaseResponse, ) - def search( - self, - *, - account_ids: SequenceNotStr[str] | Omit = omit, - cursor: str | Omit = omit, - direction: Literal["after", "before"] | Omit = omit, - inbox: Literal["primary", "low-priority", "archive"] | Omit = omit, - include_muted: Optional[bool] | Omit = omit, - last_activity_after: Union[str, datetime] | Omit = omit, - last_activity_before: Union[str, datetime] | Omit = omit, - limit: int | Omit = omit, - query: str | Omit = omit, - scope: Literal["titles", "participants"] | Omit = omit, - type: Literal["single", "group", "any"] | Omit = omit, - unread_only: Optional[bool] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Chat, AsyncCursorSearch[Chat]]: - """ - Search chats by title/network or participants using Beeper Desktop's renderer - algorithm. - - Args: - account_ids: Provide an array of account IDs to filter chats from specific messaging accounts - only - - cursor: Pagination cursor from previous response. Use with direction to navigate results - - direction: Pagination direction: "after" for newer page, "before" for older page. Defaults - to "before" when only cursor is provided. - - inbox: Filter by inbox type: "primary" (non-archived, non-low-priority), - "low-priority", or "archive". If not specified, shows all chats. - - include_muted: Include chats marked as Muted by the user, which are usually less important. - Default: true. Set to false if the user wants a more refined search. - - last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after - this time - - last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before - this time - - limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 - - query: Literal token search (non-semantic). Use single words users type (e.g., - "dinner"). When multiple words provided, ALL must match. Case-insensitive. - - scope: Search scope: 'titles' matches title + network; 'participants' matches - participant names. - - type: Specify the type of chats to retrieve: use "single" for direct messages, "group" - for group chats, or "any" to get all types - - unread_only: Set to true to only retrieve chats that have unread messages - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get_api_list( - "/v1/chats/search", - page=AsyncCursorSearch[Chat], - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "account_ids": account_ids, - "cursor": cursor, - "direction": direction, - "inbox": inbox, - "include_muted": include_muted, - "last_activity_after": last_activity_after, - "last_activity_before": last_activity_before, - "limit": limit, - "query": query, - "scope": scope, - "type": type, - "unread_only": unread_only, - }, - chat_search_params.ChatSearchParams, - ), - ), - model=Chat, - ) - class ChatsResourceWithRawResponse: def __init__(self, chats: ChatsResource) -> None: @@ -703,9 +502,6 @@ def __init__(self, chats: ChatsResource) -> None: self.archive = to_raw_response_wrapper( chats.archive, ) - self.search = to_raw_response_wrapper( - chats.search, - ) @cached_property def reminders(self) -> RemindersResourceWithRawResponse: @@ -729,9 +525,6 @@ def __init__(self, chats: AsyncChatsResource) -> None: self.archive = async_to_raw_response_wrapper( chats.archive, ) - self.search = async_to_raw_response_wrapper( - chats.search, - ) @cached_property def reminders(self) -> AsyncRemindersResourceWithRawResponse: @@ -755,9 +548,6 @@ def __init__(self, chats: ChatsResource) -> None: self.archive = to_streamed_response_wrapper( chats.archive, ) - self.search = to_streamed_response_wrapper( - chats.search, - ) @cached_property def reminders(self) -> RemindersResourceWithStreamingResponse: @@ -781,9 +571,6 @@ def __init__(self, chats: AsyncChatsResource) -> None: self.archive = async_to_streamed_response_wrapper( chats.archive, ) - self.search = async_to_streamed_response_wrapper( - chats.search, - ) @cached_property def reminders(self) -> AsyncRemindersResourceWithStreamingResponse: diff --git a/src/beeper_desktop_api/resources/chats/reminders.py b/src/beeper_desktop_api/resources/chats/reminders.py index e9da3b4..bf628ae 100644 --- a/src/beeper_desktop_api/resources/chats/reminders.py +++ b/src/beeper_desktop_api/resources/chats/reminders.py @@ -59,8 +59,7 @@ def create( Set a reminder for a chat at a specific time Args: - chat_id: The identifier of the chat to set reminder for (accepts both chatID and local - chat ID) + chat_id: Unique identifier of the chat. reminder: Reminder configuration @@ -98,8 +97,7 @@ def delete( Clear an existing reminder from a chat Args: - chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local - chat ID) + chat_id: Unique identifier of the chat. extra_headers: Send extra headers @@ -158,8 +156,7 @@ async def create( Set a reminder for a chat at a specific time Args: - chat_id: The identifier of the chat to set reminder for (accepts both chatID and local - chat ID) + chat_id: Unique identifier of the chat. reminder: Reminder configuration @@ -197,8 +194,7 @@ async def delete( Clear an existing reminder from a chat Args: - chat_id: The identifier of the chat to clear reminder from (accepts both chatID and local - chat ID) + chat_id: Unique identifier of the chat. extra_headers: Send extra headers diff --git a/src/beeper_desktop_api/resources/contacts.py b/src/beeper_desktop_api/resources/contacts.py deleted file mode 100644 index 50fc4f9..0000000 --- a/src/beeper_desktop_api/resources/contacts.py +++ /dev/null @@ -1,189 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..types import contact_search_params -from .._types import Body, Query, Headers, NotGiven, not_given -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.contact_search_response import ContactSearchResponse - -__all__ = ["ContactsResource", "AsyncContactsResource"] - - -class ContactsResource(SyncAPIResource): - """Contacts operations""" - - @cached_property - def with_raw_response(self) -> ContactsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers - """ - return ContactsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> ContactsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response - """ - return ContactsResourceWithStreamingResponse(self) - - def search( - self, - account_id: str, - *, - query: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContactSearchResponse: - """ - Search contacts across on a specific account using the network's search API. - Only use for creating new chats. - - Args: - account_id: Account ID this resource belongs to. - - query: Text to search users by. Network-specific behavior. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not account_id: - raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") - return self._get( - f"/v1/accounts/{account_id}/contacts/search", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform({"query": query}, contact_search_params.ContactSearchParams), - ), - cast_to=ContactSearchResponse, - ) - - -class AsyncContactsResource(AsyncAPIResource): - """Contacts operations""" - - @cached_property - def with_raw_response(self) -> AsyncContactsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers - """ - return AsyncContactsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response - """ - return AsyncContactsResourceWithStreamingResponse(self) - - async def search( - self, - account_id: str, - *, - query: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContactSearchResponse: - """ - Search contacts across on a specific account using the network's search API. - Only use for creating new chats. - - Args: - account_id: Account ID this resource belongs to. - - query: Text to search users by. Network-specific behavior. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not account_id: - raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") - return await self._get( - f"/v1/accounts/{account_id}/contacts/search", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform({"query": query}, contact_search_params.ContactSearchParams), - ), - cast_to=ContactSearchResponse, - ) - - -class ContactsResourceWithRawResponse: - def __init__(self, contacts: ContactsResource) -> None: - self._contacts = contacts - - self.search = to_raw_response_wrapper( - contacts.search, - ) - - -class AsyncContactsResourceWithRawResponse: - def __init__(self, contacts: AsyncContactsResource) -> None: - self._contacts = contacts - - self.search = async_to_raw_response_wrapper( - contacts.search, - ) - - -class ContactsResourceWithStreamingResponse: - def __init__(self, contacts: ContactsResource) -> None: - self._contacts = contacts - - self.search = to_streamed_response_wrapper( - contacts.search, - ) - - -class AsyncContactsResourceWithStreamingResponse: - def __init__(self, contacts: AsyncContactsResource) -> None: - self._contacts = contacts - - self.search = async_to_streamed_response_wrapper( - contacts.search, - ) diff --git a/src/beeper_desktop_api/resources/messages.py b/src/beeper_desktop_api/resources/messages.py index e77b8f1..ad07df3 100644 --- a/src/beeper_desktop_api/resources/messages.py +++ b/src/beeper_desktop_api/resources/messages.py @@ -67,13 +67,12 @@ def list( Sorted by timestamp. Args: - chat_id: Chat ID to list messages from + chat_id: Unique identifier of the chat. - cursor: Message cursor for pagination. Use with direction to navigate results. + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. - direction: Pagination direction used with 'cursor': 'before' fetches older messages, - 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is - provided. + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. extra_headers: Send extra headers @@ -176,7 +175,7 @@ def search( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/v1/messages/search", + "/v1/search/messages", page=SyncCursorSearch[Message], options=make_request_options( extra_headers=extra_headers, @@ -296,13 +295,12 @@ def list( Sorted by timestamp. Args: - chat_id: Chat ID to list messages from + chat_id: Unique identifier of the chat. - cursor: Message cursor for pagination. Use with direction to navigate results. + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. - direction: Pagination direction used with 'cursor': 'before' fetches older messages, - 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is - provided. + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. extra_headers: Send extra headers @@ -405,7 +403,7 @@ def search( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/v1/messages/search", + "/v1/search/messages", page=AsyncCursorSearch[Message], options=make_request_options( extra_headers=extra_headers, diff --git a/src/beeper_desktop_api/resources/search.py b/src/beeper_desktop_api/resources/search.py new file mode 100644 index 0000000..0653cfa --- /dev/null +++ b/src/beeper_desktop_api/resources/search.py @@ -0,0 +1,397 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Literal + +import httpx + +from ..types import search_chats_params, search_contacts_params +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncCursorSearch, AsyncCursorSearch +from ..types.chat import Chat +from .._base_client import AsyncPaginator, make_request_options +from ..types.search_contacts_response import SearchContactsResponse + +__all__ = ["SearchResource", "AsyncSearchResource"] + + +class SearchResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SearchResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return SearchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SearchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return SearchResourceWithStreamingResponse(self) + + def chats( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + inbox: Literal["primary", "low-priority", "archive"] | Omit = omit, + include_muted: Optional[bool] | Omit = omit, + last_activity_after: Union[str, datetime] | Omit = omit, + last_activity_before: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + query: str | Omit = omit, + scope: Literal["titles", "participants"] | Omit = omit, + type: Literal["single", "group", "any"] | Omit = omit, + unread_only: Optional[bool] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncCursorSearch[Chat]: + """ + Search chats by title/network or participants using Beeper Desktop's renderer + algorithm. + + Args: + account_ids: Provide an array of account IDs to filter chats from specific messaging accounts + only + + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. + + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + + inbox: Filter by inbox type: "primary" (non-archived, non-low-priority), + "low-priority", or "archive". If not specified, shows all chats. + + include_muted: Include chats marked as Muted by the user, which are usually less important. + Default: true. Set to false if the user wants a more refined search. + + last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after + this time + + last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before + this time + + limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 + + query: Literal token search (non-semantic). Use single words users type (e.g., + "dinner"). When multiple words provided, ALL must match. Case-insensitive. + + scope: Search scope: 'titles' matches title + network; 'participants' matches + participant names. + + type: Specify the type of chats to retrieve: use "single" for direct messages, "group" + for group chats, or "any" to get all types + + unread_only: Set to true to only retrieve chats that have unread messages + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/search/chats", + page=SyncCursorSearch[Chat], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "cursor": cursor, + "direction": direction, + "inbox": inbox, + "include_muted": include_muted, + "last_activity_after": last_activity_after, + "last_activity_before": last_activity_before, + "limit": limit, + "query": query, + "scope": scope, + "type": type, + "unread_only": unread_only, + }, + search_chats_params.SearchChatsParams, + ), + ), + model=Chat, + ) + + def contacts( + self, + account_id: str, + *, + query: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SearchContactsResponse: + """ + Search contacts across on a specific account using the network's search API. + Only use for creating new chats. + + Args: + account_id: Account ID this resource belongs to. + + query: Text to search users by. Network-specific behavior. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not account_id: + raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") + return self._get( + f"/v1/search/contacts/{account_id}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"query": query}, search_contacts_params.SearchContactsParams), + ), + cast_to=SearchContactsResponse, + ) + + +class AsyncSearchResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSearchResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/beeper/desktop-api-python#accessing-raw-response-data-eg-headers + """ + return AsyncSearchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSearchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/beeper/desktop-api-python#with_streaming_response + """ + return AsyncSearchResourceWithStreamingResponse(self) + + def chats( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + cursor: str | Omit = omit, + direction: Literal["after", "before"] | Omit = omit, + inbox: Literal["primary", "low-priority", "archive"] | Omit = omit, + include_muted: Optional[bool] | Omit = omit, + last_activity_after: Union[str, datetime] | Omit = omit, + last_activity_before: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + query: str | Omit = omit, + scope: Literal["titles", "participants"] | Omit = omit, + type: Literal["single", "group", "any"] | Omit = omit, + unread_only: Optional[bool] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Chat, AsyncCursorSearch[Chat]]: + """ + Search chats by title/network or participants using Beeper Desktop's renderer + algorithm. + + Args: + account_ids: Provide an array of account IDs to filter chats from specific messaging accounts + only + + cursor: Opaque pagination cursor; do not inspect. Use together with 'direction'. + + direction: Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. + + inbox: Filter by inbox type: "primary" (non-archived, non-low-priority), + "low-priority", or "archive". If not specified, shows all chats. + + include_muted: Include chats marked as Muted by the user, which are usually less important. + Default: true. Set to false if the user wants a more refined search. + + last_activity_after: Provide an ISO datetime string to only retrieve chats with last activity after + this time + + last_activity_before: Provide an ISO datetime string to only retrieve chats with last activity before + this time + + limit: Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 + + query: Literal token search (non-semantic). Use single words users type (e.g., + "dinner"). When multiple words provided, ALL must match. Case-insensitive. + + scope: Search scope: 'titles' matches title + network; 'participants' matches + participant names. + + type: Specify the type of chats to retrieve: use "single" for direct messages, "group" + for group chats, or "any" to get all types + + unread_only: Set to true to only retrieve chats that have unread messages + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/search/chats", + page=AsyncCursorSearch[Chat], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "account_ids": account_ids, + "cursor": cursor, + "direction": direction, + "inbox": inbox, + "include_muted": include_muted, + "last_activity_after": last_activity_after, + "last_activity_before": last_activity_before, + "limit": limit, + "query": query, + "scope": scope, + "type": type, + "unread_only": unread_only, + }, + search_chats_params.SearchChatsParams, + ), + ), + model=Chat, + ) + + async def contacts( + self, + account_id: str, + *, + query: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SearchContactsResponse: + """ + Search contacts across on a specific account using the network's search API. + Only use for creating new chats. + + Args: + account_id: Account ID this resource belongs to. + + query: Text to search users by. Network-specific behavior. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not account_id: + raise ValueError(f"Expected a non-empty value for `account_id` but received {account_id!r}") + return await self._get( + f"/v1/search/contacts/{account_id}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"query": query}, search_contacts_params.SearchContactsParams), + ), + cast_to=SearchContactsResponse, + ) + + +class SearchResourceWithRawResponse: + def __init__(self, search: SearchResource) -> None: + self._search = search + + self.chats = to_raw_response_wrapper( + search.chats, + ) + self.contacts = to_raw_response_wrapper( + search.contacts, + ) + + +class AsyncSearchResourceWithRawResponse: + def __init__(self, search: AsyncSearchResource) -> None: + self._search = search + + self.chats = async_to_raw_response_wrapper( + search.chats, + ) + self.contacts = async_to_raw_response_wrapper( + search.contacts, + ) + + +class SearchResourceWithStreamingResponse: + def __init__(self, search: SearchResource) -> None: + self._search = search + + self.chats = to_streamed_response_wrapper( + search.chats, + ) + self.contacts = to_streamed_response_wrapper( + search.contacts, + ) + + +class AsyncSearchResourceWithStreamingResponse: + def __init__(self, search: AsyncSearchResource) -> None: + self._search = search + + self.chats = async_to_streamed_response_wrapper( + search.chats, + ) + self.contacts = async_to_streamed_response_wrapper( + search.contacts, + ) diff --git a/src/beeper_desktop_api/types/__init__.py b/src/beeper_desktop_api/types/__init__.py index e577cbf..6aeb07d 100644 --- a/src/beeper_desktop_api/types/__init__.py +++ b/src/beeper_desktop_api/types/__init__.py @@ -12,23 +12,23 @@ BaseResponse as BaseResponse, ) from .account import Account as Account -from .open_response import OpenResponse as OpenResponse +from .focus_response import FocusResponse as FocusResponse from .search_response import SearchResponse as SearchResponse from .chat_list_params import ChatListParams as ChatListParams from .chat_create_params import ChatCreateParams as ChatCreateParams from .chat_list_response import ChatListResponse as ChatListResponse -from .chat_search_params import ChatSearchParams as ChatSearchParams -from .client_open_params import ClientOpenParams as ClientOpenParams from .chat_archive_params import ChatArchiveParams as ChatArchiveParams +from .client_focus_params import ClientFocusParams as ClientFocusParams from .message_list_params import MessageListParams as MessageListParams from .message_send_params import MessageSendParams as MessageSendParams +from .search_chats_params import SearchChatsParams as SearchChatsParams from .chat_create_response import ChatCreateResponse as ChatCreateResponse from .chat_retrieve_params import ChatRetrieveParams as ChatRetrieveParams from .client_search_params import ClientSearchParams as ClientSearchParams from .account_list_response import AccountListResponse as AccountListResponse -from .contact_search_params import ContactSearchParams as ContactSearchParams from .message_search_params import MessageSearchParams as MessageSearchParams from .message_send_response import MessageSendResponse as MessageSendResponse -from .contact_search_response import ContactSearchResponse as ContactSearchResponse +from .search_contacts_params import SearchContactsParams as SearchContactsParams from .download_asset_response import DownloadAssetResponse as DownloadAssetResponse +from .search_contacts_response import SearchContactsResponse as SearchContactsResponse from .client_download_asset_params import ClientDownloadAssetParams as ClientDownloadAssetParams diff --git a/src/beeper_desktop_api/types/chat_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py index e1d10b2..d216046 100644 --- a/src/beeper_desktop_api/types/chat_list_params.py +++ b/src/beeper_desktop_api/types/chat_list_params.py @@ -15,10 +15,7 @@ class ChatListParams(TypedDict, total=False): """Limit to specific account IDs. If omitted, fetches from all accounts.""" cursor: str - """Timestamp cursor (milliseconds since epoch) for pagination. - - Use with direction to navigate results. - """ + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" direction: Literal["after", "before"] """ diff --git a/src/beeper_desktop_api/types/client_open_params.py b/src/beeper_desktop_api/types/client_focus_params.py similarity index 91% rename from src/beeper_desktop_api/types/client_open_params.py rename to src/beeper_desktop_api/types/client_focus_params.py index 84dea5f..6359eb2 100644 --- a/src/beeper_desktop_api/types/client_open_params.py +++ b/src/beeper_desktop_api/types/client_focus_params.py @@ -6,10 +6,10 @@ from .._utils import PropertyInfo -__all__ = ["ClientOpenParams"] +__all__ = ["ClientFocusParams"] -class ClientOpenParams(TypedDict, total=False): +class ClientFocusParams(TypedDict, total=False): chat_id: Annotated[str, PropertyInfo(alias="chatID")] """Optional Beeper chat ID (or local chat ID) to focus after opening the app. diff --git a/src/beeper_desktop_api/types/open_response.py b/src/beeper_desktop_api/types/focus_response.py similarity index 76% rename from src/beeper_desktop_api/types/open_response.py rename to src/beeper_desktop_api/types/focus_response.py index 970f2ba..28875b1 100644 --- a/src/beeper_desktop_api/types/open_response.py +++ b/src/beeper_desktop_api/types/focus_response.py @@ -2,9 +2,9 @@ from .._models import BaseModel -__all__ = ["OpenResponse"] +__all__ = ["FocusResponse"] -class OpenResponse(BaseModel): +class FocusResponse(BaseModel): success: bool """Whether the app was successfully opened/focused.""" diff --git a/src/beeper_desktop_api/types/message_list_params.py b/src/beeper_desktop_api/types/message_list_params.py index d4e343a..e6a04d2 100644 --- a/src/beeper_desktop_api/types/message_list_params.py +++ b/src/beeper_desktop_api/types/message_list_params.py @@ -9,11 +9,10 @@ class MessageListParams(TypedDict, total=False): cursor: str - """Message cursor for pagination. Use with direction to navigate results.""" + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" direction: Literal["after", "before"] """ - Pagination direction used with 'cursor': 'before' fetches older messages, - 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is - provided. + Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. """ diff --git a/src/beeper_desktop_api/types/chat_search_params.py b/src/beeper_desktop_api/types/search_chats_params.py similarity index 87% rename from src/beeper_desktop_api/types/chat_search_params.py rename to src/beeper_desktop_api/types/search_chats_params.py index de94b8d..d393720 100644 --- a/src/beeper_desktop_api/types/chat_search_params.py +++ b/src/beeper_desktop_api/types/search_chats_params.py @@ -9,10 +9,10 @@ from .._types import SequenceNotStr from .._utils import PropertyInfo -__all__ = ["ChatSearchParams"] +__all__ = ["SearchChatsParams"] -class ChatSearchParams(TypedDict, total=False): +class SearchChatsParams(TypedDict, total=False): account_ids: Annotated[SequenceNotStr[str], PropertyInfo(alias="accountIDs")] """ Provide an array of account IDs to filter chats from specific messaging accounts @@ -20,15 +20,12 @@ class ChatSearchParams(TypedDict, total=False): """ cursor: str - """Pagination cursor from previous response. - - Use with direction to navigate results - """ + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" direction: Literal["after", "before"] - """Pagination direction: "after" for newer page, "before" for older page. - - Defaults to "before" when only cursor is provided. + """ + Pagination direction used with 'cursor': 'before' fetches older results, 'after' + fetches newer results. Defaults to 'before' when only 'cursor' is provided. """ inbox: Literal["primary", "low-priority", "archive"] diff --git a/src/beeper_desktop_api/types/contact_search_params.py b/src/beeper_desktop_api/types/search_contacts_params.py similarity index 75% rename from src/beeper_desktop_api/types/contact_search_params.py rename to src/beeper_desktop_api/types/search_contacts_params.py index f9063e0..3e0352b 100644 --- a/src/beeper_desktop_api/types/contact_search_params.py +++ b/src/beeper_desktop_api/types/search_contacts_params.py @@ -4,9 +4,9 @@ from typing_extensions import Required, TypedDict -__all__ = ["ContactSearchParams"] +__all__ = ["SearchContactsParams"] -class ContactSearchParams(TypedDict, total=False): +class SearchContactsParams(TypedDict, total=False): query: Required[str] """Text to search users by. Network-specific behavior.""" diff --git a/src/beeper_desktop_api/types/contact_search_response.py b/src/beeper_desktop_api/types/search_contacts_response.py similarity index 71% rename from src/beeper_desktop_api/types/contact_search_response.py rename to src/beeper_desktop_api/types/search_contacts_response.py index 71c609e..1bbf6db 100644 --- a/src/beeper_desktop_api/types/contact_search_response.py +++ b/src/beeper_desktop_api/types/search_contacts_response.py @@ -5,8 +5,8 @@ from .._models import BaseModel from .shared.user import User -__all__ = ["ContactSearchResponse"] +__all__ = ["SearchContactsResponse"] -class ContactSearchResponse(BaseModel): +class SearchContactsResponse(BaseModel): items: List[User] diff --git a/src/beeper_desktop_api/types/shared/message.py b/src/beeper_desktop_api/types/shared/message.py index b9d70ff..ff2ca3a 100644 --- a/src/beeper_desktop_api/types/shared/message.py +++ b/src/beeper_desktop_api/types/shared/message.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Union, Optional +from typing import List, Optional from datetime import datetime from pydantic import Field as FieldInfo @@ -14,21 +14,18 @@ class Message(BaseModel): id: str - """Stable message ID for cursor pagination.""" + """Message ID.""" account_id: str = FieldInfo(alias="accountID") """Beeper account ID the message belongs to.""" chat_id: str = FieldInfo(alias="chatID") - """Beeper chat/thread/room ID.""" - - message_id: str = FieldInfo(alias="messageID") - """Stable message ID (same as id).""" + """Unique identifier of the chat.""" sender_id: str = FieldInfo(alias="senderID") """Sender user ID.""" - sort_key: Union[str, float] = FieldInfo(alias="sortKey") + sort_key: str = FieldInfo(alias="sortKey") """A unique key used to sort messages""" timestamp: datetime diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py index 352bc2f..84cfdb1 100644 --- a/tests/api_resources/test_chats.py +++ b/tests/api_resources/test_chats.py @@ -14,8 +14,7 @@ ChatListResponse, ChatCreateResponse, ) -from beeper_desktop_api._utils import parse_datetime -from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList, SyncCursorSearch, AsyncCursorSearch +from beeper_desktop_api.pagination import SyncCursorList, AsyncCursorList from beeper_desktop_api.types.shared import BaseResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -131,7 +130,7 @@ def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", ], - cursor="1725489123456", + cursor="1725489123456|c29tZUltc2dQYWdl", direction="before", ) assert_matches_type(SyncCursorList[ChatListResponse], chat, path=["response"]) @@ -202,52 +201,6 @@ def test_path_params_archive(self, client: BeeperDesktop) -> None: chat_id="", ) - @parametrize - def test_method_search(self, client: BeeperDesktop) -> None: - chat = client.chats.search() - assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) - - @parametrize - def test_method_search_with_all_params(self, client: BeeperDesktop) -> None: - chat = client.chats.search( - account_ids=[ - "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", - ], - cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9", - direction="after", - inbox="primary", - include_muted=True, - last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"), - last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"), - limit=1, - query="x", - scope="titles", - type="single", - unread_only=True, - ) - assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) - - @parametrize - def test_raw_response_search(self, client: BeeperDesktop) -> None: - response = client.chats.with_raw_response.search() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - chat = response.parse() - assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) - - @parametrize - def test_streaming_response_search(self, client: BeeperDesktop) -> None: - with client.chats.with_streaming_response.search() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - chat = response.parse() - assert_matches_type(SyncCursorSearch[Chat], chat, path=["response"]) - - assert cast(Any, response.is_closed) is True - class TestAsyncChats: parametrize = pytest.mark.parametrize( @@ -361,7 +314,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncBeeperDeskto "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", ], - cursor="1725489123456", + cursor="1725489123456|c29tZUltc2dQYWdl", direction="before", ) assert_matches_type(AsyncCursorList[ChatListResponse], chat, path=["response"]) @@ -431,49 +384,3 @@ async def test_path_params_archive(self, async_client: AsyncBeeperDesktop) -> No await async_client.chats.with_raw_response.archive( chat_id="", ) - - @parametrize - async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: - chat = await async_client.chats.search() - assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) - - @parametrize - async def test_method_search_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: - chat = await async_client.chats.search( - account_ids=[ - "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", - ], - cursor="eyJvZmZzZXQiOjE3MTk5OTk5OTl9", - direction="after", - inbox="primary", - include_muted=True, - last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"), - last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"), - limit=1, - query="x", - scope="titles", - type="single", - unread_only=True, - ) - assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) - - @parametrize - async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.chats.with_raw_response.search() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - chat = await response.parse() - assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) - - @parametrize - async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.chats.with_streaming_response.search() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - chat = await response.parse() - assert_matches_type(AsyncCursorSearch[Chat], chat, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py index d5de032..54e150e 100644 --- a/tests/api_resources/test_client.py +++ b/tests/api_resources/test_client.py @@ -10,7 +10,7 @@ from tests.utils import assert_matches_type from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop from beeper_desktop_api.types import ( - OpenResponse, + FocusResponse, SearchResponse, DownloadAssetResponse, ) @@ -53,37 +53,37 @@ def test_streaming_response_download_asset(self, client: BeeperDesktop) -> None: assert cast(Any, response.is_closed) is True @parametrize - def test_method_open(self, client: BeeperDesktop) -> None: - client_ = client.open() - assert_matches_type(OpenResponse, client_, path=["response"]) + def test_method_focus(self, client: BeeperDesktop) -> None: + client_ = client.focus() + assert_matches_type(FocusResponse, client_, path=["response"]) @parametrize - def test_method_open_with_all_params(self, client: BeeperDesktop) -> None: - client_ = client.open( + def test_method_focus_with_all_params(self, client: BeeperDesktop) -> None: + client_ = client.focus( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", draft_attachment_path="draftAttachmentPath", draft_text="draftText", message_id="messageID", ) - assert_matches_type(OpenResponse, client_, path=["response"]) + assert_matches_type(FocusResponse, client_, path=["response"]) @parametrize - def test_raw_response_open(self, client: BeeperDesktop) -> None: - response = client.with_raw_response.open() + def test_raw_response_focus(self, client: BeeperDesktop) -> None: + response = client.with_raw_response.focus() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" client_ = response.parse() - assert_matches_type(OpenResponse, client_, path=["response"]) + assert_matches_type(FocusResponse, client_, path=["response"]) @parametrize - def test_streaming_response_open(self, client: BeeperDesktop) -> None: - with client.with_streaming_response.open() as response: + def test_streaming_response_focus(self, client: BeeperDesktop) -> None: + with client.with_streaming_response.focus() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" client_ = response.parse() - assert_matches_type(OpenResponse, client_, path=["response"]) + assert_matches_type(FocusResponse, client_, path=["response"]) assert cast(Any, response.is_closed) is True @@ -156,37 +156,37 @@ async def test_streaming_response_download_asset(self, async_client: AsyncBeeper assert cast(Any, response.is_closed) is True @parametrize - async def test_method_open(self, async_client: AsyncBeeperDesktop) -> None: - client = await async_client.open() - assert_matches_type(OpenResponse, client, path=["response"]) + async def test_method_focus(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.focus() + assert_matches_type(FocusResponse, client, path=["response"]) @parametrize - async def test_method_open_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: - client = await async_client.open( + async def test_method_focus_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.focus( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", draft_attachment_path="draftAttachmentPath", draft_text="draftText", message_id="messageID", ) - assert_matches_type(OpenResponse, client, path=["response"]) + assert_matches_type(FocusResponse, client, path=["response"]) @parametrize - async def test_raw_response_open(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.with_raw_response.open() + async def test_raw_response_focus(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.with_raw_response.focus() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" client = await response.parse() - assert_matches_type(OpenResponse, client, path=["response"]) + assert_matches_type(FocusResponse, client, path=["response"]) @parametrize - async def test_streaming_response_open(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.with_streaming_response.open() as response: + async def test_streaming_response_focus(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.with_streaming_response.focus() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" client = await response.parse() - assert_matches_type(OpenResponse, client, path=["response"]) + assert_matches_type(FocusResponse, client, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py deleted file mode 100644 index 158b961..0000000 --- a/tests/api_resources/test_contacts.py +++ /dev/null @@ -1,108 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop -from beeper_desktop_api.types import ContactSearchResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestContacts: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_search(self, client: BeeperDesktop) -> None: - contact = client.contacts.search( - account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - query="x", - ) - assert_matches_type(ContactSearchResponse, contact, path=["response"]) - - @parametrize - def test_raw_response_search(self, client: BeeperDesktop) -> None: - response = client.contacts.with_raw_response.search( - account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - query="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - contact = response.parse() - assert_matches_type(ContactSearchResponse, contact, path=["response"]) - - @parametrize - def test_streaming_response_search(self, client: BeeperDesktop) -> None: - with client.contacts.with_streaming_response.search( - account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - query="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - contact = response.parse() - assert_matches_type(ContactSearchResponse, contact, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_search(self, client: BeeperDesktop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"): - client.contacts.with_raw_response.search( - account_id="", - query="x", - ) - - -class TestAsyncContacts: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @parametrize - async def test_method_search(self, async_client: AsyncBeeperDesktop) -> None: - contact = await async_client.contacts.search( - account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - query="x", - ) - assert_matches_type(ContactSearchResponse, contact, path=["response"]) - - @parametrize - async def test_raw_response_search(self, async_client: AsyncBeeperDesktop) -> None: - response = await async_client.contacts.with_raw_response.search( - account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - query="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - contact = await response.parse() - assert_matches_type(ContactSearchResponse, contact, path=["response"]) - - @parametrize - async def test_streaming_response_search(self, async_client: AsyncBeeperDesktop) -> None: - async with async_client.contacts.with_streaming_response.search( - account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - query="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - contact = await response.parse() - assert_matches_type(ContactSearchResponse, contact, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_search(self, async_client: AsyncBeeperDesktop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"): - await async_client.contacts.with_raw_response.search( - account_id="", - query="x", - ) diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index dd93537..d64cf44 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -33,7 +33,7 @@ def test_method_list(self, client: BeeperDesktop) -> None: def test_method_list_with_all_params(self, client: BeeperDesktop) -> None: message = client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - cursor="821744079", + cursor="1725489123456|c29tZUltc2dQYWdl", direction="before", ) assert_matches_type(SyncCursorList[Message], message, path=["response"]) @@ -181,7 +181,7 @@ async def test_method_list(self, async_client: AsyncBeeperDesktop) -> None: async def test_method_list_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: message = await async_client.messages.list( chat_id="!NCdzlIaMjZUmvmvyHU:beeper.com", - cursor="821744079", + cursor="1725489123456|c29tZUltc2dQYWdl", direction="before", ) assert_matches_type(AsyncCursorList[Message], message, path=["response"]) diff --git a/tests/api_resources/test_search.py b/tests/api_resources/test_search.py new file mode 100644 index 0000000..1c70e2d --- /dev/null +++ b/tests/api_resources/test_search.py @@ -0,0 +1,202 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from beeper_desktop_api import BeeperDesktop, AsyncBeeperDesktop +from beeper_desktop_api.types import Chat, SearchContactsResponse +from beeper_desktop_api._utils import parse_datetime +from beeper_desktop_api.pagination import SyncCursorSearch, AsyncCursorSearch + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSearch: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_chats(self, client: BeeperDesktop) -> None: + search = client.search.chats() + assert_matches_type(SyncCursorSearch[Chat], search, path=["response"]) + + @parametrize + def test_method_chats_with_all_params(self, client: BeeperDesktop) -> None: + search = client.search.chats( + account_ids=[ + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", + ], + cursor="1725489123456|c29tZUltc2dQYWdl", + direction="before", + inbox="primary", + include_muted=True, + last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"), + last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"), + limit=1, + query="x", + scope="titles", + type="single", + unread_only=True, + ) + assert_matches_type(SyncCursorSearch[Chat], search, path=["response"]) + + @parametrize + def test_raw_response_chats(self, client: BeeperDesktop) -> None: + response = client.search.with_raw_response.chats() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + search = response.parse() + assert_matches_type(SyncCursorSearch[Chat], search, path=["response"]) + + @parametrize + def test_streaming_response_chats(self, client: BeeperDesktop) -> None: + with client.search.with_streaming_response.chats() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + search = response.parse() + assert_matches_type(SyncCursorSearch[Chat], search, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_contacts(self, client: BeeperDesktop) -> None: + search = client.search.contacts( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + assert_matches_type(SearchContactsResponse, search, path=["response"]) + + @parametrize + def test_raw_response_contacts(self, client: BeeperDesktop) -> None: + response = client.search.with_raw_response.contacts( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + search = response.parse() + assert_matches_type(SearchContactsResponse, search, path=["response"]) + + @parametrize + def test_streaming_response_contacts(self, client: BeeperDesktop) -> None: + with client.search.with_streaming_response.contacts( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + search = response.parse() + assert_matches_type(SearchContactsResponse, search, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_contacts(self, client: BeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"): + client.search.with_raw_response.contacts( + account_id="", + query="x", + ) + + +class TestAsyncSearch: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_chats(self, async_client: AsyncBeeperDesktop) -> None: + search = await async_client.search.chats() + assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"]) + + @parametrize + async def test_method_chats_with_all_params(self, async_client: AsyncBeeperDesktop) -> None: + search = await async_client.search.chats( + account_ids=[ + "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", + ], + cursor="1725489123456|c29tZUltc2dQYWdl", + direction="before", + inbox="primary", + include_muted=True, + last_activity_after=parse_datetime("2019-12-27T18:11:19.117Z"), + last_activity_before=parse_datetime("2019-12-27T18:11:19.117Z"), + limit=1, + query="x", + scope="titles", + type="single", + unread_only=True, + ) + assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"]) + + @parametrize + async def test_raw_response_chats(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.search.with_raw_response.chats() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + search = await response.parse() + assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"]) + + @parametrize + async def test_streaming_response_chats(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.search.with_streaming_response.chats() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + search = await response.parse() + assert_matches_type(AsyncCursorSearch[Chat], search, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_contacts(self, async_client: AsyncBeeperDesktop) -> None: + search = await async_client.search.contacts( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + assert_matches_type(SearchContactsResponse, search, path=["response"]) + + @parametrize + async def test_raw_response_contacts(self, async_client: AsyncBeeperDesktop) -> None: + response = await async_client.search.with_raw_response.contacts( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + search = await response.parse() + assert_matches_type(SearchContactsResponse, search, path=["response"]) + + @parametrize + async def test_streaming_response_contacts(self, async_client: AsyncBeeperDesktop) -> None: + async with async_client.search.with_streaming_response.contacts( + account_id="local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + search = await response.parse() + assert_matches_type(SearchContactsResponse, search, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_contacts(self, async_client: AsyncBeeperDesktop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_id` but received ''"): + await async_client.search.with_raw_response.contacts( + account_id="", + query="x", + ) From 5e058450070fedbd9730bd6ec57fa392974d09e1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:14:01 +0000 Subject: [PATCH 19/20] chore(internal): detect missing future annotations with ruff --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d3a4a85..4da43df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -246,6 +248,8 @@ unfixable = [ "T203", ] +extend-safe-fixes = ["FA102"] + [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" From 7f1a2a5e46fff43555450aa8d69a89a7d7b19d1a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:14:17 +0000 Subject: [PATCH 20/20] release: 0.2.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 27 +++++++++++++++++++++++++++ pyproject.toml | 2 +- src/beeper_desktop_api/_version.py | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969..10f3091 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "0.2.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b45f837 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +## 0.2.0 (2025-10-11) + +Full Changelog: [v0.0.1...v0.2.0](https://github.com/beeper/desktop-api-python/compare/v0.0.1...v0.2.0) + +### Features + +* **api:** manual updates ([86218ff](https://github.com/beeper/desktop-api-python/commit/86218ff03f8a0cd42050b0c3babdf78178fda3da)) +* **api:** manual updates ([0fcd71f](https://github.com/beeper/desktop-api-python/commit/0fcd71f9951498d349fb816b42dc21347f3ab5dc)) +* **api:** manual updates ([dce7124](https://github.com/beeper/desktop-api-python/commit/dce712498ff2678222fd203118e7bb91f13ccfc5)) +* **api:** manual updates ([48b4b7f](https://github.com/beeper/desktop-api-python/commit/48b4b7f01064d016b84e954f9aa9f327863cc1d3)) +* **api:** manual updates ([c9f3b2d](https://github.com/beeper/desktop-api-python/commit/c9f3b2d3a7fb7e2ce3b30de215497079fff3aca9)) +* **api:** manual updates ([7c655fb](https://github.com/beeper/desktop-api-python/commit/7c655fb94ba070083173c15a501be7a0f119a38b)) +* **api:** manual updates ([88bce73](https://github.com/beeper/desktop-api-python/commit/88bce73dfef13b6a1cdef0749dc3078af97255e4)) +* **api:** manual updates ([1ea87ff](https://github.com/beeper/desktop-api-python/commit/1ea87ff08b4b50541e3c26bef6f4bd581af6324c)) +* **api:** manual updates ([b1ba1c0](https://github.com/beeper/desktop-api-python/commit/b1ba1c0584b99ab402f7c1643c13c19881baa600)) +* **api:** manual updates ([545ed69](https://github.com/beeper/desktop-api-python/commit/545ed69d7251f47a309f2f46ee4f3b8e4cf1cc60)) +* **api:** remove limit from list routes ([d5cb6c2](https://github.com/beeper/desktop-api-python/commit/d5cb6c2ee132bc3d558552df145082396c80521c)) + + +### Chores + +* configure new SDK language ([d0b2ca6](https://github.com/beeper/desktop-api-python/commit/d0b2ca6bd2e9331cd42fe5143e0f94861502f11f)) +* configure new SDK language ([d11b464](https://github.com/beeper/desktop-api-python/commit/d11b4641e572db6ceb07bb4b9d47b97beadd9253)) +* **internal:** detect missing future annotations with ruff ([5e05845](https://github.com/beeper/desktop-api-python/commit/5e058450070fedbd9730bd6ec57fa392974d09e1)) +* update SDK settings ([0d38dfa](https://github.com/beeper/desktop-api-python/commit/0d38dfa50d797ff879df6d5c633bbcb43c3a98fd)) diff --git a/pyproject.toml b/pyproject.toml index 4da43df..d4f0594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "beeper_desktop_api" -version = "0.0.1" +version = "0.2.0" description = "The official Python library for the beeperdesktop API" dynamic = ["readme"] license = "MIT" diff --git a/src/beeper_desktop_api/_version.py b/src/beeper_desktop_api/_version.py index 3ba6273..49311d4 100644 --- a/src/beeper_desktop_api/_version.py +++ b/src/beeper_desktop_api/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "beeper_desktop_api" -__version__ = "0.0.1" # x-release-please-version +__version__ = "0.2.0" # x-release-please-version