Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5f647cd
feat: update by openapi refactor
mogita Feb 16, 2026
7538729
test: update test cases
mogita Feb 16, 2026
2cea235
test: suppress warning messages for webhook tests
mogita Feb 16, 2026
fb7e50f
feat: replace legacy WebSocketClientProtocol with ClientConnection
mogita Feb 16, 2026
8f2d86f
feat: update by openapi refactor
mogita Feb 17, 2026
83583ea
fix: sort serialization, datetime handling and field name mapping
mogita Feb 17, 2026
557deaf
feat: request body to use to_dict()
mogita Feb 18, 2026
069f552
feat: explicitly set return value to string
mogita Feb 18, 2026
90e55bc
fix: use the correct name of stop campaign
mogita Feb 19, 2026
2aaf810
feat: update by openapi refactor
mogita Feb 27, 2026
e05e8e9
test: fine tuning
mogita Feb 27, 2026
13e02a7
test: fine tuning
mogita Feb 27, 2026
eef6070
docs: add changelog and migration guide
mogita Feb 27, 2026
cd392f6
build: extract version from dist file
mogita Feb 27, 2026
d635d5a
feat: add chat test parity with stream-chat-python
Mar 2, 2026
41a813f
chore: regenerate SDK from latest OpenAPI spec
Mar 2, 2026
84f7c26
fix: fix test failures to match actual API responses
Mar 2, 2026
61e4d48
fix: correct skip reasons after investigating backend behavior
Mar 2, 2026
064c70e
fix: fix test_delete_message_for_me - add user as channel member
Mar 2, 2026
bc791a0
fix: fix test_query_message_flags to match getstream-go approach
Mar 2, 2026
9689bd2
feat: match chat test parity with getstream-go and split CI credentials
Mar 2, 2026
f195524
fix: add STREAM_CHAT_BASE_URL for non-video CI tests
Mar 2, 2026
c16166e
fix: properly separate video and non-video tests in CI
Mar 2, 2026
3c0ac3d
fix: fix test_delete_channels timeout and wait_for_task
Mar 2, 2026
77c45ff
style: fix ruff formatting in tests/base.py
Mar 2, 2026
c895a8e
refactor: reorganize test_chat_channel.py per code review feedback
Mar 2, 2026
e1c88c8
fix: address code review feedback in test_chat_message and test_chat_…
Mar 2, 2026
f55b775
fix: extract command names as strings when restoring channel type config
Mar 2, 2026
ead8884
test: add missing chat tests for parity with stream-chat-python
Mar 2, 2026
bd26026
test: add missing chat tests for parity with stream-chat-python
Mar 3, 2026
d354b1d
fix: raise RuntimeError on task failure in wait_for_task
Mar 3, 2026
6b3ee8d
test: skip test_permissions_roles (slow and flaky)
Mar 3, 2026
bd45c2e
fix: restore video directory doctests in CI coverage
Mar 3, 2026
fd48932
ci: make step-level credentials explicit, remove redundant job-level env
Mar 4, 2026
045e757
Merge pull request #219 from GetStream/match-chat-test-parity-codegen
mogita Mar 4, 2026
3159b37
feat: update by openapi refactor
mogita Mar 4, 2026
c042b76
fix: remove extra metadata
mogita Mar 4, 2026
affbd6a
feat: update by openapi refactor
mogita Mar 4, 2026
85b346e
feat: update by openapi refactor
mogita Mar 4, 2026
ac8f611
feat: update by openapi refactor
mogita Mar 5, 2026
0f791fd
test: fine tuning
mogita Mar 5, 2026
5e3a0cf
test: fine tuning
mogita Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ jobs:
- name: Extract package version
id: get_version
run: |
VERSION=$(uvx -q hatch version)
FILE=$(ls dist/getstream-*.tar.gz | head -1)
VERSION=$(basename "$FILE" .tar.gz | sed 's/^getstream-//')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Publish to PyPI (Trusted Publishing)
Expand Down
37 changes: 27 additions & 10 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,6 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
env:
STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }}
STREAM_API_KEY: ${{ vars.STREAM_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }}
timeout-minutes: 30
steps:
- name: Checkout
Expand All @@ -81,11 +77,32 @@ jobs:
uses: ./.github/actions/python-uv-setup
with:
python-version: ${{ matrix.python-version }}
- name: Debug environment variables
- name: Run non-video tests
env:
STREAM_API_KEY: ${{ vars.STREAM_CHAT_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_CHAT_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_CHAT_BASE_URL }}
run: |
echo "STREAM_API_KEY is set: ${{ env.STREAM_API_KEY != '' }}"
echo "STREAM_API_SECRET is set: ${{ env.STREAM_API_SECRET != '' }}"
echo "STREAM_BASE_URL is set: ${{ env.STREAM_BASE_URL != '' }}"
- name: Run tests
run: uv run pytest -m "${{ inputs.marker }}" tests/ getstream/
uv run pytest -m "${{ inputs.marker }}" tests/ getstream/ \
--ignore=tests/rtc \
--ignore=tests/test_video_examples.py \
--ignore=tests/test_video_integration.py \
--ignore=tests/test_video_openai.py \
--ignore=tests/test_signaling.py \
--ignore=tests/test_audio_stream_track.py \
--ignore=getstream/video
- name: Run video tests
env:
STREAM_API_KEY: ${{ vars.STREAM_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }}
run: |
uv run pytest -m "${{ inputs.marker }}" \
tests/rtc \
tests/test_video_examples.py \
tests/test_video_integration.py \
tests/test_video_openai.py \
tests/test_signaling.py \
tests/test_audio_stream_track.py \
getstream/video

34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0b1] - 2026-02-27

### Breaking Changes

- Type names across all products now follow the OpenAPI spec naming convention: response types are suffixed with `Response`, input types with `Request`. See [MIGRATION_v2_to_v3.md](./MIGRATION_v2_to_v3.md) for the complete rename mapping.
- `Event` (WebSocket envelope type) renamed to `WSEvent`. Base event type renamed from `BaseEvent` to `Event` (with field `type` instead of `T`).
- Event composition changed from monolithic `*Preset` embeds to modular `Has*` types.
- `Pager` renamed to `PagerResponse` and migrated from offset-based to cursor-based pagination (`next`/`prev` tokens).
- Types that were previously `dict` or `TypedDict` (e.g., `User`, `Channel`, `Message`) are now full dataclasses with typed fields.

### Added

- Full product coverage: Chat, Video, Moderation, and Feeds APIs are all supported in a single SDK.
- **Feeds**: activities, feeds, feed groups, follows, comments, reactions, collections, bookmarks, membership levels, feed views, and more.
- **Video**: calls, recordings, transcription, closed captions, SFU, call statistics, user feedback analytics, and more.
- **Moderation**: flags, review queue, moderation rules, config, appeals, moderation logs, and more.
- Push notification types, preferences, and templates.
- Webhook support: `WHEvent` envelope class for receiving webhook payloads, utility functions for decoding and verifying webhook signatures, and a full set of individual typed event dataclasses for every event across all products (Chat, Video, Moderation, Feeds) usable as discriminated event types.
- Cursor-based pagination across all list endpoints.

## [2.7.1] - 2026-02-18

## [2.7.0] - 2026-02-03

## [2.6.0] - 2025-12-11

## [2.5.22] - 2025-10-15
210 changes: 210 additions & 0 deletions MIGRATION_v2_to_v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Migration Guide: v2 → v3

This guide covers all breaking changes when upgrading from `getstream` (stream-py) v2 to v3.

## Overview

v3 is a full OpenAPI-aligned release. The primary change is a **systematic type renaming**: types that appear in API responses now have a `Response` suffix, and input types have a `Request` suffix. There are no removed features — all functionality from v2 is available in v3. Additionally, v3 adds complete coverage of the **Feeds**, **Video**, and **Moderation** product APIs.

Types that were previously plain `dict` or `TypedDict` (e.g., `User`, `Channel`, `Message`) are now full dataclasses with typed fields and IDE autocompletion support.

## Installation

```bash
pip install getstream==3.*
```

Or to install the pre-release:

```bash
pip install --pre getstream
```

## Naming Conventions

- **Classes**: `PascalCase` (e.g., `UserResponse`, `MessageRequest`)
- **Fields/attributes**: `snake_case` (e.g., `user.created_at`, `message.reply_count`)

The general renaming rules:

- Classes returned in API responses: `Foo` → `FooResponse`
- Classes used as API inputs: `Foo` → `FooRequest`
- Some moderation action payloads: `FooRequest` → `FooRequestPayload`

## Breaking Changes

### Common / Shared Types

| v2 | v3 | Notes |
| --- | --- | --- |
| `ApplicationConfig` | `AppResponseFields` | App configuration in responses |
| `ChannelPushPreferences` | `ChannelPushPreferencesResponse` | Per-channel push settings |
| `Device` | `DeviceResponse` | Device data (push, voip) |
| `Event` | `WSEvent` | WebSocket event envelope |
| `FeedsPreferences` | `FeedsPreferencesResponse` | Feeds push preferences |
| `ImportV2Task` | `ImportV2TaskItem` | V2 import task |
| `OwnUser` | `OwnUserResponse` | Authenticated user data (was `dict`) |
| `Pager` | `PagerResponse` | Now cursor-based (`next`/`prev`) |
| `PushPreferences` | `PushPreferencesResponse` | Push preferences |
| `PushTemplate` | `PushTemplateResponse` | Push template |
| `PrivacySettings` | `PrivacySettingsResponse` | Typing indicators, read receipts |
| `RateLimitInfo` | `LimitInfoResponse` | Rate limit info |
| `SortParam` | `SortParamRequest` | Sort parameter for queries |
| `User` | `UserResponse` | Full user in responses (was `dict`) |
| `UserBlock` | `BlockedUserResponse` | Blocked user details |
| `UserCustomEvent` | `CustomEvent` | Custom user event |
| `UserMute` | `UserMuteResponse` | User mute details |

### Event System

| Before (v2) | After (v3) | Notes |
| --- | --- | --- |
| `BaseEvent` (field `T`) | `Event` (field `type`) | Base event type |
| `Event` (WS envelope) | `WSEvent` | WebSocket event wrapper |
| `*Preset` embeds | `Has*` composition types | e.g., `HasChannel`, `HasMessage` |
| — | `WHEvent` | New webhook envelope type |

### Chat Types

| v2 | v3 | Notes |
| --- | --- | --- |
| `Campaign` | `CampaignResponse` | |
| `CampaignStats` | `CampaignStatsResponse` | |
| `Channel` | `ChannelResponse` | Was `dict`, now dataclass |
| `ChannelConfigFields` | `ChannelConfigWithInfo` | Channel config + commands/grants |
| `ChannelMember` | `ChannelMemberResponse` | |
| `ChannelTypeConfigWithInfo` | `ChannelTypeConfig` | |
| `ConfigOverrides` | `ConfigOverridesRequest` | |
| `DraftMessage` / `DraftMessagePayload` | `DraftResponse` | Two types merged into one |
| `Message` | `MessageResponse` | Was `dict`, now dataclass |
| `MessageReminder` | `ReminderResponseData` | |
| `PendingMessage` | `PendingMessageResponse` | |
| `Poll` | `PollResponse` | |
| `PollOption` | `PollOptionResponse` | |
| `PollVote` | `PollVoteResponse` | |
| `Reaction` | `ReactionResponse` | |
| `ReadState` | `ReadStateResponse` | |
| `Thread` | `ThreadResponse` | |

### Video Types

| v2 | v3 | Notes |
| --- | --- | --- |
| `AudioSettings` | `AudioSettingsResponse` | |
| `BackstageSettings` | `BackstageSettingsResponse` | |
| `BroadcastSettings` | `BroadcastSettingsResponse` | |
| `Call` | `CallResponse` | Was `dict`, now dataclass |
| `CallEgress` | `EgressResponse` | |
| `CallMember` | `MemberResponse` | Note: not `CallMemberResponse` |
| `CallParticipant` | `CallParticipantResponse` | |
| `CallParticipantFeedback` | *(removed)* | Use `CollectUserFeedbackRequest` |
| `CallSession` | `CallSessionResponse` | |
| `CallSettings` | `CallSettingsResponse` | |
| `CallType` | `CallTypeResponse` | |
| `EventNotificationSettings` | `EventNotificationSettingsResponse` | |
| `FrameRecordSettings` | `FrameRecordingSettingsResponse` | `Recording` inserted in name |
| `GeofenceSettings` | `GeofenceSettingsResponse` | |
| `HLSSettings` | `HLSSettingsResponse` | |
| `IndividualRecordSettings` | `IndividualRecordingSettingsResponse` | `Recording` inserted in name |
| `IngressSettings` | `IngressSettingsResponse` | |
| `IngressSource` | `IngressSourceResponse` | |
| `IngressAudioEncodingOptions` | `IngressAudioEncodingResponse` | Shortened name |
| `IngressVideoEncodingOptions` | `IngressVideoEncodingResponse` | Shortened name |
| `IngressVideoLayer` | `IngressVideoLayerResponse` | |
| `LimitsSettings` | `LimitsSettingsResponse` | |
| `NotificationSettings` | `NotificationSettingsResponse` | |
| `RawRecordSettings` | `RawRecordingSettingsResponse` | `Recording` inserted in name |
| `RecordSettings` | `RecordSettingsResponse` | |
| `RingSettings` | `RingSettingsResponse` | |
| `RTMPSettings` | `RTMPSettingsResponse` | |
| `ScreensharingSettings` | `ScreensharingSettingsResponse` | |
| `SessionSettings` | `SessionSettingsResponse` | |
| `SIPCallConfigs` | `SIPCallConfigsResponse` | |
| `SIPCallerConfigs` | `SIPCallerConfigsResponse` | |
| `SIPDirectRoutingRuleCallConfigs` | `SIPDirectRoutingRuleCallConfigsResponse` | |
| `SIPInboundRoutingRules` | `SIPInboundRoutingRuleResponse` | Plural → singular |
| `SIPPinProtectionConfigs` | `SIPPinProtectionConfigsResponse` | |
| `SIPTrunk` | `SIPTrunkResponse` | |
| `ThumbnailsSettings` | `ThumbnailsSettingsResponse` | |
| `TranscriptionSettings` | `TranscriptionSettingsResponse` | |
| `VideoSettings` | `VideoSettingsResponse` | |

### Moderation Types

| v2 | v3 | Notes |
| --- | --- | --- |
| `ActionLog` | `ActionLogResponse` | |
| `Appeal` | `AppealItemResponse` | |
| `AutomodDetails` | `AutomodDetailsResponse` | |
| `Ban` | `BanInfoResponse` | |
| `BanOptions` | *(removed)* | Merged into `BanActionRequestPayload` |
| `BanActionRequest` | `BanActionRequestPayload` | |
| `BlockActionRequest` | `BlockActionRequestPayload` | |
| `BlockedMessage` | *(removed)* | Internal only |
| `CustomActionRequest` | `CustomActionRequestPayload` | |
| `DeleteMessageRequest` | `DeleteMessageRequestPayload` | |
| `DeleteUserRequest` | `DeleteUserRequestPayload` | |
| `EntityCreator` | `EntityCreatorResponse` | |
| `Evaluation` | `EvaluationResponse` | |
| `FeedsModerationTemplate` | `QueryFeedModerationTemplate` | No `Response` suffix |
| `FeedsModerationTemplateConfig` | `FeedsModerationTemplateConfigPayload` | |
| `Flag` | *(removed)* | Use `ModerationFlagResponse` |
| `Flag2` | `ModerationFlagResponse` | Was minimal type defs, now full dataclass |
| `FlagDetails` | `FlagDetailsResponse` | |
| `FlagFeedback` | `FlagFeedbackResponse` | |
| `FlagMessageDetails` | `FlagMessageDetailsResponse` | |
| `FlagReport` | *(removed)* | Internal only |
| `FutureChannelBan` | `FutureChannelBanResponse` | |
| `MarkReviewedRequest` | `MarkReviewedRequestPayload` | |
| `Match` | `MatchResponse` | |
| `ModerationActionConfig` | `ModerationActionConfigResponse` | |
| `ModerationBulkSubmitActionRequest` | `BulkSubmitActionRequest` | `Moderation` prefix dropped |
| `ModerationConfig` | `ConfigResponse` | |
| `ModerationFlags` | *(removed)* | Use `List[ModerationFlagResponse]` |
| `ModerationLog` | *(removed)* | Use `ActionLogResponse` |
| `ModerationLogResponse` | *(removed)* | Use `QueryModerationLogsResponse` |
| `ModerationUsageStats` | `ModerationUsageStatsResponse` | |
| `RestoreActionRequest` | `RestoreActionRequestPayload` | |
| `ReviewQueueItem` | `ReviewQueueItemResponse` | |
| `Rule` | `RuleResponse` | |
| `ShadowBlockActionRequest` | `ShadowBlockActionRequestPayload` | |
| `Task` | `TaskResponse` | |
| `Trigger` | `TriggerResponse` | |
| `UnbanActionRequest` | `UnbanActionRequestPayload` | |
| `UnblockActionRequest` | `UnblockActionRequestPayload` | |
| `VideoEndCallRequest` | `VideoEndCallRequestPayload` | |
| `VideoKickUserRequest` | `VideoKickUserRequestPayload` | |

### Feeds Types

| v2 | v3 | Notes |
| --- | --- | --- |
| `Activity` | `ActivityResponse` | Was `dict`, now dataclass |
| `ActivityFeedback` | `ActivityFeedbackRequest` | Request-only (no `Response` suffix) |
| `ActivityMark` | `MarkActivityRequest` | |
| `ActivityPin` | `ActivityPinResponse` | |
| `AggregatedActivity` | `AggregatedActivityResponse` | |
| `Bookmark` | `BookmarkResponse` | |
| `BookmarkFolder` | `BookmarkFolderResponse` | |
| `Collection` | `CollectionResponse` | |
| `Comment` | `CommentResponse` | |
| `CommentMedia` | *(removed)* | Embedded inline in `CommentResponse` |
| `CommentMention` | *(removed)* | Embedded inline in `CommentResponse` |
| `DenormalizedFeedsReaction` | *(removed)* | Internal only |
| `Feed` | `FeedResponse` | |
| `FeedGroup` | `FeedGroupResponse` | |
| `FeedMember` | `FeedMemberResponse` | |
| `FeedsReaction` | `FeedsReactionResponse` | |
| `FeedsReactionGroup` | `FeedsReactionGroupResponse` | |
| `FeedSuggestion` | `FeedSuggestionResponse` | |
| `FeedView` | `FeedViewResponse` | |
| `FeedVisibilityInfo` | `FeedVisibilityResponse` | |
| `Follow` | `FollowResponse` | |
| `MembershipLevel` | `MembershipLevelResponse` | |
| `ThreadedComment` | `ThreadedCommentResponse` | |

## Getting Help

- [Stream documentation](https://getstream.io/docs/)
- [GitHub Issues](https://github.com/GetStream/stream-py/issues)
- [Stream support](https://getstream.io/contact/support/)
21 changes: 15 additions & 6 deletions getstream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _parse_response(
else:
data = cast(T, parsed_result)

except ValueError:
except (ValueError, AttributeError):
raise StreamAPIException(
response=response,
)
Expand Down Expand Up @@ -523,12 +523,21 @@ def __init__(self, response: httpx.Response) -> None:
def __str__(self) -> str:
if self.api_error:
return f'Stream error code {self.api_error.code}: {self.api_error.message}"'
else:
return f"Stream error HTTP code: {self.status_code}"
body_preview = ""
try:
text = self.http_response.text[:200] if self.http_response.text else ""
if text:
body_preview = f" body: {text}"
except Exception:
pass
return f"Stream error HTTP code: {self.status_code}{body_preview}"


def parse_duration_from_body(body: bytes) -> Optional[str]:
for prefix, event, value in ijson.parse(body):
if prefix == "duration" and event == "string":
return value
try:
for prefix, event, value in ijson.parse(body):
if prefix == "duration" and event == "string":
return value
except (ijson.common.IncompleteJSONError, ijson.common.JSONError):
pass
return None
Loading