getstream 3.4.0__tar.gz → 3.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. {getstream-3.4.0 → getstream-3.5.0}/.github/workflows/release.yml +0 -4
  2. getstream-3.5.0/CHANGELOG.md +147 -0
  3. {getstream-3.4.0 → getstream-3.5.0}/PKG-INFO +1 -1
  4. getstream-3.5.0/getstream/__init__.py +9 -0
  5. {getstream-3.4.0 → getstream-3.5.0}/getstream/base.py +132 -73
  6. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/async_rest_client.py +62 -1
  7. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/rest_client.py +60 -1
  8. {getstream-3.4.0 → getstream-3.5.0}/getstream/common/async_rest_client.py +29 -0
  9. {getstream-3.4.0 → getstream-3.5.0}/getstream/common/rest_client.py +29 -0
  10. {getstream-3.4.0 → getstream-3.5.0}/getstream/config.py +6 -0
  11. getstream-3.5.0/getstream/exceptions.py +326 -0
  12. {getstream-3.4.0 → getstream-3.5.0}/getstream/feeds/feeds.py +2 -0
  13. {getstream-3.4.0 → getstream-3.5.0}/getstream/feeds/rest_client.py +69 -0
  14. {getstream-3.4.0 → getstream-3.5.0}/getstream/models/__init__.py +1159 -47
  15. {getstream-3.4.0 → getstream-3.5.0}/getstream/moderation/async_rest_client.py +88 -4
  16. {getstream-3.4.0 → getstream-3.5.0}/getstream/moderation/rest_client.py +86 -4
  17. {getstream-3.4.0 → getstream-3.5.0}/getstream/stream.py +157 -40
  18. getstream-3.5.0/getstream/tasks.py +104 -0
  19. {getstream-3.4.0 → getstream-3.5.0}/getstream/tests/test_webhook.py +13 -1
  20. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/async_rest_client.py +9 -0
  21. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rest_client.py +9 -0
  22. {getstream-3.4.0 → getstream-3.5.0}/getstream/webhook.py +9 -3
  23. {getstream-3.4.0 → getstream-3.5.0}/pyproject.toml +2 -1
  24. {getstream-3.4.0 → getstream-3.5.0}/uv.lock +30 -4
  25. getstream-3.4.0/CHANGELOG.md +0 -69
  26. getstream-3.4.0/getstream/__init__.py +0 -2
  27. {getstream-3.4.0 → getstream-3.5.0}/.cursor/worktrees.json +0 -0
  28. {getstream-3.4.0 → getstream-3.5.0}/.env.example +0 -0
  29. {getstream-3.4.0 → getstream-3.5.0}/.github/actions/python-uv-setup/action.yml +0 -0
  30. {getstream-3.4.0 → getstream-3.5.0}/.github/workflows/ci.yml +0 -0
  31. {getstream-3.4.0 → getstream-3.5.0}/.github/workflows/run_tests.yml +0 -0
  32. {getstream-3.4.0 → getstream-3.5.0}/.github/workflows/stream-py.code-workspace +0 -0
  33. {getstream-3.4.0 → getstream-3.5.0}/.gitignore +0 -0
  34. {getstream-3.4.0 → getstream-3.5.0}/.gitmodules +0 -0
  35. {getstream-3.4.0 → getstream-3.5.0}/.pre-commit-config.yaml +0 -0
  36. {getstream-3.4.0 → getstream-3.5.0}/AGENTS.md +0 -0
  37. {getstream-3.4.0 → getstream-3.5.0}/DEVELOPMENT.md +0 -0
  38. {getstream-3.4.0 → getstream-3.5.0}/LICENSE.md +0 -0
  39. {getstream-3.4.0 → getstream-3.5.0}/MIGRATION_v2_to_v3.md +0 -0
  40. {getstream-3.4.0 → getstream-3.5.0}/Makefile +0 -0
  41. {getstream-3.4.0 → getstream-3.5.0}/README.md +0 -0
  42. {getstream-3.4.0 → getstream-3.5.0}/dev.py +0 -0
  43. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  44. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  45. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  46. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  47. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  48. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  49. {getstream-3.4.0 → getstream-3.5.0}/docs/migration-from-stream-chat-python/README.md +0 -0
  50. {getstream-3.4.0 → getstream-3.5.0}/generate.sh +0 -0
  51. {getstream-3.4.0 → getstream-3.5.0}/generate_webrtc.sh +0 -0
  52. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/__init__.py +0 -0
  53. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/async_channel.py +0 -0
  54. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/async_client.py +0 -0
  55. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/channel.py +0 -0
  56. {getstream-3.4.0 → getstream-3.5.0}/getstream/chat/client.py +0 -0
  57. {getstream-3.4.0 → getstream-3.5.0}/getstream/common/__init__.py +0 -0
  58. {getstream-3.4.0 → getstream-3.5.0}/getstream/common/async_client.py +0 -0
  59. {getstream-3.4.0 → getstream-3.5.0}/getstream/common/client.py +0 -0
  60. {getstream-3.4.0 → getstream-3.5.0}/getstream/common/telemetry.py +0 -0
  61. {getstream-3.4.0 → getstream-3.5.0}/getstream/feeds/__init__.py +0 -0
  62. {getstream-3.4.0 → getstream-3.5.0}/getstream/feeds/client.py +0 -0
  63. {getstream-3.4.0 → getstream-3.5.0}/getstream/generic.py +0 -0
  64. {getstream-3.4.0 → getstream-3.5.0}/getstream/meta.py +0 -0
  65. {getstream-3.4.0 → getstream-3.5.0}/getstream/moderation/__init__.py +0 -0
  66. {getstream-3.4.0 → getstream-3.5.0}/getstream/moderation/async_client.py +0 -0
  67. {getstream-3.4.0 → getstream-3.5.0}/getstream/moderation/client.py +0 -0
  68. {getstream-3.4.0 → getstream-3.5.0}/getstream/rate_limit.py +0 -0
  69. {getstream-3.4.0 → getstream-3.5.0}/getstream/stream_response.py +0 -0
  70. {getstream-3.4.0 → getstream-3.5.0}/getstream/utils/__init__.py +0 -0
  71. {getstream-3.4.0 → getstream-3.5.0}/getstream/utils/event_emitter.py +0 -0
  72. {getstream-3.4.0 → getstream-3.5.0}/getstream/utils/retry.py +0 -0
  73. {getstream-3.4.0 → getstream-3.5.0}/getstream/version.py +0 -0
  74. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/__init__.py +0 -0
  75. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/async_call.py +0 -0
  76. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/async_client.py +0 -0
  77. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/call.py +0 -0
  78. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/client.py +0 -0
  79. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/openai.py +0 -0
  80. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/README.md +0 -0
  81. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/__init__.py +0 -0
  82. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/audio_track.py +0 -0
  83. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/connection_manager.py +0 -0
  84. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/connection_utils.py +0 -0
  85. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/coordinator/__init__.py +0 -0
  86. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/coordinator/backoff.py +0 -0
  87. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/coordinator/errors.py +0 -0
  88. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/coordinator/ws.py +0 -0
  89. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/encoders_patches.py +0 -0
  90. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/g711.py +0 -0
  91. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/location_discovery.py +0 -0
  92. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/models.py +0 -0
  93. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/network_monitor.py +0 -0
  94. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/participants.py +0 -0
  95. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/__init__.py +0 -0
  96. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  97. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  98. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  99. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  100. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  101. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  102. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  103. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  104. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  105. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  106. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  107. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  108. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  109. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/pc.py +0 -0
  110. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/peer_connection.py +0 -0
  111. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/reconnection.py +0 -0
  112. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/recording.py +0 -0
  113. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/signaling.py +0 -0
  114. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/stats_reporter.py +0 -0
  115. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/stats_tracer.py +0 -0
  116. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/tracer.py +0 -0
  117. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/track_util.py +0 -0
  118. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/tracks.py +0 -0
  119. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/twirp_client_wrapper.py +0 -0
  120. {getstream-3.4.0 → getstream-3.5.0}/getstream/video/rtc/utils.py +0 -0
  121. {getstream-3.4.0 → getstream-3.5.0}/pytest.ini +0 -0
@@ -64,13 +64,9 @@ jobs:
64
64
  if: github.event_name == 'pull_request' && steps.already_released.outputs.value != 'true'
65
65
  env:
66
66
  PR_TITLE: ${{ github.event.pull_request.title }}
67
- PR_BODY: ${{ github.event.pull_request.body }}
68
67
  run: |
69
- PR_BODY_FILE=$(mktemp)
70
- printf '%s' "$PR_BODY" > "$PR_BODY_FILE"
71
68
  python3 scripts/release/bump_version.py \
72
69
  --title "$PR_TITLE" \
73
- --body-file "$PR_BODY_FILE" \
74
70
  --output "$GITHUB_OUTPUT"
75
71
 
76
72
  - name: Determine version bump (manual)
@@ -0,0 +1,147 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Standardized error hierarchy ([CHA-2958](https://linear.app/stream/issue/CHA-2958)).
13
+ New exception classes importable from `getstream` (also re-exported from
14
+ `getstream.exceptions`):
15
+ - `StreamException`: abstract base for every SDK error.
16
+ - `StreamApiException`: any HTTP 4xx/5xx response. Carries `status_code`,
17
+ `code`, `message`, `exception_fields`, `unrecoverable`,
18
+ `raw_response_body`, `more_info`, `details`. The `unrecoverable` flag
19
+ from `APIError` is now surfaced (was previously dropped on most paths).
20
+ - `StreamRateLimitException`: subclass of `StreamApiException` raised on
21
+ HTTP 429. Adds `retry_after: datetime.timedelta | None`, parsed from
22
+ `Retry-After` per RFC 7231 (integer seconds or HTTP-date). Missing or
23
+ unparseable headers map to `None`; past HTTP-dates clamp to `0`.
24
+ - `StreamTransportException`: raised when a network-layer failure (no
25
+ HTTP response received) propagates out of `httpx` — connection reset,
26
+ timeout, TLS handshake failure, DNS failure. Carries `error_type`
27
+ enum (`connection_reset` / `timeout` / `dns_failure` /
28
+ `tls_handshake_failed` / `unknown`). The original `httpx` exception
29
+ is preserved as `__cause__`.
30
+ - `StreamTaskException`: raised by `wait_for_task` when the polled task
31
+ ends in `status='failed'`. Carries `task_id`, `error_type`,
32
+ `description`, `stack_trace`, `version`.
33
+ - `Stream.wait_for_task(task_id, *, poll_interval=1.0, timeout=60.0)` and
34
+ the matching async coroutine on `AsyncStream`. Polls `get_task` until the
35
+ task reaches a terminal state. On `completed` returns the
36
+ `StreamResponse[GetTaskResponse]`; on `failed` raises
37
+ `StreamTaskException` populated from `ErrorResult`; on timeout raises
38
+ `StreamTransportException(error_type='timeout')`.
39
+
40
+ - Explicit HTTP connection pool configuration ([CHA-2956](https://linear.app/stream/issue/CHA-2956/connection-pooling)).
41
+ Four new kwargs on `Stream(...)` and `AsyncStream(...)`:
42
+ - `max_conns_per_host: int`: default `5`
43
+ - `idle_timeout: float` (seconds): default `55.0`
44
+ - `connect_timeout: float` (seconds): default `10.0`
45
+ - `request_timeout: float` (seconds): default `30.0` (was `6.0`; see Behavior changes)
46
+
47
+ These tune the underlying `httpx.Limits` and `httpx.Timeout`. The existing `http_client=` and `transport=` kwargs continue to act as escape hatches; when `http_client` is set, none of the four new kwargs apply. Env-var fallbacks for the new kwargs: `STREAM_MAX_CONNS_PER_HOST`, `STREAM_IDLE_TIMEOUT`, `STREAM_CONNECT_TIMEOUT`, `STREAM_REQUEST_TIMEOUT`.
48
+ - INFO log on client construction (logger `getstream`) lists the effective pool config and whether a user-supplied `http_client` is in use.
49
+
50
+ - Webhook handling spec helpers (CHA-2961): `UnknownEvent` dataclass for
51
+ forward-compat; `gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload`
52
+ primitives; `parse_event` (returns typed event or `UnknownEvent` for
53
+ unrecognized discriminators); `verify_signature` canonical alias of
54
+ `verify_webhook_signature`; `verify_and_parse_webhook` HTTP composite
55
+ (gunzip + verify + parse); `parse_sqs` and `parse_sns` queue composites
56
+ (no signature parameter: queue transports are authenticated by AWS IAM,
57
+ so the backend emits no HMAC for queue messages today). Transparent gzip
58
+ via magic-byte detection.
59
+ - New instance methods on `Stream` and `AsyncStream`:
60
+ `verify_signature(body, signature)` and
61
+ `verify_and_parse_webhook(body, signature)` that drop the api_secret parameter
62
+ in favor of the client's stored secret. Dual API: the module-level functions
63
+ in `getstream.webhook` remain available for callers who want explicit
64
+ secret control.
65
+ - New instance methods on `Stream` / `AsyncStream`: `parse_sqs(message_body)`,
66
+ `parse_sns(notification_body)` (no signature; AWS IAM).
67
+ - `InvalidWebhookError` exception type covering both signature mismatches and
68
+ malformed payloads. Distinguish failure modes via the exception message or
69
+ `__cause__` chain.
70
+ - Conformance fixture suite under `tests/fixtures/webhooks/` (13 happy-path
71
+ event directories + 8 negative cases) for SDK conformance testing across
72
+ language ports.
73
+ - Regenerated from the latest chat OpenAPI spec. New endpoints: `moderation.analyze`,
74
+ `moderation.bulk_action_appeals`, `moderation.get_setup_session`,
75
+ `moderation.upsert_setup_session`; `feeds.get_or_create_follow`,
76
+ `feeds.get_or_create_unfollow`, `feeds.get_user_interests`; `chat.create_segment`,
77
+ `chat.update_segment`, `chat.add_segment_targets`; `common.cancel_import_v2_task`;
78
+ `video.report_client_call_event`, together with the request/response models backing them.
79
+ - New webhook event types `moderation.image_analysis.complete` and
80
+ `moderation.text_analysis.complete`, with `ModerationImageAnalysisCompleteEvent`
81
+ and `ModerationTextAnalysisCompleteEvent` models.
82
+
83
+ ### Changed
84
+
85
+ - HTTP request errors raised by `httpx` (the `httpx.RequestError` family —
86
+ `ConnectError`, `ReadTimeout`, etc.) are now wrapped at the SDK boundary
87
+ in `StreamTransportException` so callers handle one Stream error category
88
+ instead of catching `httpx.RequestError` separately. The original `httpx`
89
+ exception is preserved via `__cause__` (CHA-2958).
90
+ - **Default `request_timeout` is now `30.0` seconds (was `6.0`).** Aligns stream-py with the cross-SDK contract in CHA-2956. Existing callers using `timeout=` are unaffected; `timeout` is kept as an alias for `request_timeout`. Callers relying on the 6s ceiling for fail-fast behavior should pass `request_timeout=6.0` (or `timeout=6.0`) explicitly.
91
+ - Default HTTP transport now caps connections per host at `5` and closes idle sockets after `55.0s`. Previous default was httpx's `100` max-connections with `5.0s` keep-alive expiry.
92
+ - No breaking changes. All existing webhook helpers (`verify_webhook_signature`,
93
+ `parse_webhook_event`, `get_event_type`, event type constants) are preserved.
94
+ - `FlagResponse` now represents the full flag record (`created_at`, `updated_at`,
95
+ `target_message`, `target_user`, `user`, `reason`, `details`, `custom`, and related
96
+ fields). The moderation flag-action acknowledgement, which carries `item_id` and
97
+ `duration`, moved to the new `FlagItemResponse`; `moderation.flag()` now returns
98
+ `FlagItemResponse`. The `/api/v2/moderation/flag` wire response is unchanged, so code
99
+ reading `item_id`/`duration` off the parsed response is unaffected. Code referencing
100
+ the `FlagResponse` type for those two fields should switch to `FlagItemResponse`.
101
+ - `ChannelInput.config_overrides` and `ChannelDataUpdate.config_overrides` are now typed
102
+ as `ChannelConfigOverrides` (the override-specific field set) instead of `ChannelConfig`.
103
+ - `enabled` on `DeliveryReceiptsResponse`, `ReadReceiptsResponse`, and
104
+ `TypingIndicatorsResponse` is now a required `bool` (was `Optional[bool]`).
105
+ - `LLMRule.description`, `TargetResolution.bitrate`, and `TranslationSettings.languages` /
106
+ `TranslationSettings.enabled` are now optional.
107
+
108
+ ### Deprecated
109
+
110
+ - `getstream.base.StreamAPIException` (capital `API`) is now an alias for
111
+ `getstream.exceptions.StreamApiException` (lowercase `Api`). Importing the
112
+ old name emits `DeprecationWarning`; existing `isinstance` / `except` /
113
+ `pytest.raises` checks continue to work because the alias resolves to the
114
+ same class. The legacy spelling will be removed one minor cycle after this
115
+ release (CHA-2958 §10).
116
+
117
+ ### Notes
118
+
119
+ - Per-call `timeout=httpx.Timeout(...)` continues to work through `.get(...)`, `.post(...)`, etc., and pre-empts the client-level `request_timeout`.
120
+
121
+ ## [3.0.0b1] - 2026-02-27
122
+
123
+ ### Breaking Changes
124
+
125
+ - 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.
126
+ - `Event` (WebSocket envelope type) renamed to `WSEvent`. Base event type renamed from `BaseEvent` to `Event` (with field `type` instead of `T`).
127
+ - Event composition changed from monolithic `*Preset` embeds to modular `Has*` types.
128
+ - `Pager` renamed to `PagerResponse` and migrated from offset-based to cursor-based pagination (`next`/`prev` tokens).
129
+ - Types that were previously `dict` or `TypedDict` (e.g., `User`, `Channel`, `Message`) are now full dataclasses with typed fields.
130
+
131
+ ### Added
132
+
133
+ - Full product coverage: Chat, Video, Moderation, and Feeds APIs are all supported in a single SDK.
134
+ - **Feeds**: activities, feeds, feed groups, follows, comments, reactions, collections, bookmarks, membership levels, feed views, and more.
135
+ - **Video**: calls, recordings, transcription, closed captions, SFU, call statistics, user feedback analytics, and more.
136
+ - **Moderation**: flags, review queue, moderation rules, config, appeals, moderation logs, and more.
137
+ - Push notification types, preferences, and templates.
138
+ - 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.
139
+ - Cursor-based pagination across all list endpoints.
140
+
141
+ ## [2.7.1] - 2026-02-18
142
+
143
+ ## [2.7.0] - 2026-02-03
144
+
145
+ ## [2.6.0] - 2025-12-11
146
+
147
+ ## [2.5.22] - 2025-10-15
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstream
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: GetStream Python SDK - Build scalable activity feeds, chat, and video calling applications
5
5
  Author-email: sachaarbonel <sacha.arbonel@hotmail.fr>, tbarbugli <tbarbugli@gmail.com>
6
6
  License-File: LICENSE.md
@@ -0,0 +1,9 @@
1
+ from getstream.exceptions import ( # noqa: F401
2
+ StreamApiException,
3
+ StreamException,
4
+ StreamRateLimitException,
5
+ StreamTaskException,
6
+ StreamTransportException,
7
+ )
8
+ from getstream.stream import Stream # noqa: F401
9
+ from getstream.stream import AsyncStream # noqa: F401
@@ -1,13 +1,18 @@
1
1
  import json
2
+ import logging
2
3
  import mimetypes
3
4
  import os
4
5
  import time
5
6
  import uuid
7
+ import warnings
6
8
  import asyncio
7
9
  from typing import Any, Dict, List, Optional, Tuple, Type, cast, get_origin
8
10
 
9
- from getstream.models import APIError
10
- from getstream.rate_limit import extract_rate_limit
11
+ from getstream.exceptions import (
12
+ StreamApiException,
13
+ build_api_exception,
14
+ wrap_transport_error,
15
+ )
11
16
  from getstream.stream_response import StreamResponse
12
17
  from getstream.generic import T
13
18
  import httpx
@@ -27,6 +32,51 @@ from getstream.common.telemetry import (
27
32
  import ijson
28
33
 
29
34
 
35
+ # ── Connection pool defaults (CHA-2956) ──────────────────────────────
36
+ # Kept in sync with getstream.stream constants; duplicated here so BaseClient/AsyncBaseClient can be instantiated standalone (e.g. by sub-clients constructed directly without going through Stream/AsyncStream).
37
+ DEFAULT_MAX_CONNS_PER_HOST = 5
38
+ DEFAULT_IDLE_TIMEOUT = 55.0
39
+ DEFAULT_CONNECT_TIMEOUT = 10.0
40
+
41
+
42
+ logger = logging.getLogger("getstream")
43
+
44
+
45
+ def _resolve_pool_knobs(obj):
46
+ """Pull the 3 pool knobs off ``obj`` if BaseStream has set them, else fall back to spec defaults. Top-level ``Stream``/``AsyncStream`` sets them on ``self`` before calling ``super().__init__()``, so a directly instantiated sub-client (or test fixture) still gets sane values.
47
+
48
+ `is None` (not truthiness) so an explicit `0` / `0.0` from the caller is preserved rather than silently swapped for a default.
49
+ """
50
+ max_conns_per_host = getattr(obj, "max_conns_per_host", None)
51
+ idle_timeout = getattr(obj, "idle_timeout", None)
52
+ connect_timeout = getattr(obj, "connect_timeout", None)
53
+ return (
54
+ DEFAULT_MAX_CONNS_PER_HOST
55
+ if max_conns_per_host is None
56
+ else max_conns_per_host,
57
+ DEFAULT_IDLE_TIMEOUT if idle_timeout is None else idle_timeout,
58
+ DEFAULT_CONNECT_TIMEOUT if connect_timeout is None else connect_timeout,
59
+ )
60
+
61
+
62
+ def _log_pool_config(cfg, *, user_http_client: bool) -> None:
63
+ if user_http_client:
64
+ logger.info(
65
+ "getstream connection pool: user_http_client=True (5 knobs not applied)"
66
+ )
67
+ else:
68
+ logger.info(
69
+ "getstream connection pool: "
70
+ "max_conns_per_host=%s idle_timeout=%ss "
71
+ "connect_timeout=%ss request_timeout=%ss "
72
+ "user_http_client=False",
73
+ cfg.max_conns_per_host,
74
+ cfg.idle_timeout,
75
+ cfg.connect_timeout,
76
+ cfg.timeout,
77
+ )
78
+
79
+
30
80
  def _read_file_bytes(file_path: str) -> bytes:
31
81
  with open(file_path, "rb") as f:
32
82
  return f.read()
@@ -56,9 +106,7 @@ class ResponseParserMixin:
56
106
  self, response: httpx.Response, data_type: Type[T]
57
107
  ) -> StreamResponse[T]:
58
108
  if response.status_code >= 399:
59
- raise StreamAPIException(
60
- response=response,
61
- )
109
+ raise build_api_exception(response)
62
110
 
63
111
  try:
64
112
  parsed_result = json.loads(response.text) if response.text else {}
@@ -72,10 +120,8 @@ class ResponseParserMixin:
72
120
  else:
73
121
  data = cast(T, parsed_result)
74
122
 
75
- except (ValueError, AttributeError):
76
- raise StreamAPIException(
77
- response=response,
78
- )
123
+ except (ValueError, AttributeError) as err:
124
+ raise StreamApiException(response=response) from err
79
125
 
80
126
  return StreamResponse(response, data)
81
127
 
@@ -151,12 +197,17 @@ class BaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
151
197
  timeout=None,
152
198
  user_agent=None,
153
199
  ):
200
+ # The 3 pool knobs (max_conns_per_host, idle_timeout, connect_timeout) are set on ``self`` by BaseStream prior to this call when used via the top-level ``Stream``/``AsyncStream`` constructors. Sub-clients constructed directly use the spec defaults via _resolve_pool_knobs.
201
+ max_conns_per_host, idle_timeout, connect_timeout = _resolve_pool_knobs(self)
154
202
  super().__init__(
155
203
  api_key=api_key,
156
204
  base_url=base_url,
157
205
  token=token,
158
206
  timeout=timeout,
159
207
  user_agent=user_agent,
208
+ max_conns_per_host=max_conns_per_host,
209
+ idle_timeout=idle_timeout,
210
+ connect_timeout=connect_timeout,
160
211
  )
161
212
  http_client = getattr(self, "_http_client", None)
162
213
  if http_client is not None:
@@ -173,23 +224,39 @@ class BaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
173
224
  self.client = http_client
174
225
  self._owns_http_client = False
175
226
  else:
227
+ limits = httpx.Limits(
228
+ max_connections=self.max_conns_per_host,
229
+ max_keepalive_connections=self.max_conns_per_host,
230
+ keepalive_expiry=self.idle_timeout,
231
+ )
232
+ timeout_obj = httpx.Timeout(
233
+ connect=self.connect_timeout,
234
+ read=self.timeout,
235
+ write=self.timeout,
236
+ pool=self.timeout,
237
+ )
176
238
  transport = getattr(self, "_transport", None)
177
239
  if transport is not None:
178
240
  self.client = httpx.Client(
179
241
  base_url=self.base_url or "",
180
- headers=self.headers,
242
+ headers={**self.headers, "Accept-Encoding": "gzip"},
181
243
  params=self.params,
182
- timeout=httpx.Timeout(self.timeout),
244
+ timeout=timeout_obj,
245
+ limits=limits,
183
246
  transport=transport,
184
247
  )
185
248
  else:
186
249
  self.client = httpx.Client(
187
250
  base_url=self.base_url or "",
188
- headers=self.headers,
251
+ headers={**self.headers, "Accept-Encoding": "gzip"},
189
252
  params=self.params,
190
- timeout=httpx.Timeout(self.timeout),
253
+ timeout=timeout_obj,
254
+ limits=limits,
191
255
  )
192
256
  self._owns_http_client = True
257
+ # The pool-config INFO line is emitted once by BaseStream after the
258
+ # top-level client is built, not here, to avoid one line per
259
+ # sub-client construction.
193
260
 
194
261
  def __enter__(self):
195
262
  return self
@@ -224,9 +291,12 @@ class BaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
224
291
  ) as span:
225
292
  call_kwargs = dict(kwargs)
226
293
  call_kwargs.pop("path_params", None)
227
- response = getattr(self.client, method.lower())(
228
- url_path, params=query_params, *args, **call_kwargs
229
- )
294
+ try:
295
+ response = getattr(self.client, method.lower())(
296
+ url_path, params=query_params, *args, **call_kwargs
297
+ )
298
+ except httpx.RequestError as err:
299
+ raise wrap_transport_error(err) from err
230
300
  duration = parse_duration_from_body(response.content)
231
301
  if duration:
232
302
  span.set_attribute("http.server.duration", duration)
@@ -376,7 +446,7 @@ class BaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
376
446
  Close HTTPX client.
377
447
 
378
448
  If the client was provided externally via ``http_client``, this is a
379
- no-op — the caller that created the client is responsible for closing
449
+ no-op; the caller that created the client is responsible for closing
380
450
  it.
381
451
  """
382
452
  if getattr(self, "_owns_http_client", True):
@@ -392,12 +462,17 @@ class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, A
392
462
  timeout=None,
393
463
  user_agent=None,
394
464
  ):
465
+ # The 3 pool knobs (max_conns_per_host, idle_timeout, connect_timeout) are set on ``self`` by BaseStream prior to this call when used via the top-level ``Stream``/``AsyncStream`` constructors. Sub-clients constructed directly use the spec defaults via _resolve_pool_knobs.
466
+ max_conns_per_host, idle_timeout, connect_timeout = _resolve_pool_knobs(self)
395
467
  super().__init__(
396
468
  api_key=api_key,
397
469
  base_url=base_url,
398
470
  token=token,
399
471
  timeout=timeout,
400
472
  user_agent=user_agent,
473
+ max_conns_per_host=max_conns_per_host,
474
+ idle_timeout=idle_timeout,
475
+ connect_timeout=connect_timeout,
401
476
  )
402
477
  http_client = getattr(self, "_http_client", None)
403
478
  if http_client is not None:
@@ -414,23 +489,39 @@ class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, A
414
489
  self.client = http_client
415
490
  self._owns_http_client = False
416
491
  else:
492
+ limits = httpx.Limits(
493
+ max_connections=self.max_conns_per_host,
494
+ max_keepalive_connections=self.max_conns_per_host,
495
+ keepalive_expiry=self.idle_timeout,
496
+ )
497
+ timeout_obj = httpx.Timeout(
498
+ connect=self.connect_timeout,
499
+ read=self.timeout,
500
+ write=self.timeout,
501
+ pool=self.timeout,
502
+ )
417
503
  transport = getattr(self, "_transport", None)
418
504
  if transport is not None:
419
505
  self.client = httpx.AsyncClient(
420
506
  base_url=self.base_url or "",
421
- headers=self.headers,
507
+ headers={**self.headers, "Accept-Encoding": "gzip"},
422
508
  params=self.params,
423
- timeout=httpx.Timeout(self.timeout),
509
+ timeout=timeout_obj,
510
+ limits=limits,
424
511
  transport=transport,
425
512
  )
426
513
  else:
427
514
  self.client = httpx.AsyncClient(
428
515
  base_url=self.base_url or "",
429
- headers=self.headers,
516
+ headers={**self.headers, "Accept-Encoding": "gzip"},
430
517
  params=self.params,
431
- timeout=httpx.Timeout(self.timeout),
518
+ timeout=timeout_obj,
519
+ limits=limits,
432
520
  )
433
521
  self._owns_http_client = True
522
+ # The pool-config INFO line is emitted once by BaseStream after the
523
+ # top-level client is built, not here, to avoid one line per
524
+ # sub-client construction.
434
525
 
435
526
  async def __aenter__(self):
436
527
  return self
@@ -442,7 +533,7 @@ class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, A
442
533
  """Close HTTPX async client (closes pools/keep-alives).
443
534
 
444
535
  If the client was provided externally via ``http_client``, this is a
445
- no-op — the caller that created the client is responsible for closing
536
+ no-op; the caller that created the client is responsible for closing
446
537
  it.
447
538
  """
448
539
  if getattr(self, "_owns_http_client", True):
@@ -516,9 +607,12 @@ class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, A
516
607
  call_kwargs["headers"] = call_kwargs.get("headers", {})
517
608
  call_kwargs["headers"]["Content-Type"] = "application/json"
518
609
 
519
- response = await getattr(self.client, method.lower())(
520
- url_path, params=query_params, *args, **call_kwargs
521
- )
610
+ try:
611
+ response = await getattr(self.client, method.lower())(
612
+ url_path, params=query_params, *args, **call_kwargs
613
+ )
614
+ except httpx.RequestError as err:
615
+ raise wrap_transport_error(err) from err
522
616
  duration = parse_duration_from_body(response.content)
523
617
  if duration:
524
618
  span.set_attribute("http.server.duration", duration)
@@ -633,54 +727,19 @@ class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, A
633
727
  )
634
728
 
635
729
 
636
- class StreamAPIException(Exception):
637
- """
638
- A custom exception for handling errors from a Stream API response.
639
-
640
- This exception is raised when an API call encounters an issue, providing
641
- detailed information from the HTTP response. It attempts to parse the response
642
- content into a structured API error. If the response content is not JSON or
643
- lacks the expected structure, it will simply report the HTTP status code.
644
-
645
- Attributes:
646
- api_error (Optional[APIError]): An optional APIError object that is
647
- populated if the response content contains structured error information.
648
- rate_limit_info (RateLimitInfo): Information about the API's rate limiting
649
- controls extracted from the response headers.
650
- http_response (httpx.Response): The full HTTP response object from httpx.
651
- status_code (int): The HTTP status code from the response.
652
-
653
- Args:
654
- response (httpx.Response): The HTTP response received from the Stream API.
655
-
656
- Raises:
657
- ValueError: If the response content cannot be parsed into JSON, indicating
658
- that the server's response was not in the expected format.
659
- """
660
-
661
- def __init__(self, response: httpx.Response) -> None:
662
- self.api_error: Optional[APIError] = None
663
- self.rate_limit_info = extract_rate_limit(response)
664
- self.http_response = response
665
- self.status_code = response.status_code
666
-
667
- try:
668
- parsed_response: Dict = json.loads(response.content)
669
- self.api_error = APIError.from_dict(parsed_response)
670
- except ValueError:
671
- pass
672
-
673
- def __str__(self) -> str:
674
- if self.api_error:
675
- return f'Stream error code {self.api_error.code}: {self.api_error.message}"'
676
- body_preview = ""
677
- try:
678
- text = self.http_response.text[:200] if self.http_response.text else ""
679
- if text:
680
- body_preview = f" body: {text}"
681
- except Exception:
682
- pass
683
- return f"Stream error HTTP code: {self.status_code}{body_preview}"
730
+ def __getattr__(name: str):
731
+ """StreamApiException is exported under its new name; resolve here lazily and warn once."""
732
+ if name == "StreamAPIException":
733
+ warnings.warn(
734
+ "getstream.base.StreamAPIException is deprecated; import "
735
+ "StreamApiException from getstream (or getstream.exceptions) "
736
+ "instead. The legacy alias will be removed one minor cycle after "
737
+ "this release.",
738
+ DeprecationWarning,
739
+ stacklevel=2,
740
+ )
741
+ return StreamApiException
742
+ raise AttributeError(f"module 'getstream.base' has no attribute {name!r}")
684
743
 
685
744
 
686
745
  def parse_duration_from_body(body: bytes) -> Optional[str]:
@@ -282,10 +282,11 @@ class ChatRestClient(AsyncBaseClient):
282
282
  self,
283
283
  limit: Optional[int] = None,
284
284
  user_id: Optional[str] = None,
285
+ groups: Optional[Dict[str, GroupedChannelsGroupRequest]] = None,
285
286
  user: Optional[UserRequest] = None,
286
287
  ) -> StreamResponse[GroupedQueryChannelsResponse]:
287
288
  json = GroupedQueryChannelsRequest(
288
- limit=limit, user_id=user_id, user=user
289
+ limit=limit, user_id=user_id, groups=groups, user=user
289
290
  ).to_dict()
290
291
  return await self.post(
291
292
  "/api/v2/chat/channels/grouped", GroupedQueryChannelsResponse, json=json
@@ -1657,6 +1658,30 @@ class ChatRestClient(AsyncBaseClient):
1657
1658
  "/api/v2/chat/search", SearchResponse, query_params=query_params
1658
1659
  )
1659
1660
 
1661
+ @telemetry.operation_name("getstream.api.chat.create_segment")
1662
+ async def create_segment(
1663
+ self,
1664
+ type: str,
1665
+ all_sender_channels: Optional[bool] = None,
1666
+ all_users: Optional[bool] = None,
1667
+ description: Optional[str] = None,
1668
+ id: Optional[str] = None,
1669
+ name: Optional[str] = None,
1670
+ filter: Optional[Dict[str, object]] = None,
1671
+ ) -> StreamResponse[CreateSegmentResponse]:
1672
+ json = CreateSegmentRequest(
1673
+ type=type,
1674
+ all_sender_channels=all_sender_channels,
1675
+ all_users=all_users,
1676
+ description=description,
1677
+ id=id,
1678
+ name=name,
1679
+ filter=filter,
1680
+ ).to_dict()
1681
+ return await self.post(
1682
+ "/api/v2/chat/segments", CreateSegmentResponse, json=json
1683
+ )
1684
+
1660
1685
  @telemetry.operation_name("getstream.api.chat.query_segments")
1661
1686
  async def query_segments(
1662
1687
  self,
@@ -1691,6 +1716,42 @@ class ChatRestClient(AsyncBaseClient):
1691
1716
  "/api/v2/chat/segments/{id}", GetSegmentResponse, path_params=path_params
1692
1717
  )
1693
1718
 
1719
+ @telemetry.operation_name("getstream.api.chat.update_segment")
1720
+ async def update_segment(
1721
+ self,
1722
+ id: str,
1723
+ description: Optional[str] = None,
1724
+ name: Optional[str] = None,
1725
+ filter: Optional[Dict[str, object]] = None,
1726
+ ) -> StreamResponse[UpdateSegmentResponse]:
1727
+ path_params = {
1728
+ "id": id,
1729
+ }
1730
+ json = UpdateSegmentRequest(
1731
+ description=description, name=name, filter=filter
1732
+ ).to_dict()
1733
+ return await self.put(
1734
+ "/api/v2/chat/segments/{id}",
1735
+ UpdateSegmentResponse,
1736
+ path_params=path_params,
1737
+ json=json,
1738
+ )
1739
+
1740
+ @telemetry.operation_name("getstream.api.chat.add_segment_targets")
1741
+ async def add_segment_targets(
1742
+ self, id: str, target_ids: List[str]
1743
+ ) -> StreamResponse[Response]:
1744
+ path_params = {
1745
+ "id": id,
1746
+ }
1747
+ json = AddSegmentTargetsRequest(target_ids=target_ids).to_dict()
1748
+ return await self.post(
1749
+ "/api/v2/chat/segments/{id}/addtargets",
1750
+ Response,
1751
+ path_params=path_params,
1752
+ json=json,
1753
+ )
1754
+
1694
1755
  @telemetry.operation_name("getstream.api.chat.delete_segment_targets")
1695
1756
  async def delete_segment_targets(
1696
1757
  self, id: str, target_ids: List[str]