diff --git a/.env.example b/.env.example index e5f5694..afcb531 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ STREAM_API_KEY=892s22ypvt6m STREAM_API_SECRET=5cssrefv55rs3cnkk38kfjam2k7c2ykwn4h79dqh66ym89gm65cxy4h9jx4cypd6 -STREAM_BASE_URL=http://127.0.0.1:3030 \ No newline at end of file +STREAM_BASE_URL=http://127.0.0.1:3030 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2c1b70e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.3 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files diff --git a/CHANGELOG.md b/CHANGELOG.md index aa26013..836797b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,4 +12,4 @@ - New: query_users - breaking change: rename Apierror -> ApiError -# 0.1.2: Initial release of the package to PyPI (25-10-2023) \ No newline at end of file +# 0.1.2: Initial release of the package to PyPI (25-10-2023) diff --git a/LICENSE.md b/LICENSE.md index f2d1eaf..49088d4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -27,7 +27,7 @@ IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE -SOFTWARE. +SOFTWARE. 1. SOFTWARE. The Stream.io software accompanying this Agreement, may include Source Code, Executable Object Code, associated media, printed materials and diff --git a/README.md b/README.md index f34b095..cc695e2 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ To install the development dependencies, run the following command: ```sh poetry install +pre-commit install ``` To activate the virtual environment, run the following command: @@ -102,17 +103,30 @@ To activate the virtual environment, run the following command: poetry shell ``` -To run tests, create a `.env` using the `.env.example` and adjust it to have valid API credentials +To run tests, create a `.env` using the `.env.example` and adjust it to have valid API credentials ```sh poetry run pytest tests/ getstream/ ``` -Before pushing changes make sure to run the linter: +Before pushing changes make sure to have git hooks installed correctly, so that you get linting done locally `pre-commit install` + +You can also run the code formatting yourself if needed: ```sh poetry run ruff format getstream/ tests/ ``` +### Writing new tests + +pytest is used to run tests and to inject fixtures, simple tests can be written as simple python functions making assert calls. Make sure to have a look at the available test fixtures under `tests/fixtures.py` + +### Generate code from spec + +To regenerate the Python source from OpenAPI, just run the `./generate.sh` script from this repo. + +> [!NOTE] +> Code generation currently relies on tooling that is not publicly available, only Stream devs can regenerate SDK source code from the OpenAPI spec. + ## License This project is licensed under the [MIT License](LICENSE). diff --git a/generate.sh b/generate.sh new file mode 100755 index 0000000..ad808bb --- /dev/null +++ b/generate.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +SOURCE_PATH=../chat + +if [ ! -d $SOURCE_PATH ] +then + echo "cannot find chat path on the parent folder (${SOURCE_PATH}), do you have a copy of the API source?"; + exit 1; +fi + +if ! poetry -V &> /dev/null +then + echo "cannot find poetry in path, did you setup this repo correctly?"; + exit 1; +fi + +set -ex + +# cd in API repo, generate new spec and then generate code from it +( 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/ ) + +# lint generated code with ruff +poetry run ruff format getstream/ tests/ diff --git a/getstream/chat/rest_client.py b/getstream/chat/rest_client.py index 5490560..17b29d2 100644 --- a/getstream/chat/rest_client.py +++ b/getstream/chat/rest_client.py @@ -20,51 +20,6 @@ def __init__(self, api_key: str, base_url: str, timeout: float, token: str): token=token, ) - def list_block_lists(self) -> StreamResponse[ListBlockListResponse]: - return self.get("/api/v2/chat/blocklists", ListBlockListResponse) - - def create_block_list( - self, name: str, words: List[str], type: Optional[str] = None - ) -> StreamResponse[Response]: - json = build_body_dict(name=name, words=words, type=type) - - return self.post("/api/v2/chat/blocklists", Response, json=json) - - def delete_block_list(self, name: str) -> StreamResponse[Response]: - path_params = { - "name": name, - } - - return self.delete( - "/api/v2/chat/blocklists/{name}", Response, path_params=path_params - ) - - def get_block_list(self, name: str) -> StreamResponse[GetBlockListResponse]: - path_params = { - "name": name, - } - - return self.get( - "/api/v2/chat/blocklists/{name}", - GetBlockListResponse, - path_params=path_params, - ) - - def update_block_list( - self, name: str, words: Optional[List[str]] = None - ) -> StreamResponse[Response]: - path_params = { - "name": name, - } - json = build_body_dict(words=words) - - return self.put( - "/api/v2/chat/blocklists/{name}", - Response, - path_params=path_params, - json=json, - ) - def query_channels( self, limit: Optional[int] = None, @@ -727,6 +682,7 @@ def export_channels( channels: List[ChannelExport], clear_deleted_message_text: Optional[bool] = None, export_users: Optional[bool] = None, + include_soft_deleted_channels: Optional[bool] = None, include_truncated_messages: Optional[bool] = None, version: Optional[str] = None, ) -> StreamResponse[ExportChannelsResponse]: @@ -734,6 +690,7 @@ def export_channels( channels=channels, clear_deleted_message_text=clear_deleted_message_text, export_users=export_users, + include_soft_deleted_channels=include_soft_deleted_channels, include_truncated_messages=include_truncated_messages, version=version, ) @@ -1035,6 +992,7 @@ def get_replies( created_at_before: Optional[datetime] = None, id_around: Optional[str] = None, created_at_around: Optional[datetime] = None, + sort: Optional[List[Optional[SortParam]]] = None, ) -> StreamResponse[GetRepliesResponse]: query_params = build_query_param( id_gte=id_gte, @@ -1047,6 +1005,7 @@ def get_replies( created_at_before=created_at_before, id_around=id_around, created_at_around=created_at_around, + sort=sort, ) path_params = { "parent_id": parent_id, @@ -1059,71 +1018,6 @@ def get_replies( path_params=path_params, ) - def unban( - self, - target_user_id: str, - type: Optional[str] = None, - id: Optional[str] = None, - created_by: Optional[str] = None, - ) -> StreamResponse[Response]: - query_params = build_query_param( - target_user_id=target_user_id, type=type, id=id, created_by=created_by - ) - - return self.delete( - "/api/v2/chat/moderation/ban", Response, query_params=query_params - ) - - def ban( - self, - target_user_id: str, - banned_by_id: Optional[str] = None, - id: Optional[str] = None, - ip_ban: Optional[bool] = None, - reason: Optional[str] = None, - shadow: Optional[bool] = None, - timeout: Optional[int] = None, - type: Optional[str] = None, - user_id: Optional[str] = None, - banned_by: Optional[UserRequest] = None, - user: Optional[UserRequest] = None, - ) -> StreamResponse[Response]: - json = build_body_dict( - target_user_id=target_user_id, - banned_by_id=banned_by_id, - id=id, - ip_ban=ip_ban, - reason=reason, - shadow=shadow, - timeout=timeout, - type=type, - user_id=user_id, - banned_by=banned_by, - user=user, - ) - - return self.post("/api/v2/chat/moderation/ban", Response, json=json) - - def flag( - self, - reason: Optional[str] = None, - target_message_id: Optional[str] = None, - target_user_id: Optional[str] = None, - user_id: Optional[str] = None, - custom: Optional[Dict[str, object]] = None, - user: Optional[UserRequest] = None, - ) -> StreamResponse[FlagResponse]: - json = build_body_dict( - reason=reason, - target_message_id=target_message_id, - target_user_id=target_user_id, - user_id=user_id, - custom=custom, - user=user, - ) - - return self.post("/api/v2/chat/moderation/flag", FlagResponse, json=json) - def query_message_flags( self, payload: Optional[QueryMessageFlagsRequest] = None ) -> StreamResponse[QueryMessageFlagsResponse]: @@ -1135,19 +1029,6 @@ def query_message_flags( query_params=query_params, ) - def mute_user( - self, - timeout: int, - user_id: Optional[str] = None, - target_ids: Optional[List[str]] = None, - user: Optional[UserRequest] = None, - ) -> StreamResponse[MuteUserResponse]: - json = build_body_dict( - timeout=timeout, user_id=user_id, target_ids=target_ids, user=user - ) - - return self.post("/api/v2/chat/moderation/mute", MuteUserResponse, json=json) - def mute_channel( self, expiration: Optional[int] = None, @@ -1163,19 +1044,6 @@ def mute_channel( "/api/v2/chat/moderation/mute/channel", MuteChannelResponse, json=json ) - def unmute_user( - self, - timeout: int, - user_id: Optional[str] = None, - target_ids: Optional[List[str]] = None, - user: Optional[UserRequest] = None, - ) -> StreamResponse[UnmuteResponse]: - json = build_body_dict( - timeout=timeout, user_id=user_id, target_ids=target_ids, user=user - ) - - return self.post("/api/v2/chat/moderation/unmute", UnmuteResponse, json=json) - def unmute_channel( self, expiration: Optional[int] = None, @@ -1457,6 +1325,7 @@ def search( def query_threads( self, limit: Optional[int] = None, + member_limit: Optional[int] = None, next: Optional[str] = None, participant_limit: Optional[int] = None, prev: Optional[str] = None, @@ -1466,6 +1335,7 @@ def query_threads( ) -> StreamResponse[QueryThreadsResponse]: json = build_body_dict( limit=limit, + member_limit=member_limit, next=next, participant_limit=participant_limit, prev=prev, @@ -1482,11 +1352,13 @@ def get_thread( connection_id: Optional[str] = None, reply_limit: Optional[int] = None, participant_limit: Optional[int] = None, + member_limit: Optional[int] = None, ) -> StreamResponse[GetThreadResponse]: query_params = build_query_param( connection_id=connection_id, reply_limit=reply_limit, participant_limit=participant_limit, + member_limit=member_limit, ) path_params = { "message_id": message_id, diff --git a/getstream/common/rest_client.py b/getstream/common/rest_client.py index 9546332..39bbd1e 100644 --- a/getstream/common/rest_client.py +++ b/getstream/common/rest_client.py @@ -111,6 +111,46 @@ def update_app( return self.patch("/api/v2/app", Response, json=json) + def list_block_lists(self) -> StreamResponse[ListBlockListResponse]: + return self.get("/api/v2/blocklists", ListBlockListResponse) + + def create_block_list( + self, name: str, words: List[str], type: Optional[str] = None + ) -> StreamResponse[Response]: + json = build_body_dict(name=name, words=words, type=type) + + return self.post("/api/v2/blocklists", Response, json=json) + + def delete_block_list(self, name: str) -> StreamResponse[Response]: + path_params = { + "name": name, + } + + return self.delete( + "/api/v2/blocklists/{name}", Response, path_params=path_params + ) + + def get_block_list(self, name: str) -> StreamResponse[GetBlockListResponse]: + path_params = { + "name": name, + } + + return self.get( + "/api/v2/blocklists/{name}", GetBlockListResponse, path_params=path_params + ) + + def update_block_list( + self, name: str, words: Optional[List[str]] = None + ) -> StreamResponse[Response]: + path_params = { + "name": name, + } + json = build_body_dict(words=words) + + return self.put( + "/api/v2/blocklists/{name}", Response, path_params=path_params, json=json + ) + def check_push( self, apn_template: Optional[str] = None, @@ -231,6 +271,96 @@ def get_import(self, id: str) -> StreamResponse[GetImportResponse]: "/api/v2/imports/{id}", GetImportResponse, path_params=path_params ) + def unban( + self, + target_user_id: str, + channel_cid: Optional[str] = None, + created_by: Optional[str] = None, + ) -> StreamResponse[Response]: + query_params = build_query_param( + target_user_id=target_user_id, + channel_cid=channel_cid, + created_by=created_by, + ) + + return self.delete( + "/api/v2/moderation/ban", Response, query_params=query_params + ) + + def ban( + self, + target_user_id: str, + banned_by_id: Optional[str] = None, + channel_cid: Optional[str] = None, + ip_ban: Optional[bool] = None, + reason: Optional[str] = None, + shadow: Optional[bool] = None, + timeout: Optional[int] = None, + user_id: Optional[str] = None, + banned_by: Optional[UserRequest] = None, + user: Optional[UserRequest] = None, + ) -> StreamResponse[Response]: + json = build_body_dict( + target_user_id=target_user_id, + banned_by_id=banned_by_id, + channel_cid=channel_cid, + ip_ban=ip_ban, + reason=reason, + shadow=shadow, + timeout=timeout, + user_id=user_id, + banned_by=banned_by, + user=user, + ) + + return self.post("/api/v2/moderation/ban", Response, json=json) + + def flag( + self, + reason: Optional[str] = None, + target_message_id: Optional[str] = None, + target_user_id: Optional[str] = None, + user_id: Optional[str] = None, + custom: Optional[Dict[str, object]] = None, + user: Optional[UserRequest] = None, + ) -> StreamResponse[FlagResponse]: + json = build_body_dict( + reason=reason, + target_message_id=target_message_id, + target_user_id=target_user_id, + user_id=user_id, + custom=custom, + user=user, + ) + + return self.post("/api/v2/moderation/flag", FlagResponse, json=json) + + def mute_user( + self, + timeout: int, + user_id: Optional[str] = None, + target_ids: Optional[List[str]] = None, + user: Optional[UserRequest] = None, + ) -> StreamResponse[MuteUserResponse]: + json = build_body_dict( + timeout=timeout, user_id=user_id, target_ids=target_ids, user=user + ) + + return self.post("/api/v2/moderation/mute", MuteUserResponse, json=json) + + def unmute_user( + self, + timeout: int, + user_id: Optional[str] = None, + target_ids: Optional[List[str]] = None, + user: Optional[UserRequest] = None, + ) -> StreamResponse[UnmuteResponse]: + json = build_body_dict( + timeout=timeout, user_id=user_id, target_ids=target_ids, user=user + ) + + return self.post("/api/v2/moderation/unmute", UnmuteResponse, json=json) + def get_og(self, url: str) -> StreamResponse[GetOGResponse]: query_params = build_query_param(url=url) diff --git a/getstream/models/__init__.py b/getstream/models/__init__.py index 753b27b..cdcba08 100644 --- a/getstream/models/__init__.py +++ b/getstream/models/__init__.py @@ -101,10 +101,13 @@ class Action(DataClassJsonMixin): @dataclass -class App(DataClassJsonMixin): +class AppResponseFields(DataClassJsonMixin): async_url_enrich_enabled: bool = dc_field( metadata=dc_config(field_name="async_url_enrich_enabled") ) + auto_translation_enabled: bool = dc_field( + metadata=dc_config(field_name="auto_translation_enabled") + ) campaign_enabled: bool = dc_field(metadata=dc_config(field_name="campaign_enabled")) cdn_expiration_seconds: int = dc_field( metadata=dc_config(field_name="cdn_expiration_seconds") @@ -173,9 +176,6 @@ class App(DataClassJsonMixin): push_notifications: "PushNotificationFields" = dc_field( metadata=dc_config(field_name="push_notifications") ) - auto_translation_enabled: Optional[bool] = dc_field( - default=None, metadata=dc_config(field_name="auto_translation_enabled") - ) before_message_send_hook_url: Optional[str] = dc_field( default=None, metadata=dc_config(field_name="before_message_send_hook_url") ) @@ -403,7 +403,9 @@ class BanRequest(DataClassJsonMixin): banned_by_id: Optional[str] = dc_field( default=None, metadata=dc_config(field_name="banned_by_id") ) - id: Optional[str] = dc_field(default=None, metadata=dc_config(field_name="id")) + channel_cid: Optional[str] = dc_field( + default=None, metadata=dc_config(field_name="channel_cid") + ) ip_ban: Optional[bool] = dc_field( default=None, metadata=dc_config(field_name="ip_ban") ) @@ -416,7 +418,6 @@ class BanRequest(DataClassJsonMixin): timeout: Optional[int] = dc_field( default=None, metadata=dc_config(field_name="timeout") ) - type: Optional[str] = dc_field(default=None, metadata=dc_config(field_name="type")) user_id: Optional[str] = dc_field( default=None, metadata=dc_config(field_name="user_id") ) @@ -822,9 +823,6 @@ class CallSettingsResponse(DataClassJsonMixin): @dataclass class CallStateResponseFields(DataClassJsonMixin): - blocked_users: "List[UserResponse]" = dc_field( - metadata=dc_config(field_name="blocked_users") - ) members: "List[MemberResponse]" = dc_field(metadata=dc_config(field_name="members")) own_capabilities: "List[OwnCapability]" = dc_field( metadata=dc_config(field_name="own_capabilities") @@ -2424,6 +2422,9 @@ class ExportChannelsRequest(DataClassJsonMixin): export_users: Optional[bool] = dc_field( default=None, metadata=dc_config(field_name="export_users") ) + include_soft_deleted_channels: Optional[bool] = dc_field( + default=None, metadata=dc_config(field_name="include_soft_deleted_channels") + ) include_truncated_messages: Optional[bool] = dc_field( default=None, metadata=dc_config(field_name="include_truncated_messages") ) @@ -2719,7 +2720,9 @@ class FullUserResponse(DataClassJsonMixin): ) ) id: str = dc_field(metadata=dc_config(field_name="id")) + image: str = dc_field(metadata=dc_config(field_name="image")) language: str = dc_field(metadata=dc_config(field_name="language")) + name: str = dc_field(metadata=dc_config(field_name="name")) online: bool = dc_field(metadata=dc_config(field_name="online")) role: str = dc_field(metadata=dc_config(field_name="role")) shadow_banned: bool = dc_field(metadata=dc_config(field_name="shadow_banned")) @@ -2763,9 +2766,6 @@ class FullUserResponse(DataClassJsonMixin): mm_field=fields.DateTime(format="iso"), ), ) - image: Optional[str] = dc_field( - default=None, metadata=dc_config(field_name="image") - ) invisible: Optional[bool] = dc_field( default=None, metadata=dc_config(field_name="invisible") ) @@ -2778,7 +2778,6 @@ class FullUserResponse(DataClassJsonMixin): mm_field=fields.DateTime(format="iso"), ), ) - name: Optional[str] = dc_field(default=None, metadata=dc_config(field_name="name")) revoke_tokens_issued_before: Optional[datetime] = dc_field( default=None, metadata=dc_config( @@ -2791,6 +2790,9 @@ class FullUserResponse(DataClassJsonMixin): latest_hidden_channels: Optional[List[str]] = dc_field( default=None, metadata=dc_config(field_name="latest_hidden_channels") ) + privacy_settings: "Optional[PrivacySettings]" = dc_field( + default=None, metadata=dc_config(field_name="privacy_settings") + ) push_notifications: "Optional[PushNotificationSettings]" = dc_field( default=None, metadata=dc_config(field_name="push_notifications") ) @@ -2844,7 +2846,7 @@ class GeolocationResult(DataClassJsonMixin): @dataclass class GetApplicationResponse(DataClassJsonMixin): duration: str = dc_field(metadata=dc_config(field_name="duration")) - app: "App" = dc_field(metadata=dc_config(field_name="app")) + app: "AppResponseFields" = dc_field(metadata=dc_config(field_name="app")) @dataclass @@ -2858,9 +2860,6 @@ class GetBlockListResponse(DataClassJsonMixin): @dataclass class GetCallResponse(DataClassJsonMixin): duration: str = dc_field(metadata=dc_config(field_name="duration")) - blocked_users: "List[UserResponse]" = dc_field( - metadata=dc_config(field_name="blocked_users") - ) members: "List[MemberResponse]" = dc_field(metadata=dc_config(field_name="members")) own_capabilities: "List[OwnCapability]" = dc_field( metadata=dc_config(field_name="own_capabilities") @@ -3111,9 +3110,6 @@ class GetOrCreateCallRequest(DataClassJsonMixin): class GetOrCreateCallResponse(DataClassJsonMixin): created: bool = dc_field(metadata=dc_config(field_name="created")) duration: str = dc_field(metadata=dc_config(field_name="duration")) - blocked_users: "List[UserResponse]" = dc_field( - metadata=dc_config(field_name="blocked_users") - ) members: "List[MemberResponse]" = dc_field(metadata=dc_config(field_name="members")) own_capabilities: "List[OwnCapability]" = dc_field( metadata=dc_config(field_name="own_capabilities") @@ -3625,6 +3621,14 @@ class MarkUnreadRequest(DataClassJsonMixin): ) +@dataclass +class MediaPubSubHint(DataClassJsonMixin): + audio_published: bool = dc_field(metadata=dc_config(field_name="audio_published")) + audio_subscribed: bool = dc_field(metadata=dc_config(field_name="audio_subscribed")) + video_published: bool = dc_field(metadata=dc_config(field_name="video_published")) + video_subscribed: bool = dc_field(metadata=dc_config(field_name="video_subscribed")) + + @dataclass class MemberRequest(DataClassJsonMixin): user_id: str = dc_field(metadata=dc_config(field_name="user_id")) @@ -4380,6 +4384,9 @@ class MuteUsersRequest(DataClassJsonMixin): mute_all_users: Optional[bool] = dc_field( default=None, metadata=dc_config(field_name="mute_all_users") ) + muted_by_id: Optional[str] = dc_field( + default=None, metadata=dc_config(field_name="muted_by_id") + ) screenshare: Optional[bool] = dc_field( default=None, metadata=dc_config(field_name="screenshare") ) @@ -4392,6 +4399,9 @@ class MuteUsersRequest(DataClassJsonMixin): user_ids: Optional[List[str]] = dc_field( default=None, metadata=dc_config(field_name="user_ids") ) + muted_by: "Optional[UserRequest]" = dc_field( + default=None, metadata=dc_config(field_name="muted_by") + ) @dataclass @@ -4556,6 +4566,9 @@ class OwnUser(DataClassJsonMixin): teams: Optional[List[str]] = dc_field( default=None, metadata=dc_config(field_name="teams") ) + privacy_settings: "Optional[PrivacySettings]" = dc_field( + default=None, metadata=dc_config(field_name="privacy_settings") + ) push_notifications: "Optional[PushNotificationSettings]" = dc_field( default=None, metadata=dc_config(field_name="push_notifications") ) @@ -4893,6 +4906,16 @@ class PollVotesResponse(DataClassJsonMixin): prev: Optional[str] = dc_field(default=None, metadata=dc_config(field_name="prev")) +@dataclass +class PrivacySettings(DataClassJsonMixin): + read_receipts: "Optional[ReadReceipts]" = dc_field( + default=None, metadata=dc_config(field_name="read_receipts") + ) + typing_indicators: "Optional[TypingIndicators]" = dc_field( + default=None, metadata=dc_config(field_name="typing_indicators") + ) + + @dataclass class PushConfig(DataClassJsonMixin): version: str = dc_field(metadata=dc_config(field_name="version")) @@ -4953,7 +4976,7 @@ class PushProvider(DataClassJsonMixin): ) ) name: str = dc_field(metadata=dc_config(field_name="name")) - type: int = dc_field(metadata=dc_config(field_name="type")) + type: str = dc_field(metadata=dc_config(field_name="type")) updated_at: datetime = dc_field( metadata=dc_config( field_name="updated_at", @@ -5417,6 +5440,9 @@ class QueryThreadsRequest(DataClassJsonMixin): limit: Optional[int] = dc_field( default=None, metadata=dc_config(field_name="limit") ) + member_limit: Optional[int] = dc_field( + default=None, metadata=dc_config(field_name="member_limit") + ) next: Optional[str] = dc_field(default=None, metadata=dc_config(field_name="next")) participant_limit: Optional[int] = dc_field( default=None, metadata=dc_config(field_name="participant_limit") @@ -5664,6 +5690,13 @@ class Read(DataClassJsonMixin): ) +@dataclass +class ReadReceipts(DataClassJsonMixin): + enabled: Optional[bool] = dc_field( + default=None, metadata=dc_config(field_name="enabled") + ) + + @dataclass class ReadStateResponse(DataClassJsonMixin): last_read: datetime = dc_field( @@ -6197,6 +6230,9 @@ class Subsession(DataClassJsonMixin): ended_at: int = dc_field(metadata=dc_config(field_name="ended_at")) joined_at: int = dc_field(metadata=dc_config(field_name="joined_at")) sfu_id: str = dc_field(metadata=dc_config(field_name="sfu_id")) + pub_sub_hint: "Optional[MediaPubSubHint]" = dc_field( + default=None, metadata=dc_config(field_name="pub_sub_hint") + ) @dataclass @@ -6554,6 +6590,7 @@ class TranscriptionSettings(DataClassJsonMixin): metadata=dc_config(field_name="closed_caption_mode") ) mode: str = dc_field(metadata=dc_config(field_name="mode")) + languages: List[str] = dc_field(metadata=dc_config(field_name="languages")) @dataclass @@ -6562,6 +6599,9 @@ class TranscriptionSettingsRequest(DataClassJsonMixin): closed_caption_mode: Optional[str] = dc_field( default=None, metadata=dc_config(field_name="closed_caption_mode") ) + languages: Optional[List[str]] = dc_field( + default=None, metadata=dc_config(field_name="languages") + ) @dataclass @@ -6570,6 +6610,7 @@ class TranscriptionSettingsResponse(DataClassJsonMixin): metadata=dc_config(field_name="closed_caption_mode") ) mode: str = dc_field(metadata=dc_config(field_name="mode")) + languages: List[str] = dc_field(metadata=dc_config(field_name="languages")) @dataclass @@ -6616,6 +6657,13 @@ class TruncateChannelResponse(DataClassJsonMixin): ) +@dataclass +class TypingIndicators(DataClassJsonMixin): + enabled: Optional[bool] = dc_field( + default=None, metadata=dc_config(field_name="enabled") + ) + + @dataclass class UnblockUserRequest(DataClassJsonMixin): user_id: str = dc_field(metadata=dc_config(field_name="user_id")) @@ -6922,9 +6970,6 @@ class UpdateCallRequest(DataClassJsonMixin): @dataclass class UpdateCallResponse(DataClassJsonMixin): duration: str = dc_field(metadata=dc_config(field_name="duration")) - blocked_users: "List[UserResponse]" = dc_field( - metadata=dc_config(field_name="blocked_users") - ) members: "List[MemberResponse]" = dc_field(metadata=dc_config(field_name="members")) own_capabilities: "List[OwnCapability]" = dc_field( metadata=dc_config(field_name="own_capabilities") @@ -7588,6 +7633,9 @@ class UserObject(DataClassJsonMixin): teams: Optional[List[str]] = dc_field( default=None, metadata=dc_config(field_name="teams") ) + privacy_settings: "Optional[PrivacySettings]" = dc_field( + default=None, metadata=dc_config(field_name="privacy_settings") + ) push_notifications: "Optional[PushNotificationSettings]" = dc_field( default=None, metadata=dc_config(field_name="push_notifications") ) @@ -7613,6 +7661,9 @@ class UserRequest(DataClassJsonMixin): custom: Optional[Dict[str, object]] = dc_field( default=None, metadata=dc_config(field_name="custom") ) + privacy_settings: "Optional[PrivacySettings]" = dc_field( + default=None, metadata=dc_config(field_name="privacy_settings") + ) push_notifications: "Optional[PushNotificationSettingsInput]" = dc_field( default=None, metadata=dc_config(field_name="push_notifications") ) @@ -7630,7 +7681,10 @@ class UserResponse(DataClassJsonMixin): ) ) id: str = dc_field(metadata=dc_config(field_name="id")) + image: str = dc_field(metadata=dc_config(field_name="image")) + invisible: bool = dc_field(metadata=dc_config(field_name="invisible")) language: str = dc_field(metadata=dc_config(field_name="language")) + name: str = dc_field(metadata=dc_config(field_name="name")) online: bool = dc_field(metadata=dc_config(field_name="online")) role: str = dc_field(metadata=dc_config(field_name="role")) shadow_banned: bool = dc_field(metadata=dc_config(field_name="shadow_banned")) @@ -7665,12 +7719,6 @@ class UserResponse(DataClassJsonMixin): mm_field=fields.DateTime(format="iso"), ), ) - image: Optional[str] = dc_field( - default=None, metadata=dc_config(field_name="image") - ) - invisible: Optional[bool] = dc_field( - default=None, metadata=dc_config(field_name="invisible") - ) last_active: Optional[datetime] = dc_field( default=None, metadata=dc_config( @@ -7680,7 +7728,6 @@ class UserResponse(DataClassJsonMixin): mm_field=fields.DateTime(format="iso"), ), ) - name: Optional[str] = dc_field(default=None, metadata=dc_config(field_name="name")) revoke_tokens_issued_before: Optional[datetime] = dc_field( default=None, metadata=dc_config( @@ -7709,6 +7756,9 @@ class UserSessionStats(DataClassJsonMixin): packet_loss_fraction: float = dc_field( metadata=dc_config(field_name="packet_loss_fraction") ) + publisher_packet_loss_fraction: float = dc_field( + metadata=dc_config(field_name="publisher_packet_loss_fraction") + ) publishing_duration_seconds: int = dc_field( metadata=dc_config(field_name="publishing_duration_seconds") ) @@ -7769,7 +7819,7 @@ class UserSessionStats(DataClassJsonMixin): webrtc_version: Optional[str] = dc_field( default=None, metadata=dc_config(field_name="webrtc_version") ) - subsessions: "Optional[List[Subsession]]" = dc_field( + subsessions: "Optional[List[Optional[Subsession]]]" = dc_field( default=None, metadata=dc_config(field_name="subsessions") ) geolocation: "Optional[GeolocationResult]" = dc_field( @@ -7787,12 +7837,33 @@ class UserSessionStats(DataClassJsonMixin): max_receiving_video_quality: "Optional[VideoQuality]" = dc_field( default=None, metadata=dc_config(field_name="max_receiving_video_quality") ) + pub_sub_hints: "Optional[MediaPubSubHint]" = dc_field( + default=None, metadata=dc_config(field_name="pub_sub_hints") + ) publisher_audio_mos: "Optional[MOSStats]" = dc_field( default=None, metadata=dc_config(field_name="publisher_audio_mos") ) + publisher_jitter: "Optional[Stats]" = dc_field( + default=None, metadata=dc_config(field_name="publisher_jitter") + ) + publisher_latency: "Optional[Stats]" = dc_field( + default=None, metadata=dc_config(field_name="publisher_latency") + ) + publisher_video_quality_limitation_duration_seconds: "Optional[Dict[str, float]]" = dc_field( + default=None, + metadata=dc_config( + field_name="publisher_video_quality_limitation_duration_seconds" + ), + ) subscriber_audio_mos: "Optional[MOSStats]" = dc_field( default=None, metadata=dc_config(field_name="subscriber_audio_mos") ) + subscriber_jitter: "Optional[Stats]" = dc_field( + default=None, metadata=dc_config(field_name="subscriber_jitter") + ) + subscriber_latency: "Optional[Stats]" = dc_field( + default=None, metadata=dc_config(field_name="subscriber_latency") + ) timeline: "Optional[CallTimeline]" = dc_field( default=None, metadata=dc_config(field_name="timeline") ) diff --git a/getstream/video/call.py b/getstream/video/call.py index 19d87ec..bd78b82 100644 --- a/getstream/video/call.py +++ b/getstream/video/call.py @@ -146,20 +146,24 @@ def mute_users( self, audio: Optional[bool] = None, mute_all_users: Optional[bool] = None, + muted_by_id: Optional[str] = None, screenshare: Optional[bool] = None, screenshare_audio: Optional[bool] = None, video: Optional[bool] = None, user_ids: Optional[List[str]] = None, + muted_by: Optional[UserRequest] = None, ) -> StreamResponse[MuteUsersResponse]: response = self.client.mute_users( type=self.call_type, id=self.id, audio=audio, mute_all_users=mute_all_users, + muted_by_id=muted_by_id, screenshare=screenshare, screenshare_audio=screenshare_audio, video=video, user_ids=user_ids, + muted_by=muted_by, ) self._sync_from_response(response.data) return response diff --git a/getstream/video/rest_client.py b/getstream/video/rest_client.py index 4a4466e..f37fbba 100644 --- a/getstream/video/rest_client.py +++ b/getstream/video/rest_client.py @@ -266,10 +266,12 @@ def mute_users( id: str, audio: Optional[bool] = None, mute_all_users: Optional[bool] = None, + muted_by_id: Optional[str] = None, screenshare: Optional[bool] = None, screenshare_audio: Optional[bool] = None, video: Optional[bool] = None, user_ids: Optional[List[str]] = None, + muted_by: Optional[UserRequest] = None, ) -> StreamResponse[MuteUsersResponse]: path_params = { "type": type, @@ -278,10 +280,12 @@ def mute_users( json = build_body_dict( audio=audio, mute_all_users=mute_all_users, + muted_by_id=muted_by_id, screenshare=screenshare, screenshare_audio=screenshare_audio, video=video, user_ids=user_ids, + muted_by=muted_by, ) return self.post( diff --git a/poetry.lock b/poetry.lock index 2c2293e..9c4b027 100644 --- a/poetry.lock +++ b/poetry.lock @@ -33,6 +33,17 @@ files = [ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -59,6 +70,17 @@ files = [ marshmallow = ">=3.18.0,<4.0.0" typing-inspect = ">=0.4.0,<1" +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -73,6 +95,22 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filelock" +version = "3.14.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "flake8" version = "6.1.0" @@ -145,6 +183,20 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[[package]] +name = "identify" +version = "2.5.36" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.7" @@ -208,6 +260,20 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "24.0" @@ -219,6 +285,22 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +[[package]] +name = "platformdirs" +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -234,6 +316,24 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "3.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pycodestyle" version = "2.11.1" @@ -323,6 +423,66 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "ruff" version = "0.4.2" @@ -349,6 +509,22 @@ files = [ {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -408,7 +584,27 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "virtualenv" +version = "20.26.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, + {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [metadata] lock-version = "2.0" -python-versions = ">=3.8.1,<4.0.0" -content-hash = "02a3d9a5d8b7c7ee127dbe641ba8ac851a0d0991cf0a3d2b52563dec0ff8bd35" +python-versions = ">=3.9,<4.0.0" +content-hash = "a4506a52588ea3e61f9e094104b901834abc57b76a2e4f49d996b4032eb66822" diff --git a/pyproject.toml b/pyproject.toml index 624107d..46882e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ readme = "README.md" [tool.poetry.dependencies] -python = ">=3.8.1,<4.0.0" +python = ">=3.9,<4.0.0" httpx = "^0.27.0" pyjwt = "^2.8.0" dataclasses-json = "^0.6.0" @@ -24,6 +24,7 @@ flake8 = "^6.0.0" [tool.poetry.group.dev-dependencies.dependencies] python-dotenv = "^1.0.1" ruff = "^0.4.1" +pre-commit = "^3.7.0" [build-system] requires = ["poetry-core"] diff --git a/pytest.ini b/pytest.ini index 2bed0f3..df3eb51 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = --doctest-modules \ No newline at end of file +addopts = --doctest-modules diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..cfcd55a --- /dev/null +++ b/tests/base.py @@ -0,0 +1,13 @@ +from abc import ABC + +from getstream import Stream +from getstream.video.call import Call + + +class VideoTestClass(ABC): + """ + Abstract base class for video tests that need to share the same call and client objects using pytest fixture + """ + + client: Stream + call: Call diff --git a/tests/conftest.py b/tests/conftest.py index 777f6fa..7b84560 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,8 @@ import pytest from dotenv import load_dotenv -from tests.fixtures import client, call +from tests.fixtures import client, call, get_user, shared_call -__all__ = ["client", "call"] +__all__ = ["client", "call", "get_user", "shared_call"] @pytest.fixture(scope="session", autouse=True) diff --git a/tests/fixtures.py b/tests/fixtures.py index d130fe7..c9c2501 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,15 +1,14 @@ import os import uuid +from typing import Dict import pytest -from getstream import Stream -CALL_TYPE = "default" -CALL_ID = str(uuid.uuid4()) +from getstream import Stream +from getstream.models import UserRequest, FullUserResponse -@pytest.fixture -def client(): +def _client(): return Stream( api_key=os.environ.get("STREAM_API_KEY"), api_secret=os.environ.get("STREAM_API_SECRET"), @@ -17,6 +16,39 @@ def client(): ) +@pytest.fixture +def client(): + return _client() + + @pytest.fixture def call(client: Stream): - return client.video.call(CALL_TYPE, CALL_ID) + return client.video.call("default", str(uuid.uuid4())) + + +@pytest.fixture(scope="class") +def shared_call(request): + """ + Use this fixture to decorate test classes subclassing base.VideoTestClass + + """ + request.cls.client = _client() + request.cls.call = request.cls.client.video.call("default", str(uuid.uuid4())) + + +@pytest.fixture +def get_user(client: Stream): + def inner( + name: str = None, image: str = None, custom: Dict[str, object] = None + ) -> FullUserResponse: + id = str(uuid.uuid4()) + return client.upsert_users( + UserRequest( + id=id, + name=name, + image=image, + custom=custom, + ), + ).data.users[id] + + return inner diff --git a/tests/test.env.example b/tests/test.env.example index 9a5ffbb..635ba3a 100644 --- a/tests/test.env.example +++ b/tests/test.env.example @@ -1,3 +1,3 @@ STREAM_BASE_URL=https://api.getstream.io STREAM_API_KEY=key -STREAM_API_SECRET=secret \ No newline at end of file +STREAM_API_SECRET=secret diff --git a/tests/test_chat_integration.py b/tests/test_chat_integration.py index d44a066..a787655 100644 --- a/tests/test_chat_integration.py +++ b/tests/test_chat_integration.py @@ -41,7 +41,7 @@ def test_update_users_partial(client: Stream): ] ) assert user_id in response.data.users - assert response.data.users[user_id].name is None + assert not response.data.users[user_id].name assert response.data.users[user_id].role == "admin" assert response.data.users[user_id].custom["color"] == "blue" diff --git a/tests/test_video_examples.py b/tests/test_video_examples.py index b16b02f..fbcb9a8 100644 --- a/tests/test_video_examples.py +++ b/tests/test_video_examples.py @@ -1,4 +1,13 @@ +import uuid + from getstream import Stream +from getstream.models import ( + CallRequest, + CallSettingsRequest, + ScreensharingSettingsRequest, + OwnCapability, +) +from getstream.video.call import Call def test_setup_client(): @@ -44,3 +53,128 @@ def test_create_call_with_members(client: Stream): ], ), ) + + +def test_ban_unban_user(client: Stream, get_user): + bad_user = get_user() + moderator = get_user() + client.ban( + target_user_id=bad_user.id, + banned_by_id=moderator.id, + reason="Banned user and all users sharing the same IP for half hour", + ip_ban=True, + timeout=30, + ) + + client.unban(target_user_id=bad_user.id) + + +def test_block_unblock_user_from_calls(client: Stream, call: Call, get_user): + bad_user = get_user() + call.get_or_create( + data=CallRequest( + created_by_id="tommaso-id", + ) + ) + call.block_user(bad_user.id) + response = call.get() + assert len(response.data.call.blocked_user_ids) == 1 + + call.unblock_user(bad_user.id) + response = call.get() + assert len(response.data.call.blocked_user_ids) == 0 + + +def test_update_settings(call: Call): + user_id = str(uuid.uuid4()) + + call.get_or_create( + data=CallRequest( + created_by_id=user_id, + ) + ) + + call.update( + settings_override=CallSettingsRequest( + screensharing=ScreensharingSettingsRequest( + enabled=True, access_request_enabled=True + ), + ), + ) + + +def test_mute_all(call: Call): + user_id = str(uuid.uuid4()) + call.get_or_create( + data=CallRequest( + created_by_id=user_id, + ) + ) + + call.mute_users( + muted_by_id=user_id, + mute_all_users=True, + audio=True, + ) + + +def test_mute_some_users(call: Call, get_user): + alice = get_user() + bob = get_user() + + user_id = str(uuid.uuid4()) + call.get_or_create( + data=CallRequest( + created_by_id=user_id, + ) + ) + + call.mute_users( + muted_by_id=user_id, + user_ids=[alice.id, bob.id], + audio=True, + video=True, + screenshare=True, + screenshare_audio=True, + ) + + +def test_update_user_permissions(call: Call, get_user): + user_id = str(uuid.uuid4()) + call.get_or_create( + data=CallRequest( + created_by_id=user_id, + ) + ) + + alice = get_user() + call.update_user_permissions( + user_id=alice.id, + revoke_permissions=[OwnCapability.SEND_AUDIO], + ) + + call.update_user_permissions( + user_id=alice.id, + grant_permissions=[OwnCapability.SEND_AUDIO], + ) + + +def test_deactivate_user(client: Stream, get_user): + alice = get_user() + bob = get_user() + + # deactivate one user + client.deactivate_user(user_id=alice.id) + + # reactivates the user + client.reactivate_user(user_id=alice.id) + + # deactivates users in bulk, this is an async operation + response = client.deactivate_users(user_ids=[alice.id, bob.id]) + task_id = response.data.task_id + + # this is just an example, in reality it can take a few seconds for a task to be processed + task_status = client.get_task(task_id) + + if task_status.data.status == "completed": + print(task_status.data.result) diff --git a/tests/test_video_integration.py b/tests/test_video_integration.py index 8a4f210..ded1b14 100644 --- a/tests/test_video_integration.py +++ b/tests/test_video_integration.py @@ -1,4 +1,5 @@ import time + import pytest import uuid import jwt @@ -21,7 +22,7 @@ ) from getstream.stream import Stream -from getstream.video.call import Call +from tests.base import VideoTestClass CALL_TYPE_NAME = f"calltype{uuid.uuid4()}" EXTERNAL_STORAGE_NAME = f"storage{uuid.uuid4()}" @@ -82,7 +83,7 @@ def test_teams(client: Stream): assert len(response.data.calls) > 0 -class TestExternalStorage: +class TestCallTypes: def test_creating_storage_with_reserved_name_should_fail(self, client: Stream): with pytest.raises(Exception) as exc_info: client.video.create_external_storage( @@ -101,8 +102,6 @@ def test_creating_storage_with_reserved_name_should_fail(self, client: Stream): def test_should_be_able_to_list_external_storage(self, client: Stream): client.video.list_external_storage() - -class TestCallTypes: def test_create_call_type(self, client: Stream): response = client.video.create_call_type( name=CALL_TYPE_NAME, @@ -274,9 +273,10 @@ def test_delete_call_type(self, client: Stream): assert response.status_code() == 200 -class TestCalls: - def test_create_call(self, call: Call): - response = call.get_or_create( +@pytest.mark.usefixtures("shared_call") +class TestCall(VideoTestClass): + def test_create_call(self): + response = self.call.get_or_create( data=CallRequest( created_by_id="john", settings_override=CallSettingsRequest( @@ -295,8 +295,8 @@ def test_create_call(self, call: Call): assert response.data.call.settings.geofencing.names == ["canada"] assert response.data.call.settings.screensharing.enabled is False - def test_update_call(self, call: Call): - response = call.update( + def test_update_call(self): + response = self.call.update( settings_override=CallSettingsRequest( audio=AudioSettingsRequest( mic_default_on=True, @@ -306,20 +306,20 @@ def test_update_call(self, call: Call): ) assert response.data.call.settings.audio.mic_default_on is True - def test_rtmp_address(self, call: Call): - response = call.get_or_create( + def test_rtmp_address(self): + response = self.call.get_or_create( data=CallRequest( created_by_id="john", ), ) - assert call.id in response.data.call.ingress.rtmp.address + assert self.call.id in response.data.call.ingress.rtmp.address - def test_query_calls(self, client: Stream): - response = client.video.query_calls() + def test_query_calls(self): + response = self.client.video.query_calls() assert len(response.data.calls) >= 1 - def test_enable_call_recording(self, call: Call): - response = call.update( + def test_enable_call_recording(self): + response = self.call.update( settings_override=CallSettingsRequest( recording=RecordSettingsRequest( mode="available", @@ -328,8 +328,8 @@ def test_enable_call_recording(self, call: Call): ) assert response.data.call.settings.recording.mode == "available" - def test_enable_backstage_mode(self, call: Call): - response = call.update( + def test_enable_backstage_mode(self): + response = self.call.update( settings_override=CallSettingsRequest( backstage=BackstageSettingsRequest( enabled=True,