getstream 7.0.0__tar.gz → 8.0.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 (113) hide show
  1. {getstream-7.0.0 → getstream-8.0.0}/.release-please-manifest.json +1 -1
  2. {getstream-7.0.0 → getstream-8.0.0}/CHANGELOG.md +36 -0
  3. getstream-8.0.0/MIGRATION_v7_to_v8.md +114 -0
  4. {getstream-7.0.0 → getstream-8.0.0}/PKG-INFO +13 -1
  5. {getstream-7.0.0 → getstream-8.0.0}/README.md +12 -0
  6. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/README.md +1 -0
  7. {getstream-7.0.0 → getstream-8.0.0}/getstream/__init__.py +1 -0
  8. {getstream-7.0.0 → getstream-8.0.0}/getstream/base.py +24 -14
  9. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/async_channel.py +96 -1
  10. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/async_client.py +3 -37
  11. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/async_rest_client.py +367 -88
  12. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/channel.py +96 -1
  13. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/client.py +3 -37
  14. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/rest_client.py +365 -88
  15. getstream-8.0.0/getstream/common/async_client.py +12 -0
  16. {getstream-7.0.0 → getstream-8.0.0}/getstream/common/async_rest_client.py +136 -44
  17. getstream-8.0.0/getstream/common/client.py +12 -0
  18. {getstream-7.0.0 → getstream-8.0.0}/getstream/common/rest_client.py +132 -44
  19. {getstream-7.0.0 → getstream-8.0.0}/getstream/feeds/feeds.py +23 -0
  20. {getstream-7.0.0 → getstream-8.0.0}/getstream/feeds/rest_client.py +335 -67
  21. {getstream-7.0.0 → getstream-8.0.0}/getstream/models/__init__.py +10286 -5495
  22. {getstream-7.0.0 → getstream-8.0.0}/getstream/moderation/async_rest_client.py +234 -23
  23. {getstream-7.0.0 → getstream-8.0.0}/getstream/moderation/rest_client.py +236 -23
  24. {getstream-7.0.0 → getstream-8.0.0}/getstream/tests/test_webhook.py +125 -9
  25. {getstream-7.0.0 → getstream-8.0.0}/getstream/utils/__init__.py +18 -9
  26. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/async_call.py +9 -2
  27. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/async_rest_client.py +50 -20
  28. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/call.py +9 -2
  29. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rest_client.py +50 -20
  30. {getstream-7.0.0 → getstream-8.0.0}/getstream/webhook.py +114 -57
  31. {getstream-7.0.0 → getstream-8.0.0}/pyproject.toml +1 -1
  32. {getstream-7.0.0 → getstream-8.0.0}/uv.lock +1 -1
  33. getstream-7.0.0/getstream/common/async_client.py +0 -58
  34. getstream-7.0.0/getstream/common/client.py +0 -58
  35. {getstream-7.0.0 → getstream-8.0.0}/.cursor/worktrees.json +0 -0
  36. {getstream-7.0.0 → getstream-8.0.0}/.env.example +0 -0
  37. {getstream-7.0.0 → getstream-8.0.0}/.github/actions/build-rtc-wheel/action.yml +0 -0
  38. {getstream-7.0.0 → getstream-8.0.0}/.github/actions/python-uv-setup/action.yml +0 -0
  39. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/ci.yml +0 -0
  40. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/pr_title.yml +0 -0
  41. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/release.yml +0 -0
  42. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/rtc_wheels.yml +0 -0
  43. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/run_integration.yml +0 -0
  44. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/run_tests.yml +0 -0
  45. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/scheduled_test.yml +0 -0
  46. {getstream-7.0.0 → getstream-8.0.0}/.github/workflows/stream-py.code-workspace +0 -0
  47. {getstream-7.0.0 → getstream-8.0.0}/.gitignore +0 -0
  48. {getstream-7.0.0 → getstream-8.0.0}/.gitmodules +0 -0
  49. {getstream-7.0.0 → getstream-8.0.0}/.pre-commit-config.yaml +0 -0
  50. {getstream-7.0.0 → getstream-8.0.0}/AGENTS.md +0 -0
  51. {getstream-7.0.0 → getstream-8.0.0}/DEVELOPMENT.md +0 -0
  52. {getstream-7.0.0 → getstream-8.0.0}/LICENSE.md +0 -0
  53. {getstream-7.0.0 → getstream-8.0.0}/MIGRATION_v2_to_v3.md +0 -0
  54. {getstream-7.0.0 → getstream-8.0.0}/MIGRATION_v6_to_v7.md +0 -0
  55. {getstream-7.0.0 → getstream-8.0.0}/Makefile +0 -0
  56. {getstream-7.0.0 → getstream-8.0.0}/dev.py +0 -0
  57. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  58. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  59. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  60. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  61. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  62. {getstream-7.0.0 → getstream-8.0.0}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  63. {getstream-7.0.0 → getstream-8.0.0}/generate.sh +0 -0
  64. {getstream-7.0.0 → getstream-8.0.0}/generate_webrtc.sh +0 -0
  65. {getstream-7.0.0 → getstream-8.0.0}/getstream/chat/__init__.py +0 -0
  66. {getstream-7.0.0 → getstream-8.0.0}/getstream/common/__init__.py +0 -0
  67. {getstream-7.0.0 → getstream-8.0.0}/getstream/common/telemetry.py +0 -0
  68. {getstream-7.0.0 → getstream-8.0.0}/getstream/config.py +0 -0
  69. {getstream-7.0.0 → getstream-8.0.0}/getstream/exceptions.py +0 -0
  70. {getstream-7.0.0 → getstream-8.0.0}/getstream/feeds/__init__.py +0 -0
  71. {getstream-7.0.0 → getstream-8.0.0}/getstream/feeds/client.py +0 -0
  72. {getstream-7.0.0 → getstream-8.0.0}/getstream/generic.py +0 -0
  73. {getstream-7.0.0 → getstream-8.0.0}/getstream/logging_utils.py +0 -0
  74. {getstream-7.0.0 → getstream-8.0.0}/getstream/meta.py +0 -0
  75. {getstream-7.0.0 → getstream-8.0.0}/getstream/moderation/__init__.py +0 -0
  76. {getstream-7.0.0 → getstream-8.0.0}/getstream/moderation/async_client.py +0 -0
  77. {getstream-7.0.0 → getstream-8.0.0}/getstream/moderation/client.py +0 -0
  78. {getstream-7.0.0 → getstream-8.0.0}/getstream/rate_limit.py +0 -0
  79. {getstream-7.0.0 → getstream-8.0.0}/getstream/stream.py +0 -0
  80. {getstream-7.0.0 → getstream-8.0.0}/getstream/stream_response.py +0 -0
  81. {getstream-7.0.0 → getstream-8.0.0}/getstream/tasks.py +0 -0
  82. {getstream-7.0.0 → getstream-8.0.0}/getstream/utils/event_emitter.py +0 -0
  83. {getstream-7.0.0 → getstream-8.0.0}/getstream/version.py +0 -0
  84. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/__init__.py +0 -0
  85. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/async_client.py +0 -0
  86. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/client.py +0 -0
  87. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/openai.py +0 -0
  88. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/__init__.py +0 -0
  89. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/audio_forwarder.py +0 -0
  90. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/audio_track.py +0 -0
  91. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/connection_manager.py +0 -0
  92. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/g711.py +0 -0
  93. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/participants.py +0 -0
  94. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/__init__.py +0 -0
  95. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  96. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  97. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  98. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  99. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  100. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  101. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  102. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  103. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  104. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  105. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  106. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  107. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  108. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/track_util.py +0 -0
  109. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/tracks.py +0 -0
  110. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/utils.py +0 -0
  111. {getstream-7.0.0 → getstream-8.0.0}/getstream/video/rtc/video_forwarder.py +0 -0
  112. {getstream-7.0.0 → getstream-8.0.0}/pytest.ini +0 -0
  113. {getstream-7.0.0 → getstream-8.0.0}/release-please-config.json +0 -0
@@ -1,4 +1,4 @@
1
1
  {
2
- ".": "7.0.0",
2
+ ".": "8.0.0",
3
3
  "getstream-rtc": "0.1.0"
4
4
  }
@@ -7,6 +7,42 @@ Entries from 6.1.1 on are generated by release-please from commit messages; earl
7
7
  ones were written by hand in the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
8
8
  format.
9
9
 
10
+ ## [8.0.0](https://github.com/GetStream/stream-py/compare/v7.1.0...v8.0.0) (2026-11-11)
11
+
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * new optional parameters sit in the middle of many method signatures, and a few models and return types changed. See [#313](https://github.com/GetStream/stream-py/pull/313) and the [migration guide](https://github.com/GetStream/stream-py/blob/main/MIGRATION_v7_to_v8.md) for all changes. The main changes:
16
+ * pass optional parameters by keyword: 41 methods, among them `send_message`, `query_channels`, `get_or_create_channel`, `ban`, `unban`, `get_activity` and the poll methods, have new parameters before existing ones
17
+ * `update_channel_type` makes `automod`, `automod_behavior` and `max_message_length` optional, and `submit_moderation_feedback` makes `message`, `published_at` and `reference` optional; both move them behind the required parameters
18
+ * `update_campaign` returns `UpdateCampaignResponse` and `stop_campaign` returns `StopCampaignResponse`; read the campaign from `.campaign`
19
+ * the empty request models `CancelImportV2TaskRequest`, `CommitMessageRequest`, `EndCallRequest`, `ExportFeedUserDataRequest`, `RestoreFeedGroupRequest`, `StartHLSBroadcastingRequest`, `StartPolicyTestRunRequest`, `StopAllRTMPBroadcastsRequest`, `StopCampaignRequest`, `StopFrameRecordingRequest`, `StopHLSBroadcastingRequest`, `VideoEndCallRequestPayload` and `VideoKickUserRequestPayload` are removed, as is `Time`; `EnrichedReaction.created_at` and `updated_at` are `str`
20
+ * `AIVideoConfig.provider` is removed
21
+
22
+ ### Features
23
+
24
+ * add `fork_channel`, `fork_message_ids` and `rewind_channel`, also on `Channel` as `fork`, `fork_message_ids` and `rewind` ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
25
+ * add `query_channel_bans`, `search_all`, `claim_guest_user`, `check_gcp_pub_sub` and the history window team methods `get_history_window_teams`, `set_history_window_teams` and `delete_history_window_teams` ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
26
+ * add `query_deleted_channels` to list deleted channels ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
27
+ * add scheduled messages: `send_message(scheduled_at=...)`, `query_scheduled_messages`, `update_scheduled_message`, `delete_scheduled_message` and `send_scheduled_message_now` ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
28
+ * **feeds:** add feed exports (`export_feeds_app`, `export_feeds_by_type`, `cancel_export_feeds`), retention policies (`feeds_get_retention_policy`, `feeds_set_retention_policy`, `feeds_delete_retention_policy`, `feeds_get_retention_policy_runs`), user interests (`upsert_user_interests`, `delete_user_interests`), `comment_feedback` and `get_notification_status` ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
29
+ * **moderation:** add config versions (`save_config_version`, `list_config_versions`, `get_config_version`, `publish_config_version`), `query_moderation_feedback` and `reveal_media`, plus `matched_blocklists` on `ModerationRulesTriggeredEvent` and `severities` on `ClosedCaptionRuleParameters` ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
30
+ * page through `list_imports` with `limit` and `offset`, and set `ImportV2TaskSettings.custom_format` to read custom data from the root of each item or from a nested `custom` object ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
31
+ * regenerate the client from the current Stream API ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
32
+ * **video:** add `query_call_ring_attempts` ([cc5538e](https://github.com/GetStream/stream-py/commit/cc5538ec188fc7a683c82da7959eaa18358990e7))
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * stop double-encoding list query parameters ([#306](https://github.com/GetStream/stream-py/issues/306)) ([a584118](https://github.com/GetStream/stream-py/commit/a584118a15db9bd60b7daf571fa13d52e459b6d2)), closes [#305](https://github.com/GetStream/stream-py/issues/305)
38
+
39
+ ## [7.1.0](https://github.com/GetStream/stream-py/compare/v7.0.0...v7.1.0) (2026-11-04)
40
+
41
+
42
+ ### Features
43
+
44
+ * add NULL to send an explicit null in request bodies ([#314](https://github.com/GetStream/stream-py/issues/314)) ([955535e](https://github.com/GetStream/stream-py/commit/955535ed73d2016ccffc7924ed7c0ce60c641284))
45
+
10
46
  ## [7.0.0](https://github.com/GetStream/stream-py/compare/v6.1.1...v7.0.0) (2026-10-22)
11
47
 
12
48
 
@@ -0,0 +1,114 @@
1
+ # Migration Guide: v7 → v8
2
+
3
+ v8 regenerates the client from the current Stream API. Most of the changes add new methods, models and optional parameters. This guide lists the changes that can break code written for stream-py 7.1.0.
4
+
5
+ ```bash
6
+ pip install "getstream==8.*"
7
+ ```
8
+
9
+ ## Pass optional parameters by keyword
10
+
11
+ Generated method parameters are positional-or-keyword, and v8 adds new optional parameters in the middle of many signatures. A call that passes optional arguments by position can now send a value to the wrong parameter. Calls that pass them by keyword work unchanged.
12
+
13
+ ```python
14
+ # v7: the fourth positional argument was `user_id`
15
+ client.feeds.get_activity(activity_id, "first", 10, "jane")
16
+
17
+ # v8: "jane" would go to the new `skip_own_followings`, so pass it by keyword
18
+ client.feeds.get_activity(activity_id, comment_sort="first", comment_limit=10, user_id="jane")
19
+ ```
20
+
21
+ These methods have new parameters before existing ones. Each async client has the same changes.
22
+
23
+ | Method | New parameters | First moved parameter |
24
+ |---|---|---|
25
+ | `Channel.get_or_create`, `chat.get_or_create_channel`, `chat.get_or_create_distinct_channel` | `include_scheduled_messages` | `state` |
26
+ | `Channel.send_message`, `chat.send_message` | `scheduled_at` | `skip_enrich_url` |
27
+ | `chat.query_channels` | `include_active_member_count` | `limit` |
28
+ | `chat.grouped_query_channels` | `sort` | `groups` |
29
+ | `chat.channel_batch_update` | `synchronous` | `custom_unset` |
30
+ | `chat.create_channel_type` | `quotes`, `reminders`, `automod_thresholds` | `reactions` |
31
+ | `chat.create_reminder`, `chat.update_reminder` | `expires_at` | `remind_at` |
32
+ | `chat.cast_poll_vote`, `feeds.cast_poll_vote` | `config_key`, `config_team` | `user_id` |
33
+ | `create_poll` | `config_key`, `config_team`, `team` | `description` |
34
+ | `update_poll` | `config_key`, `config_team` | `description` |
35
+ | `update_poll_partial`, `create_poll_option`, `update_poll_option` | `config_key`, `config_team` | `user_id` |
36
+ | `update_app` | `two_way_block_effect_enabled` | `user_response_time_enabled` |
37
+ | `Feed.get_or_create`, `feeds.get_or_create_feed` | `discard_actors` | `data` |
38
+ | `Feed.query_pinned_activities`, `feeds.query_pinned_activities` | `include_expired_activities`, `include_moderated_activities`, `include_soft_deleted_activities`, `user_id` | `limit` |
39
+ | `feeds.add_activity` | `skip_existing` | `skip_push` |
40
+ | `feeds.get_activity` | `skip_own_followings` | `user_id` |
41
+ | `feeds.query_activities` | `include_moderated_activities` | `include_private_activities` |
42
+ | `feeds.query_comments` | `include_soft_deleted_comments` | `limit` |
43
+ | `feeds.query_follows` | `include_group_counts` | `limit` |
44
+ | `feeds.follow_batch`, `feeds.get_or_create_follows` | `_async` | `create_users` |
45
+ | `feeds.unfollow_batch`, `feeds.get_or_create_unfollows` | `_async` | `delete_notification_activity` |
46
+ | `moderation.analyze` | `country_code`, `country_codes` | `entity_creator_id` |
47
+ | `moderation.check` | `country_code`, `country_codes` | `test_mode` |
48
+ | `moderation.appeal` | `channel_cid` | `review_queue_item_id` |
49
+ | `moderation.ban` | `ban_from_future_channels`, `delete_reactions` | `banned_by_id` |
50
+ | `moderation.unban` | `remove_future_channels_ban` | `unbanned_by_id` |
51
+ | `moderation.submit_action` | `delete_poll`, `delete_poll_answer`, `delete_poll_option` | `delete_reaction` |
52
+ | `moderation.upsert_config` | `engine`, `intent_config` | `flood_config` |
53
+
54
+ ## Parameters that are now optional
55
+
56
+ These parameters are now optional, so they moved behind the required ones. Pass them by keyword.
57
+
58
+ - `chat.update_channel_type`: `automod`, `automod_behavior` and `max_message_length`. `max_message_length` now comes after `mark_messages_pending`.
59
+ - `moderation.submit_moderation_feedback`: `message`, `published_at` and `reference`. They now come after `channel_id` and the new `confidence`, `entity_creator_id`, `entity_id`, `entity_type`, `false_positive` and `feedback_type` parameters, and before the new `review_queue_item_id` and `user_id`.
60
+
61
+ The matching request models, `UpdateChannelTypeRequest` and `SubmitModerationFeedbackRequest`, give these fields a `None` default, so they also moved in the dataclass field order.
62
+
63
+ ## Campaign responses
64
+
65
+ `update_campaign` returns `UpdateCampaignResponse` and `stop_campaign` returns `StopCampaignResponse`. v7 decoded both into `CampaignResponse`, whose fields came back as `None` because the API returns the campaign inside a `campaign` field. Read the campaign from that field.
66
+
67
+ ```python
68
+ # v7
69
+ resp = client.chat.stop_campaign(id=campaign_id)
70
+ status = resp.data.status # None
71
+
72
+ # v8
73
+ resp = client.chat.stop_campaign(id=campaign_id)
74
+ status = resp.data.campaign.status
75
+ ```
76
+
77
+ ## Removed models
78
+
79
+ These request models had no fields, because their endpoints take no request body. The methods that call those endpoints keep their signatures.
80
+
81
+ - `CancelImportV2TaskRequest`
82
+ - `CommitMessageRequest`
83
+ - `EndCallRequest`
84
+ - `ExportFeedUserDataRequest`
85
+ - `RestoreFeedGroupRequest`
86
+ - `StartHLSBroadcastingRequest`
87
+ - `StartPolicyTestRunRequest`
88
+ - `StopAllRTMPBroadcastsRequest`
89
+ - `StopCampaignRequest`
90
+ - `StopFrameRecordingRequest`
91
+ - `StopHLSBroadcastingRequest`
92
+ - `VideoEndCallRequestPayload`
93
+ - `VideoKickUserRequestPayload`
94
+
95
+ `Time` was also removed. `EnrichedReaction.created_at` and `EnrichedReaction.updated_at` are now `Optional[str]`.
96
+
97
+ ## Removed field
98
+
99
+ `AIVideoConfig.provider` was removed. Drop it from `AIVideoConfig(...)` calls in your moderation config.
100
+
101
+ ## Type annotation changes
102
+
103
+ These changes affect only type checkers. Runtime values are unchanged.
104
+
105
+ - `own_capabilities` on call, channel and feed responses is annotated as `List[str]`. The constants classes `OwnCapability`, `ChannelOwnCapability` and `FeedOwnCapability` still exist, and their values are strings.
106
+ - `CampaignResponse.sender_mode` and `CampaignResponse.sender_visibility` are now `Optional[str]`.
107
+
108
+ ## Required response fields
109
+
110
+ `ChannelMemberResponse.user_id` and `OwnUserResponse.blocked_user_ids` are now required and have no default. Responses the SDK decodes are unaffected, but code that builds these models itself, such as test fixtures, must pass both fields, or the constructor raises `TypeError` and a direct `from_dict` without `infer_missing=True` raises `KeyError`.
111
+
112
+ ## Uploads
113
+
114
+ `upload_file`, `upload_image`, `upload_channel_file` and `upload_channel_image` keep their signatures and still send `multipart/form-data`. `file` is a keyword with a `None` default in the signature, but the upload raises `ValueError` without it.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: getstream
3
- Version: 7.0.0
3
+ Version: 8.0.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
@@ -186,6 +186,18 @@ client = Stream(api_key=..., api_secret=..., retry=RetryConfig(enabled=True, max
186
186
 
187
187
  Only idempotent `GET`/`HEAD` requests are retried, and only on HTTP 429 (unless the backend marked it unrecoverable) or a transport-level failure (timeout, connection reset, DNS, TLS). A 429's `Retry-After` header is honored (clamped to `max_backoff`); otherwise the delay uses full jitter over an exponential backoff. A retried failure logs `http.request.failed` at DEBUG; a final, non-retried failure logs it at ERROR (or not at all for a final 429, since that's already covered by `http.response.received`).
188
188
 
189
+ ### Sending null
190
+
191
+ Request bodies drop `None` values inside objects, so an unset optional field never overwrites stored data. `None` items in a list are kept and sent as `null`. To store an explicit `null`, for example inside custom data, pass `NULL`:
192
+
193
+ ```python
194
+ from getstream import NULL
195
+
196
+ channel.update_channel_partial(set={"expires_at": NULL})
197
+ ```
198
+
199
+ To remove a field instead, use `unset`, for example `unset=["expires_at"]`.
200
+
189
201
  ### App configuration
190
202
 
191
203
  ```python
@@ -143,6 +143,18 @@ client = Stream(api_key=..., api_secret=..., retry=RetryConfig(enabled=True, max
143
143
 
144
144
  Only idempotent `GET`/`HEAD` requests are retried, and only on HTTP 429 (unless the backend marked it unrecoverable) or a transport-level failure (timeout, connection reset, DNS, TLS). A 429's `Retry-After` header is honored (clamped to `max_backoff`); otherwise the delay uses full jitter over an exponential backoff. A retried failure logs `http.request.failed` at DEBUG; a final, non-retried failure logs it at ERROR (or not at all for a final 429, since that's already covered by `http.response.received`).
145
145
 
146
+ ### Sending null
147
+
148
+ Request bodies drop `None` values inside objects, so an unset optional field never overwrites stored data. `None` items in a list are kept and sent as `null`. To store an explicit `null`, for example inside custom data, pass `NULL`:
149
+
150
+ ```python
151
+ from getstream import NULL
152
+
153
+ channel.update_channel_partial(set={"expires_at": NULL})
154
+ ```
155
+
156
+ To remove a field instead, use `unset`, for example `unset=["expires_at"]`.
157
+
146
158
  ### App configuration
147
159
 
148
160
  ```python
@@ -20,6 +20,7 @@
20
20
  | Custom fields | Top-level dict keys | Nested in `custom` dict |
21
21
  | Response access | Dict keys (`response["users"]`) | Typed properties (`response.data.users`) |
22
22
  | Env vars | `STREAM_CHAT_TIMEOUT` | `STREAM_API_KEY`, `STREAM_API_SECRET`, `STREAM_TIMEOUT` |
23
+ | `None` in request data | Sent as `null` | Dropped from objects, kept in lists; pass `getstream.NULL` to send `null` |
23
24
 
24
25
  ## Quick Example
25
26
 
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
 
3
+ from getstream.utils import NULL # noqa: F401
3
4
  from getstream.config import RetryConfig # noqa: F401
4
5
  from getstream.exceptions import ( # noqa: F401
5
6
  StreamApiException,
@@ -7,7 +7,7 @@ import time
7
7
  import uuid
8
8
  import warnings
9
9
  import asyncio
10
- from typing import Any, Dict, List, Optional, Tuple, Type, cast, get_origin
10
+ from typing import Any, Dict, Optional, Tuple, Type, cast, get_origin
11
11
 
12
12
  from getstream.exceptions import (
13
13
  StreamApiException,
@@ -22,6 +22,7 @@ from getstream.generic import T
22
22
  import httpx
23
23
  from getstream.config import BaseConfig
24
24
  from getstream.version import VERSION
25
+ from getstream.utils import NULL
25
26
  from urllib.parse import quote
26
27
  from abc import ABC
27
28
  from getstream.common.telemetry import (
@@ -142,9 +143,26 @@ def _read_file_bytes(file_path: str) -> bytes:
142
143
  return f.read()
143
144
 
144
145
 
146
+ def _multipart_form(body: Dict[str, Any]) -> Tuple[str, Dict[str, str]]:
147
+ """Split an upload body into the file path and the other set fields as form
148
+ values: strings as-is, anything else JSON-encoded, which is how the API reads
149
+ "user" and "upload_sizes"."""
150
+ fields = _strip_none(body)
151
+ file_path = fields.pop("file", None)
152
+ if not file_path:
153
+ raise ValueError("file path must be provided")
154
+ return file_path, {
155
+ name: value if isinstance(value, str) else json.dumps(value)
156
+ for name, value in fields.items()
157
+ }
158
+
159
+
145
160
  def _strip_none(obj):
146
161
  """Recursively remove None values from dicts so unset optional fields
147
- are omitted from the JSON body instead of being sent as null."""
162
+ are omitted from the JSON body instead of being sent as null. ``NULL``
163
+ becomes null."""
164
+ if obj is NULL:
165
+ return None
148
166
  if isinstance(obj, dict):
149
167
  return {k: _strip_none(v) for k, v in obj.items() if v is not None}
150
168
  if isinstance(obj, list):
@@ -565,23 +583,19 @@ class BaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
565
583
  self,
566
584
  path: str,
567
585
  data_type: Type[T],
568
- file_path: str,
569
586
  *,
570
587
  path_params: Optional[Dict[str, str]] = None,
571
588
  query_params: Optional[Dict[str, str]] = None,
572
- form_fields: Optional[List[Tuple[str, str]]] = None,
589
+ json: Dict[str, Any],
573
590
  ) -> StreamResponse[T]:
574
591
  """Send a multipart/form-data upload request, matching Go/PHP SDK behavior."""
592
+ file_path, data = _multipart_form(json)
575
593
  file_name = os.path.basename(file_path)
576
594
  content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
577
595
  with open(file_path, "rb") as f:
578
596
  file_content = f.read()
579
597
 
580
598
  files = {"file": (file_name, file_content, content_type)}
581
- data: Dict[str, str] = {}
582
- for field_name, field_value in form_fields or []:
583
- data[field_name] = field_value
584
-
585
599
  kwargs: Dict[str, Any] = {"files": files}
586
600
  if data:
587
601
  kwargs["data"] = data
@@ -696,23 +710,19 @@ class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, A
696
710
  self,
697
711
  path: str,
698
712
  data_type: Type[T],
699
- file_path: str,
700
713
  *,
701
714
  path_params: Optional[Dict[str, str]] = None,
702
715
  query_params: Optional[Dict[str, str]] = None,
703
- form_fields: Optional[List[Tuple[str, str]]] = None,
716
+ json: Dict[str, Any],
704
717
  ) -> StreamResponse[T]:
705
718
  """Send a multipart/form-data upload request, matching Go/PHP SDK behavior."""
719
+ file_path, data = _multipart_form(json)
706
720
  file_name = os.path.basename(file_path)
707
721
  content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
708
722
 
709
723
  file_content = await asyncio.to_thread(_read_file_bytes, file_path)
710
724
 
711
725
  files = {"file": (file_name, file_content, content_type)}
712
- data: Dict[str, str] = {}
713
- for field_name, field_value in form_fields or []:
714
- data[field_name] = field_value
715
-
716
726
  kwargs: Dict[str, Any] = {"files": files}
717
727
  if data:
718
728
  kwargs["data"] = data
@@ -18,7 +18,12 @@ class Channel:
18
18
  self.custom_data = custom_data or {}
19
19
 
20
20
  def _sync_from_response(self, data):
21
- if hasattr(data, "channel") and isinstance(data.channel, ChannelResponse):
21
+ # A fork's response carries the new channel, not this one.
22
+ if (
23
+ hasattr(data, "channel")
24
+ and isinstance(data.channel, ChannelResponse)
25
+ and data.channel.cid == f"{self.channel_type}:{self.channel_id}"
26
+ ):
22
27
  self.custom_data = data.channel.custom
23
28
 
24
29
  @attach_channel_cid_async
@@ -131,6 +136,33 @@ class Channel:
131
136
  self._sync_from_response(response.data)
132
137
  return response
133
138
 
139
+ @attach_channel_cid_async
140
+ async def query_channel_bans(
141
+ self,
142
+ exclude_expired_bans: Optional[bool] = None,
143
+ include_total: Optional[bool] = None,
144
+ limit: Optional[int] = None,
145
+ next: Optional[str] = None,
146
+ prev: Optional[str] = None,
147
+ user_id: Optional[str] = None,
148
+ filter_conditions: Optional[Dict[str, object]] = None,
149
+ user: Optional[UserRequest] = None,
150
+ ) -> StreamResponse[QueryChannelBansResponse]:
151
+ response = await self.client.query_channel_bans(
152
+ type=self.channel_type,
153
+ id=self.channel_id,
154
+ exclude_expired_bans=exclude_expired_bans,
155
+ include_total=include_total,
156
+ limit=limit,
157
+ next=next,
158
+ prev=prev,
159
+ user_id=user_id,
160
+ filter_conditions=filter_conditions,
161
+ user=user,
162
+ )
163
+ self._sync_from_response(response.data)
164
+ return response
165
+
134
166
  @attach_channel_cid_async
135
167
  async def delete_draft(
136
168
  self, parent_id: Optional[str] = None, user_id: Optional[str] = None
@@ -185,6 +217,44 @@ class Channel:
185
217
  self._sync_from_response(response.data)
186
218
  return response
187
219
 
220
+ @attach_channel_cid_async
221
+ async def fork(
222
+ self,
223
+ message_id: str,
224
+ channel: ForkChannelTarget,
225
+ exclude_attachments: Optional[bool] = None,
226
+ user_id: Optional[str] = None,
227
+ user: Optional[UserRequest] = None,
228
+ ) -> StreamResponse[ForkChannelResponse]:
229
+ response = await self.client.fork_channel(
230
+ type=self.channel_type,
231
+ id=self.channel_id,
232
+ message_id=message_id,
233
+ channel=channel,
234
+ exclude_attachments=exclude_attachments,
235
+ user_id=user_id,
236
+ user=user,
237
+ )
238
+ self._sync_from_response(response.data)
239
+ return response
240
+
241
+ @attach_channel_cid_async
242
+ async def fork_message_ids(
243
+ self,
244
+ source_message_ids: List[str],
245
+ user_id: Optional[str] = None,
246
+ user: Optional[UserRequest] = None,
247
+ ) -> StreamResponse[ForkMessageIDsResponse]:
248
+ response = await self.client.fork_message_ids(
249
+ type=self.channel_type,
250
+ id=self.channel_id,
251
+ source_message_ids=source_message_ids,
252
+ user_id=user_id,
253
+ user=user,
254
+ )
255
+ self._sync_from_response(response.data)
256
+ return response
257
+
188
258
  @attach_channel_cid_async
189
259
  async def hide(
190
260
  self,
@@ -255,6 +325,7 @@ class Channel:
255
325
  include_mentioned_members: Optional[bool] = None,
256
326
  keep_channel_hidden: Optional[bool] = None,
257
327
  pending: Optional[bool] = None,
328
+ scheduled_at: Optional[datetime] = None,
258
329
  skip_enrich_url: Optional[bool] = None,
259
330
  skip_push: Optional[bool] = None,
260
331
  pending_message_metadata: Optional[Dict[str, str]] = None,
@@ -268,6 +339,7 @@ class Channel:
268
339
  include_mentioned_members=include_mentioned_members,
269
340
  keep_channel_hidden=keep_channel_hidden,
270
341
  pending=pending,
342
+ scheduled_at=scheduled_at,
271
343
  skip_enrich_url=skip_enrich_url,
272
344
  skip_push=skip_push,
273
345
  pending_message_metadata=pending_message_metadata,
@@ -333,6 +405,7 @@ class Channel:
333
405
  async def get_or_create(
334
406
  self,
335
407
  hide_for_creator: Optional[bool] = None,
408
+ include_scheduled_messages: Optional[bool] = None,
336
409
  state: Optional[bool] = None,
337
410
  thread_unread_counts: Optional[bool] = None,
338
411
  member_custom_include: Optional[List[str]] = None,
@@ -345,6 +418,7 @@ class Channel:
345
418
  type=self.channel_type,
346
419
  id=self.channel_id,
347
420
  hide_for_creator=hide_for_creator,
421
+ include_scheduled_messages=include_scheduled_messages,
348
422
  state=state,
349
423
  thread_unread_counts=thread_unread_counts,
350
424
  member_custom_include=member_custom_include,
@@ -375,6 +449,27 @@ class Channel:
375
449
  self._sync_from_response(response.data)
376
450
  return response
377
451
 
452
+ @attach_channel_cid_async
453
+ async def rewind(
454
+ self,
455
+ expected_last_message_id: str,
456
+ message_id: str,
457
+ inclusive: Optional[bool] = None,
458
+ user_id: Optional[str] = None,
459
+ user: Optional[UserRequest] = None,
460
+ ) -> StreamResponse[RewindChannelResponse]:
461
+ response = await self.client.rewind_channel(
462
+ type=self.channel_type,
463
+ id=self.channel_id,
464
+ expected_last_message_id=expected_last_message_id,
465
+ message_id=message_id,
466
+ inclusive=inclusive,
467
+ user_id=user_id,
468
+ user=user,
469
+ )
470
+ self._sync_from_response(response.data)
471
+ return response
472
+
378
473
  @attach_channel_cid_async
379
474
  async def show(
380
475
  self, user_id: Optional[str] = None, user: Optional[UserRequest] = None
@@ -1,13 +1,10 @@
1
- import json
2
1
  from typing import List, Optional
3
2
 
4
3
  from getstream.chat.async_channel import Channel
5
4
  from getstream.chat.async_rest_client import ChatRestClient
6
- from getstream.common import telemetry
7
5
  from getstream.models import (
8
6
  ImageSize,
9
7
  OnlyUserID,
10
- UploadChannelFileResponse,
11
8
  UploadChannelResponse,
12
9
  )
13
10
  from getstream.stream_response import StreamResponse
@@ -27,26 +24,7 @@ class ChatClient(ChatRestClient):
27
24
  def channel(self, call_type: str, id: str) -> Channel:
28
25
  return Channel(self, call_type, id)
29
26
 
30
- @telemetry.operation_name("getstream.api.chat.upload_channel_file")
31
- async def upload_channel_file(
32
- self,
33
- type: str,
34
- id: str,
35
- file: str,
36
- user: Optional[OnlyUserID] = None,
37
- ) -> StreamResponse[UploadChannelFileResponse]:
38
- form_fields = []
39
- if user is not None:
40
- form_fields.append(("user", json.dumps(user.to_dict())))
41
- return await self._upload_multipart(
42
- "/api/v2/chat/channels/{type}/{id}/file",
43
- UploadChannelFileResponse,
44
- file,
45
- path_params={"type": type, "id": id},
46
- form_fields=form_fields,
47
- )
48
-
49
- @telemetry.operation_name("getstream.api.chat.upload_channel_image")
27
+ # Keeps the `channel_type` name this method has always taken; the generated one takes `type`.
50
28
  async def upload_channel_image(
51
29
  self,
52
30
  channel_type: Optional[str] = None,
@@ -56,7 +34,6 @@ class ChatClient(ChatRestClient):
56
34
  user: Optional[OnlyUserID] = None,
57
35
  **kwargs,
58
36
  ) -> StreamResponse[UploadChannelResponse]:
59
- # Backward compatibility for generated wrappers passing `type=...`.
60
37
  if channel_type is None:
61
38
  channel_type = kwargs.pop("type", None)
62
39
  if kwargs:
@@ -69,17 +46,6 @@ class ChatClient(ChatRestClient):
69
46
  raise TypeError("upload_channel_image() missing required argument: 'id'")
70
47
  if file is None:
71
48
  raise TypeError("upload_channel_image() missing required argument: 'file'")
72
- form_fields = []
73
- if user is not None:
74
- form_fields.append(("user", json.dumps(user.to_dict())))
75
- if upload_sizes is not None:
76
- form_fields.append(
77
- ("upload_sizes", json.dumps([s.to_dict() for s in upload_sizes]))
78
- )
79
- return await self._upload_multipart(
80
- "/api/v2/chat/channels/{type}/{id}/image",
81
- UploadChannelResponse,
82
- file,
83
- path_params={"type": channel_type, "id": id},
84
- form_fields=form_fields,
49
+ return await super().upload_channel_image(
50
+ channel_type, id, file=file, upload_sizes=upload_sizes, user=user
85
51
  )