Skip to content

Commit 6de7e2c

Browse files
committed
Merge remote-tracking branch 'origin/main' into file_upload_fix
Made-with: Cursor # Conflicts: # getstream/base.py # tests/test_chat_misc.py
2 parents c0452a3 + 9bb07b2 commit 6de7e2c

35 files changed

+4750
-173
lines changed

.github/workflows/run_tests.yml

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ jobs:
2424
uses: actions/checkout@v5
2525
- name: Install dependencies
2626
uses: ./.github/actions/python-uv-setup
27-
- name: Run Ruff linter
28-
run: uv run ruff check .
29-
- name: Run Ruff formatter
30-
run: uv run ruff format --check .
27+
- name: Run linter
28+
run: make lint
3129

3230
typecheck:
3331
name: Type Check (ty)
@@ -37,31 +35,14 @@ jobs:
3735
uses: actions/checkout@v5
3836
- name: Install dependencies
3937
uses: ./.github/actions/python-uv-setup
40-
- name: Run ty type checker
41-
run: |
42-
uvx ty check getstream/ \
43-
--exclude "getstream/models/" \
44-
--exclude "getstream/video/rtc/pb/" \
45-
--exclude "**/rest_client.py" \
46-
--exclude "**/async_rest_client.py" \
47-
--exclude "getstream/chat/channel.py" \
48-
--exclude "getstream/chat/async_channel.py" \
49-
--exclude "getstream/chat/client.py" \
50-
--exclude "getstream/chat/async_client.py" \
51-
--exclude "getstream/common/client.py" \
52-
--exclude "getstream/common/async_client.py" \
53-
--exclude "getstream/moderation/client.py" \
54-
--exclude "getstream/moderation/async_client.py" \
55-
--exclude "getstream/video/client.py" \
56-
--exclude "getstream/video/async_client.py" \
57-
--exclude "getstream/video/call.py" \
58-
--exclude "getstream/video/async_call.py" \
59-
--exclude "getstream/feeds/client.py" \
60-
--exclude "getstream/feeds/feeds.py" \
61-
--exclude "getstream/stream.py"
38+
- name: Run type checker
39+
run: make typecheck
6240

63-
test:
64-
name: Test "${{ inputs.marker }}"
41+
# ── Non-video tests (chat, feeds, etc.) ────────────────────────────
42+
# Uses STREAM_CHAT_* credentials which do NOT have video enabled.
43+
# Video paths and manual test paths are defined in the Makefile.
44+
test-non-video:
45+
name: Non-video tests (${{ matrix.python-version }})
6546
environment:
6647
name: ci
6748
runs-on: ubuntu-latest
@@ -77,32 +58,35 @@ jobs:
7758
uses: ./.github/actions/python-uv-setup
7859
with:
7960
python-version: ${{ matrix.python-version }}
80-
- name: Run non-video tests
61+
- name: Run tests
8162
env:
8263
STREAM_API_KEY: ${{ vars.STREAM_CHAT_API_KEY }}
8364
STREAM_API_SECRET: ${{ secrets.STREAM_CHAT_API_SECRET }}
8465
STREAM_BASE_URL: ${{ vars.STREAM_CHAT_BASE_URL }}
85-
run: |
86-
uv run pytest -m "${{ inputs.marker }}" tests/ getstream/ \
87-
--ignore=tests/rtc \
88-
--ignore=tests/test_video_examples.py \
89-
--ignore=tests/test_video_integration.py \
90-
--ignore=tests/test_video_openai.py \
91-
--ignore=tests/test_signaling.py \
92-
--ignore=tests/test_audio_stream_track.py \
93-
--ignore=getstream/video
94-
- name: Run video tests
66+
run: make test MARKER="${{ inputs.marker }}"
67+
68+
# ── Video tests (video-enabled credentials) ─────────────────────
69+
# Uses STREAM_* credentials which have video enabled.
70+
test-video:
71+
name: Video tests (${{ matrix.python-version }})
72+
environment:
73+
name: ci
74+
runs-on: ubuntu-latest
75+
strategy:
76+
fail-fast: false
77+
matrix:
78+
python-version: ["3.10", "3.11", "3.12", "3.13"]
79+
timeout-minutes: 30
80+
steps:
81+
- name: Checkout
82+
uses: actions/checkout@v5
83+
- name: Install dependencies
84+
uses: ./.github/actions/python-uv-setup
85+
with:
86+
python-version: ${{ matrix.python-version }}
87+
- name: Run tests
9588
env:
9689
STREAM_API_KEY: ${{ vars.STREAM_API_KEY }}
9790
STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }}
9891
STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }}
99-
run: |
100-
uv run pytest -m "${{ inputs.marker }}" \
101-
tests/rtc \
102-
tests/test_video_examples.py \
103-
tests/test_video_integration.py \
104-
tests/test_video_openai.py \
105-
tests/test_signaling.py \
106-
tests/test_audio_stream_track.py \
107-
getstream/video
108-
92+
run: make test-video MARKER="${{ inputs.marker }}"

DEVELOPMENT.md

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,71 @@
11
# Getstream Python SDK
22

3-
### Running tests
3+
### Setup
44

5-
Clone the repo and sync
5+
Clone the repo and install dependencies:
66

77
```
88
git clone git@github.com:GetStream/stream-py.git
99
uv sync --no-sources --all-packages --all-extras --dev
10+
cp .env.example .env # fill in your Stream credentials
11+
pre-commit install # optional: enable commit hooks
1012
```
1113

12-
Env setup
14+
### Running tests
15+
16+
Run `make help` to see all available targets. The most common ones:
1317

1418
```
15-
cp .env.example .env
19+
make test # non-video tests (chat, feeds, moderation, etc.)
20+
make test-video # video/WebRTC tests only
21+
make test-all # both of the above
1622
```
1723

18-
Run tests
24+
Non-video and video tests are split because they require different Stream credentials.
25+
The `MARKER` variable defaults to `"not integration"`. Override it for integration tests:
1926

2027
```
21-
uv run pytest -m "not integration" tests/ getstream/
28+
make test-integration # runs both groups with -m "integration"
29+
make test MARKER="integration" # or target a single group
2230
```
2331

24-
### Commit hook
32+
Two manual tests exist for local telemetry inspection (excluded from CI):
2533

2634
```
27-
pre-commit install
35+
make test-jaeger # requires local Jaeger (docker run ... jaegertracing/all-in-one)
36+
make test-prometheus # requires getstream[telemetry] deps
2837
```
2938

30-
### Check
31-
32-
Shortcut to ruff, ty (type checker) and non integration tests:
39+
### Linting and type checking
3340

3441
```
35-
uv run python dev.py
42+
make lint # ruff check + format check
43+
make typecheck # ty type checker (excludes generated code)
3644
```
3745

38-
### Formatting
46+
To auto-fix lint issues and format:
3947

4048
```
41-
uv run ruff check --fix
49+
make format
4250
```
4351

44-
### Type checking (ty)
45-
46-
Type checking is run via the `ty` type checker, excluding generated code:
52+
Run lint, typecheck, and non-video tests in one go:
4753

4854
```
49-
uv run python dev.py ty
55+
make check
5056
```
5157

52-
Or manually (note: requires exclude flags for generated code - see dev.py for the full list):
58+
### Code generation
59+
5360
```
54-
uvx ty check getstream/ --exclude "getstream/models/" --exclude "getstream/video/rtc/pb/" ...
61+
make regen # regenerate OpenAPI + WebRTC protobuf code
5562
```
5663

64+
### Legacy: dev.py
65+
66+
`dev.py` is an older CLI tool that predates the Makefile. It still works but does not
67+
handle the video/non-video test split or manual test exclusions. Prefer `make` targets.
68+
5769
## Release
5870

5971
Create a new release on Github, CI handles the rest. If you do need to do it manually follow these instructions:

MIGRATION_v2_to_v3.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,26 @@ The general renaming rules:
203203
| `MembershipLevel` | `MembershipLevelResponse` | |
204204
| `ThreadedComment` | `ThreadedCommentResponse` | |
205205

206+
## JSON Serialization of Optional Fields
207+
208+
Optional fields in request objects are now omitted from the JSON body when not set, instead of being sent as explicit `null`. Previously, every unset field was serialized as `null`, which caused the backend to zero out existing values on partial updates.
209+
210+
**Before:**
211+
```python
212+
client.update_app(enforce_unique_usernames="no")
213+
# Wire: {"enforce_unique_usernames":"no","webhook_url":null,"multi_tenant_enabled":null,...}
214+
# Backend: sets enforce_unique_usernames="no", but ALSO resets webhook_url="", multi_tenant_enabled=false, etc.
215+
```
216+
217+
**After:**
218+
```python
219+
client.update_app(enforce_unique_usernames="no")
220+
# Wire: {"enforce_unique_usernames":"no"}
221+
# Backend: sets enforce_unique_usernames="no", all other fields preserved
222+
```
223+
224+
List and dict fields are still serialized when set (including as empty `[]`/`{}`), so you can continue to send an empty list to clear a list field. Unset collection fields (`None`) are now also omitted.
225+
206226
## Getting Help
207227

208228
- [Stream documentation](https://getstream.io/docs/)

Makefile

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
.DEFAULT_GOAL := help
2+
3+
# ── Pytest marker expression ───────────────────────────────────────
4+
# Override on the command line: make test MARKER="integration"
5+
# Default excludes integration tests for everyday local development.
6+
MARKER ?= not integration
7+
8+
# ── Video-related test paths (single source of truth) ──────────────
9+
# When you add a new video test file, add it here. Both test (via
10+
# --ignore) and test-video (via positional args) stay in sync.
11+
VIDEO_PATHS := \
12+
getstream/video \
13+
tests/rtc \
14+
tests/test_audio_stream_track.py \
15+
tests/test_connection_manager.py \
16+
tests/test_connection_utils.py \
17+
tests/test_signaling.py \
18+
tests/test_video_examples.py \
19+
tests/test_video_integration.py \
20+
tests/test_video_openai.py \
21+
tests/test_webrtc_generation.py
22+
23+
# ── Manual tests (require local infrastructure, never run in CI) ───
24+
MANUAL_PATHS := \
25+
tests/test_tracing_jaeger_manual.py \
26+
tests/test_metrics_prometheus_manual.py
27+
28+
# ── Derived ignore flags ──────────────────────────────────────────
29+
VIDEO_IGNORE := $(addprefix --ignore=,$(VIDEO_PATHS))
30+
MANUAL_IGNORE := $(addprefix --ignore=,$(MANUAL_PATHS))
31+
32+
# ── Typecheck exclusions ──────────────────────────────────────────
33+
TY_EXCLUDES := \
34+
--exclude "getstream/models/" \
35+
--exclude "getstream/video/rtc/pb/" \
36+
--exclude "**/rest_client.py" \
37+
--exclude "**/async_rest_client.py" \
38+
--exclude "getstream/chat/channel.py" \
39+
--exclude "getstream/chat/async_channel.py" \
40+
--exclude "getstream/chat/client.py" \
41+
--exclude "getstream/chat/async_client.py" \
42+
--exclude "getstream/common/client.py" \
43+
--exclude "getstream/common/async_client.py" \
44+
--exclude "getstream/moderation/client.py" \
45+
--exclude "getstream/moderation/async_client.py" \
46+
--exclude "getstream/video/client.py" \
47+
--exclude "getstream/video/async_client.py" \
48+
--exclude "getstream/video/call.py" \
49+
--exclude "getstream/video/async_call.py" \
50+
--exclude "getstream/feeds/client.py" \
51+
--exclude "getstream/feeds/feeds.py" \
52+
--exclude "getstream/stream.py"
53+
54+
# ── Targets ───────────────────────────────────────────────────────
55+
56+
.PHONY: test test-video test-all test-integration test-jaeger test-prometheus lint format typecheck check regen help
57+
58+
## Run non-video tests (chat, feeds, moderation, etc.)
59+
test:
60+
uv run pytest -m "$(MARKER)" tests/ getstream/ $(VIDEO_IGNORE) $(MANUAL_IGNORE)
61+
62+
## Run video/WebRTC tests only
63+
test-video:
64+
uv run pytest -m "$(MARKER)" $(VIDEO_PATHS)
65+
66+
## Run all tests (non-video + video), excluding manual tests
67+
test-all:
68+
uv run pytest -m "$(MARKER)" tests/ getstream/ $(MANUAL_IGNORE)
69+
70+
## Run integration tests for both groups
71+
test-integration:
72+
$(MAKE) test MARKER="integration"
73+
$(MAKE) test-video MARKER="integration"
74+
75+
## Run the Jaeger tracing manual test (requires local Jaeger on :4317)
76+
test-jaeger:
77+
uv run pytest -m "integration" tests/test_tracing_jaeger_manual.py
78+
79+
## Run the Prometheus metrics manual test (requires telemetry deps)
80+
test-prometheus:
81+
uv run pytest -m "integration" tests/test_metrics_prometheus_manual.py
82+
83+
## Run Ruff linter and formatter check
84+
lint:
85+
uv run ruff check .
86+
uv run ruff format --check .
87+
88+
## Auto-fix lint issues and format
89+
format:
90+
uv run ruff check --fix .
91+
uv run ruff format .
92+
93+
## Run ty type checker
94+
typecheck:
95+
uvx ty@0.0.24 check getstream/ $(TY_EXCLUDES)
96+
97+
## Run full check: lint + typecheck + non-video tests
98+
check: lint typecheck test
99+
100+
## Regenerate all generated code (OpenAPI + WebRTC protobuf)
101+
regen:
102+
./generate.sh
103+
./generate_webrtc.sh
104+
105+
## Show available targets
106+
help:
107+
@echo "Usage: make <target> [MARKER=\"...\"]"
108+
@echo ""
109+
@echo "Test targets:"
110+
@echo " test Non-video tests (default MARKER='not integration')"
111+
@echo " test-video Video/WebRTC tests only"
112+
@echo " test-all All tests except manual infrastructure tests"
113+
@echo " test-integration Integration tests (both non-video and video)"
114+
@echo " test-jaeger Jaeger tracing manual test (needs Docker Jaeger)"
115+
@echo " test-prometheus Prometheus metrics manual test (needs telemetry deps)"
116+
@echo ""
117+
@echo "Quality targets:"
118+
@echo " lint Ruff linter + format check"
119+
@echo " format Auto-fix lint issues and format code"
120+
@echo " typecheck ty type checker"
121+
@echo " check Full check: lint + typecheck + non-video tests"
122+
@echo ""
123+
@echo "Code generation:"
124+
@echo " regen Regenerate OpenAPI + WebRTC protobuf code"

dev.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
"""
33
Development CLI tool for getstream SDK.
44
Essential dev commands for testing, linting, and type checking.
5+
6+
NOTE: Prefer using the Makefile instead (run `make help` for available targets).
7+
The Makefile is a superset of this script.
8+
9+
This script is kept for backwards compatibility.
510
"""
611

712
import os

getstream/base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ def _read_file_bytes(file_path: str) -> bytes:
3232
return f.read()
3333

3434

35+
def _strip_none(obj):
36+
"""Recursively remove None values from dicts so unset optional fields
37+
are omitted from the JSON body instead of being sent as null."""
38+
if isinstance(obj, dict):
39+
return {k: _strip_none(v) for k, v in obj.items() if v is not None}
40+
if isinstance(obj, list):
41+
return [_strip_none(item) for item in obj]
42+
return obj
43+
44+
3545
def build_path(path: str, path_params: Optional[Dict[str, Any]]) -> str:
3646
if path_params is None:
3747
return path
@@ -176,6 +186,8 @@ def _request_sync(
176186
data_type: Optional[Type[T]] = None,
177187
):
178188
kwargs = kwargs or {}
189+
if "json" in kwargs and kwargs["json"] is not None:
190+
kwargs["json"] = _strip_none(kwargs["json"])
179191
url_path, url_full, endpoint, attrs = self._prepare_request(
180192
method, path, query_params, kwargs
181193
)
@@ -421,6 +433,8 @@ async def _request_async(
421433
data_type: Optional[Type[T]] = None,
422434
):
423435
kwargs = kwargs or {}
436+
if "json" in kwargs and kwargs["json"] is not None:
437+
kwargs["json"] = _strip_none(kwargs["json"])
424438
query_params = query_params or {}
425439
url_path, url_full, endpoint, attrs = self._prepare_request(
426440
method, path, query_params, kwargs

0 commit comments

Comments
 (0)