getstream 3.3.4__tar.gz → 3.4.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 (119) hide show
  1. getstream-3.4.0/.github/workflows/release.yml +219 -0
  2. {getstream-3.3.4 → getstream-3.4.0}/.github/workflows/run_tests.yml +3 -3
  3. {getstream-3.3.4 → getstream-3.4.0}/CHANGELOG.md +35 -0
  4. {getstream-3.3.4 → getstream-3.4.0}/Makefile +4 -2
  5. {getstream-3.3.4 → getstream-3.4.0}/PKG-INFO +24 -5
  6. {getstream-3.3.4 → getstream-3.4.0}/README.md +18 -1
  7. {getstream-3.3.4 → getstream-3.4.0}/generate.sh +4 -1
  8. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/async_rest_client.py +108 -0
  9. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/rest_client.py +106 -0
  10. {getstream-3.3.4 → getstream-3.4.0}/getstream/common/async_rest_client.py +24 -0
  11. {getstream-3.3.4 → getstream-3.4.0}/getstream/common/rest_client.py +24 -0
  12. {getstream-3.3.4 → getstream-3.4.0}/getstream/config.py +1 -0
  13. {getstream-3.3.4 → getstream-3.4.0}/getstream/feeds/feeds.py +23 -2
  14. {getstream-3.3.4 → getstream-3.4.0}/getstream/feeds/rest_client.py +223 -16
  15. {getstream-3.3.4 → getstream-3.4.0}/getstream/models/__init__.py +1707 -143
  16. {getstream-3.3.4 → getstream-3.4.0}/getstream/moderation/async_rest_client.py +203 -4
  17. {getstream-3.3.4 → getstream-3.4.0}/getstream/moderation/rest_client.py +205 -4
  18. {getstream-3.3.4 → getstream-3.4.0}/getstream/stream.py +184 -10
  19. {getstream-3.3.4 → getstream-3.4.0}/getstream/tests/test_webhook.py +156 -0
  20. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/async_rest_client.py +20 -0
  21. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rest_client.py +20 -0
  22. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/connection_manager.py +2 -0
  23. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/connection_utils.py +26 -31
  24. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/coordinator/ws.py +3 -9
  25. {getstream-3.3.4 → getstream-3.4.0}/getstream/webhook.py +236 -3
  26. {getstream-3.3.4 → getstream-3.4.0}/pyproject.toml +7 -9
  27. {getstream-3.3.4 → getstream-3.4.0}/pytest.ini +2 -0
  28. {getstream-3.3.4 → getstream-3.4.0}/uv.lock +187 -7
  29. getstream-3.3.4/.github/workflows/release.yml +0 -60
  30. getstream-3.3.4/getstream/video/rtc/coordinator_api.py +0 -81
  31. {getstream-3.3.4 → getstream-3.4.0}/.cursor/worktrees.json +0 -0
  32. {getstream-3.3.4 → getstream-3.4.0}/.env.example +0 -0
  33. {getstream-3.3.4 → getstream-3.4.0}/.github/actions/python-uv-setup/action.yml +0 -0
  34. {getstream-3.3.4 → getstream-3.4.0}/.github/workflows/ci.yml +0 -0
  35. {getstream-3.3.4 → getstream-3.4.0}/.github/workflows/stream-py.code-workspace +0 -0
  36. {getstream-3.3.4 → getstream-3.4.0}/.gitignore +0 -0
  37. {getstream-3.3.4 → getstream-3.4.0}/.gitmodules +0 -0
  38. {getstream-3.3.4 → getstream-3.4.0}/.pre-commit-config.yaml +0 -0
  39. {getstream-3.3.4 → getstream-3.4.0}/AGENTS.md +0 -0
  40. {getstream-3.3.4 → getstream-3.4.0}/DEVELOPMENT.md +0 -0
  41. {getstream-3.3.4 → getstream-3.4.0}/LICENSE.md +0 -0
  42. {getstream-3.3.4 → getstream-3.4.0}/MIGRATION_v2_to_v3.md +0 -0
  43. {getstream-3.3.4 → getstream-3.4.0}/dev.py +0 -0
  44. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  45. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  46. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  47. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  48. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  49. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  50. {getstream-3.3.4 → getstream-3.4.0}/docs/migration-from-stream-chat-python/README.md +0 -0
  51. {getstream-3.3.4 → getstream-3.4.0}/generate_webrtc.sh +0 -0
  52. {getstream-3.3.4 → getstream-3.4.0}/getstream/__init__.py +0 -0
  53. {getstream-3.3.4 → getstream-3.4.0}/getstream/base.py +0 -0
  54. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/__init__.py +0 -0
  55. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/async_channel.py +0 -0
  56. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/async_client.py +0 -0
  57. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/channel.py +0 -0
  58. {getstream-3.3.4 → getstream-3.4.0}/getstream/chat/client.py +0 -0
  59. {getstream-3.3.4 → getstream-3.4.0}/getstream/common/__init__.py +0 -0
  60. {getstream-3.3.4 → getstream-3.4.0}/getstream/common/async_client.py +0 -0
  61. {getstream-3.3.4 → getstream-3.4.0}/getstream/common/client.py +0 -0
  62. {getstream-3.3.4 → getstream-3.4.0}/getstream/common/telemetry.py +0 -0
  63. {getstream-3.3.4 → getstream-3.4.0}/getstream/feeds/__init__.py +0 -0
  64. {getstream-3.3.4 → getstream-3.4.0}/getstream/feeds/client.py +0 -0
  65. {getstream-3.3.4 → getstream-3.4.0}/getstream/generic.py +0 -0
  66. {getstream-3.3.4 → getstream-3.4.0}/getstream/meta.py +0 -0
  67. {getstream-3.3.4 → getstream-3.4.0}/getstream/moderation/__init__.py +0 -0
  68. {getstream-3.3.4 → getstream-3.4.0}/getstream/moderation/async_client.py +0 -0
  69. {getstream-3.3.4 → getstream-3.4.0}/getstream/moderation/client.py +0 -0
  70. {getstream-3.3.4 → getstream-3.4.0}/getstream/rate_limit.py +0 -0
  71. {getstream-3.3.4 → getstream-3.4.0}/getstream/stream_response.py +0 -0
  72. {getstream-3.3.4 → getstream-3.4.0}/getstream/utils/__init__.py +0 -0
  73. {getstream-3.3.4 → getstream-3.4.0}/getstream/utils/event_emitter.py +0 -0
  74. {getstream-3.3.4 → getstream-3.4.0}/getstream/utils/retry.py +0 -0
  75. {getstream-3.3.4 → getstream-3.4.0}/getstream/version.py +0 -0
  76. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/__init__.py +0 -0
  77. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/async_call.py +0 -0
  78. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/async_client.py +0 -0
  79. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/call.py +0 -0
  80. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/client.py +0 -0
  81. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/openai.py +0 -0
  82. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/README.md +0 -0
  83. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/__init__.py +0 -0
  84. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/audio_track.py +0 -0
  85. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/coordinator/__init__.py +0 -0
  86. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/coordinator/backoff.py +0 -0
  87. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/coordinator/errors.py +0 -0
  88. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/encoders_patches.py +0 -0
  89. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/g711.py +0 -0
  90. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/location_discovery.py +0 -0
  91. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/models.py +0 -0
  92. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/network_monitor.py +0 -0
  93. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/participants.py +0 -0
  94. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/__init__.py +0 -0
  95. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  96. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  97. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  98. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  99. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  100. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  101. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  102. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  103. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  104. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  105. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  106. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  107. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  108. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/pc.py +0 -0
  109. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/peer_connection.py +0 -0
  110. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/reconnection.py +0 -0
  111. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/recording.py +0 -0
  112. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/signaling.py +0 -0
  113. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/stats_reporter.py +0 -0
  114. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/stats_tracer.py +0 -0
  115. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/tracer.py +0 -0
  116. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/track_util.py +0 -0
  117. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/tracks.py +0 -0
  118. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/twirp_client_wrapper.py +0 -0
  119. {getstream-3.3.4 → getstream-3.4.0}/getstream/video/rtc/utils.py +0 -0
@@ -0,0 +1,219 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version_bump:
7
+ description: 'Version bump type for manual release'
8
+ required: true
9
+ default: 'patch'
10
+ type: choice
11
+ options:
12
+ - patch
13
+ - minor
14
+ - major
15
+ use_current_version:
16
+ description: 'Skip version bump and publish the version already set in pyproject.toml'
17
+ required: false
18
+ default: false
19
+ type: boolean
20
+ pull_request:
21
+ types: [closed]
22
+ branches:
23
+ - main
24
+
25
+ concurrency:
26
+ group: release-${{ github.event.pull_request.base.ref || github.ref_name }}
27
+ cancel-in-progress: false
28
+
29
+ permissions:
30
+ contents: read
31
+
32
+ jobs:
33
+ prepare:
34
+ name: Prepare release
35
+ if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
36
+ runs-on: ubuntu-latest
37
+ outputs:
38
+ should_release: ${{ steps.release_meta_final.outputs.should_release }}
39
+ bump: ${{ steps.release_meta_final.outputs.bump }}
40
+ previous_version: ${{ steps.release_meta_final.outputs.previous_version }}
41
+ version: ${{ steps.release_meta_final.outputs.version }}
42
+ tag: ${{ steps.release_meta_final.outputs.tag }}
43
+ steps:
44
+ - uses: actions/checkout@v5
45
+ with:
46
+ fetch-depth: 0
47
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}
48
+
49
+ - name: Skip when PR is already released
50
+ id: already_released
51
+ run: |
52
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
53
+ echo "value=false" >> "$GITHUB_OUTPUT"
54
+ else
55
+ if git log --oneline --grep="(pr #${{ github.event.pull_request.number }})" -n 1 | grep -q "chore(release):"; then
56
+ echo "value=true" >> "$GITHUB_OUTPUT"
57
+ else
58
+ echo "value=false" >> "$GITHUB_OUTPUT"
59
+ fi
60
+ fi
61
+
62
+ - name: Determine version bump (from PR metadata)
63
+ id: release_meta
64
+ if: github.event_name == 'pull_request' && steps.already_released.outputs.value != 'true'
65
+ env:
66
+ PR_TITLE: ${{ github.event.pull_request.title }}
67
+ PR_BODY: ${{ github.event.pull_request.body }}
68
+ run: |
69
+ PR_BODY_FILE=$(mktemp)
70
+ printf '%s' "$PR_BODY" > "$PR_BODY_FILE"
71
+ python3 scripts/release/bump_version.py \
72
+ --title "$PR_TITLE" \
73
+ --body-file "$PR_BODY_FILE" \
74
+ --output "$GITHUB_OUTPUT"
75
+
76
+ - name: Determine version bump (manual)
77
+ id: release_meta_manual
78
+ if: github.event_name == 'workflow_dispatch'
79
+ run: |
80
+ python3 scripts/release/bump_version.py \
81
+ --manual-bump "${{ github.event.inputs.version_bump }}" \
82
+ --use-current-version "${{ github.event.inputs.use_current_version }}" \
83
+ --output "$GITHUB_OUTPUT"
84
+
85
+ - name: Consolidate release metadata
86
+ id: release_meta_final
87
+ run: |
88
+ if [ "${{ steps.already_released.outputs.value }}" = "true" ]; then
89
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
90
+ echo "bump=none" >> "$GITHUB_OUTPUT"
91
+ exit 0
92
+ fi
93
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
94
+ echo "should_release=${{ steps.release_meta_manual.outputs.should_release }}" >> "$GITHUB_OUTPUT"
95
+ echo "bump=${{ steps.release_meta_manual.outputs.bump }}" >> "$GITHUB_OUTPUT"
96
+ echo "previous_version=${{ steps.release_meta_manual.outputs.previous_version }}" >> "$GITHUB_OUTPUT"
97
+ echo "version=${{ steps.release_meta_manual.outputs.version }}" >> "$GITHUB_OUTPUT"
98
+ echo "tag=${{ steps.release_meta_manual.outputs.tag }}" >> "$GITHUB_OUTPUT"
99
+ else
100
+ echo "should_release=${{ steps.release_meta.outputs.should_release }}" >> "$GITHUB_OUTPUT"
101
+ echo "bump=${{ steps.release_meta.outputs.bump }}" >> "$GITHUB_OUTPUT"
102
+ echo "previous_version=${{ steps.release_meta.outputs.previous_version }}" >> "$GITHUB_OUTPUT"
103
+ echo "version=${{ steps.release_meta.outputs.version }}" >> "$GITHUB_OUTPUT"
104
+ echo "tag=${{ steps.release_meta.outputs.tag }}" >> "$GITHUB_OUTPUT"
105
+ fi
106
+
107
+ test-unit:
108
+ name: Test (unit)
109
+ needs: prepare
110
+ if: needs.prepare.outputs.should_release == 'true'
111
+ uses: ./.github/workflows/run_tests.yml
112
+ with:
113
+ marker: 'not integration'
114
+ secrets: inherit
115
+
116
+ test-integration:
117
+ name: Test (integration)
118
+ needs: prepare
119
+ if: needs.prepare.outputs.should_release == 'true'
120
+ uses: ./.github/workflows/run_tests.yml
121
+ with:
122
+ marker: 'integration'
123
+ secrets: inherit
124
+
125
+ release:
126
+ name: 🚀 Release
127
+ needs: [prepare, test-unit, test-integration]
128
+ if: needs.prepare.outputs.should_release == 'true'
129
+ runs-on: ubuntu-latest
130
+ environment: pypi
131
+ permissions:
132
+ contents: write
133
+ id-token: write
134
+ steps:
135
+ - uses: actions/checkout@v5
136
+ with:
137
+ fetch-depth: 0
138
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}
139
+
140
+ - uses: ./.github/actions/python-uv-setup
141
+
142
+ - name: Apply version bump
143
+ env:
144
+ VERSION: ${{ needs.prepare.outputs.version }}
145
+ run: |
146
+ python3 - <<'PY'
147
+ import os, re
148
+ from pathlib import Path
149
+ version = os.environ['VERSION']
150
+ path = Path('pyproject.toml')
151
+ text = path.read_text(encoding='utf-8')
152
+ pattern = re.compile(r'^version\s*=\s*"\d+\.\d+\.\d+"\s*$', re.MULTILINE)
153
+ new_text, count = pattern.subn(f'version = "{version}"', text, count=1)
154
+ if count == 0:
155
+ raise SystemExit('Could not update version line in pyproject.toml')
156
+ path.write_text(new_text, encoding='utf-8')
157
+ PY
158
+
159
+ - name: Commit version bump
160
+ env:
161
+ VERSION: ${{ needs.prepare.outputs.version }}
162
+ run: |
163
+ git config user.name "github-actions[bot]"
164
+ git config user.email "github-actions[bot]@users.noreply.github.com"
165
+ git add pyproject.toml
166
+ if git diff --cached --quiet; then
167
+ echo "No version changes to commit."
168
+ exit 0
169
+ fi
170
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
171
+ git commit -m "chore(release): v${VERSION} (manual)"
172
+ git push origin "HEAD:${{ github.ref_name }}"
173
+ else
174
+ git commit -m "chore(release): v${VERSION} (pr #${{ github.event.pull_request.number }})"
175
+ git push origin "HEAD:${{ github.event.pull_request.base.ref }}"
176
+ fi
177
+
178
+ - name: Create release tag
179
+ env:
180
+ TAG: ${{ needs.prepare.outputs.tag }}
181
+ run: |
182
+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
183
+ echo "Tag ${TAG} already exists; skipping tag creation."
184
+ exit 0
185
+ fi
186
+ git tag "${TAG}"
187
+ git push origin "${TAG}"
188
+
189
+ - name: Clean dist directory
190
+ run: rm -rf dist
191
+
192
+ - name: Build distributions
193
+ run: uv build
194
+
195
+ - name: Publish to PyPI (Trusted Publishing)
196
+ run: uv publish
197
+
198
+ - name: Create release on GitHub
199
+ uses: ncipollo/release-action@v1
200
+ with:
201
+ tag: ${{ needs.prepare.outputs.tag }}
202
+ token: ${{ secrets.GITHUB_TOKEN }}
203
+ skipIfReleaseExists: true
204
+ body: |
205
+ Release v${{ needs.prepare.outputs.version }}
206
+
207
+ - Bump type: `${{ needs.prepare.outputs.bump }}`
208
+ - Previous: `${{ needs.prepare.outputs.previous_version }}`
209
+ - Next: `${{ needs.prepare.outputs.version }}`
210
+ - Trigger: `${{ github.event_name }}`
211
+
212
+ Install with: `pip install getstream==${{ needs.prepare.outputs.version }}`
213
+
214
+ - name: Verify pip install
215
+ env:
216
+ UV_NO_SOURCES: "1"
217
+ run: |
218
+ uv pip install "getstream==${{ needs.prepare.outputs.version }}" || \
219
+ echo "WARNING: pip install verification failed (PyPI index may need a moment to propagate)"
@@ -8,7 +8,7 @@ on:
8
8
  type: string
9
9
  secrets: { }
10
10
  concurrency:
11
- group: ${{ github.workflow }}-${{ github.ref }}
11
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.marker }}
12
12
  cancel-in-progress: true
13
13
 
14
14
  env:
@@ -49,7 +49,7 @@ jobs:
49
49
  strategy:
50
50
  fail-fast: false
51
51
  matrix:
52
- python-version: ["3.10", "3.11", "3.12", "3.13"]
52
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
53
53
  timeout-minutes: 30
54
54
  steps:
55
55
  - name: Checkout
@@ -75,7 +75,7 @@ jobs:
75
75
  strategy:
76
76
  fail-fast: false
77
77
  matrix:
78
- python-version: ["3.10", "3.11", "3.12", "3.13"]
78
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
79
79
  timeout-minutes: 30
80
80
  steps:
81
81
  - name: Checkout
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Webhook handling spec helpers (CHA-2961): `UnknownEvent` dataclass for
13
+ forward-compat; `gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload`
14
+ primitives; `parse_event` (returns typed event or `UnknownEvent` for
15
+ unrecognized discriminators); `verify_signature` canonical alias of
16
+ `verify_webhook_signature`; `verify_and_parse_webhook` HTTP composite
17
+ (gunzip + verify + parse); `parse_sqs` and `parse_sns` queue composites
18
+ (no signature parameter — queue transports are authenticated by AWS IAM,
19
+ so the backend emits no HMAC for queue messages today). Transparent gzip
20
+ via magic-byte detection.
21
+ - New instance methods on `Stream` and `AsyncStream`:
22
+ `verify_signature(body, signature)` and
23
+ `verify_and_parse_webhook(body, signature)` — drop the api_secret parameter
24
+ in favor of the client's stored secret. Dual API: the module-level functions
25
+ in `getstream.webhook` remain available for callers who want explicit
26
+ secret control.
27
+ - New instance methods on `Stream` / `AsyncStream`: `parse_sqs(message_body)`,
28
+ `parse_sns(notification_body)` (no signature; AWS IAM).
29
+ - `InvalidWebhookError` exception type covering both signature mismatches and
30
+ malformed payloads. Distinguish failure modes via the exception message or
31
+ `__cause__` chain.
32
+ - Conformance fixture suite under `tests/fixtures/webhooks/` (13 happy-path
33
+ event directories + 8 negative cases) for SDK conformance testing across
34
+ language ports.
35
+
36
+ ### Changed
37
+
38
+ - No breaking changes. All existing webhook helpers (`verify_webhook_signature`,
39
+ `parse_webhook_event`, `get_event_type`, event type constants) are preserved.
40
+
41
+ [Spec](https://www.notion.so/stream-wiki/Server-Side-SDK-Webhook-Handling-Spec-34b6a5d7f9f681e78003c443f227493c)
42
+
8
43
  ## [3.0.0b1] - 2026-02-27
9
44
 
10
45
  ### Breaking Changes
@@ -56,11 +56,13 @@ TY_EXCLUDES := \
56
56
 
57
57
  ## Run non-video tests (chat, feeds, moderation, etc.)
58
58
  test:
59
- uv run pytest -m "$(MARKER)" tests/ getstream/ $(VIDEO_IGNORE) $(MANUAL_IGNORE)
59
+ @uv run pytest -m "$(MARKER)" tests/ getstream/ $(VIDEO_IGNORE) $(MANUAL_IGNORE); \
60
+ ec=$$?; if [ $$ec -eq 5 ]; then echo "No tests matched marker '$(MARKER)' — treating as success."; exit 0; else exit $$ec; fi
60
61
 
61
62
  ## Run video/WebRTC tests only
62
63
  test-video:
63
- uv run pytest -m "$(MARKER)" $(VIDEO_PATHS)
64
+ @uv run pytest -m "$(MARKER)" $(VIDEO_PATHS); \
65
+ ec=$$?; if [ $$ec -eq 5 ]; then echo "No tests matched marker '$(MARKER)' — treating as success."; exit 0; else exit $$ec; fi
64
66
 
65
67
  ## Run all tests (non-video + video), excluding manual tests
66
68
  test-all:
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstream
3
- Version: 3.3.4
3
+ Version: 3.4.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
7
7
  Classifier: Programming Language :: Python :: 3
8
- Classifier: Programming Language :: Python :: 3.9
9
8
  Classifier: Programming Language :: Python :: 3.10
10
9
  Classifier: Programming Language :: Python :: 3.11
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
13
  Requires-Python: <4.0.0,>=3.10
14
14
  Requires-Dist: dataclasses-json<0.7,>=0.6.0
15
15
  Requires-Dist: httpx>=0.28.1
@@ -31,13 +31,15 @@ Provides-Extra: webrtc
31
31
  Requires-Dist: aiohttp<4,>=3.13.2; extra == 'webrtc'
32
32
  Requires-Dist: aiortc<1.15.0,>=1.14.0; extra == 'webrtc'
33
33
  Requires-Dist: av<17,>=14.2.0; extra == 'webrtc'
34
- Requires-Dist: numpy<2.3,>=2.2.6; extra == 'webrtc'
34
+ Requires-Dist: numpy<2.3,>=2.2.6; (python_version < '3.14') and extra == 'webrtc'
35
+ Requires-Dist: numpy<2.4,>=2.3.2; (python_version >= '3.14') and extra == 'webrtc'
35
36
  Requires-Dist: numpy>=2.2.6; extra == 'webrtc'
36
37
  Requires-Dist: ping3>=4.0.8; extra == 'webrtc'
37
38
  Requires-Dist: protobuf>=4.25.1; extra == 'webrtc'
38
39
  Requires-Dist: protobuf>=6.31.1; extra == 'webrtc'
39
40
  Requires-Dist: python-dateutil>=2.8.2; extra == 'webrtc'
40
- Requires-Dist: scipy<1.16,>=1.15.3; extra == 'webrtc'
41
+ Requires-Dist: scipy<1.16,>=1.15.3; (python_version < '3.14') and extra == 'webrtc'
42
+ Requires-Dist: scipy<1.17,>=1.16.1; (python_version >= '3.14') and extra == 'webrtc'
41
43
  Requires-Dist: scipy>=1.15.3; extra == 'webrtc'
42
44
  Requires-Dist: soundfile>=0.13.1; extra == 'webrtc'
43
45
  Requires-Dist: structlog<24; extra == 'webrtc'
@@ -175,7 +177,7 @@ We use [uv](https://github.com/astral-sh/uv) to manage dependencies and run test
175
177
  ### 🚀 Quick Start
176
178
 
177
179
  **Prerequisites:**
178
- - Python 3.9+ (recommended: 3.12.2)
180
+ - Python 3.10+ (recommended: 3.12.2)
179
181
  - [uv](https://github.com/astral-sh/uv) package manager
180
182
 
181
183
  **Setup:**
@@ -280,6 +282,23 @@ uv run pytest -v -s
280
282
  uv run pytest tests/test_video.py::test_specific_function -v
281
283
  ```
282
284
 
285
+ ## Releases
286
+
287
+ Releases use two paths:
288
+
289
+ - **Default**: automatic release when a PR is merged to `main`. The PR title (and body) drives the semver bump.
290
+ - **Fallback**: manual release via the `Release` workflow's `workflow_dispatch` (admin use). Select a `version_bump` (`patch`/`minor`/`major`). `use_current_version=true` skips the bump and publishes whatever is already in `pyproject.toml`.
291
+
292
+ Automatic semver bump rules:
293
+
294
+ - `feat:` -> minor
295
+ - `fix:` (or `bug:`) -> patch
296
+ - `feat!:`, `<type>(scope)!:`, or `BREAKING CHANGE` in the PR body/title -> major
297
+
298
+ PRs with any other prefix do not trigger a release.
299
+
300
+ The release pipeline runs lint, type-check, and the full test matrix (unit + integration, across all supported Python versions) on the merged commit before publishing to PyPI via Trusted Publishing (OIDC). Each step in the publish job is idempotent: a failed run can be re-dispatched from the Actions UI.
301
+
283
302
  ## License
284
303
 
285
304
  This project is licensed under the [MIT License](LICENSE).
@@ -125,7 +125,7 @@ We use [uv](https://github.com/astral-sh/uv) to manage dependencies and run test
125
125
  ### 🚀 Quick Start
126
126
 
127
127
  **Prerequisites:**
128
- - Python 3.9+ (recommended: 3.12.2)
128
+ - Python 3.10+ (recommended: 3.12.2)
129
129
  - [uv](https://github.com/astral-sh/uv) package manager
130
130
 
131
131
  **Setup:**
@@ -230,6 +230,23 @@ uv run pytest -v -s
230
230
  uv run pytest tests/test_video.py::test_specific_function -v
231
231
  ```
232
232
 
233
+ ## Releases
234
+
235
+ Releases use two paths:
236
+
237
+ - **Default**: automatic release when a PR is merged to `main`. The PR title (and body) drives the semver bump.
238
+ - **Fallback**: manual release via the `Release` workflow's `workflow_dispatch` (admin use). Select a `version_bump` (`patch`/`minor`/`major`). `use_current_version=true` skips the bump and publishes whatever is already in `pyproject.toml`.
239
+
240
+ Automatic semver bump rules:
241
+
242
+ - `feat:` -> minor
243
+ - `fix:` (or `bug:`) -> patch
244
+ - `feat!:`, `<type>(scope)!:`, or `BREAKING CHANGE` in the PR body/title -> major
245
+
246
+ PRs with any other prefix do not trigger a release.
247
+
248
+ The release pipeline runs lint, type-check, and the full test matrix (unit + integration, across all supported Python versions) on the merged commit before publishing to PyPI via Trusted Publishing (OIDC). Each step in the publish job is idempotent: a failed run can be re-dispatched from the Actions UI.
249
+
233
250
  ## License
234
251
 
235
252
  This project is licensed under the [MIT License](LICENSE).
@@ -17,7 +17,10 @@ fi
17
17
  set -ex
18
18
 
19
19
  # cd in API repo, generate new spec and then generate code from it
20
- ( cd $SOURCE_PATH ; make openapi ; go run ./cmd/chat-manager openapi generate-client --language python --spec ./releases/v2/serverside-api.yaml --output ../stream-py/getstream/ )
20
+ ( cd $SOURCE_PATH ; make openapi ; ./build/chat-manager openapi generate-client --language python --spec ./releases/v2/serverside-api.yaml --output ../stream-py/getstream/ )
21
+
22
+ # Regenerate webhook conformance fixtures (CHA-2961)
23
+ ( cd $SOURCE_PATH ; ./build/chat-manager openapi generate-webhook-fixtures --output ../stream-py/tests/assets/webhooks --time-format=unix-ns )
21
24
 
22
25
  # lint + auto-fix, then format generated code with ruff (align with pre-commit)
23
26
  uv run ruff check --fix getstream/ tests/
@@ -31,6 +31,44 @@ class ChatRestClient(AsyncBaseClient):
31
31
  user_agent=user_agent,
32
32
  )
33
33
 
34
+ @telemetry.operation_name("getstream.api.chat.create_campaign")
35
+ async def create_campaign(
36
+ self,
37
+ sender_id: str,
38
+ message_template: CampaignMessageTemplate,
39
+ create_channels: Optional[bool] = None,
40
+ description: Optional[str] = None,
41
+ id: Optional[str] = None,
42
+ name: Optional[str] = None,
43
+ sender_mode: Optional[str] = None,
44
+ sender_visibility: Optional[str] = None,
45
+ show_channels: Optional[bool] = None,
46
+ skip_push: Optional[bool] = None,
47
+ skip_webhook: Optional[bool] = None,
48
+ segment_ids: Optional[List[str]] = None,
49
+ user_ids: Optional[List[str]] = None,
50
+ channel_template: Optional[CampaignChannelTemplate] = None,
51
+ ) -> StreamResponse[CreateCampaignResponse]:
52
+ json = CreateCampaignRequest(
53
+ sender_id=sender_id,
54
+ message_template=message_template,
55
+ create_channels=create_channels,
56
+ description=description,
57
+ id=id,
58
+ name=name,
59
+ sender_mode=sender_mode,
60
+ sender_visibility=sender_visibility,
61
+ show_channels=show_channels,
62
+ skip_push=skip_push,
63
+ skip_webhook=skip_webhook,
64
+ segment_ids=segment_ids,
65
+ user_ids=user_ids,
66
+ channel_template=channel_template,
67
+ ).to_dict()
68
+ return await self.post(
69
+ "/api/v2/chat/campaigns", CreateCampaignResponse, json=json
70
+ )
71
+
34
72
  @telemetry.operation_name("getstream.api.chat.query_campaigns")
35
73
  async def query_campaigns(
36
74
  self,
@@ -53,6 +91,17 @@ class ChatRestClient(AsyncBaseClient):
53
91
  "/api/v2/chat/campaigns/query", QueryCampaignsResponse, json=json
54
92
  )
55
93
 
94
+ @telemetry.operation_name("getstream.api.chat.delete_campaign")
95
+ async def delete_campaign(self, id: str) -> StreamResponse[DeleteCampaignResponse]:
96
+ path_params = {
97
+ "id": id,
98
+ }
99
+ return await self.delete(
100
+ "/api/v2/chat/campaigns/{id}",
101
+ DeleteCampaignResponse,
102
+ path_params=path_params,
103
+ )
104
+
56
105
  @telemetry.operation_name("getstream.api.chat.get_campaign")
57
106
  async def get_campaign(
58
107
  self,
@@ -72,6 +121,51 @@ class ChatRestClient(AsyncBaseClient):
72
121
  path_params=path_params,
73
122
  )
74
123
 
124
+ @telemetry.operation_name("getstream.api.chat.update_campaign")
125
+ async def update_campaign(
126
+ self,
127
+ id: str,
128
+ sender_id: str,
129
+ message_template: CampaignMessageTemplate,
130
+ create_channels: Optional[bool] = None,
131
+ description: Optional[str] = None,
132
+ id_: Optional[str] = None,
133
+ name: Optional[str] = None,
134
+ sender_mode: Optional[str] = None,
135
+ sender_visibility: Optional[str] = None,
136
+ show_channels: Optional[bool] = None,
137
+ skip_push: Optional[bool] = None,
138
+ skip_webhook: Optional[bool] = None,
139
+ segment_ids: Optional[List[str]] = None,
140
+ user_ids: Optional[List[str]] = None,
141
+ channel_template: Optional[CampaignChannelTemplate] = None,
142
+ ) -> StreamResponse[CampaignResponse]:
143
+ path_params = {
144
+ "id": id,
145
+ }
146
+ json = UpdateCampaignRequest(
147
+ sender_id=sender_id,
148
+ message_template=message_template,
149
+ create_channels=create_channels,
150
+ description=description,
151
+ id=id_,
152
+ name=name,
153
+ sender_mode=sender_mode,
154
+ sender_visibility=sender_visibility,
155
+ show_channels=show_channels,
156
+ skip_push=skip_push,
157
+ skip_webhook=skip_webhook,
158
+ segment_ids=segment_ids,
159
+ user_ids=user_ids,
160
+ channel_template=channel_template,
161
+ ).to_dict()
162
+ return await self.put(
163
+ "/api/v2/chat/campaigns/{id}",
164
+ CampaignResponse,
165
+ path_params=path_params,
166
+ json=json,
167
+ )
168
+
75
169
  @telemetry.operation_name("getstream.api.chat.start_campaign")
76
170
  async def start_campaign(
77
171
  self,
@@ -183,6 +277,20 @@ class ChatRestClient(AsyncBaseClient):
183
277
  json=json,
184
278
  )
185
279
 
280
+ @telemetry.operation_name("getstream.api.chat.grouped_query_channels")
281
+ async def grouped_query_channels(
282
+ self,
283
+ limit: Optional[int] = None,
284
+ user_id: Optional[str] = None,
285
+ user: Optional[UserRequest] = None,
286
+ ) -> StreamResponse[GroupedQueryChannelsResponse]:
287
+ json = GroupedQueryChannelsRequest(
288
+ limit=limit, user_id=user_id, user=user
289
+ ).to_dict()
290
+ return await self.post(
291
+ "/api/v2/chat/channels/grouped", GroupedQueryChannelsResponse, json=json
292
+ )
293
+
186
294
  @telemetry.operation_name("getstream.api.chat.mark_channels_read")
187
295
  async def mark_channels_read(
188
296
  self,
@@ -31,6 +31,42 @@ class ChatRestClient(BaseClient):
31
31
  user_agent=user_agent,
32
32
  )
33
33
 
34
+ @telemetry.operation_name("getstream.api.chat.create_campaign")
35
+ def create_campaign(
36
+ self,
37
+ sender_id: str,
38
+ message_template: CampaignMessageTemplate,
39
+ create_channels: Optional[bool] = None,
40
+ description: Optional[str] = None,
41
+ id: Optional[str] = None,
42
+ name: Optional[str] = None,
43
+ sender_mode: Optional[str] = None,
44
+ sender_visibility: Optional[str] = None,
45
+ show_channels: Optional[bool] = None,
46
+ skip_push: Optional[bool] = None,
47
+ skip_webhook: Optional[bool] = None,
48
+ segment_ids: Optional[List[str]] = None,
49
+ user_ids: Optional[List[str]] = None,
50
+ channel_template: Optional[CampaignChannelTemplate] = None,
51
+ ) -> StreamResponse[CreateCampaignResponse]:
52
+ json = CreateCampaignRequest(
53
+ sender_id=sender_id,
54
+ message_template=message_template,
55
+ create_channels=create_channels,
56
+ description=description,
57
+ id=id,
58
+ name=name,
59
+ sender_mode=sender_mode,
60
+ sender_visibility=sender_visibility,
61
+ show_channels=show_channels,
62
+ skip_push=skip_push,
63
+ skip_webhook=skip_webhook,
64
+ segment_ids=segment_ids,
65
+ user_ids=user_ids,
66
+ channel_template=channel_template,
67
+ ).to_dict()
68
+ return self.post("/api/v2/chat/campaigns", CreateCampaignResponse, json=json)
69
+
34
70
  @telemetry.operation_name("getstream.api.chat.query_campaigns")
35
71
  def query_campaigns(
36
72
  self,
@@ -53,6 +89,17 @@ class ChatRestClient(BaseClient):
53
89
  "/api/v2/chat/campaigns/query", QueryCampaignsResponse, json=json
54
90
  )
55
91
 
92
+ @telemetry.operation_name("getstream.api.chat.delete_campaign")
93
+ def delete_campaign(self, id: str) -> StreamResponse[DeleteCampaignResponse]:
94
+ path_params = {
95
+ "id": id,
96
+ }
97
+ return self.delete(
98
+ "/api/v2/chat/campaigns/{id}",
99
+ DeleteCampaignResponse,
100
+ path_params=path_params,
101
+ )
102
+
56
103
  @telemetry.operation_name("getstream.api.chat.get_campaign")
57
104
  def get_campaign(
58
105
  self,
@@ -72,6 +119,51 @@ class ChatRestClient(BaseClient):
72
119
  path_params=path_params,
73
120
  )
74
121
 
122
+ @telemetry.operation_name("getstream.api.chat.update_campaign")
123
+ def update_campaign(
124
+ self,
125
+ id: str,
126
+ sender_id: str,
127
+ message_template: CampaignMessageTemplate,
128
+ create_channels: Optional[bool] = None,
129
+ description: Optional[str] = None,
130
+ id_: Optional[str] = None,
131
+ name: Optional[str] = None,
132
+ sender_mode: Optional[str] = None,
133
+ sender_visibility: Optional[str] = None,
134
+ show_channels: Optional[bool] = None,
135
+ skip_push: Optional[bool] = None,
136
+ skip_webhook: Optional[bool] = None,
137
+ segment_ids: Optional[List[str]] = None,
138
+ user_ids: Optional[List[str]] = None,
139
+ channel_template: Optional[CampaignChannelTemplate] = None,
140
+ ) -> StreamResponse[CampaignResponse]:
141
+ path_params = {
142
+ "id": id,
143
+ }
144
+ json = UpdateCampaignRequest(
145
+ sender_id=sender_id,
146
+ message_template=message_template,
147
+ create_channels=create_channels,
148
+ description=description,
149
+ id=id_,
150
+ name=name,
151
+ sender_mode=sender_mode,
152
+ sender_visibility=sender_visibility,
153
+ show_channels=show_channels,
154
+ skip_push=skip_push,
155
+ skip_webhook=skip_webhook,
156
+ segment_ids=segment_ids,
157
+ user_ids=user_ids,
158
+ channel_template=channel_template,
159
+ ).to_dict()
160
+ return self.put(
161
+ "/api/v2/chat/campaigns/{id}",
162
+ CampaignResponse,
163
+ path_params=path_params,
164
+ json=json,
165
+ )
166
+
75
167
  @telemetry.operation_name("getstream.api.chat.start_campaign")
76
168
  def start_campaign(
77
169
  self,
@@ -181,6 +273,20 @@ class ChatRestClient(BaseClient):
181
273
  json=json,
182
274
  )
183
275
 
276
+ @telemetry.operation_name("getstream.api.chat.grouped_query_channels")
277
+ def grouped_query_channels(
278
+ self,
279
+ limit: Optional[int] = None,
280
+ user_id: Optional[str] = None,
281
+ user: Optional[UserRequest] = None,
282
+ ) -> StreamResponse[GroupedQueryChannelsResponse]:
283
+ json = GroupedQueryChannelsRequest(
284
+ limit=limit, user_id=user_id, user=user
285
+ ).to_dict()
286
+ return self.post(
287
+ "/api/v2/chat/channels/grouped", GroupedQueryChannelsResponse, json=json
288
+ )
289
+
184
290
  @telemetry.operation_name("getstream.api.chat.mark_channels_read")
185
291
  def mark_channels_read(
186
292
  self,