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..10f3091 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.2.0" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index abfa216..831b150 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 +configured_endpoints: 15 +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/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/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..f06fa5b 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] @@ -33,8 +33,7 @@ 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) +accounts = client.accounts.list() ``` While you can provide a `access_token` keyword argument, @@ -57,8 +56,7 @@ client = AsyncBeeperDesktop( async def main() -> None: - user_info = await client.token.info() - print(user_info.sub) + accounts = await client.accounts.list() asyncio.run(main()) @@ -73,8 +71,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()`: @@ -90,8 +88,7 @@ async def main() -> None: access_token="My Access Token", http_client=DefaultAioHttpClient(), ) as client: - user_info = await client.token.info() - print(user_info.sub) + accounts = await client.accounts.list() asyncio.run(main()) @@ -106,6 +103,22 @@ 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`. +## 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 +135,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 +181,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 +204,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,16 +245,16 @@ 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/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` @@ -247,7 +263,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(): @@ -342,7 +358,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/api.md b/api.md index 83f0189..119d6f4 100644 --- a/api.md +++ b/api.md @@ -4,30 +4,77 @@ from beeper_desktop_api.types import Attachment, BaseResponse, Error, Message, Reaction, User ``` +# BeeperDesktop + +Types: + +```python +from beeper_desktop_api.types import DownloadAssetResponse, FocusResponse, SearchResponse +``` + +Methods: + +- client.download_asset(\*\*params) -> DownloadAssetResponse +- client.focus(\*\*params) -> FocusResponse +- 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 + +# Search + +Types: + +```python +from beeper_desktop_api.types import SearchContactsResponse ``` +Methods: + +- client.search.chats(\*\*params) -> SyncCursorSearch[Chat] +- client.search.contacts(account_id, \*\*params) -> SearchContactsResponse + # Chats Types: ```python -from beeper_desktop_api.types import Chat +from beeper_desktop_api.types import Chat, ChatCreateResponse, ChatListResponse ``` -# Token +Methods: + +- client.chats.create(\*\*params) -> ChatCreateResponse +- client.chats.retrieve(chat_id, \*\*params) -> Chat +- client.chats.list(\*\*params) -> SyncCursorList[ChatListResponse] +- client.chats.archive(chat_id, \*\*params) -> BaseResponse + +## 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 UserInfo +from beeper_desktop_api.types import MessageSendResponse ``` Methods: -- client.token.info() -> UserInfo +- client.messages.list(chat_id, \*\*params) -> SyncCursorList[Message] +- client.messages.search(\*\*params) -> SyncCursorSearch[Message] +- client.messages.send(chat_id, \*\*params) -> MessageSendResponse 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..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" @@ -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"] @@ -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" 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/_client.py b/src/beeper_desktop_api/_client.py index 44866d0..82c9d75 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_focus_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 search, accounts, 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.focus_response import FocusResponse +from .types.search_response import SearchResponse +from .types.download_asset_response import DownloadAssetResponse __all__ = [ "Timeout", @@ -43,7 +64,10 @@ class BeeperDesktop(SyncAPIClient): - token: token.TokenResource + accounts: accounts.AccountsResource + search: search.SearchResource + chats: chats.ChatsResource + messages: messages.MessagesResource with_raw_response: BeeperDesktopWithRawResponse with_streaming_response: BeeperDesktopWithStreamedResponse @@ -101,7 +125,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.token = token.TokenResource(self) + self.accounts = accounts.AccountsResource(self) + self.search = search.SearchResource(self) + self.chats = chats.ChatsResource(self) + self.messages = messages.MessagesResource(self) self.with_raw_response = BeeperDesktopWithRawResponse(self) self.with_streaming_response = BeeperDesktopWithStreamedResponse(self) @@ -176,6 +203,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/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 focus( + 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, + ) -> FocusResponse: + """ + Focus 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/focus", + body=maybe_transform( + { + "chat_id": chat_id, + "draft_attachment_path": draft_attachment_path, + "draft_text": draft_text, + "message_id": message_id, + }, + client_focus_params.ClientFocusParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FocusResponse, + ) + + 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,7 +365,10 @@ def _make_status_error( class AsyncBeeperDesktop(AsyncAPIClient): - token: token.AsyncTokenResource + accounts: accounts.AsyncAccountsResource + search: search.AsyncSearchResource + chats: chats.AsyncChatsResource + messages: messages.AsyncMessagesResource with_raw_response: AsyncBeeperDesktopWithRawResponse with_streaming_response: AsyncBeeperDesktopWithStreamedResponse @@ -269,7 +426,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.token = token.AsyncTokenResource(self) + self.accounts = accounts.AsyncAccountsResource(self) + self.search = search.AsyncSearchResource(self) + self.chats = chats.AsyncChatsResource(self) + self.messages = messages.AsyncMessagesResource(self) self.with_raw_response = AsyncBeeperDesktopWithRawResponse(self) self.with_streaming_response = AsyncBeeperDesktopWithStreamedResponse(self) @@ -344,6 +504,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/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 focus( + 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, + ) -> FocusResponse: + """ + Focus 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/focus", + body=await async_maybe_transform( + { + "chat_id": chat_id, + "draft_attachment_path": draft_attachment_path, + "draft_text": draft_text, + "message_id": message_id, + }, + client_focus_params.ClientFocusParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FocusResponse, + ) + + 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,22 +667,74 @@ def _make_status_error( class BeeperDesktopWithRawResponse: def __init__(self, client: BeeperDesktop) -> None: - self.token = token.TokenResourceWithRawResponse(client.token) + self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts) + 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.focus = to_raw_response_wrapper( + client.focus, + ) + self.search = to_raw_response_wrapper( + client.search, + ) class AsyncBeeperDesktopWithRawResponse: def __init__(self, client: AsyncBeeperDesktop) -> None: - self.token = token.AsyncTokenResourceWithRawResponse(client.token) + self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts) + 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.focus = async_to_raw_response_wrapper( + client.focus, + ) + self.search = async_to_raw_response_wrapper( + client.search, + ) class BeeperDesktopWithStreamedResponse: def __init__(self, client: BeeperDesktop) -> None: - self.token = token.TokenResourceWithStreamingResponse(client.token) + self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts) + 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.focus = to_streamed_response_wrapper( + client.focus, + ) + self.search = to_streamed_response_wrapper( + client.search, + ) class AsyncBeeperDesktopWithStreamedResponse: def __init__(self, client: AsyncBeeperDesktop) -> None: - self.token = token.AsyncTokenResourceWithStreamingResponse(client.token) + self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts) + 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.focus = async_to_streamed_response_wrapper( + client.focus, + ) + self.search = async_to_streamed_response_wrapper( + client.search, + ) Client = BeeperDesktop diff --git a/src/beeper_desktop_api/_version.py b/src/beeper_desktop_api/_version.py index 72a9009..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" +__version__ = "0.2.0" # x-release-please-version diff --git a/src/beeper_desktop_api/pagination.py b/src/beeper_desktop_api/pagination.py index 4606312..7fd745f 100644 --- a/src/beeper_desktop_api/pagination.py +++ b/src/beeper_desktop_api/pagination.py @@ -1,18 +1,23 @@ # 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 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]): +@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) oldest_cursor: Optional[str] = FieldInfo(alias="oldestCursor", default=None) @@ -42,7 +47,7 @@ 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) @@ -70,3 +75,69 @@ def next_page_info(self) -> Optional[PageInfo]: 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) + + @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]: + items = self.items + if not items: + return None + + 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) + + @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]: + items = self.items + if not items: + return None + + 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/__init__.py b/src/beeper_desktop_api/resources/__init__.py index 7c3b25f..eedc9bc 100644 --- a/src/beeper_desktop_api/resources/__init__.py +++ b/src/beeper_desktop_api/resources/__init__.py @@ -1,19 +1,61 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from .token import ( - TokenResource, - AsyncTokenResource, - TokenResourceWithRawResponse, - AsyncTokenResourceWithRawResponse, - TokenResourceWithStreamingResponse, - AsyncTokenResourceWithStreamingResponse, +from .chats import ( + ChatsResource, + AsyncChatsResource, + ChatsResourceWithRawResponse, + AsyncChatsResourceWithRawResponse, + ChatsResourceWithStreamingResponse, + AsyncChatsResourceWithStreamingResponse, +) +from .search import ( + SearchResource, + AsyncSearchResource, + SearchResourceWithRawResponse, + AsyncSearchResourceWithRawResponse, + SearchResourceWithStreamingResponse, + AsyncSearchResourceWithStreamingResponse, +) +from .accounts import ( + AccountsResource, + AsyncAccountsResource, + AccountsResourceWithRawResponse, + AsyncAccountsResourceWithRawResponse, + AccountsResourceWithStreamingResponse, + AsyncAccountsResourceWithStreamingResponse, +) +from .messages import ( + MessagesResource, + AsyncMessagesResource, + MessagesResourceWithRawResponse, + AsyncMessagesResourceWithRawResponse, + MessagesResourceWithStreamingResponse, + AsyncMessagesResourceWithStreamingResponse, ) __all__ = [ - "TokenResource", - "AsyncTokenResource", - "TokenResourceWithRawResponse", - "AsyncTokenResourceWithRawResponse", - "TokenResourceWithStreamingResponse", - "AsyncTokenResourceWithStreamingResponse", + "AccountsResource", + "AsyncAccountsResource", + "AccountsResourceWithRawResponse", + "AsyncAccountsResourceWithRawResponse", + "AccountsResourceWithStreamingResponse", + "AsyncAccountsResourceWithStreamingResponse", + "SearchResource", + "AsyncSearchResource", + "SearchResourceWithRawResponse", + "AsyncSearchResourceWithRawResponse", + "SearchResourceWithStreamingResponse", + "AsyncSearchResourceWithStreamingResponse", + "ChatsResource", + "AsyncChatsResource", + "ChatsResourceWithRawResponse", + "AsyncChatsResourceWithRawResponse", + "ChatsResourceWithStreamingResponse", + "AsyncChatsResourceWithStreamingResponse", + "MessagesResource", + "AsyncMessagesResource", + "MessagesResourceWithRawResponse", + "AsyncMessagesResourceWithRawResponse", + "MessagesResourceWithStreamingResponse", + "AsyncMessagesResourceWithStreamingResponse", ] diff --git a/src/beeper_desktop_api/resources/accounts.py b/src/beeper_desktop_api/resources/accounts.py new file mode 100644 index 0000000..1210fce --- /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): + """Manage connected chat accounts""" + + @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): + """Manage connected chat accounts""" + + @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..7752636 --- /dev/null +++ b/src/beeper_desktop_api/resources/chats/chats.py @@ -0,0 +1,578 @@ +# 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 Literal + +import httpx + +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 +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 SyncCursorList, AsyncCursorList +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 + +__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. + + 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 list( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + 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. + # 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, + ) -> SyncCursorList[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: 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. + + 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=SyncCursorList[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, + }, + chat_list_params.ChatListParams, + ), + ), + model=ChatListResponse, + ) + + 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: Unique identifier of the chat. + + 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, + ) + + +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. + + 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, + ) + + def list( + self, + *, + account_ids: SequenceNotStr[str] | Omit = omit, + 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. + # 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, AsyncCursorList[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: 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. + + 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=AsyncCursorList[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, + }, + chat_list_params.ChatListParams, + ), + ), + model=ChatListResponse, + ) + + 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: Unique identifier of the chat. + + 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, + ) + + +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.list = to_raw_response_wrapper( + chats.list, + ) + self.archive = to_raw_response_wrapper( + chats.archive, + ) + + @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.list = async_to_raw_response_wrapper( + chats.list, + ) + self.archive = async_to_raw_response_wrapper( + chats.archive, + ) + + @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.list = to_streamed_response_wrapper( + chats.list, + ) + self.archive = to_streamed_response_wrapper( + chats.archive, + ) + + @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.list = async_to_streamed_response_wrapper( + chats.list, + ) + self.archive = async_to_streamed_response_wrapper( + chats.archive, + ) + + @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..bf628ae --- /dev/null +++ b/src/beeper_desktop_api/resources/chats/reminders.py @@ -0,0 +1,263 @@ +# 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: Unique identifier of the chat. + + 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: Unique identifier of the chat. + + 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: Unique identifier of the chat. + + 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: Unique identifier of the chat. + + 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/messages.py b/src/beeper_desktop_api/resources/messages.py new file mode 100644 index 0000000..ad07df3 --- /dev/null +++ b/src/beeper_desktop_api/resources/messages.py @@ -0,0 +1,543 @@ +# 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_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 +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 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 + +__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 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. + # 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, + ) -> SyncCursorList[Message]: + """List all messages in a chat with cursor-based pagination. + + Sorted by timestamp. + + Args: + chat_id: Unique identifier of the chat. + + 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. + + 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_api_list( + f"/v1/chats/{chat_id}/messages", + page=SyncCursorList[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "cursor": cursor, + "direction": direction, + }, + message_list_params.MessageListParams, + ), + ), + model=Message, + ) + + 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, + ) -> SyncCursorSearch[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. + + 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/search/messages", + page=SyncCursorSearch[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 + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + f"/v1/chats/{chat_id}/messages", + body=maybe_transform( + { + "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 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. + # 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, AsyncCursorList[Message]]: + """List all messages in a chat with cursor-based pagination. + + Sorted by timestamp. + + Args: + chat_id: Unique identifier of the chat. + + 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. + + 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_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=maybe_transform( + { + "cursor": cursor, + "direction": direction, + }, + message_list_params.MessageListParams, + ), + ), + model=Message, + ) + + 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, AsyncCursorSearch[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. + + 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/search/messages", + page=AsyncCursorSearch[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 + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + f"/v1/chats/{chat_id}/messages", + body=await async_maybe_transform( + { + "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.list = to_raw_response_wrapper( + messages.list, + ) + 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.list = async_to_raw_response_wrapper( + messages.list, + ) + 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.list = to_streamed_response_wrapper( + messages.list, + ) + 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.list = async_to_streamed_response_wrapper( + messages.list, + ) + 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/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/resources/token.py b/src/beeper_desktop_api/resources/token.py deleted file mode 100644 index fbf0425..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/stainless-sdks/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/stainless-sdks/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/stainless-sdks/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/stainless-sdks/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 bb86833..6aeb07d 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,24 @@ Attachment as Attachment, BaseResponse as BaseResponse, ) -from .user_info import UserInfo as UserInfo +from .account import Account as Account +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_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 .message_search_params import MessageSearchParams as MessageSearchParams +from .message_send_response import MessageSendResponse as MessageSendResponse +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/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_list_params.py b/src/beeper_desktop_api/types/chat_list_params.py new file mode 100644 index 0000000..d216046 --- /dev/null +++ b/src/beeper_desktop_api/types/chat_list_params.py @@ -0,0 +1,24 @@ +# 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 + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" + + 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. + """ 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/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/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_focus_params.py b/src/beeper_desktop_api/types/client_focus_params.py new file mode 100644 index 0000000..6359eb2 --- /dev/null +++ b/src/beeper_desktop_api/types/client_focus_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__ = ["ClientFocusParams"] + + +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. + + 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/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/focus_response.py b/src/beeper_desktop_api/types/focus_response.py new file mode 100644 index 0000000..28875b1 --- /dev/null +++ b/src/beeper_desktop_api/types/focus_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["FocusResponse"] + + +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 new file mode 100644 index 0000000..e6a04d2 --- /dev/null +++ b/src/beeper_desktop_api/types/message_list_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_extensions import Literal, TypedDict + +__all__ = ["MessageListParams"] + + +class MessageListParams(TypedDict, total=False): + cursor: str + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" + + 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. + """ 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..93fbd63 --- /dev/null +++ b/src/beeper_desktop_api/types/message_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 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.""" + + 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..840e745 --- /dev/null +++ b/src/beeper_desktop_api/types/message_send_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 Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["MessageSendParams"] + + +class MessageSendParams(TypedDict, total=False): + 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/search_chats_params.py b/src/beeper_desktop_api/types/search_chats_params.py new file mode 100644 index 0000000..d393720 --- /dev/null +++ b/src/beeper_desktop_api/types/search_chats_params.py @@ -0,0 +1,78 @@ +# 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__ = ["SearchChatsParams"] + + +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 + only + """ + + cursor: str + """Opaque pagination cursor; do not inspect. Use together with 'direction'.""" + + 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. + """ + + 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/search_contacts_params.py b/src/beeper_desktop_api/types/search_contacts_params.py new file mode 100644 index 0000000..3e0352b --- /dev/null +++ b/src/beeper_desktop_api/types/search_contacts_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__ = ["SearchContactsParams"] + + +class SearchContactsParams(TypedDict, total=False): + query: Required[str] + """Text to search users by. Network-specific behavior.""" diff --git a/src/beeper_desktop_api/types/search_contacts_response.py b/src/beeper_desktop_api/types/search_contacts_response.py new file mode 100644 index 0000000..1bbf6db --- /dev/null +++ b/src/beeper_desktop_api/types/search_contacts_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__ = ["SearchContactsResponse"] + + +class SearchContactsResponse(BaseModel): + items: List[User] 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/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/src/beeper_desktop_api/types/user_info.py b/src/beeper_desktop_api/types/user_info.py deleted file mode 100644 index d023e31..0000000 --- a/src/beeper_desktop_api/types/user_info.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__ = ["UserInfo"] - - -class UserInfo(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/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..84cfdb1 --- /dev/null +++ b/tests/api_resources/test_chats.py @@ -0,0 +1,386 @@ +# 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, + ChatListResponse, + ChatCreateResponse, +) +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") + + +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_list(self, client: BeeperDesktop) -> None: + chat = client.chats.list() + assert_matches_type(SyncCursorList[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|c29tZUltc2dQYWdl", + direction="before", + ) + assert_matches_type(SyncCursorList[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(SyncCursorList[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(SyncCursorList[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( + 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="", + ) + + +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_list(self, async_client: AsyncBeeperDesktop) -> None: + chat = await async_client.chats.list() + assert_matches_type(AsyncCursorList[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|c29tZUltc2dQYWdl", + direction="before", + ) + assert_matches_type(AsyncCursorList[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(AsyncCursorList[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(AsyncCursorList[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( + 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="", + ) diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py new file mode 100644 index 0000000..54e150e --- /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 ( + FocusResponse, + 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_focus(self, client: BeeperDesktop) -> None: + client_ = client.focus() + assert_matches_type(FocusResponse, client_, path=["response"]) + + @parametrize + 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(FocusResponse, client_, path=["response"]) + + @parametrize + 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(FocusResponse, client_, path=["response"]) + + @parametrize + 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(FocusResponse, 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_focus(self, async_client: AsyncBeeperDesktop) -> None: + client = await async_client.focus() + assert_matches_type(FocusResponse, client, path=["response"]) + + @parametrize + 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(FocusResponse, client, path=["response"]) + + @parametrize + 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(FocusResponse, client, path=["response"]) + + @parametrize + 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(FocusResponse, 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_messages.py b/tests/api_resources/test_messages.py new file mode 100644 index 0000000..d64cf44 --- /dev/null +++ b/tests/api_resources/test_messages.py @@ -0,0 +1,313 @@ +# 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 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") + + +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(SyncCursorList[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="1725489123456|c29tZUltc2dQYWdl", + direction="before", + ) + assert_matches_type(SyncCursorList[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(SyncCursorList[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(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() + assert_matches_type(SyncCursorSearch[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(SyncCursorSearch[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(SyncCursorSearch[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(SyncCursorSearch[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 + + @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( + "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(AsyncCursorList[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="1725489123456|c29tZUltc2dQYWdl", + direction="before", + ) + assert_matches_type(AsyncCursorList[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(AsyncCursorList[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(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() + assert_matches_type(AsyncCursorSearch[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(AsyncCursorSearch[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(AsyncCursorSearch[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(AsyncCursorSearch[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 + + @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="", + ) 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", + ) 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 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"