diff --git a/stream_chat/async_chat/campaign.py b/stream_chat/async_chat/campaign.py new file mode 100644 index 0000000..521e7a8 --- /dev/null +++ b/stream_chat/async_chat/campaign.py @@ -0,0 +1,50 @@ +import datetime +from typing import Any, Optional, Union + +from stream_chat.base.campaign import CampaignInterface +from stream_chat.types.campaign import CampaignData +from stream_chat.types.stream_response import StreamResponse + + +class Campaign(CampaignInterface): + async def create( + self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None + ) -> StreamResponse: + if campaign_id is not None: + self.campaign_id = campaign_id + if data is not None: + self.data = data + state = await self.client.create_campaign( # type: ignore + campaign_id=self.campaign_id, data=self.data + ) + + if self.campaign_id is None and state.is_ok() and "campaign" in state: + self.campaign_id = state["campaign"]["id"] + return state + + async def get(self) -> StreamResponse: + return await self.client.get_campaign( # type: ignore + campaign_id=self.campaign_id + ) + + async def update(self, data: CampaignData) -> StreamResponse: + return await self.client.update_campaign( # type: ignore + campaign_id=self.campaign_id, data=data + ) + + async def delete(self, **options: Any) -> StreamResponse: + return await self.client.delete_campaign( # type: ignore + campaign_id=self.campaign_id, **options + ) + + async def start( + self, scheduled_for: Optional[Union[str, datetime.datetime]] = None + ) -> StreamResponse: + return await self.client.start_campaign( # type: ignore + campaign_id=self.campaign_id, scheduled_for=scheduled_for + ) + + async def stop(self) -> StreamResponse: + return await self.client.stop_campaign( # type: ignore + campaign_id=self.campaign_id + ) diff --git a/stream_chat/async_chat/client.py b/stream_chat/async_chat/client.py index bf28d93..05ad3e8 100644 --- a/stream_chat/async_chat/client.py +++ b/stream_chat/async_chat/client.py @@ -13,9 +13,21 @@ Optional, Type, Union, + cast, ) from urllib.parse import urlparse +from stream_chat.async_chat.campaign import Campaign +from stream_chat.async_chat.segment import Segment +from stream_chat.types.base import SortParam +from stream_chat.types.campaign import CampaignData, QueryCampaignsOptions +from stream_chat.types.segment import ( + QuerySegmentsOptions, + QuerySegmentTargetsOptions, + SegmentData, + SegmentType, +) + if sys.version_info >= (3, 8): from typing import Literal else: @@ -537,45 +549,143 @@ async def delete_role(self, name: str) -> StreamResponse: async def list_roles(self) -> StreamResponse: return await self.get("roles") - async def create_segment(self, segment: Dict) -> StreamResponse: - return await self.post("segments", data={"segment": segment}) + def segment( # type: ignore + self, + segment_type: SegmentType, + segment_id: Optional[str] = None, + data: Optional[SegmentData] = None, + ) -> Segment: + return Segment( + client=self, segment_type=segment_type, segment_id=segment_id, data=data + ) + + async def create_segment( + self, + segment_type: SegmentType, + segment_id: Optional[str] = None, + data: Optional[SegmentData] = None, + ) -> StreamResponse: + payload = {"type": segment_type.value} + if segment_id is not None: + payload["id"] = segment_id + if data is not None: + payload.update(cast(dict, data)) + return await self.post("segments", data=payload) - async def query_segments(self, **params: Any) -> StreamResponse: - return await self.get("segments", params={"payload": json.dumps(params)}) + async def get_segment(self, segment_id: str) -> StreamResponse: + return await self.get(f"segments/{segment_id}") - async def update_segment(self, segment_id: str, data: Dict) -> StreamResponse: - return await self.put(f"segments/{segment_id}", data={"segment": data}) + async def query_segments( + self, + filter_conditions: Optional[Dict[str, Any]] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentsOptions] = None, + ) -> StreamResponse: + payload = {} + if filter_conditions is not None: + payload["filter"] = filter_conditions + if sort is not None: + payload["sort"] = sort # type: ignore + if options is not None: + payload.update(cast(dict, options)) + return await self.post("segments/query", data=payload) + + async def update_segment( + self, segment_id: str, data: SegmentData + ) -> StreamResponse: + return await self.put(f"segments/{segment_id}", data=data) async def delete_segment(self, segment_id: str) -> StreamResponse: return await self.delete(f"segments/{segment_id}") - async def create_campaign(self, campaign: Dict) -> StreamResponse: - return await self.post("campaigns", data={"campaign": campaign}) + async def segment_target_exists( + self, segment_id: str, target_id: str + ) -> StreamResponse: + return await self.get(f"segments/{segment_id}/target/{target_id}") - async def query_campaigns(self, **params: Any) -> StreamResponse: - return await self.get("campaigns", params={"payload": json.dumps(params)}) + async def add_segment_targets( + self, segment_id: str, target_ids: List[str] + ) -> StreamResponse: + return await self.post( + f"segments/{segment_id}/addtargets", data={"target_ids": target_ids} + ) - async def update_campaign(self, campaign_id: str, data: Dict) -> StreamResponse: - return await self.put(f"campaigns/{campaign_id}", data={"campaign": data}) + async def query_segment_targets( + self, + segment_id: str, + filter_conditions: Optional[Dict[str, Any]] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentTargetsOptions] = None, + ) -> StreamResponse: + payload = {} + if filter_conditions is not None: + payload["filter"] = filter_conditions + if sort is not None: + payload["sort"] = sort # type: ignore + if options is not None: + payload.update(cast(dict, options)) + return await self.post(f"segments/{segment_id}/targets/query", data=payload) + + async def remove_segment_targets( + self, segment_id: str, target_ids: List[str] + ) -> StreamResponse: + return await self.post( + f"segments/{segment_id}/deletetargets", data={"target_ids": target_ids} + ) - async def delete_campaign(self, campaign_id: str, **options: Any) -> StreamResponse: - return await self.delete(f"campaigns/{campaign_id}", params=options) + def campaign( # type: ignore + self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None + ) -> Campaign: + return Campaign(client=self, campaign_id=campaign_id, data=data) - async def schedule_campaign( - self, campaign_id: str, scheduled_for: int = None + async def create_campaign( + self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None ) -> StreamResponse: - return await self.patch( - f"campaigns/{campaign_id}/schedule", data={"scheduled_for": scheduled_for} - ) + payload = {"id": campaign_id} + if data is not None: + payload.update(cast(dict, data)) + return await self.post("campaigns", data=payload) - async def query_recipients(self, **params: Any) -> StreamResponse: - return await self.get("recipients", params={"payload": json.dumps(params)}) + async def get_campaign(self, campaign_id: str) -> StreamResponse: + return await self.get(f"campaigns/{campaign_id}") - async def stop_campaign(self, campaign_id: str) -> StreamResponse: - return await self.patch(f"campaigns/{campaign_id}/stop") + async def query_campaigns( + self, + filter_conditions: Optional[Dict[str, Any]] = None, + sort: Optional[List[SortParam]] = None, + options: QueryCampaignsOptions = None, + ) -> StreamResponse: + payload = {} + if filter_conditions is not None: + payload["filter"] = filter_conditions + if sort is not None: + payload["sort"] = sort # type: ignore + if options is not None: + payload.update(cast(dict, options)) + return await self.post("campaigns/query", data=payload) + + async def update_campaign( + self, campaign_id: str, data: CampaignData + ) -> StreamResponse: + return await self.put(f"campaigns/{campaign_id}", data=data) + + async def delete_campaign(self, campaign_id: str, **options: Any) -> StreamResponse: + return await self.delete(f"campaigns/{campaign_id}", options) - async def resume_campaign(self, campaign_id: str) -> StreamResponse: - return await self.patch(f"campaigns/{campaign_id}/resume") + async def start_campaign( + self, + campaign_id: str, + scheduled_for: Optional[Union[str, datetime.datetime]] = None, + ) -> StreamResponse: + payload = {} + if scheduled_for is not None: + if isinstance(scheduled_for, datetime.datetime): + scheduled_for = scheduled_for.isoformat() + payload["scheduled_for"] = scheduled_for + return await self.post(f"campaigns/{campaign_id}/start", data=payload) + + async def stop_campaign(self, campaign_id: str) -> StreamResponse: + return await self.post(f"campaigns/{campaign_id}/stop") async def test_campaign( self, campaign_id: str, users: Iterable[str] diff --git a/stream_chat/async_chat/segment.py b/stream_chat/async_chat/segment.py new file mode 100644 index 0000000..e0e09a3 --- /dev/null +++ b/stream_chat/async_chat/segment.py @@ -0,0 +1,65 @@ +from typing import Dict, List, Optional + +from stream_chat.base.segment import SegmentInterface +from stream_chat.types.base import SortParam +from stream_chat.types.segment import QuerySegmentTargetsOptions, SegmentData +from stream_chat.types.stream_response import StreamResponse + + +class Segment(SegmentInterface): + async def create( + self, segment_id: Optional[str] = None, data: Optional[SegmentData] = None + ) -> StreamResponse: + if segment_id is not None: + self.segment_id = segment_id + if data is not None: + self.data = data + + state = await self.client.create_segment( # type: ignore + segment_type=self.segment_type, segment_id=self.segment_id, data=self.data + ) + + if self.segment_id is None and state.is_ok() and "segment" in state: + self.segment_id = state["segment"]["id"] + return state + + async def get(self) -> StreamResponse: + return await self.client.get_segment(segment_id=self.segment_id) # type: ignore + + async def update(self, data: SegmentData) -> StreamResponse: + return await self.client.update_segment( # type: ignore + segment_id=self.segment_id, data=data + ) + + async def delete(self) -> StreamResponse: + return await self.client.delete_segment( # type: ignore + segment_id=self.segment_id + ) + + async def target_exists(self, target_id: str) -> StreamResponse: + return await self.client.segment_target_exists( # type: ignore + segment_id=self.segment_id, target_id=target_id + ) + + async def add_targets(self, target_ids: list) -> StreamResponse: + return await self.client.add_segment_targets( # type: ignore + segment_id=self.segment_id, target_ids=target_ids + ) + + async def query_targets( + self, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentTargetsOptions] = None, + ) -> StreamResponse: + return await self.client.query_segment_targets( # type: ignore + segment_id=self.segment_id, + filter_conditions=filter_conditions, + sort=sort, + options=options, + ) + + async def remove_targets(self, target_ids: list) -> StreamResponse: + return await self.client.remove_segment_targets( # type: ignore + segment_id=self.segment_id, target_ids=target_ids + ) diff --git a/stream_chat/base/campaign.py b/stream_chat/base/campaign.py new file mode 100644 index 0000000..bbe5f19 --- /dev/null +++ b/stream_chat/base/campaign.py @@ -0,0 +1,49 @@ +import abc +import datetime +from typing import Awaitable, Optional, Union + +from stream_chat.base.client import StreamChatInterface +from stream_chat.types.campaign import CampaignData +from stream_chat.types.stream_response import StreamResponse + + +class CampaignInterface(abc.ABC): + def __init__( + self, + client: StreamChatInterface, + campaign_id: Optional[str] = None, + data: CampaignData = None, + ): + self.client = client + self.campaign_id = campaign_id + self.data = data + + @abc.abstractmethod + def create( + self, campaign_id: Optional[str], data: Optional[CampaignData] + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def get(self) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def update( + self, data: CampaignData + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def delete(self) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def start( + self, scheduled_for: Optional[Union[str, datetime.datetime]] = None + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def stop(self) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass diff --git a/stream_chat/base/client.py b/stream_chat/base/client.py index 11ceaa6..38ef942 100644 --- a/stream_chat/base/client.py +++ b/stream_chat/base/client.py @@ -5,7 +5,16 @@ import hmac import os import sys -from typing import Any, Awaitable, Dict, Iterable, List, TypeVar, Union +from typing import Any, Awaitable, Dict, Iterable, List, Optional, TypeVar, Union + +from stream_chat.types.base import SortParam +from stream_chat.types.campaign import CampaignData, QueryCampaignsOptions +from stream_chat.types.segment import ( + QuerySegmentsOptions, + QuerySegmentTargetsOptions, + SegmentData, + SegmentType, +) if sys.version_info >= (3, 8): from typing import Literal @@ -18,6 +27,10 @@ TChannel = TypeVar("TChannel") +TSegment = TypeVar("TSegment") + +TCampaign = TypeVar("TCampaign") + class StreamChatInterface(abc.ABC): def __init__( @@ -585,7 +598,6 @@ def channel( ) -> TChannel: # type: ignore[type-var] """ Creates a channel object - :param channel_type: the channel type :param channel_id: the id of the channel :param data: additional data, ie: {"members":[id1, id2, ...]} @@ -918,18 +930,49 @@ def list_roles(self) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ pass + @abc.abstractmethod + def segment( + self, + segment_type: SegmentType, + segment_id: Optional[str], + data: Optional[SegmentData], + ) -> TSegment: # type: ignore[type-var] + """ + Creates a channel object + :param segment_type: the segment type + :param segment_id: the id of the segment + :param data: the segment data, ie: {"members":[id1, id2, ...]} + :return: Segment + """ + pass + @abc.abstractmethod def create_segment( - self, segment: Dict + self, + segment_type: SegmentType, + segment_id: Optional[str], + data: Optional[SegmentData] = None, ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ Create a segment """ pass + @abc.abstractmethod + def get_segment( + self, segment_id: str + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Query segments + """ + pass + @abc.abstractmethod def query_segments( - self, **params: Any + self, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentsOptions] = None, ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ Query segments @@ -938,7 +981,7 @@ def query_segments( @abc.abstractmethod def update_segment( - self, segment_id: str, data: Dict + self, segment_id: str, data: SegmentData ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ Update a segment by id @@ -954,9 +997,70 @@ def delete_segment( """ pass + @abc.abstractmethod + def segment_target_exists( + self, segment_id: str, target_id: str + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Check if a target exists in a segment + """ + pass + + @abc.abstractmethod + def add_segment_targets( + self, segment_id: str, target_ids: List[str] + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Add targets to a segment + """ + pass + + @abc.abstractmethod + def query_segment_targets( + self, + segment_id: str, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentTargetsOptions] = None, + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Query targets in a segment + """ + pass + + @abc.abstractmethod + def remove_segment_targets( + self, segment_id: str, target_ids: List[str] + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Delete targets from a segment + """ + pass + + @abc.abstractmethod + def campaign( + self, campaign_id: Optional[str], data: Optional[CampaignData] + ) -> TCampaign: # type: ignore[type-var] + """ + Creates a campaign object + :param campaign_id: the campaign id + :param data: campaign_id data + :return: Campaign + """ + pass + @abc.abstractmethod def create_campaign( - self, campaign: Dict + self, campaign_id: Optional[str], data: Optional[CampaignData] + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Create a campaign + """ + pass + + @abc.abstractmethod + def get_campaign( + self, campaign_id: str ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ Create a campaign @@ -965,7 +1069,10 @@ def create_campaign( @abc.abstractmethod def query_campaigns( - self, **params: Any + self, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QueryCampaignsOptions] = None, ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ Query campaigns @@ -974,7 +1081,7 @@ def query_campaigns( @abc.abstractmethod def update_campaign( - self, campaign_id: str, data: Dict + self, campaign_id: str, data: CampaignData ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ Update a campaign @@ -991,11 +1098,13 @@ def delete_campaign( pass @abc.abstractmethod - def schedule_campaign( - self, campaign_id: str, scheduled_for: int = None + def start_campaign( + self, + campaign_id: str, + scheduled_for: Optional[Union[str, datetime.datetime]] = None, ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ - Schedule a campaign at given time + Start a campaign at given time or now if not specified """ pass @@ -1004,16 +1113,7 @@ def stop_campaign( self, campaign_id: str ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ - Stop a in progress campaign - """ - pass - - @abc.abstractmethod - def resume_campaign( - self, campaign_id: str - ) -> Union[StreamResponse, Awaitable[StreamResponse]]: - """ - Resume a stopped campaign + Stop an in progress campaign """ pass @@ -1026,15 +1126,6 @@ def test_campaign( """ pass - @abc.abstractmethod - def query_recipients( - self, **params: Any - ) -> Union[StreamResponse, Awaitable[StreamResponse]]: - """ - Query recipients - """ - pass - @abc.abstractmethod def revoke_tokens( self, since: Union[str, datetime.datetime] diff --git a/stream_chat/base/segment.py b/stream_chat/base/segment.py new file mode 100644 index 0000000..d282807 --- /dev/null +++ b/stream_chat/base/segment.py @@ -0,0 +1,72 @@ +import abc +from typing import Awaitable, Dict, List, Optional, Union + +from stream_chat.base.client import StreamChatInterface +from stream_chat.types.base import SortParam +from stream_chat.types.segment import ( + QuerySegmentTargetsOptions, + SegmentData, + SegmentType, +) +from stream_chat.types.stream_response import StreamResponse + + +class SegmentInterface(abc.ABC): + def __init__( + self, + client: StreamChatInterface, + segment_type: SegmentType, + segment_id: Optional[str] = None, + data: Optional[SegmentData] = None, + ): + self.segment_type = segment_type + self.segment_id = segment_id + self.client = client + self.data = data + + @abc.abstractmethod + def create( + self, segment_id: Optional[str] = None, data: Optional[SegmentData] = None + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def get(self) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def update( + self, data: SegmentData + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def delete(self) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def target_exists( + self, target_id: str + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def add_targets( + self, target_ids: List[str] + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def query_targets( + self, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentTargetsOptions] = None, + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass + + @abc.abstractmethod + def remove_targets( + self, target_ids: List[str] + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + pass diff --git a/stream_chat/campaign.py b/stream_chat/campaign.py new file mode 100644 index 0000000..3063a8c --- /dev/null +++ b/stream_chat/campaign.py @@ -0,0 +1,58 @@ +import datetime +from typing import Any, Optional, Union + +from stream_chat.base.campaign import CampaignInterface +from stream_chat.types.campaign import CampaignData +from stream_chat.types.stream_response import StreamResponse + + +class Campaign(CampaignInterface): + def create( + self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None + ) -> StreamResponse: + if campaign_id is not None: + self.campaign_id = campaign_id + if data is not None: + self.data = self._merge_campaign_data(self.data, data) + state = self.client.create_campaign( + campaign_id=self.campaign_id, data=self.data + ) + + if self.campaign_id is None and state.is_ok() and "campaign" in state: # type: ignore + self.campaign_id = state["campaign"]["id"] # type: ignore + return state # type: ignore + + def get(self) -> StreamResponse: + return self.client.get_campaign(campaign_id=self.campaign_id) # type: ignore + + def update(self, data: CampaignData) -> StreamResponse: + return self.client.update_campaign( # type: ignore + campaign_id=self.campaign_id, data=data + ) + + def delete(self, **options: Any) -> StreamResponse: + return self.client.delete_campaign( # type: ignore + campaign_id=self.campaign_id, **options + ) + + def start( + self, scheduled_for: Optional[Union[str, datetime.datetime]] = None + ) -> StreamResponse: + return self.client.start_campaign( # type: ignore + campaign_id=self.campaign_id, scheduled_for=scheduled_for + ) + + def stop(self) -> StreamResponse: + return self.client.stop_campaign(campaign_id=self.campaign_id) # type: ignore + + @staticmethod + def _merge_campaign_data( + data1: Optional[CampaignData], + data2: Optional[CampaignData], + ) -> CampaignData: + if data1 is None: + return data2 + if data2 is None: + return data1 + data1.update(data2) # type: ignore + return data1 diff --git a/stream_chat/client.py b/stream_chat/client.py index c0cd5e4..9b4aac4 100644 --- a/stream_chat/client.py +++ b/stream_chat/client.py @@ -2,16 +2,26 @@ import json import sys import warnings -from typing import Any, Callable, Dict, Iterable, List, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast from urllib.parse import urlparse from urllib.request import Request, urlopen +from stream_chat.campaign import Campaign +from stream_chat.segment import Segment +from stream_chat.types.base import SortParam +from stream_chat.types.campaign import CampaignData, QueryCampaignsOptions +from stream_chat.types.segment import ( + QuerySegmentsOptions, + QuerySegmentTargetsOptions, + SegmentData, + SegmentType, +) + if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal - import requests from stream_chat.__pkg__ import __version__ @@ -518,49 +528,141 @@ def delete_role(self, name: str) -> StreamResponse: def list_roles(self) -> StreamResponse: return self.get("roles") - def create_segment(self, segment: Dict) -> StreamResponse: - return self.post("segments", data={"segment": segment}) + def segment( # type: ignore + self, + segment_type: SegmentType, + segment_id: Optional[str] = None, + data: Optional[SegmentData] = None, + ) -> Segment: + return Segment( + client=self, segment_type=segment_type, segment_id=segment_id, data=data + ) + + def create_segment( + self, + segment_type: SegmentType, + segment_id: Optional[str] = None, + data: Optional[SegmentData] = None, + ) -> StreamResponse: + payload = {"type": segment_type.value} + if segment_id is not None: + payload["id"] = segment_id + if data is not None: + payload.update(cast(dict, data)) + return self.post("segments", data=payload) - def query_segments(self, **params: Any) -> StreamResponse: - return self.get("segments", params={"payload": json.dumps(params)}) + def get_segment(self, segment_id: str) -> StreamResponse: + return self.get(f"segments/{segment_id}") - def update_segment(self, segment_id: str, data: Dict) -> StreamResponse: - return self.put(f"segments/{segment_id}", data={"segment": data}) + def query_segments( + self, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentsOptions] = None, + ) -> StreamResponse: + payload = {} + if filter_conditions is not None: + payload["filter"] = filter_conditions + if sort is not None: + payload["sort"] = sort # type: ignore + if options is not None: + payload.update(cast(dict, options)) + return self.post("segments/query", data=payload) + + def update_segment(self, segment_id: str, data: SegmentData) -> StreamResponse: + return self.put(f"segments/{segment_id}", data=data) def delete_segment(self, segment_id: str) -> StreamResponse: return self.delete(f"segments/{segment_id}") - def create_campaign(self, campaign: Dict) -> StreamResponse: - return self.post("campaigns", data={"campaign": campaign}) + def segment_target_exists(self, segment_id: str, target_id: str) -> StreamResponse: + return self.get(f"segments/{segment_id}/target/{target_id}") + + def add_segment_targets( + self, segment_id: str, target_ids: List[str] + ) -> StreamResponse: + return self.post( + f"segments/{segment_id}/addtargets", data={"target_ids": target_ids} + ) + + def query_segment_targets( + self, + segment_id: str, + filter_conditions: Optional[Dict[str, Any]] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentTargetsOptions] = None, + ) -> StreamResponse: + payload: Dict[str, Union[Dict[str, Any], List[SortParam]]] = {} + if filter_conditions is not None: + payload["filter"] = filter_conditions + if sort is not None: + payload["sort"] = sort + if options is not None: + payload.update(cast(dict, options)) + return self.post(f"segments/{segment_id}/targets/query", data=payload) + + def remove_segment_targets( + self, segment_id: str, target_ids: List[str] + ) -> StreamResponse: + return self.post( + f"segments/{segment_id}/deletetargets", data={"target_ids": target_ids} + ) + + def campaign( # type: ignore + self, campaign_id: Optional[str] = None, data: CampaignData = None + ) -> Campaign: + return Campaign(client=self, campaign_id=campaign_id, data=data) + + def create_campaign( + self, campaign_id: Optional[str] = None, data: CampaignData = None + ) -> StreamResponse: + payload = {"id": campaign_id} + if data is not None: + payload.update(cast(dict, data)) + return self.post("campaigns", data=payload) - def query_campaigns(self, **params: Any) -> StreamResponse: - return self.get("campaigns", params={"payload": json.dumps(params)}) + def get_campaign(self, campaign_id: str) -> StreamResponse: + return self.get(f"campaigns/{campaign_id}") - def update_campaign(self, campaign_id: str, data: Dict) -> StreamResponse: - return self.put(f"campaigns/{campaign_id}", data={"campaign": data}) + def query_campaigns( + self, + filter_conditions: Optional[Dict[str, Any]] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QueryCampaignsOptions] = None, + ) -> StreamResponse: + payload = {} + if filter_conditions is not None: + payload["filter"] = filter_conditions + if sort is not None: + payload["sort"] = sort # type: ignore + if options is not None: + payload.update(cast(dict, options)) + return self.post("campaigns/query", data=payload) + + def update_campaign(self, campaign_id: str, data: CampaignData) -> StreamResponse: + return self.put(f"campaigns/{campaign_id}", data=data) def delete_campaign(self, campaign_id: str, **options: Any) -> StreamResponse: - return self.delete(f"campaigns/{campaign_id}", params=options) + return self.delete(f"campaigns/{campaign_id}", options) - def schedule_campaign( - self, campaign_id: str, scheduled_for: int = None + def start_campaign( + self, + campaign_id: str, + scheduled_for: Optional[Union[str, datetime.datetime]] = None, ) -> StreamResponse: - return self.patch( - f"campaigns/{campaign_id}/schedule", data={"scheduled_for": scheduled_for} - ) + payload = {} + if scheduled_for is not None: + if isinstance(scheduled_for, datetime.datetime): + scheduled_for = scheduled_for.isoformat() + payload["scheduled_for"] = scheduled_for + return self.post(f"campaigns/{campaign_id}/start", data=payload) def stop_campaign(self, campaign_id: str) -> StreamResponse: - return self.patch(f"campaigns/{campaign_id}/stop") - - def resume_campaign(self, campaign_id: str) -> StreamResponse: - return self.patch(f"campaigns/{campaign_id}/resume") + return self.post(f"campaigns/{campaign_id}/stop") def test_campaign(self, campaign_id: str, users: Iterable[str]) -> StreamResponse: return self.post(f"campaigns/{campaign_id}/test", data={"users": users}) - def query_recipients(self, **params: Any) -> StreamResponse: - return self.get("recipients", params={"payload": json.dumps(params)}) - def revoke_tokens(self, since: Union[str, datetime.datetime]) -> StreamResponse: if isinstance(since, datetime.datetime): since = since.isoformat() diff --git a/stream_chat/segment.py b/stream_chat/segment.py new file mode 100644 index 0000000..f4544f5 --- /dev/null +++ b/stream_chat/segment.py @@ -0,0 +1,63 @@ +from typing import Dict, List, Optional + +from stream_chat.base.segment import SegmentInterface +from stream_chat.types.base import SortParam +from stream_chat.types.segment import QuerySegmentTargetsOptions, SegmentData +from stream_chat.types.stream_response import StreamResponse + + +class Segment(SegmentInterface): + def create( + self, segment_id: Optional[str] = None, data: Optional[SegmentData] = None + ) -> StreamResponse: + if segment_id is not None: + self.segment_id = segment_id + if data is not None: + self.data = data + + state = self.client.create_segment( + segment_type=self.segment_type, segment_id=self.segment_id, data=self.data + ) + + if self.segment_id is None and state.is_ok() and "segment" in state: # type: ignore + self.segment_id = state["segment"]["id"] # type: ignore + return state # type: ignore + + def get(self) -> StreamResponse: + return self.client.get_segment(segment_id=self.segment_id) # type: ignore + + def update(self, data: SegmentData) -> StreamResponse: + return self.client.update_segment( # type: ignore + segment_id=self.segment_id, data=data + ) + + def delete(self) -> StreamResponse: + return self.client.delete_segment(segment_id=self.segment_id) # type: ignore + + def target_exists(self, target_id: str) -> StreamResponse: + return self.client.segment_target_exists( # type: ignore + segment_id=self.segment_id, target_id=target_id + ) + + def add_targets(self, target_ids: list) -> StreamResponse: + return self.client.add_segment_targets( # type: ignore + segment_id=self.segment_id, target_ids=target_ids + ) + + def query_targets( + self, + filter_conditions: Optional[Dict] = None, + sort: Optional[List[SortParam]] = None, + options: Optional[QuerySegmentTargetsOptions] = None, + ) -> StreamResponse: + return self.client.query_segment_targets( # type: ignore + segment_id=self.segment_id, + sort=sort, + filter_conditions=filter_conditions, + options=options, + ) + + def remove_targets(self, target_ids: list) -> StreamResponse: + return self.client.remove_segment_targets( # type: ignore + segment_id=self.segment_id, target_ids=target_ids + ) diff --git a/stream_chat/tests/async_chat/conftest.py b/stream_chat/tests/async_chat/conftest.py index 1790cb5..7dcd57c 100644 --- a/stream_chat/tests/async_chat/conftest.py +++ b/stream_chat/tests/async_chat/conftest.py @@ -33,7 +33,8 @@ def event_loop(): loop.close() -@pytest.fixture(scope="module") +@pytest.fixture(scope="function", autouse=True) +@pytest.mark.asyncio async def client(): base_url = os.environ.get("STREAM_HOST") options = {"base_url": base_url} if base_url else {} @@ -101,7 +102,8 @@ async def command(client: StreamChatAsync): await client.delete_command(response["command"]["name"]) -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") +@pytest.mark.asyncio async def fellowship_of_the_ring(client: StreamChatAsync): members: List[Dict] = [ {"id": "frodo-baggins", "name": "Frodo Baggins", "race": "Hobbit", "age": 50}, diff --git a/stream_chat/tests/async_chat/test_campaign.py b/stream_chat/tests/async_chat/test_campaign.py new file mode 100644 index 0000000..beefb1a --- /dev/null +++ b/stream_chat/tests/async_chat/test_campaign.py @@ -0,0 +1,160 @@ +import datetime +from typing import Dict + +import pytest + +from stream_chat import StreamChatAsync +from stream_chat.types.base import SortOrder +from stream_chat.types.segment import SegmentType + + +@pytest.mark.incremental +@pytest.mark.skip(reason="endpoints are not available in the API yet.") +class TestCampaign: + async def test_campaign_crud(self, client: StreamChatAsync, random_user: Dict): + segment = await client.create_segment(segment_type=SegmentType.USER) + segment_id = segment["segment"]["id"] + + sender_id = random_user["id"] + + campaign = client.campaign( + data={ + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "some name", + } + ) + created = await campaign.create() + assert created.is_ok() + assert "campaign" in created + assert "id" in created["campaign"] + assert "name" in created["campaign"] + + received = await campaign.get() + assert received.is_ok() + assert "campaign" in received + assert "id" in received["campaign"] + assert "name" in received["campaign"] + assert received["campaign"]["name"] == created["campaign"]["name"] + + updated = await campaign.update( + { + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "updated_name", + } + ) + assert updated.is_ok() + assert "campaign" in updated + assert "id" in updated["campaign"] + assert "name" in updated["campaign"] + assert updated["campaign"]["name"] == "updated_name" + + deleted = await campaign.delete() + assert deleted.is_ok() + + await client.delete_segment(segment_id=segment_id) + + async def test_campaign_start_stop( + self, client: StreamChatAsync, random_user: Dict + ): + segment = await client.create_segment(segment_type=SegmentType.USER) + segment_id = segment["segment"]["id"] + + sender_id = random_user["id"] + + target_added = await client.add_segment_targets( + segment_id=segment_id, target_ids=[sender_id] + ) + assert target_added.is_ok() + + campaign = client.campaign( + data={ + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "some name", + } + ) + created = await campaign.create() + assert created.is_ok() + assert "campaign" in created + assert "id" in created["campaign"] + assert "name" in created["campaign"] + + now = datetime.datetime.now(datetime.timezone.utc) + one_hour_later = now + datetime.timedelta(hours=1) + + started = await campaign.start(scheduled_for=one_hour_later) + assert started.is_ok() + assert "campaign" in started + assert "id" in started["campaign"] + assert "name" in started["campaign"] + + stopped = await campaign.stop() + assert stopped.is_ok() + assert "campaign" in stopped + assert "id" in stopped["campaign"] + assert "name" in stopped["campaign"] + + deleted = await campaign.delete() + assert deleted.is_ok() + + await client.delete_segment(segment_id=segment_id) + + async def test_query_campaigns(self, client: StreamChatAsync, random_user: Dict): + segment_created = await client.create_segment(segment_type=SegmentType.USER) + segment_id = segment_created["segment"]["id"] + + sender_id = random_user["id"] + + target_added = await client.add_segment_targets( + segment_id=segment_id, target_ids=[sender_id] + ) + assert target_added.is_ok() + + created = await client.create_campaign( + data={ + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "some name", + } + ) + assert created.is_ok() + assert "campaign" in created + assert "id" in created["campaign"] + assert "name" in created["campaign"] + campaign_id = created["campaign"]["id"] + + query_campaigns = await client.query_campaigns( + filter_conditions={ + "id": { + "$eq": campaign_id, + } + }, + sort=[{"field": "created_at", "direction": SortOrder.DESC}], + options={ + "limit": 10, + }, + ) + assert query_campaigns.is_ok() + assert "campaigns" in query_campaigns + assert len(query_campaigns["campaigns"]) == 1 + assert query_campaigns["campaigns"][0]["id"] == campaign_id + + deleted = await client.delete_campaign(campaign_id=campaign_id) + assert deleted.is_ok() + + segment_deleted = await client.delete_segment(segment_id=segment_id) + assert segment_deleted.is_ok() diff --git a/stream_chat/tests/async_chat/test_segment.py b/stream_chat/tests/async_chat/test_segment.py new file mode 100644 index 0000000..fafdb10 --- /dev/null +++ b/stream_chat/tests/async_chat/test_segment.py @@ -0,0 +1,122 @@ +import uuid + +import pytest + +from stream_chat.async_chat.client import StreamChatAsync +from stream_chat.types.base import SortOrder +from stream_chat.types.segment import SegmentType + + +@pytest.mark.incremental +@pytest.mark.skip(reason="endpoints are not available in the API yet.") +class TestSegment: + async def test_segment_crud(self, client: StreamChatAsync): + segment = client.segment( + SegmentType.USER, + data={ + "name": "test_segment", + "description": "test_description", + }, + ) + created = await segment.create() + assert created.is_ok() + assert "segment" in created + assert "id" in created["segment"] + assert "name" in created["segment"] + + received = await segment.get() + assert received.is_ok() + assert "segment" in received + assert "id" in received["segment"] + assert "name" in received["segment"] + assert received["segment"]["name"] == created["segment"]["name"] + + updated = await segment.update( + { + "name": "updated_name", + "description": "updated_description", + } + ) + assert updated.is_ok() + assert "segment" in updated + assert "id" in updated["segment"] + assert "name" in updated["segment"] + assert updated["segment"]["name"] == "updated_name" + assert updated["segment"]["description"] == "updated_description" + + deleted = await segment.delete() + assert deleted.is_ok() + + async def test_segment_targets(self, client: StreamChatAsync): + segment = client.segment(segment_type=SegmentType.USER) + created = await segment.create() + assert created.is_ok() + assert "segment" in created + assert "id" in created["segment"] + assert "name" in created["segment"] + + target_ids = [str(uuid.uuid4()) for _ in range(10)] + target_added = await segment.add_targets(target_ids=target_ids) + assert target_added.is_ok() + + target_exists = await segment.target_exists(target_id=target_ids[0]) + assert target_exists.is_ok() + + query_targets_1 = await segment.query_targets( + options={ + "limit": 3, + }, + ) + assert query_targets_1.is_ok() + assert "targets" in query_targets_1 + assert "next" in query_targets_1 + assert len(query_targets_1["targets"]) == 3 + + query_targets_2 = await segment.query_targets( + filter_conditions={"target_id": {"$lte": ""}}, + sort=[{"field": "target_id", "direction": SortOrder.DESC}], + options={ + "limit": 3, + "next": query_targets_1["next"], + }, + ) + assert query_targets_2.is_ok() + assert "targets" in query_targets_2 + assert "next" in query_targets_2 + assert len(query_targets_2["targets"]) == 3 + + target_deleted = await segment.remove_targets(target_ids=target_ids) + assert target_deleted.is_ok() + + deleted = await segment.delete() + assert deleted.is_ok() + + async def test_query_segments(self, client: StreamChatAsync): + created = await client.create_segment(segment_type=SegmentType.USER) + assert created.is_ok() + assert "segment" in created + assert "id" in created["segment"] + assert "name" in created["segment"] + segment_id = created["segment"]["id"] + + target_ids = [str(uuid.uuid4()) for _ in range(10)] + target_added = await client.add_segment_targets( + segment_id=segment_id, target_ids=target_ids + ) + assert target_added.is_ok() + + query_segments = await client.query_segments( + filter_conditions={"id": {"$eq": segment_id}}, + sort=[{"field": "created_at", "direction": SortOrder.DESC}], + ) + assert query_segments.is_ok() + assert "segments" in query_segments + assert len(query_segments["segments"]) == 1 + + target_deleted = await client.remove_segment_targets( + segment_id=segment_id, target_ids=target_ids + ) + assert target_deleted.is_ok() + + deleted = await client.delete_segment(segment_id=segment_id) + assert deleted.is_ok() diff --git a/stream_chat/tests/test_campaign.py b/stream_chat/tests/test_campaign.py new file mode 100644 index 0000000..b8d9c89 --- /dev/null +++ b/stream_chat/tests/test_campaign.py @@ -0,0 +1,164 @@ +import datetime +from typing import Dict + +import pytest + +from stream_chat import StreamChat +from stream_chat.types.base import SortOrder +from stream_chat.types.segment import SegmentType + + +@pytest.mark.incremental +@pytest.mark.skip(reason="endpoints are not available in the API yet.") +class TestCampaign: + def test_campaign_crud(self, client: StreamChat, random_user: Dict): + segment = client.create_segment(segment_type=SegmentType.USER) + segment_id = segment["segment"]["id"] + + sender_id = random_user["id"] + + campaign = client.campaign( + data={ + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "some name", + } + ) + created = campaign.create( + data={ + "name": "created name", + } + ) + assert created.is_ok() + assert "campaign" in created + assert "id" in created["campaign"] + assert "name" in created["campaign"] + assert created["campaign"]["name"] == "created name" + + received = campaign.get() + assert received.is_ok() + assert "campaign" in received + assert "id" in received["campaign"] + assert "name" in received["campaign"] + assert received["campaign"]["name"] == created["campaign"]["name"] + + updated = campaign.update( + { + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "updated_name", + } + ) + assert updated.is_ok() + assert "campaign" in updated + assert "id" in updated["campaign"] + assert "name" in updated["campaign"] + assert updated["campaign"]["name"] == "updated_name" + + deleted = campaign.delete() + assert deleted.is_ok() + + segment_deleted = client.delete_segment(segment_id=segment_id) + assert segment_deleted.is_ok() + + def test_campaign_start_stop(self, client: StreamChat, random_user: Dict): + segment = client.create_segment(segment_type=SegmentType.USER) + segment_id = segment["segment"]["id"] + + sender_id = random_user["id"] + + target_added = client.add_segment_targets( + segment_id=segment_id, target_ids=[sender_id] + ) + assert target_added.is_ok() + + campaign = client.campaign( + data={ + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "some name", + } + ) + created = campaign.create() + assert created.is_ok() + assert "campaign" in created + assert "id" in created["campaign"] + assert "name" in created["campaign"] + + now = datetime.datetime.now(datetime.timezone.utc) + one_hour_later = now + datetime.timedelta(hours=1) + + started = campaign.start(scheduled_for=one_hour_later) + assert started.is_ok() + assert "campaign" in started + assert "id" in started["campaign"] + assert "name" in started["campaign"] + + stopped = campaign.stop() + assert stopped.is_ok() + assert "campaign" in stopped + assert "id" in stopped["campaign"] + assert "name" in stopped["campaign"] + + deleted = campaign.delete() + assert deleted.is_ok() + + client.delete_segment(segment_id=segment_id) + + def test_query_campaigns(self, client: StreamChat, random_user: Dict): + segment_created = client.create_segment(segment_type=SegmentType.USER) + segment_id = segment_created["segment"]["id"] + + sender_id = random_user["id"] + + target_added = client.add_segment_targets( + segment_id=segment_id, target_ids=[sender_id] + ) + assert target_added.is_ok() + + created = client.create_campaign( + data={ + "message_template": { + "text": "{Hello}", + }, + "segment_ids": [segment_id], + "sender_id": sender_id, + "name": "some name", + } + ) + assert created.is_ok() + assert "campaign" in created + assert "id" in created["campaign"] + assert "name" in created["campaign"] + campaign_id = created["campaign"]["id"] + + query_campaigns = client.query_campaigns( + filter_conditions={ + "id": { + "$eq": campaign_id, + } + }, + sort=[{"field": "created_at", "direction": SortOrder.DESC}], + options={ + "limit": 10, + }, + ) + assert query_campaigns.is_ok() + assert "campaigns" in query_campaigns + assert len(query_campaigns["campaigns"]) == 1 + assert query_campaigns["campaigns"][0]["id"] == campaign_id + + deleted = client.delete_campaign(campaign_id=campaign_id) + assert deleted.is_ok() + + segment_deleted = client.delete_segment(segment_id=segment_id) + assert segment_deleted.is_ok() diff --git a/stream_chat/tests/test_segment.py b/stream_chat/tests/test_segment.py new file mode 100644 index 0000000..a5482e2 --- /dev/null +++ b/stream_chat/tests/test_segment.py @@ -0,0 +1,122 @@ +import uuid + +import pytest + +from stream_chat import StreamChat +from stream_chat.types.base import SortOrder +from stream_chat.types.segment import SegmentType + + +@pytest.mark.incremental +@pytest.mark.skip(reason="endpoints are not available in the API yet.") +class TestSegment: + def test_segment_crud(self, client: StreamChat): + segment = client.segment( + SegmentType.USER, + data={ + "name": "test_segment", + "description": "test_description", + }, + ) + created = segment.create() + assert created.is_ok() + assert "segment" in created + assert "id" in created["segment"] + assert "name" in created["segment"] + + received = segment.get() + assert received.is_ok() + assert "segment" in received + assert "id" in received["segment"] + assert "name" in received["segment"] + assert received["segment"]["name"] == created["segment"]["name"] + + updated = segment.update( + { + "name": "updated_name", + "description": "updated_description", + } + ) + assert updated.is_ok() + assert "segment" in updated + assert "id" in updated["segment"] + assert "name" in updated["segment"] + assert updated["segment"]["name"] == "updated_name" + assert updated["segment"]["description"] == "updated_description" + + deleted = segment.delete() + assert deleted.is_ok() + + def test_segment_targets(self, client: StreamChat): + segment = client.segment(segment_type=SegmentType.USER) + created = segment.create() + assert created.is_ok() + assert "segment" in created + assert "id" in created["segment"] + assert "name" in created["segment"] + + target_ids = [str(uuid.uuid4()) for _ in range(10)] + target_added = segment.add_targets(target_ids=target_ids) + assert target_added.is_ok() + + target_exists = segment.target_exists(target_id=target_ids[0]) + assert target_exists.is_ok() + + query_targets_1 = segment.query_targets( + options={ + "limit": 3, + } + ) + assert query_targets_1.is_ok() + assert "targets" in query_targets_1 + assert "next" in query_targets_1 + assert len(query_targets_1["targets"]) == 3 + + query_targets_2 = segment.query_targets( + filter_conditions={"target_id": {"$lte": ""}}, + sort=[{"field": "target_id", "direction": SortOrder.DESC}], + options={ + "limit": 3, + "next": query_targets_1["next"], + }, + ) + assert query_targets_2.is_ok() + assert "targets" in query_targets_2 + assert "next" in query_targets_2 + assert len(query_targets_2["targets"]) == 3 + + target_deleted = segment.remove_targets(target_ids=target_ids) + assert target_deleted.is_ok() + + deleted = segment.delete() + assert deleted.is_ok() + + def test_query_segments(self, client: StreamChat): + created = client.create_segment(segment_type=SegmentType.USER) + assert created.is_ok() + assert "segment" in created + assert "id" in created["segment"] + assert "name" in created["segment"] + segment_id = created["segment"]["id"] + + target_ids = [str(uuid.uuid4()) for _ in range(10)] + target_added = client.add_segment_targets( + segment_id=segment_id, target_ids=target_ids + ) + assert target_added.is_ok() + + query_segments = client.query_segments( + filter_conditions={"id": {"$eq": segment_id}}, + sort=[{"field": "created_at", "direction": SortOrder.DESC}], + ) + assert query_segments.is_ok() + assert "segments" in query_segments + assert len(query_segments["segments"]) == 1 + + target_deleted = client.remove_segment_targets( + segment_id=segment_id, target_ids=target_ids + ) + assert target_deleted.is_ok() + + deleted = client.delete_segment(segment_id=segment_id) + assert deleted.is_ok() diff --git a/stream_chat/types/base.py b/stream_chat/types/base.py new file mode 100644 index 0000000..bf7742d --- /dev/null +++ b/stream_chat/types/base.py @@ -0,0 +1,45 @@ +import sys +from enum import IntEnum +from typing import Optional + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +class SortOrder(IntEnum): + """ + Represents the sort order for a query. + """ + + ASC = 1 + DESC = -1 + + +class SortParam(TypedDict, total=False): + """ + Represents a sort parameter for a query. + + Parameters: + field: The field to sort by. + direction: The direction to sort by. + """ + + field: str + direction: SortOrder + + +class Pager(TypedDict, total=False): + """ + Represents the data structure for a pager. + + Parameters: + limit: The maximum number of items to return. + next: The next page token. + prev: The previous page token. + """ + + limit: Optional[int] + next: Optional[str] + prev: Optional[str] diff --git a/stream_chat/types/campaign.py b/stream_chat/types/campaign.py new file mode 100644 index 0000000..2e401dc --- /dev/null +++ b/stream_chat/types/campaign.py @@ -0,0 +1,76 @@ +import sys +from typing import Dict, List, Optional + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + +from stream_chat.types.base import Pager + + +class MessageTemplate(TypedDict, total=False): + """ + Represents the data structure for a message template. + + Parameters: + text: The text of the message. + attachments: List of the message attachments. + custom: Custom data. + """ + + text: str + attachments: Optional[List[Dict]] + custom: Optional[Dict] + + +class ChannelTemplate(TypedDict, total=False): + """ + Represents the data structure for a channel template. + + Parameters: + type: The type of channel. + id: The ID of the channel. + custom: Custom data. + """ + + type: str + id: str + custom: Optional[Dict] + + +class CampaignData(TypedDict, total=False): + """ + Represents the data structure for a campaign. + + Either `segment_ids` or `user_ids` must be provided, but not both. + + If `create_channels` is True, `channel_template` must be provided. + + Parameters: + message_template: The template for the message to be sent in the campaign. + sender_id: The ID of the user who is sending the campaign. + segment_ids: List of segment IDs the campaign is targeting. + user_ids: List of individual user IDs the campaign is targeting. + create_channels: Flag to indicate if new channels should be created for the campaign. + channel_template: The template for channels to be created, if applicable. + name: The name of the campaign. + description: A description of the campaign. + skip_push: Flag to indicate if push notifications should be skipped. + skip_webhook: Flag to indicate if webhooks should be skipped. + """ + + message_template: MessageTemplate + sender_id: str + segment_ids: Optional[List[str]] + user_ids: Optional[List[str]] + create_channels: Optional[bool] + channel_template: Optional[ChannelTemplate] + name: Optional[str] + description: Optional[str] + skip_push: Optional[bool] + skip_webhook: Optional[bool] + + +class QueryCampaignsOptions(Pager, total=False): + pass diff --git a/stream_chat/types/segment.py b/stream_chat/types/segment.py new file mode 100644 index 0000000..7ce3bbd --- /dev/null +++ b/stream_chat/types/segment.py @@ -0,0 +1,46 @@ +import sys +from enum import Enum +from typing import Dict, Optional + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + +from stream_chat.types.base import Pager + + +class SegmentType(Enum): + """ + Represents the type of segment. + + Attributes: + CHANNEL: A segment targeting channels. + USER: A segment targeting users. + """ + + CHANNEL = "channel" + USER = "user" + + +class SegmentData(TypedDict, total=False): + """ + Represents the data structure for a segment. + + Parameters: + name: The name of the segment. + description: A description of the segment. + filter: A filter to apply to the segment. + """ + + name: Optional[str] + description: Optional[str] + filter: Optional[Dict] + + +class QuerySegmentsOptions(Pager, total=False): + pass + + +class QuerySegmentTargetsOptions(Pager, total=False): + pass diff --git a/stream_chat/types/stream_response.py b/stream_chat/types/stream_response.py index a52e538..b42ac88 100644 --- a/stream_chat/types/stream_response.py +++ b/stream_chat/types/stream_response.py @@ -65,3 +65,7 @@ def headers(self) -> Dict[str, Any]: def status_code(self) -> int: """Returns the HTTP status code of the response.""" return self.__status_code + + def is_ok(self) -> bool: + """Returns True if the status code is in the 200 range.""" + return 200 <= self.__status_code < 300