getstream 7.0.0__tar.gz → 7.2.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 (110) hide show
  1. {getstream-7.0.0 → getstream-7.2.0}/.release-please-manifest.json +1 -1
  2. {getstream-7.0.0 → getstream-7.2.0}/CHANGELOG.md +20 -0
  3. {getstream-7.0.0 → getstream-7.2.0}/PKG-INFO +13 -1
  4. {getstream-7.0.0 → getstream-7.2.0}/README.md +12 -0
  5. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/README.md +1 -0
  6. {getstream-7.0.0 → getstream-7.2.0}/getstream/__init__.py +1 -0
  7. {getstream-7.0.0 → getstream-7.2.0}/getstream/base.py +5 -1
  8. {getstream-7.0.0 → getstream-7.2.0}/getstream/models/__init__.py +12 -0
  9. {getstream-7.0.0 → getstream-7.2.0}/getstream/tests/test_webhook.py +12 -0
  10. {getstream-7.0.0 → getstream-7.2.0}/getstream/utils/__init__.py +18 -9
  11. {getstream-7.0.0 → getstream-7.2.0}/getstream/webhook.py +24 -10
  12. {getstream-7.0.0 → getstream-7.2.0}/pyproject.toml +1 -1
  13. {getstream-7.0.0 → getstream-7.2.0}/uv.lock +1 -1
  14. {getstream-7.0.0 → getstream-7.2.0}/.cursor/worktrees.json +0 -0
  15. {getstream-7.0.0 → getstream-7.2.0}/.env.example +0 -0
  16. {getstream-7.0.0 → getstream-7.2.0}/.github/actions/build-rtc-wheel/action.yml +0 -0
  17. {getstream-7.0.0 → getstream-7.2.0}/.github/actions/python-uv-setup/action.yml +0 -0
  18. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/ci.yml +0 -0
  19. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/pr_title.yml +0 -0
  20. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/release.yml +0 -0
  21. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/rtc_wheels.yml +0 -0
  22. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/run_integration.yml +0 -0
  23. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/run_tests.yml +0 -0
  24. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/scheduled_test.yml +0 -0
  25. {getstream-7.0.0 → getstream-7.2.0}/.github/workflows/stream-py.code-workspace +0 -0
  26. {getstream-7.0.0 → getstream-7.2.0}/.gitignore +0 -0
  27. {getstream-7.0.0 → getstream-7.2.0}/.gitmodules +0 -0
  28. {getstream-7.0.0 → getstream-7.2.0}/.pre-commit-config.yaml +0 -0
  29. {getstream-7.0.0 → getstream-7.2.0}/AGENTS.md +0 -0
  30. {getstream-7.0.0 → getstream-7.2.0}/DEVELOPMENT.md +0 -0
  31. {getstream-7.0.0 → getstream-7.2.0}/LICENSE.md +0 -0
  32. {getstream-7.0.0 → getstream-7.2.0}/MIGRATION_v2_to_v3.md +0 -0
  33. {getstream-7.0.0 → getstream-7.2.0}/MIGRATION_v6_to_v7.md +0 -0
  34. {getstream-7.0.0 → getstream-7.2.0}/Makefile +0 -0
  35. {getstream-7.0.0 → getstream-7.2.0}/dev.py +0 -0
  36. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  37. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  38. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  39. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  40. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  41. {getstream-7.0.0 → getstream-7.2.0}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  42. {getstream-7.0.0 → getstream-7.2.0}/generate.sh +0 -0
  43. {getstream-7.0.0 → getstream-7.2.0}/generate_webrtc.sh +0 -0
  44. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/__init__.py +0 -0
  45. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/async_channel.py +0 -0
  46. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/async_client.py +0 -0
  47. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/async_rest_client.py +0 -0
  48. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/channel.py +0 -0
  49. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/client.py +0 -0
  50. {getstream-7.0.0 → getstream-7.2.0}/getstream/chat/rest_client.py +0 -0
  51. {getstream-7.0.0 → getstream-7.2.0}/getstream/common/__init__.py +0 -0
  52. {getstream-7.0.0 → getstream-7.2.0}/getstream/common/async_client.py +0 -0
  53. {getstream-7.0.0 → getstream-7.2.0}/getstream/common/async_rest_client.py +0 -0
  54. {getstream-7.0.0 → getstream-7.2.0}/getstream/common/client.py +0 -0
  55. {getstream-7.0.0 → getstream-7.2.0}/getstream/common/rest_client.py +0 -0
  56. {getstream-7.0.0 → getstream-7.2.0}/getstream/common/telemetry.py +0 -0
  57. {getstream-7.0.0 → getstream-7.2.0}/getstream/config.py +0 -0
  58. {getstream-7.0.0 → getstream-7.2.0}/getstream/exceptions.py +0 -0
  59. {getstream-7.0.0 → getstream-7.2.0}/getstream/feeds/__init__.py +0 -0
  60. {getstream-7.0.0 → getstream-7.2.0}/getstream/feeds/client.py +0 -0
  61. {getstream-7.0.0 → getstream-7.2.0}/getstream/feeds/feeds.py +0 -0
  62. {getstream-7.0.0 → getstream-7.2.0}/getstream/feeds/rest_client.py +0 -0
  63. {getstream-7.0.0 → getstream-7.2.0}/getstream/generic.py +0 -0
  64. {getstream-7.0.0 → getstream-7.2.0}/getstream/logging_utils.py +0 -0
  65. {getstream-7.0.0 → getstream-7.2.0}/getstream/meta.py +0 -0
  66. {getstream-7.0.0 → getstream-7.2.0}/getstream/moderation/__init__.py +0 -0
  67. {getstream-7.0.0 → getstream-7.2.0}/getstream/moderation/async_client.py +0 -0
  68. {getstream-7.0.0 → getstream-7.2.0}/getstream/moderation/async_rest_client.py +0 -0
  69. {getstream-7.0.0 → getstream-7.2.0}/getstream/moderation/client.py +0 -0
  70. {getstream-7.0.0 → getstream-7.2.0}/getstream/moderation/rest_client.py +0 -0
  71. {getstream-7.0.0 → getstream-7.2.0}/getstream/rate_limit.py +0 -0
  72. {getstream-7.0.0 → getstream-7.2.0}/getstream/stream.py +0 -0
  73. {getstream-7.0.0 → getstream-7.2.0}/getstream/stream_response.py +0 -0
  74. {getstream-7.0.0 → getstream-7.2.0}/getstream/tasks.py +0 -0
  75. {getstream-7.0.0 → getstream-7.2.0}/getstream/utils/event_emitter.py +0 -0
  76. {getstream-7.0.0 → getstream-7.2.0}/getstream/version.py +0 -0
  77. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/__init__.py +0 -0
  78. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/async_call.py +0 -0
  79. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/async_client.py +0 -0
  80. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/async_rest_client.py +0 -0
  81. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/call.py +0 -0
  82. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/client.py +0 -0
  83. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/openai.py +0 -0
  84. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rest_client.py +0 -0
  85. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/__init__.py +0 -0
  86. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/audio_forwarder.py +0 -0
  87. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/audio_track.py +0 -0
  88. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/connection_manager.py +0 -0
  89. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/g711.py +0 -0
  90. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/participants.py +0 -0
  91. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/__init__.py +0 -0
  92. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  93. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  94. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  95. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  96. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  97. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  98. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  99. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  100. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  101. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  102. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  103. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  104. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  105. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/track_util.py +0 -0
  106. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/tracks.py +0 -0
  107. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/utils.py +0 -0
  108. {getstream-7.0.0 → getstream-7.2.0}/getstream/video/rtc/video_forwarder.py +0 -0
  109. {getstream-7.0.0 → getstream-7.2.0}/pytest.ini +0 -0
  110. {getstream-7.0.0 → getstream-7.2.0}/release-please-config.json +0 -0
@@ -1,4 +1,4 @@
1
1
  {
2
- ".": "7.0.0",
2
+ ".": "7.2.0",
3
3
  "getstream-rtc": "0.1.0"
4
4
  }
@@ -7,6 +7,26 @@ 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
+ ## [7.2.0](https://github.com/GetStream/stream-py/compare/v7.1.0...v7.2.0) (2026-11-12)
11
+
12
+
13
+ ### Features
14
+
15
+ * **moderation:** add `expected_value` to `UserCustomPropertyParameters`, `CallCustomPropertyParameters`, `ContentCustomPropertyParameters` and `ContentCustomPropertyCountParameters`, so value-operator custom-property rules can be set ([138e50a](https://github.com/GetStream/stream-py/commit/138e50ad2a713a85eef61508d6cf854d64692460))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * cap decompressed gzip webhook bodies at 10 MiB (`getstream.webhook.MAX_WEBHOOK_PAYLOAD_BYTES`) and reject data after the gzip stream, because decompression runs before the signature check ([138e50a](https://github.com/GetStream/stream-py/commit/138e50ad2a713a85eef61508d6cf854d64692460))
21
+ * 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)
22
+
23
+ ## [7.1.0](https://github.com/GetStream/stream-py/compare/v7.0.0...v7.1.0) (2026-11-04)
24
+
25
+
26
+ ### Features
27
+
28
+ * 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))
29
+
10
30
  ## [7.0.0](https://github.com/GetStream/stream-py/compare/v6.1.1...v7.0.0) (2026-10-22)
11
31
 
12
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: getstream
3
- Version: 7.0.0
3
+ Version: 7.2.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,
@@ -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 (
@@ -144,7 +145,10 @@ def _read_file_bytes(file_path: str) -> bytes:
144
145
 
145
146
  def _strip_none(obj):
146
147
  """Recursively remove None values from dicts so unset optional fields
147
- are omitted from the JSON body instead of being sent as null."""
148
+ are omitted from the JSON body instead of being sent as null. ``NULL``
149
+ becomes null."""
150
+ if obj is NULL:
151
+ return None
148
152
  if isinstance(obj, dict):
149
153
  return {k: _strip_none(v) for k, v in obj.items() if v is not None}
150
154
  if isinstance(obj, list):
@@ -3927,6 +3927,9 @@ class CallCreatedEvent(DataClassJsonMixin):
3927
3927
 
3928
3928
  @dataclass
3929
3929
  class CallCustomPropertyParameters(DataClassJsonMixin):
3930
+ expected_value: object | None = dc_field(
3931
+ default=None, metadata=dc_config(field_name="expected_value")
3932
+ )
3930
3933
  operator: str | None = dc_field(
3931
3934
  default=None, metadata=dc_config(field_name="operator")
3932
3935
  )
@@ -9268,6 +9271,9 @@ class ContentCountRuleParameters(DataClassJsonMixin):
9268
9271
 
9269
9272
  @dataclass
9270
9273
  class ContentCustomPropertyCountParameters(DataClassJsonMixin):
9274
+ expected_value: object | None = dc_field(
9275
+ default=None, metadata=dc_config(field_name="expected_value")
9276
+ )
9271
9277
  operator: str | None = dc_field(
9272
9278
  default=None, metadata=dc_config(field_name="operator")
9273
9279
  )
@@ -9284,6 +9290,9 @@ class ContentCustomPropertyCountParameters(DataClassJsonMixin):
9284
9290
 
9285
9291
  @dataclass
9286
9292
  class ContentCustomPropertyParameters(DataClassJsonMixin):
9293
+ expected_value: object | None = dc_field(
9294
+ default=None, metadata=dc_config(field_name="expected_value")
9295
+ )
9287
9296
  operator: str | None = dc_field(
9288
9297
  default=None, metadata=dc_config(field_name="operator")
9289
9298
  )
@@ -30303,6 +30312,9 @@ class UserCustomEventRequest(DataClassJsonMixin):
30303
30312
 
30304
30313
  @dataclass
30305
30314
  class UserCustomPropertyParameters(DataClassJsonMixin):
30315
+ expected_value: object | None = dc_field(
30316
+ default=None, metadata=dc_config(field_name="expected_value")
30317
+ )
30306
30318
  operator: str | None = dc_field(
30307
30319
  default=None, metadata=dc_config(field_name="operator")
30308
30320
  )
@@ -1,6 +1,7 @@
1
1
  # Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT.
2
2
 
3
3
  import hashlib
4
+ import gzip
4
5
  import hmac
5
6
  import warnings
6
7
  from pathlib import Path
@@ -11,6 +12,7 @@ from getstream.webhook import (
11
12
  InvalidWebhookError,
12
13
  UnknownEvent,
13
14
  get_event_type,
15
+ gunzip_payload,
14
16
  parse_event,
15
17
  parse_sns,
16
18
  parse_sqs,
@@ -1261,6 +1263,16 @@ class TestWebhookConformanceNegative:
1261
1263
  with pytest.raises(InvalidWebhookError, match=InvalidWebhookError.GZIP_FAILED):
1262
1264
  verify_and_parse_webhook(body, sig, CANONICAL_TEST_SECRET)
1263
1265
 
1266
+ def test_oversized_compression(self):
1267
+ body = _read_bytes(self._neg("oversized_compression") / "body.gz")
1268
+ sig = _read_str(self._neg("oversized_compression") / "signature.txt")
1269
+ with pytest.raises(InvalidWebhookError, match="payload exceeds"):
1270
+ verify_and_parse_webhook(body, sig, CANONICAL_TEST_SECRET)
1271
+
1272
+ def test_trailing_data_after_gzip(self):
1273
+ with pytest.raises(InvalidWebhookError, match="trailing data"):
1274
+ gunzip_payload(gzip.compress(b"{}") + b"XYZ")
1275
+
1264
1276
  def test_bad_base64(self):
1265
1277
  # Per CHA-3071 wire format: decode_sqs_payload falls back to raw bytes when
1266
1278
  # base64 decoding fails (uncompressed wire format). For input that is
@@ -1,7 +1,7 @@
1
+ import enum
1
2
  import json
2
3
  import asyncio
3
4
  from typing import Dict, List, Optional, Union, cast
4
- from urllib.parse import quote
5
5
  from datetime import datetime
6
6
  from datetime import timezone
7
7
  from urllib.parse import urlparse, urlunparse
@@ -36,18 +36,27 @@ def validate_and_clean_url(url):
36
36
  return cleaned_url
37
37
 
38
38
 
39
- def encode_datetime(date: Optional[datetime]) -> Optional[str]:
39
+ # An Enum member because dataclasses-json deep-copies dict values, and a deep copy of an Enum member is the member itself. Its value None makes JSON encoders write null.
40
+ class _Null(enum.Enum):
41
+ NULL = None
42
+
43
+
44
+ NULL = _Null.NULL
45
+ """Sends an explicit JSON ``null``. Request bodies drop ``None`` values inside objects, so use ``NULL`` where ``null`` must reach the API, for example inside custom data."""
46
+
47
+
48
+ def encode_datetime(date: Union[datetime, _Null, None]) -> Union[str, _Null, None]:
40
49
  """
41
50
  Encodes a datetime object into an ISO 8601 formatted string.
42
51
 
43
52
  Args:
44
- date (Optional[datetime]): The datetime object to encode.
53
+ date (Union[datetime, _Null, None]): The datetime object to encode.
45
54
 
46
55
  Returns:
47
- Optional[str]: The ISO 8601 string representation of the datetime, or None if input is None.
56
+ Union[str, _Null, None]: The ISO 8601 string representation of the datetime, or the input unchanged if it is None or NULL.
48
57
  """
49
- if date is None:
50
- return None
58
+ if not isinstance(date, datetime):
59
+ return date
51
60
  return date.isoformat()
52
61
 
53
62
 
@@ -110,7 +119,7 @@ def build_query_param(**kwargs):
110
119
  - JSON-serializable objects with a `to_json` method will be serialized using that method.
111
120
  - datetime objects are converted to ISO 8601 strings.
112
121
  - Booleans are converted to lowercase strings.
113
- - Lists are converted to comma-separated strings with URL-encoded values.
122
+ - Lists are converted to comma-separated strings.
114
123
  - Other types (strings, integers, dictionaries) are handled appropriately.
115
124
 
116
125
  Args:
@@ -132,7 +141,7 @@ def build_query_param(**kwargs):
132
141
  elif isinstance(value, (str, int)):
133
142
  params[key] = str(value)
134
143
  elif isinstance(value, list):
135
- params[key] = ",".join(quote(_serialize_query_value(v)) for v in value)
144
+ params[key] = ",".join(_serialize_query_value(v) for v in value)
136
145
  else:
137
146
  # For dictionaries or any other types of objects
138
147
  params[key] = json.dumps(value)
@@ -148,7 +157,7 @@ async def build_query_param_async(**kwargs):
148
157
  This function handles various data types:
149
158
  - JSON-serializable objects with a `to_json` method will be serialized using that method.
150
159
  - Booleans are converted to lowercase strings.
151
- - Lists are converted to comma-separated strings with URL-encoded values.
160
+ - Lists are converted to comma-separated strings.
152
161
  - Other types (strings, integers, dictionaries) are handled appropriately.
153
162
 
154
163
  Args:
@@ -1,7 +1,6 @@
1
1
  # Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT.
2
2
 
3
3
  import base64
4
- import gzip
5
4
  import hashlib
6
5
  import hmac
7
6
  import json
@@ -650,6 +649,11 @@ def verify_webhook_signature(body: bytes | str, signature: str, secret: str) ->
650
649
 
651
650
  _GZIP_MAGIC = b"\x1f\x8b"
652
651
 
652
+ # Caps the decompressed size of a gzip webhook body. gunzip_payload runs before
653
+ # the signature check, so without a cap an unsigned gzip bomb could exhaust
654
+ # memory. Raise it if your events are larger.
655
+ MAX_WEBHOOK_PAYLOAD_BYTES = 10 * 1024 * 1024
656
+
653
657
 
654
658
  def gunzip_payload(body: bytes) -> bytes:
655
659
  """Decompress body if gzip-prefixed, else pass through.
@@ -658,13 +662,8 @@ def gunzip_payload(body: bytes) -> bytes:
658
662
  Stream webhook bodies are always JSON, and JSON cannot start with 0x1F.
659
663
 
660
664
  Raises InvalidWebhookError if body has the gzip magic prefix but isn't a
661
- valid gzip stream. gzip.decompress can fail with at least three different
662
- exception types depending on where decoding breaks:
663
- * gzip.BadGzipFile (subclass of OSError) - malformed header.
664
- * zlib.error - corrupt compressed stream.
665
- * EOFError - stream truncated mid-block.
666
- The first is OSError-derived; the latter two are not. Catch all three so
667
- every "looks like gzip but isn't" failure mode surfaces as InvalidWebhookError.
665
+ valid gzip stream (corrupt or truncated), or decompresses to more than
666
+ MAX_WEBHOOK_PAYLOAD_BYTES.
668
667
  """
669
668
  if not isinstance(body, (bytes, bytearray)):
670
669
  raise InvalidWebhookError(
@@ -672,10 +671,25 @@ def gunzip_payload(body: bytes) -> bytes:
672
671
  )
673
672
  if len(body) < 2 or bytes(body[:2]) != _GZIP_MAGIC:
674
673
  return bytes(body)
674
+ limit = MAX_WEBHOOK_PAYLOAD_BYTES
675
+ decompressor = zlib.decompressobj(wbits=31)
675
676
  try:
676
- return gzip.decompress(bytes(body))
677
- except (OSError, zlib.error, EOFError) as e:
677
+ out = decompressor.decompress(bytes(body), limit + 1)
678
+ except zlib.error as e:
678
679
  raise InvalidWebhookError(f"{InvalidWebhookError.GZIP_FAILED}: {e}") from e
680
+ if len(out) > limit:
681
+ raise InvalidWebhookError(
682
+ f"{InvalidWebhookError.GZIP_FAILED}: payload exceeds {limit} bytes"
683
+ )
684
+ if not decompressor.eof:
685
+ raise InvalidWebhookError(
686
+ f"{InvalidWebhookError.GZIP_FAILED}: truncated gzip stream"
687
+ )
688
+ if decompressor.unused_data:
689
+ raise InvalidWebhookError(
690
+ f"{InvalidWebhookError.GZIP_FAILED}: trailing data after gzip stream"
691
+ )
692
+ return out
679
693
 
680
694
 
681
695
  def decode_sqs_payload(message_body: str) -> bytes:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "getstream"
3
- version = "7.0.0"
3
+ version = "7.2.0"
4
4
  description = "GetStream Python SDK - Build scalable activity feeds, chat, and video calling applications"
5
5
  authors = [
6
6
  { name = "sachaarbonel", email = "sacha.arbonel@hotmail.fr" },
@@ -863,7 +863,7 @@ wheels = [
863
863
 
864
864
  [[package]]
865
865
  name = "getstream"
866
- version = "7.0.0"
866
+ version = "7.2.0"
867
867
  source = { editable = "." }
868
868
  dependencies = [
869
869
  { name = "dataclasses-json" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes