stream-chat 4.11.0__py3-none-any.whl → 4.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stream_chat/__pkg__.py +1 -1
- stream_chat/async_chat/campaign.py +52 -0
- stream_chat/async_chat/client.py +141 -26
- stream_chat/async_chat/segment.py +65 -0
- stream_chat/base/campaign.py +51 -0
- stream_chat/base/client.py +123 -31
- stream_chat/base/segment.py +72 -0
- stream_chat/campaign.py +60 -0
- stream_chat/client.py +135 -28
- stream_chat/segment.py +63 -0
- stream_chat/types/base.py +45 -0
- stream_chat/types/campaign.py +78 -0
- stream_chat/types/segment.py +46 -0
- stream_chat/types/stream_response.py +4 -0
- {stream_chat-4.11.0.dist-info → stream_chat-4.12.1.dist-info}/METADATA +2 -2
- stream_chat-4.12.1.dist-info/RECORD +29 -0
- stream_chat-4.11.0.dist-info/RECORD +0 -20
- {stream_chat-4.11.0.dist-info → stream_chat-4.12.1.dist-info}/LICENSE +0 -0
- {stream_chat-4.11.0.dist-info → stream_chat-4.12.1.dist-info}/WHEEL +0 -0
- {stream_chat-4.11.0.dist-info → stream_chat-4.12.1.dist-info}/top_level.txt +0 -0
stream_chat/__pkg__.py
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
from stream_chat.base.campaign import CampaignInterface
|
|
5
|
+
from stream_chat.types.campaign import CampaignData
|
|
6
|
+
from stream_chat.types.stream_response import StreamResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Campaign(CampaignInterface):
|
|
10
|
+
async def create(
|
|
11
|
+
self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None
|
|
12
|
+
) -> StreamResponse:
|
|
13
|
+
if campaign_id is not None:
|
|
14
|
+
self.campaign_id = campaign_id
|
|
15
|
+
if data is not None:
|
|
16
|
+
self.data = data
|
|
17
|
+
state = await self.client.create_campaign( # type: ignore
|
|
18
|
+
campaign_id=self.campaign_id, data=self.data
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if self.campaign_id is None and state.is_ok() and "campaign" in state:
|
|
22
|
+
self.campaign_id = state["campaign"]["id"]
|
|
23
|
+
return state
|
|
24
|
+
|
|
25
|
+
async def get(self) -> StreamResponse:
|
|
26
|
+
return await self.client.get_campaign( # type: ignore
|
|
27
|
+
campaign_id=self.campaign_id
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def update(self, data: CampaignData) -> StreamResponse:
|
|
31
|
+
return await self.client.update_campaign( # type: ignore
|
|
32
|
+
campaign_id=self.campaign_id, data=data
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def delete(self, **options: Any) -> StreamResponse:
|
|
36
|
+
return await self.client.delete_campaign( # type: ignore
|
|
37
|
+
campaign_id=self.campaign_id, **options
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def start(
|
|
41
|
+
self,
|
|
42
|
+
scheduled_for: Optional[Union[str, datetime.datetime]] = None,
|
|
43
|
+
stop_at: Optional[Union[str, datetime.datetime]] = None,
|
|
44
|
+
) -> StreamResponse:
|
|
45
|
+
return await self.client.start_campaign( # type: ignore
|
|
46
|
+
campaign_id=self.campaign_id, scheduled_for=scheduled_for, stop_at=stop_at
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def stop(self) -> StreamResponse:
|
|
50
|
+
return await self.client.stop_campaign( # type: ignore
|
|
51
|
+
campaign_id=self.campaign_id
|
|
52
|
+
)
|
stream_chat/async_chat/client.py
CHANGED
|
@@ -13,9 +13,21 @@ from typing import (
|
|
|
13
13
|
Optional,
|
|
14
14
|
Type,
|
|
15
15
|
Union,
|
|
16
|
+
cast,
|
|
16
17
|
)
|
|
17
18
|
from urllib.parse import urlparse
|
|
18
19
|
|
|
20
|
+
from stream_chat.async_chat.campaign import Campaign
|
|
21
|
+
from stream_chat.async_chat.segment import Segment
|
|
22
|
+
from stream_chat.types.base import SortParam
|
|
23
|
+
from stream_chat.types.campaign import CampaignData, QueryCampaignsOptions
|
|
24
|
+
from stream_chat.types.segment import (
|
|
25
|
+
QuerySegmentsOptions,
|
|
26
|
+
QuerySegmentTargetsOptions,
|
|
27
|
+
SegmentData,
|
|
28
|
+
SegmentType,
|
|
29
|
+
)
|
|
30
|
+
|
|
19
31
|
if sys.version_info >= (3, 8):
|
|
20
32
|
from typing import Literal
|
|
21
33
|
else:
|
|
@@ -473,7 +485,7 @@ class StreamChatAsync(StreamChatInterface, AsyncContextManager):
|
|
|
473
485
|
return await self._parse_response(response)
|
|
474
486
|
|
|
475
487
|
async def create_blocklist(
|
|
476
|
-
self, name: str, words: Iterable[str], type: str = "
|
|
488
|
+
self, name: str, words: Iterable[str], type: str = "word"
|
|
477
489
|
) -> StreamResponse:
|
|
478
490
|
return await self.post(
|
|
479
491
|
"blocklists", data={"name": name, "words": words, "type": type}
|
|
@@ -537,45 +549,148 @@ class StreamChatAsync(StreamChatInterface, AsyncContextManager):
|
|
|
537
549
|
async def list_roles(self) -> StreamResponse:
|
|
538
550
|
return await self.get("roles")
|
|
539
551
|
|
|
540
|
-
|
|
541
|
-
|
|
552
|
+
def segment( # type: ignore
|
|
553
|
+
self,
|
|
554
|
+
segment_type: SegmentType,
|
|
555
|
+
segment_id: Optional[str] = None,
|
|
556
|
+
data: Optional[SegmentData] = None,
|
|
557
|
+
) -> Segment:
|
|
558
|
+
return Segment(
|
|
559
|
+
client=self, segment_type=segment_type, segment_id=segment_id, data=data
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
async def create_segment(
|
|
563
|
+
self,
|
|
564
|
+
segment_type: SegmentType,
|
|
565
|
+
segment_id: Optional[str] = None,
|
|
566
|
+
data: Optional[SegmentData] = None,
|
|
567
|
+
) -> StreamResponse:
|
|
568
|
+
payload = {"type": segment_type.value}
|
|
569
|
+
if segment_id is not None:
|
|
570
|
+
payload["id"] = segment_id
|
|
571
|
+
if data is not None:
|
|
572
|
+
payload.update(cast(dict, data))
|
|
573
|
+
return await self.post("segments", data=payload)
|
|
542
574
|
|
|
543
|
-
async def
|
|
544
|
-
return await self.get("segments
|
|
575
|
+
async def get_segment(self, segment_id: str) -> StreamResponse:
|
|
576
|
+
return await self.get(f"segments/{segment_id}")
|
|
545
577
|
|
|
546
|
-
async def
|
|
547
|
-
|
|
578
|
+
async def query_segments(
|
|
579
|
+
self,
|
|
580
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
|
581
|
+
sort: Optional[List[SortParam]] = None,
|
|
582
|
+
options: Optional[QuerySegmentsOptions] = None,
|
|
583
|
+
) -> StreamResponse:
|
|
584
|
+
payload = {}
|
|
585
|
+
if filter_conditions is not None:
|
|
586
|
+
payload["filter"] = filter_conditions
|
|
587
|
+
if sort is not None:
|
|
588
|
+
payload["sort"] = sort # type: ignore
|
|
589
|
+
if options is not None:
|
|
590
|
+
payload.update(cast(dict, options))
|
|
591
|
+
return await self.post("segments/query", data=payload)
|
|
592
|
+
|
|
593
|
+
async def update_segment(
|
|
594
|
+
self, segment_id: str, data: SegmentData
|
|
595
|
+
) -> StreamResponse:
|
|
596
|
+
return await self.put(f"segments/{segment_id}", data=data)
|
|
548
597
|
|
|
549
598
|
async def delete_segment(self, segment_id: str) -> StreamResponse:
|
|
550
599
|
return await self.delete(f"segments/{segment_id}")
|
|
551
600
|
|
|
552
|
-
async def
|
|
553
|
-
|
|
601
|
+
async def segment_target_exists(
|
|
602
|
+
self, segment_id: str, target_id: str
|
|
603
|
+
) -> StreamResponse:
|
|
604
|
+
return await self.get(f"segments/{segment_id}/target/{target_id}")
|
|
554
605
|
|
|
555
|
-
async def
|
|
556
|
-
|
|
606
|
+
async def add_segment_targets(
|
|
607
|
+
self, segment_id: str, target_ids: List[str]
|
|
608
|
+
) -> StreamResponse:
|
|
609
|
+
return await self.post(
|
|
610
|
+
f"segments/{segment_id}/addtargets", data={"target_ids": target_ids}
|
|
611
|
+
)
|
|
557
612
|
|
|
558
|
-
async def
|
|
559
|
-
|
|
613
|
+
async def query_segment_targets(
|
|
614
|
+
self,
|
|
615
|
+
segment_id: str,
|
|
616
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
|
617
|
+
sort: Optional[List[SortParam]] = None,
|
|
618
|
+
options: Optional[QuerySegmentTargetsOptions] = None,
|
|
619
|
+
) -> StreamResponse:
|
|
620
|
+
payload = {}
|
|
621
|
+
if filter_conditions is not None:
|
|
622
|
+
payload["filter"] = filter_conditions
|
|
623
|
+
if sort is not None:
|
|
624
|
+
payload["sort"] = sort # type: ignore
|
|
625
|
+
if options is not None:
|
|
626
|
+
payload.update(cast(dict, options))
|
|
627
|
+
return await self.post(f"segments/{segment_id}/targets/query", data=payload)
|
|
628
|
+
|
|
629
|
+
async def remove_segment_targets(
|
|
630
|
+
self, segment_id: str, target_ids: List[str]
|
|
631
|
+
) -> StreamResponse:
|
|
632
|
+
return await self.post(
|
|
633
|
+
f"segments/{segment_id}/deletetargets", data={"target_ids": target_ids}
|
|
634
|
+
)
|
|
560
635
|
|
|
561
|
-
|
|
562
|
-
|
|
636
|
+
def campaign( # type: ignore
|
|
637
|
+
self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None
|
|
638
|
+
) -> Campaign:
|
|
639
|
+
return Campaign(client=self, campaign_id=campaign_id, data=data)
|
|
563
640
|
|
|
564
|
-
async def
|
|
565
|
-
self, campaign_id: str,
|
|
641
|
+
async def create_campaign(
|
|
642
|
+
self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None
|
|
566
643
|
) -> StreamResponse:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
644
|
+
payload = {"id": campaign_id}
|
|
645
|
+
if data is not None:
|
|
646
|
+
payload.update(cast(dict, data))
|
|
647
|
+
return await self.post("campaigns", data=payload)
|
|
570
648
|
|
|
571
|
-
async def
|
|
572
|
-
return await self.get("
|
|
649
|
+
async def get_campaign(self, campaign_id: str) -> StreamResponse:
|
|
650
|
+
return await self.get(f"campaigns/{campaign_id}")
|
|
573
651
|
|
|
574
|
-
async def
|
|
575
|
-
|
|
652
|
+
async def query_campaigns(
|
|
653
|
+
self,
|
|
654
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
|
655
|
+
sort: Optional[List[SortParam]] = None,
|
|
656
|
+
options: QueryCampaignsOptions = None,
|
|
657
|
+
) -> StreamResponse:
|
|
658
|
+
payload = {}
|
|
659
|
+
if filter_conditions is not None:
|
|
660
|
+
payload["filter"] = filter_conditions
|
|
661
|
+
if sort is not None:
|
|
662
|
+
payload["sort"] = sort # type: ignore
|
|
663
|
+
if options is not None:
|
|
664
|
+
payload.update(cast(dict, options))
|
|
665
|
+
return await self.post("campaigns/query", data=payload)
|
|
666
|
+
|
|
667
|
+
async def update_campaign(
|
|
668
|
+
self, campaign_id: str, data: CampaignData
|
|
669
|
+
) -> StreamResponse:
|
|
670
|
+
return await self.put(f"campaigns/{campaign_id}", data=data)
|
|
671
|
+
|
|
672
|
+
async def delete_campaign(self, campaign_id: str, **options: Any) -> StreamResponse:
|
|
673
|
+
return await self.delete(f"campaigns/{campaign_id}", options)
|
|
576
674
|
|
|
577
|
-
async def
|
|
578
|
-
|
|
675
|
+
async def start_campaign(
|
|
676
|
+
self,
|
|
677
|
+
campaign_id: str,
|
|
678
|
+
scheduled_for: Optional[Union[str, datetime.datetime]] = None,
|
|
679
|
+
stop_at: Optional[Union[str, datetime.datetime]] = None,
|
|
680
|
+
) -> StreamResponse:
|
|
681
|
+
payload = {}
|
|
682
|
+
if scheduled_for is not None:
|
|
683
|
+
if isinstance(scheduled_for, datetime.datetime):
|
|
684
|
+
scheduled_for = scheduled_for.isoformat()
|
|
685
|
+
payload["scheduled_for"] = scheduled_for
|
|
686
|
+
if stop_at is not None:
|
|
687
|
+
if isinstance(stop_at, datetime.datetime):
|
|
688
|
+
stop_at = stop_at.isoformat()
|
|
689
|
+
payload["stop_at"] = stop_at
|
|
690
|
+
return await self.post(f"campaigns/{campaign_id}/start", data=payload)
|
|
691
|
+
|
|
692
|
+
async def stop_campaign(self, campaign_id: str) -> StreamResponse:
|
|
693
|
+
return await self.post(f"campaigns/{campaign_id}/stop")
|
|
579
694
|
|
|
580
695
|
async def test_campaign(
|
|
581
696
|
self, campaign_id: str, users: Iterable[str]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from stream_chat.base.segment import SegmentInterface
|
|
4
|
+
from stream_chat.types.base import SortParam
|
|
5
|
+
from stream_chat.types.segment import QuerySegmentTargetsOptions, SegmentData
|
|
6
|
+
from stream_chat.types.stream_response import StreamResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Segment(SegmentInterface):
|
|
10
|
+
async def create(
|
|
11
|
+
self, segment_id: Optional[str] = None, data: Optional[SegmentData] = None
|
|
12
|
+
) -> StreamResponse:
|
|
13
|
+
if segment_id is not None:
|
|
14
|
+
self.segment_id = segment_id
|
|
15
|
+
if data is not None:
|
|
16
|
+
self.data = data
|
|
17
|
+
|
|
18
|
+
state = await self.client.create_segment( # type: ignore
|
|
19
|
+
segment_type=self.segment_type, segment_id=self.segment_id, data=self.data
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if self.segment_id is None and state.is_ok() and "segment" in state:
|
|
23
|
+
self.segment_id = state["segment"]["id"]
|
|
24
|
+
return state
|
|
25
|
+
|
|
26
|
+
async def get(self) -> StreamResponse:
|
|
27
|
+
return await self.client.get_segment(segment_id=self.segment_id) # type: ignore
|
|
28
|
+
|
|
29
|
+
async def update(self, data: SegmentData) -> StreamResponse:
|
|
30
|
+
return await self.client.update_segment( # type: ignore
|
|
31
|
+
segment_id=self.segment_id, data=data
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def delete(self) -> StreamResponse:
|
|
35
|
+
return await self.client.delete_segment( # type: ignore
|
|
36
|
+
segment_id=self.segment_id
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def target_exists(self, target_id: str) -> StreamResponse:
|
|
40
|
+
return await self.client.segment_target_exists( # type: ignore
|
|
41
|
+
segment_id=self.segment_id, target_id=target_id
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def add_targets(self, target_ids: list) -> StreamResponse:
|
|
45
|
+
return await self.client.add_segment_targets( # type: ignore
|
|
46
|
+
segment_id=self.segment_id, target_ids=target_ids
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def query_targets(
|
|
50
|
+
self,
|
|
51
|
+
filter_conditions: Optional[Dict] = None,
|
|
52
|
+
sort: Optional[List[SortParam]] = None,
|
|
53
|
+
options: Optional[QuerySegmentTargetsOptions] = None,
|
|
54
|
+
) -> StreamResponse:
|
|
55
|
+
return await self.client.query_segment_targets( # type: ignore
|
|
56
|
+
segment_id=self.segment_id,
|
|
57
|
+
filter_conditions=filter_conditions,
|
|
58
|
+
sort=sort,
|
|
59
|
+
options=options,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def remove_targets(self, target_ids: list) -> StreamResponse:
|
|
63
|
+
return await self.client.remove_segment_targets( # type: ignore
|
|
64
|
+
segment_id=self.segment_id, target_ids=target_ids
|
|
65
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import Awaitable, Optional, Union
|
|
4
|
+
|
|
5
|
+
from stream_chat.base.client import StreamChatInterface
|
|
6
|
+
from stream_chat.types.campaign import CampaignData
|
|
7
|
+
from stream_chat.types.stream_response import StreamResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CampaignInterface(abc.ABC):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
client: StreamChatInterface,
|
|
14
|
+
campaign_id: Optional[str] = None,
|
|
15
|
+
data: CampaignData = None,
|
|
16
|
+
):
|
|
17
|
+
self.client = client
|
|
18
|
+
self.campaign_id = campaign_id
|
|
19
|
+
self.data = data
|
|
20
|
+
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
def create(
|
|
23
|
+
self, campaign_id: Optional[str], data: Optional[CampaignData]
|
|
24
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abc.abstractmethod
|
|
28
|
+
def get(self) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abc.abstractmethod
|
|
32
|
+
def update(
|
|
33
|
+
self, data: CampaignData
|
|
34
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abc.abstractmethod
|
|
38
|
+
def delete(self) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
def start(
|
|
43
|
+
self,
|
|
44
|
+
scheduled_for: Optional[Union[str, datetime.datetime]] = None,
|
|
45
|
+
stop_at: Optional[Union[str, datetime.datetime]] = None,
|
|
46
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
def stop(self) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
51
|
+
pass
|
stream_chat/base/client.py
CHANGED
|
@@ -5,7 +5,16 @@ import hashlib
|
|
|
5
5
|
import hmac
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
-
from typing import Any, Awaitable, Dict, Iterable, List, TypeVar, Union
|
|
8
|
+
from typing import Any, Awaitable, Dict, Iterable, List, Optional, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
from stream_chat.types.base import SortParam
|
|
11
|
+
from stream_chat.types.campaign import CampaignData, QueryCampaignsOptions
|
|
12
|
+
from stream_chat.types.segment import (
|
|
13
|
+
QuerySegmentsOptions,
|
|
14
|
+
QuerySegmentTargetsOptions,
|
|
15
|
+
SegmentData,
|
|
16
|
+
SegmentType,
|
|
17
|
+
)
|
|
9
18
|
|
|
10
19
|
if sys.version_info >= (3, 8):
|
|
11
20
|
from typing import Literal
|
|
@@ -18,6 +27,10 @@ from stream_chat.types.stream_response import StreamResponse
|
|
|
18
27
|
|
|
19
28
|
TChannel = TypeVar("TChannel")
|
|
20
29
|
|
|
30
|
+
TSegment = TypeVar("TSegment")
|
|
31
|
+
|
|
32
|
+
TCampaign = TypeVar("TCampaign")
|
|
33
|
+
|
|
21
34
|
|
|
22
35
|
class StreamChatInterface(abc.ABC):
|
|
23
36
|
def __init__(
|
|
@@ -585,7 +598,6 @@ class StreamChatInterface(abc.ABC):
|
|
|
585
598
|
) -> TChannel: # type: ignore[type-var]
|
|
586
599
|
"""
|
|
587
600
|
Creates a channel object
|
|
588
|
-
|
|
589
601
|
:param channel_type: the channel type
|
|
590
602
|
:param channel_id: the id of the channel
|
|
591
603
|
:param data: additional data, ie: {"members":[id1, id2, ...]}
|
|
@@ -753,7 +765,7 @@ class StreamChatInterface(abc.ABC):
|
|
|
753
765
|
|
|
754
766
|
@abc.abstractmethod
|
|
755
767
|
def create_blocklist(
|
|
756
|
-
self, name: str, words: Iterable[str], type: str = "
|
|
768
|
+
self, name: str, words: Iterable[str], type: str = "word"
|
|
757
769
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
758
770
|
"""
|
|
759
771
|
Create a blocklist
|
|
@@ -918,18 +930,49 @@ class StreamChatInterface(abc.ABC):
|
|
|
918
930
|
"""
|
|
919
931
|
pass
|
|
920
932
|
|
|
933
|
+
@abc.abstractmethod
|
|
934
|
+
def segment(
|
|
935
|
+
self,
|
|
936
|
+
segment_type: SegmentType,
|
|
937
|
+
segment_id: Optional[str],
|
|
938
|
+
data: Optional[SegmentData],
|
|
939
|
+
) -> TSegment: # type: ignore[type-var]
|
|
940
|
+
"""
|
|
941
|
+
Creates a channel object
|
|
942
|
+
:param segment_type: the segment type
|
|
943
|
+
:param segment_id: the id of the segment
|
|
944
|
+
:param data: the segment data, ie: {"members":[id1, id2, ...]}
|
|
945
|
+
:return: Segment
|
|
946
|
+
"""
|
|
947
|
+
pass
|
|
948
|
+
|
|
921
949
|
@abc.abstractmethod
|
|
922
950
|
def create_segment(
|
|
923
|
-
self,
|
|
951
|
+
self,
|
|
952
|
+
segment_type: SegmentType,
|
|
953
|
+
segment_id: Optional[str],
|
|
954
|
+
data: Optional[SegmentData] = None,
|
|
924
955
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
925
956
|
"""
|
|
926
957
|
Create a segment
|
|
927
958
|
"""
|
|
928
959
|
pass
|
|
929
960
|
|
|
961
|
+
@abc.abstractmethod
|
|
962
|
+
def get_segment(
|
|
963
|
+
self, segment_id: str
|
|
964
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
965
|
+
"""
|
|
966
|
+
Query segments
|
|
967
|
+
"""
|
|
968
|
+
pass
|
|
969
|
+
|
|
930
970
|
@abc.abstractmethod
|
|
931
971
|
def query_segments(
|
|
932
|
-
self,
|
|
972
|
+
self,
|
|
973
|
+
filter_conditions: Optional[Dict] = None,
|
|
974
|
+
sort: Optional[List[SortParam]] = None,
|
|
975
|
+
options: Optional[QuerySegmentsOptions] = None,
|
|
933
976
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
934
977
|
"""
|
|
935
978
|
Query segments
|
|
@@ -938,7 +981,7 @@ class StreamChatInterface(abc.ABC):
|
|
|
938
981
|
|
|
939
982
|
@abc.abstractmethod
|
|
940
983
|
def update_segment(
|
|
941
|
-
self, segment_id: str, data:
|
|
984
|
+
self, segment_id: str, data: SegmentData
|
|
942
985
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
943
986
|
"""
|
|
944
987
|
Update a segment by id
|
|
@@ -954,9 +997,70 @@ class StreamChatInterface(abc.ABC):
|
|
|
954
997
|
"""
|
|
955
998
|
pass
|
|
956
999
|
|
|
1000
|
+
@abc.abstractmethod
|
|
1001
|
+
def segment_target_exists(
|
|
1002
|
+
self, segment_id: str, target_id: str
|
|
1003
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1004
|
+
"""
|
|
1005
|
+
Check if a target exists in a segment
|
|
1006
|
+
"""
|
|
1007
|
+
pass
|
|
1008
|
+
|
|
1009
|
+
@abc.abstractmethod
|
|
1010
|
+
def add_segment_targets(
|
|
1011
|
+
self, segment_id: str, target_ids: List[str]
|
|
1012
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1013
|
+
"""
|
|
1014
|
+
Add targets to a segment
|
|
1015
|
+
"""
|
|
1016
|
+
pass
|
|
1017
|
+
|
|
1018
|
+
@abc.abstractmethod
|
|
1019
|
+
def query_segment_targets(
|
|
1020
|
+
self,
|
|
1021
|
+
segment_id: str,
|
|
1022
|
+
filter_conditions: Optional[Dict] = None,
|
|
1023
|
+
sort: Optional[List[SortParam]] = None,
|
|
1024
|
+
options: Optional[QuerySegmentTargetsOptions] = None,
|
|
1025
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1026
|
+
"""
|
|
1027
|
+
Query targets in a segment
|
|
1028
|
+
"""
|
|
1029
|
+
pass
|
|
1030
|
+
|
|
1031
|
+
@abc.abstractmethod
|
|
1032
|
+
def remove_segment_targets(
|
|
1033
|
+
self, segment_id: str, target_ids: List[str]
|
|
1034
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1035
|
+
"""
|
|
1036
|
+
Delete targets from a segment
|
|
1037
|
+
"""
|
|
1038
|
+
pass
|
|
1039
|
+
|
|
1040
|
+
@abc.abstractmethod
|
|
1041
|
+
def campaign(
|
|
1042
|
+
self, campaign_id: Optional[str], data: Optional[CampaignData]
|
|
1043
|
+
) -> TCampaign: # type: ignore[type-var]
|
|
1044
|
+
"""
|
|
1045
|
+
Creates a campaign object
|
|
1046
|
+
:param campaign_id: the campaign id
|
|
1047
|
+
:param data: campaign_id data
|
|
1048
|
+
:return: Campaign
|
|
1049
|
+
"""
|
|
1050
|
+
pass
|
|
1051
|
+
|
|
957
1052
|
@abc.abstractmethod
|
|
958
1053
|
def create_campaign(
|
|
959
|
-
self,
|
|
1054
|
+
self, campaign_id: Optional[str], data: Optional[CampaignData]
|
|
1055
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1056
|
+
"""
|
|
1057
|
+
Create a campaign
|
|
1058
|
+
"""
|
|
1059
|
+
pass
|
|
1060
|
+
|
|
1061
|
+
@abc.abstractmethod
|
|
1062
|
+
def get_campaign(
|
|
1063
|
+
self, campaign_id: str
|
|
960
1064
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
961
1065
|
"""
|
|
962
1066
|
Create a campaign
|
|
@@ -965,7 +1069,10 @@ class StreamChatInterface(abc.ABC):
|
|
|
965
1069
|
|
|
966
1070
|
@abc.abstractmethod
|
|
967
1071
|
def query_campaigns(
|
|
968
|
-
self,
|
|
1072
|
+
self,
|
|
1073
|
+
filter_conditions: Optional[Dict] = None,
|
|
1074
|
+
sort: Optional[List[SortParam]] = None,
|
|
1075
|
+
options: Optional[QueryCampaignsOptions] = None,
|
|
969
1076
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
970
1077
|
"""
|
|
971
1078
|
Query campaigns
|
|
@@ -974,7 +1081,7 @@ class StreamChatInterface(abc.ABC):
|
|
|
974
1081
|
|
|
975
1082
|
@abc.abstractmethod
|
|
976
1083
|
def update_campaign(
|
|
977
|
-
self, campaign_id: str, data:
|
|
1084
|
+
self, campaign_id: str, data: CampaignData
|
|
978
1085
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
979
1086
|
"""
|
|
980
1087
|
Update a campaign
|
|
@@ -991,11 +1098,14 @@ class StreamChatInterface(abc.ABC):
|
|
|
991
1098
|
pass
|
|
992
1099
|
|
|
993
1100
|
@abc.abstractmethod
|
|
994
|
-
def
|
|
995
|
-
self,
|
|
1101
|
+
def start_campaign(
|
|
1102
|
+
self,
|
|
1103
|
+
campaign_id: str,
|
|
1104
|
+
scheduled_for: Optional[Union[str, datetime.datetime]] = None,
|
|
1105
|
+
stop_at: Optional[Union[str, datetime.datetime]] = None,
|
|
996
1106
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
997
1107
|
"""
|
|
998
|
-
|
|
1108
|
+
Start a campaign at given time or now if not specified
|
|
999
1109
|
"""
|
|
1000
1110
|
pass
|
|
1001
1111
|
|
|
@@ -1004,16 +1114,7 @@ class StreamChatInterface(abc.ABC):
|
|
|
1004
1114
|
self, campaign_id: str
|
|
1005
1115
|
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1006
1116
|
"""
|
|
1007
|
-
Stop
|
|
1008
|
-
"""
|
|
1009
|
-
pass
|
|
1010
|
-
|
|
1011
|
-
@abc.abstractmethod
|
|
1012
|
-
def resume_campaign(
|
|
1013
|
-
self, campaign_id: str
|
|
1014
|
-
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1015
|
-
"""
|
|
1016
|
-
Resume a stopped campaign
|
|
1117
|
+
Stop an in progress campaign
|
|
1017
1118
|
"""
|
|
1018
1119
|
pass
|
|
1019
1120
|
|
|
@@ -1026,15 +1127,6 @@ class StreamChatInterface(abc.ABC):
|
|
|
1026
1127
|
"""
|
|
1027
1128
|
pass
|
|
1028
1129
|
|
|
1029
|
-
@abc.abstractmethod
|
|
1030
|
-
def query_recipients(
|
|
1031
|
-
self, **params: Any
|
|
1032
|
-
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
1033
|
-
"""
|
|
1034
|
-
Query recipients
|
|
1035
|
-
"""
|
|
1036
|
-
pass
|
|
1037
|
-
|
|
1038
1130
|
@abc.abstractmethod
|
|
1039
1131
|
def revoke_tokens(
|
|
1040
1132
|
self, since: Union[str, datetime.datetime]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Awaitable, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
from stream_chat.base.client import StreamChatInterface
|
|
5
|
+
from stream_chat.types.base import SortParam
|
|
6
|
+
from stream_chat.types.segment import (
|
|
7
|
+
QuerySegmentTargetsOptions,
|
|
8
|
+
SegmentData,
|
|
9
|
+
SegmentType,
|
|
10
|
+
)
|
|
11
|
+
from stream_chat.types.stream_response import StreamResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SegmentInterface(abc.ABC):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
client: StreamChatInterface,
|
|
18
|
+
segment_type: SegmentType,
|
|
19
|
+
segment_id: Optional[str] = None,
|
|
20
|
+
data: Optional[SegmentData] = None,
|
|
21
|
+
):
|
|
22
|
+
self.segment_type = segment_type
|
|
23
|
+
self.segment_id = segment_id
|
|
24
|
+
self.client = client
|
|
25
|
+
self.data = data
|
|
26
|
+
|
|
27
|
+
@abc.abstractmethod
|
|
28
|
+
def create(
|
|
29
|
+
self, segment_id: Optional[str] = None, data: Optional[SegmentData] = None
|
|
30
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abc.abstractmethod
|
|
34
|
+
def get(self) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abc.abstractmethod
|
|
38
|
+
def update(
|
|
39
|
+
self, data: SegmentData
|
|
40
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abc.abstractmethod
|
|
44
|
+
def delete(self) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abc.abstractmethod
|
|
48
|
+
def target_exists(
|
|
49
|
+
self, target_id: str
|
|
50
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
def add_targets(
|
|
55
|
+
self, target_ids: List[str]
|
|
56
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abc.abstractmethod
|
|
60
|
+
def query_targets(
|
|
61
|
+
self,
|
|
62
|
+
filter_conditions: Optional[Dict] = None,
|
|
63
|
+
sort: Optional[List[SortParam]] = None,
|
|
64
|
+
options: Optional[QuerySegmentTargetsOptions] = None,
|
|
65
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@abc.abstractmethod
|
|
69
|
+
def remove_targets(
|
|
70
|
+
self, target_ids: List[str]
|
|
71
|
+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
|
|
72
|
+
pass
|
stream_chat/campaign.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
from stream_chat.base.campaign import CampaignInterface
|
|
5
|
+
from stream_chat.types.campaign import CampaignData
|
|
6
|
+
from stream_chat.types.stream_response import StreamResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Campaign(CampaignInterface):
|
|
10
|
+
def create(
|
|
11
|
+
self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None
|
|
12
|
+
) -> StreamResponse:
|
|
13
|
+
if campaign_id is not None:
|
|
14
|
+
self.campaign_id = campaign_id
|
|
15
|
+
if data is not None:
|
|
16
|
+
self.data = self._merge_campaign_data(self.data, data)
|
|
17
|
+
state = self.client.create_campaign(
|
|
18
|
+
campaign_id=self.campaign_id, data=self.data
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if self.campaign_id is None and state.is_ok() and "campaign" in state: # type: ignore
|
|
22
|
+
self.campaign_id = state["campaign"]["id"] # type: ignore
|
|
23
|
+
return state # type: ignore
|
|
24
|
+
|
|
25
|
+
def get(self) -> StreamResponse:
|
|
26
|
+
return self.client.get_campaign(campaign_id=self.campaign_id) # type: ignore
|
|
27
|
+
|
|
28
|
+
def update(self, data: CampaignData) -> StreamResponse:
|
|
29
|
+
return self.client.update_campaign( # type: ignore
|
|
30
|
+
campaign_id=self.campaign_id, data=data
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def delete(self, **options: Any) -> StreamResponse:
|
|
34
|
+
return self.client.delete_campaign( # type: ignore
|
|
35
|
+
campaign_id=self.campaign_id, **options
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def start(
|
|
39
|
+
self,
|
|
40
|
+
scheduled_for: Optional[Union[str, datetime.datetime]] = None,
|
|
41
|
+
stop_at: Optional[Union[str, datetime.datetime]] = None,
|
|
42
|
+
) -> StreamResponse:
|
|
43
|
+
return self.client.start_campaign( # type: ignore
|
|
44
|
+
campaign_id=self.campaign_id, scheduled_for=scheduled_for, stop_at=stop_at
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def stop(self) -> StreamResponse:
|
|
48
|
+
return self.client.stop_campaign(campaign_id=self.campaign_id) # type: ignore
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _merge_campaign_data(
|
|
52
|
+
data1: Optional[CampaignData],
|
|
53
|
+
data2: Optional[CampaignData],
|
|
54
|
+
) -> CampaignData:
|
|
55
|
+
if data1 is None:
|
|
56
|
+
return data2
|
|
57
|
+
if data2 is None:
|
|
58
|
+
return data1
|
|
59
|
+
data1.update(data2) # type: ignore
|
|
60
|
+
return data1
|
stream_chat/client.py
CHANGED
|
@@ -2,16 +2,26 @@ import datetime
|
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
4
|
import warnings
|
|
5
|
-
from typing import Any, Callable, Dict, Iterable, List, Union
|
|
5
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
from urllib.request import Request, urlopen
|
|
8
8
|
|
|
9
|
+
from stream_chat.campaign import Campaign
|
|
10
|
+
from stream_chat.segment import Segment
|
|
11
|
+
from stream_chat.types.base import SortParam
|
|
12
|
+
from stream_chat.types.campaign import CampaignData, QueryCampaignsOptions
|
|
13
|
+
from stream_chat.types.segment import (
|
|
14
|
+
QuerySegmentsOptions,
|
|
15
|
+
QuerySegmentTargetsOptions,
|
|
16
|
+
SegmentData,
|
|
17
|
+
SegmentType,
|
|
18
|
+
)
|
|
19
|
+
|
|
9
20
|
if sys.version_info >= (3, 8):
|
|
10
21
|
from typing import Literal
|
|
11
22
|
else:
|
|
12
23
|
from typing_extensions import Literal
|
|
13
24
|
|
|
14
|
-
|
|
15
25
|
import requests
|
|
16
26
|
|
|
17
27
|
from stream_chat.__pkg__ import __version__
|
|
@@ -457,7 +467,7 @@ class StreamChat(StreamChatInterface):
|
|
|
457
467
|
return self._parse_response(response)
|
|
458
468
|
|
|
459
469
|
def create_blocklist(
|
|
460
|
-
self, name: str, words: Iterable[str], type: str = "
|
|
470
|
+
self, name: str, words: Iterable[str], type: str = "word"
|
|
461
471
|
) -> StreamResponse:
|
|
462
472
|
return self.post(
|
|
463
473
|
"blocklists", data={"name": name, "words": words, "type": type}
|
|
@@ -518,49 +528,146 @@ class StreamChat(StreamChatInterface):
|
|
|
518
528
|
def list_roles(self) -> StreamResponse:
|
|
519
529
|
return self.get("roles")
|
|
520
530
|
|
|
521
|
-
def
|
|
522
|
-
|
|
531
|
+
def segment( # type: ignore
|
|
532
|
+
self,
|
|
533
|
+
segment_type: SegmentType,
|
|
534
|
+
segment_id: Optional[str] = None,
|
|
535
|
+
data: Optional[SegmentData] = None,
|
|
536
|
+
) -> Segment:
|
|
537
|
+
return Segment(
|
|
538
|
+
client=self, segment_type=segment_type, segment_id=segment_id, data=data
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def create_segment(
|
|
542
|
+
self,
|
|
543
|
+
segment_type: SegmentType,
|
|
544
|
+
segment_id: Optional[str] = None,
|
|
545
|
+
data: Optional[SegmentData] = None,
|
|
546
|
+
) -> StreamResponse:
|
|
547
|
+
payload = {"type": segment_type.value}
|
|
548
|
+
if segment_id is not None:
|
|
549
|
+
payload["id"] = segment_id
|
|
550
|
+
if data is not None:
|
|
551
|
+
payload.update(cast(dict, data))
|
|
552
|
+
return self.post("segments", data=payload)
|
|
523
553
|
|
|
524
|
-
def
|
|
525
|
-
return self.get("segments
|
|
554
|
+
def get_segment(self, segment_id: str) -> StreamResponse:
|
|
555
|
+
return self.get(f"segments/{segment_id}")
|
|
526
556
|
|
|
527
|
-
def
|
|
528
|
-
|
|
557
|
+
def query_segments(
|
|
558
|
+
self,
|
|
559
|
+
filter_conditions: Optional[Dict] = None,
|
|
560
|
+
sort: Optional[List[SortParam]] = None,
|
|
561
|
+
options: Optional[QuerySegmentsOptions] = None,
|
|
562
|
+
) -> StreamResponse:
|
|
563
|
+
payload = {}
|
|
564
|
+
if filter_conditions is not None:
|
|
565
|
+
payload["filter"] = filter_conditions
|
|
566
|
+
if sort is not None:
|
|
567
|
+
payload["sort"] = sort # type: ignore
|
|
568
|
+
if options is not None:
|
|
569
|
+
payload.update(cast(dict, options))
|
|
570
|
+
return self.post("segments/query", data=payload)
|
|
571
|
+
|
|
572
|
+
def update_segment(self, segment_id: str, data: SegmentData) -> StreamResponse:
|
|
573
|
+
return self.put(f"segments/{segment_id}", data=data)
|
|
529
574
|
|
|
530
575
|
def delete_segment(self, segment_id: str) -> StreamResponse:
|
|
531
576
|
return self.delete(f"segments/{segment_id}")
|
|
532
577
|
|
|
533
|
-
def
|
|
534
|
-
return self.
|
|
578
|
+
def segment_target_exists(self, segment_id: str, target_id: str) -> StreamResponse:
|
|
579
|
+
return self.get(f"segments/{segment_id}/target/{target_id}")
|
|
580
|
+
|
|
581
|
+
def add_segment_targets(
|
|
582
|
+
self, segment_id: str, target_ids: List[str]
|
|
583
|
+
) -> StreamResponse:
|
|
584
|
+
return self.post(
|
|
585
|
+
f"segments/{segment_id}/addtargets", data={"target_ids": target_ids}
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def query_segment_targets(
|
|
589
|
+
self,
|
|
590
|
+
segment_id: str,
|
|
591
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
|
592
|
+
sort: Optional[List[SortParam]] = None,
|
|
593
|
+
options: Optional[QuerySegmentTargetsOptions] = None,
|
|
594
|
+
) -> StreamResponse:
|
|
595
|
+
payload: Dict[str, Union[Dict[str, Any], List[SortParam]]] = {}
|
|
596
|
+
if filter_conditions is not None:
|
|
597
|
+
payload["filter"] = filter_conditions
|
|
598
|
+
if sort is not None:
|
|
599
|
+
payload["sort"] = sort
|
|
600
|
+
if options is not None:
|
|
601
|
+
payload.update(cast(dict, options))
|
|
602
|
+
return self.post(f"segments/{segment_id}/targets/query", data=payload)
|
|
603
|
+
|
|
604
|
+
def remove_segment_targets(
|
|
605
|
+
self, segment_id: str, target_ids: List[str]
|
|
606
|
+
) -> StreamResponse:
|
|
607
|
+
return self.post(
|
|
608
|
+
f"segments/{segment_id}/deletetargets", data={"target_ids": target_ids}
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def campaign( # type: ignore
|
|
612
|
+
self, campaign_id: Optional[str] = None, data: Optional[CampaignData] = None
|
|
613
|
+
) -> Campaign:
|
|
614
|
+
return Campaign(client=self, campaign_id=campaign_id, data=data)
|
|
615
|
+
|
|
616
|
+
def create_campaign(
|
|
617
|
+
self, campaign_id: Optional[str] = None, data: CampaignData = None
|
|
618
|
+
) -> StreamResponse:
|
|
619
|
+
payload = {"id": campaign_id}
|
|
620
|
+
if data is not None:
|
|
621
|
+
payload.update(cast(dict, data))
|
|
622
|
+
return self.post("campaigns", data=payload)
|
|
535
623
|
|
|
536
|
-
def
|
|
537
|
-
return self.get("campaigns
|
|
624
|
+
def get_campaign(self, campaign_id: str) -> StreamResponse:
|
|
625
|
+
return self.get(f"campaigns/{campaign_id}")
|
|
538
626
|
|
|
539
|
-
def
|
|
540
|
-
|
|
627
|
+
def query_campaigns(
|
|
628
|
+
self,
|
|
629
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
|
630
|
+
sort: Optional[List[SortParam]] = None,
|
|
631
|
+
options: Optional[QueryCampaignsOptions] = None,
|
|
632
|
+
) -> StreamResponse:
|
|
633
|
+
payload = {}
|
|
634
|
+
if filter_conditions is not None:
|
|
635
|
+
payload["filter"] = filter_conditions
|
|
636
|
+
if sort is not None:
|
|
637
|
+
payload["sort"] = sort # type: ignore
|
|
638
|
+
if options is not None:
|
|
639
|
+
payload.update(cast(dict, options))
|
|
640
|
+
return self.post("campaigns/query", data=payload)
|
|
641
|
+
|
|
642
|
+
def update_campaign(self, campaign_id: str, data: CampaignData) -> StreamResponse:
|
|
643
|
+
return self.put(f"campaigns/{campaign_id}", data=data)
|
|
541
644
|
|
|
542
645
|
def delete_campaign(self, campaign_id: str, **options: Any) -> StreamResponse:
|
|
543
|
-
return self.delete(f"campaigns/{campaign_id}",
|
|
646
|
+
return self.delete(f"campaigns/{campaign_id}", options)
|
|
544
647
|
|
|
545
|
-
def
|
|
546
|
-
self,
|
|
648
|
+
def start_campaign(
|
|
649
|
+
self,
|
|
650
|
+
campaign_id: str,
|
|
651
|
+
scheduled_for: Optional[Union[str, datetime.datetime]] = None,
|
|
652
|
+
stop_at: Optional[Union[str, datetime.datetime]] = None,
|
|
547
653
|
) -> StreamResponse:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
654
|
+
payload = {}
|
|
655
|
+
if scheduled_for is not None:
|
|
656
|
+
if isinstance(scheduled_for, datetime.datetime):
|
|
657
|
+
scheduled_for = scheduled_for.isoformat()
|
|
658
|
+
payload["scheduled_for"] = scheduled_for
|
|
659
|
+
if stop_at is not None:
|
|
660
|
+
if isinstance(stop_at, datetime.datetime):
|
|
661
|
+
stop_at = stop_at.isoformat()
|
|
662
|
+
payload["stop_at"] = stop_at
|
|
663
|
+
return self.post(f"campaigns/{campaign_id}/start", data=payload)
|
|
551
664
|
|
|
552
665
|
def stop_campaign(self, campaign_id: str) -> StreamResponse:
|
|
553
|
-
return self.
|
|
554
|
-
|
|
555
|
-
def resume_campaign(self, campaign_id: str) -> StreamResponse:
|
|
556
|
-
return self.patch(f"campaigns/{campaign_id}/resume")
|
|
666
|
+
return self.post(f"campaigns/{campaign_id}/stop")
|
|
557
667
|
|
|
558
668
|
def test_campaign(self, campaign_id: str, users: Iterable[str]) -> StreamResponse:
|
|
559
669
|
return self.post(f"campaigns/{campaign_id}/test", data={"users": users})
|
|
560
670
|
|
|
561
|
-
def query_recipients(self, **params: Any) -> StreamResponse:
|
|
562
|
-
return self.get("recipients", params={"payload": json.dumps(params)})
|
|
563
|
-
|
|
564
671
|
def revoke_tokens(self, since: Union[str, datetime.datetime]) -> StreamResponse:
|
|
565
672
|
if isinstance(since, datetime.datetime):
|
|
566
673
|
since = since.isoformat()
|
stream_chat/segment.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from stream_chat.base.segment import SegmentInterface
|
|
4
|
+
from stream_chat.types.base import SortParam
|
|
5
|
+
from stream_chat.types.segment import QuerySegmentTargetsOptions, SegmentData
|
|
6
|
+
from stream_chat.types.stream_response import StreamResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Segment(SegmentInterface):
|
|
10
|
+
def create(
|
|
11
|
+
self, segment_id: Optional[str] = None, data: Optional[SegmentData] = None
|
|
12
|
+
) -> StreamResponse:
|
|
13
|
+
if segment_id is not None:
|
|
14
|
+
self.segment_id = segment_id
|
|
15
|
+
if data is not None:
|
|
16
|
+
self.data = data
|
|
17
|
+
|
|
18
|
+
state = self.client.create_segment(
|
|
19
|
+
segment_type=self.segment_type, segment_id=self.segment_id, data=self.data
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if self.segment_id is None and state.is_ok() and "segment" in state: # type: ignore
|
|
23
|
+
self.segment_id = state["segment"]["id"] # type: ignore
|
|
24
|
+
return state # type: ignore
|
|
25
|
+
|
|
26
|
+
def get(self) -> StreamResponse:
|
|
27
|
+
return self.client.get_segment(segment_id=self.segment_id) # type: ignore
|
|
28
|
+
|
|
29
|
+
def update(self, data: SegmentData) -> StreamResponse:
|
|
30
|
+
return self.client.update_segment( # type: ignore
|
|
31
|
+
segment_id=self.segment_id, data=data
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def delete(self) -> StreamResponse:
|
|
35
|
+
return self.client.delete_segment(segment_id=self.segment_id) # type: ignore
|
|
36
|
+
|
|
37
|
+
def target_exists(self, target_id: str) -> StreamResponse:
|
|
38
|
+
return self.client.segment_target_exists( # type: ignore
|
|
39
|
+
segment_id=self.segment_id, target_id=target_id
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def add_targets(self, target_ids: list) -> StreamResponse:
|
|
43
|
+
return self.client.add_segment_targets( # type: ignore
|
|
44
|
+
segment_id=self.segment_id, target_ids=target_ids
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def query_targets(
|
|
48
|
+
self,
|
|
49
|
+
filter_conditions: Optional[Dict] = None,
|
|
50
|
+
sort: Optional[List[SortParam]] = None,
|
|
51
|
+
options: Optional[QuerySegmentTargetsOptions] = None,
|
|
52
|
+
) -> StreamResponse:
|
|
53
|
+
return self.client.query_segment_targets( # type: ignore
|
|
54
|
+
segment_id=self.segment_id,
|
|
55
|
+
sort=sort,
|
|
56
|
+
filter_conditions=filter_conditions,
|
|
57
|
+
options=options,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def remove_targets(self, target_ids: list) -> StreamResponse:
|
|
61
|
+
return self.client.remove_segment_targets( # type: ignore
|
|
62
|
+
segment_id=self.segment_id, target_ids=target_ids
|
|
63
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 8):
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
else:
|
|
8
|
+
from typing_extensions import TypedDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SortOrder(IntEnum):
|
|
12
|
+
"""
|
|
13
|
+
Represents the sort order for a query.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
ASC = 1
|
|
17
|
+
DESC = -1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SortParam(TypedDict, total=False):
|
|
21
|
+
"""
|
|
22
|
+
Represents a sort parameter for a query.
|
|
23
|
+
|
|
24
|
+
Parameters:
|
|
25
|
+
field: The field to sort by.
|
|
26
|
+
direction: The direction to sort by.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
field: str
|
|
30
|
+
direction: SortOrder
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Pager(TypedDict, total=False):
|
|
34
|
+
"""
|
|
35
|
+
Represents the data structure for a pager.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
limit: The maximum number of items to return.
|
|
39
|
+
next: The next page token.
|
|
40
|
+
prev: The previous page token.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
limit: Optional[int]
|
|
44
|
+
next: Optional[str]
|
|
45
|
+
prev: Optional[str]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
if sys.version_info >= (3, 8):
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
else:
|
|
7
|
+
from typing_extensions import TypedDict
|
|
8
|
+
|
|
9
|
+
from stream_chat.types.base import Pager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageTemplate(TypedDict, total=False):
|
|
13
|
+
"""
|
|
14
|
+
Represents the data structure for a message template.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
text: The text of the message.
|
|
18
|
+
attachments: List of the message attachments.
|
|
19
|
+
custom: Custom data.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
text: str
|
|
23
|
+
attachments: Optional[List[Dict]]
|
|
24
|
+
custom: Optional[Dict]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChannelTemplate(TypedDict, total=False):
|
|
28
|
+
"""
|
|
29
|
+
Represents the data structure for a channel template.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
type: The type of channel.
|
|
33
|
+
id: The ID of the channel.
|
|
34
|
+
members: List of member IDs.
|
|
35
|
+
custom: Custom data.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
type: str
|
|
39
|
+
id: Optional[str]
|
|
40
|
+
members: Optional[List[str]]
|
|
41
|
+
custom: Optional[Dict]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CampaignData(TypedDict, total=False):
|
|
45
|
+
"""
|
|
46
|
+
Represents the data structure for a campaign.
|
|
47
|
+
|
|
48
|
+
Either `segment_ids` or `user_ids` must be provided, but not both.
|
|
49
|
+
|
|
50
|
+
If `create_channels` is True, `channel_template` must be provided.
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
message_template: The template for the message to be sent in the campaign.
|
|
54
|
+
sender_id: The ID of the user who is sending the campaign.
|
|
55
|
+
segment_ids: List of segment IDs the campaign is targeting.
|
|
56
|
+
user_ids: List of individual user IDs the campaign is targeting.
|
|
57
|
+
create_channels: Flag to indicate if new channels should be created for the campaign.
|
|
58
|
+
channel_template: The template for channels to be created, if applicable.
|
|
59
|
+
name: The name of the campaign.
|
|
60
|
+
description: A description of the campaign.
|
|
61
|
+
skip_push: Flag to indicate if push notifications should be skipped.
|
|
62
|
+
skip_webhook: Flag to indicate if webhooks should be skipped.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
message_template: MessageTemplate
|
|
66
|
+
sender_id: str
|
|
67
|
+
segment_ids: Optional[List[str]]
|
|
68
|
+
user_ids: Optional[List[str]]
|
|
69
|
+
create_channels: Optional[bool]
|
|
70
|
+
channel_template: Optional[ChannelTemplate]
|
|
71
|
+
name: Optional[str]
|
|
72
|
+
description: Optional[str]
|
|
73
|
+
skip_push: Optional[bool]
|
|
74
|
+
skip_webhook: Optional[bool]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class QueryCampaignsOptions(Pager, total=False):
|
|
78
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 8):
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
else:
|
|
8
|
+
from typing_extensions import TypedDict
|
|
9
|
+
|
|
10
|
+
from stream_chat.types.base import Pager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SegmentType(Enum):
|
|
14
|
+
"""
|
|
15
|
+
Represents the type of segment.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
CHANNEL: A segment targeting channels.
|
|
19
|
+
USER: A segment targeting users.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
CHANNEL = "channel"
|
|
23
|
+
USER = "user"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SegmentData(TypedDict, total=False):
|
|
27
|
+
"""
|
|
28
|
+
Represents the data structure for a segment.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
name: The name of the segment.
|
|
32
|
+
description: A description of the segment.
|
|
33
|
+
filter: A filter to apply to the segment.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name: Optional[str]
|
|
37
|
+
description: Optional[str]
|
|
38
|
+
filter: Optional[Dict]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class QuerySegmentsOptions(Pager, total=False):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class QuerySegmentTargetsOptions(Pager, total=False):
|
|
46
|
+
pass
|
|
@@ -65,3 +65,7 @@ class StreamResponse(dict):
|
|
|
65
65
|
def status_code(self) -> int:
|
|
66
66
|
"""Returns the HTTP status code of the response."""
|
|
67
67
|
return self.__status_code
|
|
68
|
+
|
|
69
|
+
def is_ok(self) -> bool:
|
|
70
|
+
"""Returns True if the status code is in the 200 range."""
|
|
71
|
+
return 200 <= self.__status_code < 300
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stream-chat
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.12.1
|
|
4
4
|
Summary: Client for Stream Chat.
|
|
5
5
|
Home-page: https://github.com/GetStream/stream-chat-python
|
|
6
6
|
Author: Tommaso Barbugli
|
|
7
7
|
Author-email: support@getstream.io
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/GetStream/stream-chat-python/issues
|
|
9
9
|
Project-URL: Documentation, https://getstream.io/activity-feeds/docs/python/?language=python
|
|
10
|
-
Project-URL: Release Notes, https://github.com/GetStream/stream-chat-python/releases/tag/v4.
|
|
10
|
+
Project-URL: Release Notes, https://github.com/GetStream/stream-chat-python/releases/tag/v4.12.1
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: System Administrators
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
stream_chat/__init__.py,sha256=xYQuC8xcPLJxJnFWzaNaO-sVUc7IJZYe13OIPRaDBEA,116
|
|
2
|
+
stream_chat/__pkg__.py,sha256=n_cQUeLvytzgt4sytXXqvw9Fe8FMTfvFz-1c7JdKYjk,206
|
|
3
|
+
stream_chat/campaign.py,sha256=Z7bBo2rGMs02JkA1k9_206J0spcSLecjdhQuRnrc2Eo,2156
|
|
4
|
+
stream_chat/channel.py,sha256=SxlnRM8U1yo_k3wDBC4azz3_LDRPclDV5GBmGCfqB8U,7766
|
|
5
|
+
stream_chat/client.py,sha256=0PacvOGFy7FKIwYEex1VVkl604EWMeEJdmQ3NjpfW14,28160
|
|
6
|
+
stream_chat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
stream_chat/segment.py,sha256=NqfjNZ9i7RYlLkBohxu3-Xc4l3zo3fnKoh2IMLAUrmo,2387
|
|
8
|
+
stream_chat/async_chat/__init__.py,sha256=V6x7yDCdXbP3vMuSan6Xm7RE_njZUvflImqOxfKRt4Y,67
|
|
9
|
+
stream_chat/async_chat/campaign.py,sha256=Bc3iHZigxserUycZK4wDuXU0wXVH2G6fIDiYNVYPkeE,1885
|
|
10
|
+
stream_chat/async_chat/channel.py,sha256=yn9P7u1lJdwoCVw9gKotadq6s1rdYSM_wnxhdaZpnTI,8124
|
|
11
|
+
stream_chat/async_chat/client.py,sha256=KRebkAv-eMO87XYGbDWKlag3ba99KpEghjkbWmw5LrU,30314
|
|
12
|
+
stream_chat/async_chat/segment.py,sha256=qo8waiGTkRRDRFYKW0GtrcSgnBkDF-PVSaCSBEnYwAg,2473
|
|
13
|
+
stream_chat/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
stream_chat/base/campaign.py,sha256=dAVkTYyirEpgyOPn8JyDebNMKeJu02vTuqgmPvdQoWU,1449
|
|
15
|
+
stream_chat/base/channel.py,sha256=G6thUgND74z0gvI-N3b3vEsrDJ-D4MrAfW2um_E4x9w,13788
|
|
16
|
+
stream_chat/base/client.py,sha256=uY0OsIqOPHs1vWtdov6o4kAoo2IdEYqMb-KBG_kewqQ,40247
|
|
17
|
+
stream_chat/base/exceptions.py,sha256=eh1qW5d6zjUrlgsHNEBebAr0jVH2UupZ06w8sp2cseI,819
|
|
18
|
+
stream_chat/base/segment.py,sha256=_UONbdF4OjaGIpsTzyY5UN_U3VAfuZyAkJnS3RfsM5k,2020
|
|
19
|
+
stream_chat/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
stream_chat/types/base.py,sha256=JKlOxQVPvMClTo0ev63Ya-K-dX-98j95qSE-KKzziEE,858
|
|
21
|
+
stream_chat/types/campaign.py,sha256=JyAn08oMaEsxPSqmVTdsfJK4xpfErPQV7Xr7zLQm99M,2293
|
|
22
|
+
stream_chat/types/rate_limit.py,sha256=v3Z4Ur0yoEdFLiHa1CNABEej2nxPlHQ6Bpy2XxW-TYQ,165
|
|
23
|
+
stream_chat/types/segment.py,sha256=28pLB3ADJcF8Y5PZHa4VACVg-ySxM94nCiP4uNvQxfs,925
|
|
24
|
+
stream_chat/types/stream_response.py,sha256=jWKPrOU7u6dZ2SyOK53uQCXTstrL1HshO9fA7R6Bt_A,2391
|
|
25
|
+
stream_chat-4.12.1.dist-info/LICENSE,sha256=H66SBDuPWSRHzKPEdyjAk3C0THQRcGPfqqve4naQuu0,14424
|
|
26
|
+
stream_chat-4.12.1.dist-info/METADATA,sha256=URkKgtoi9hOx2F1cKXNLX27lb5xKPz559eGDPt4cKWY,7332
|
|
27
|
+
stream_chat-4.12.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
28
|
+
stream_chat-4.12.1.dist-info/top_level.txt,sha256=26uTfg4bWcEaFrVKlzGGgfONH1p5DDe21G07EKfYSvo,12
|
|
29
|
+
stream_chat-4.12.1.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
stream_chat/__init__.py,sha256=xYQuC8xcPLJxJnFWzaNaO-sVUc7IJZYe13OIPRaDBEA,116
|
|
2
|
-
stream_chat/__pkg__.py,sha256=cKE0dhcuFNoI1Qxw7fK9cL1RP8-7ACqiQHVOJ5MyS4g,206
|
|
3
|
-
stream_chat/channel.py,sha256=SxlnRM8U1yo_k3wDBC4azz3_LDRPclDV5GBmGCfqB8U,7766
|
|
4
|
-
stream_chat/client.py,sha256=yIdkT1vTDKluM369jxo5F9xdbDVZBOUMOVZcFVE4Kv8,24333
|
|
5
|
-
stream_chat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
stream_chat/async_chat/__init__.py,sha256=V6x7yDCdXbP3vMuSan6Xm7RE_njZUvflImqOxfKRt4Y,67
|
|
7
|
-
stream_chat/async_chat/channel.py,sha256=yn9P7u1lJdwoCVw9gKotadq6s1rdYSM_wnxhdaZpnTI,8124
|
|
8
|
-
stream_chat/async_chat/client.py,sha256=hzxydYb2Aj5rQqMF8Im-5GFQUYr0Lyyb2CyVQDCDyFU,26405
|
|
9
|
-
stream_chat/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
stream_chat/base/channel.py,sha256=G6thUgND74z0gvI-N3b3vEsrDJ-D4MrAfW2um_E4x9w,13788
|
|
11
|
-
stream_chat/base/client.py,sha256=FjfkHOh-BMpoejHAeNKa0q_5L1mGk6dE9aLpvxHyXac,37443
|
|
12
|
-
stream_chat/base/exceptions.py,sha256=eh1qW5d6zjUrlgsHNEBebAr0jVH2UupZ06w8sp2cseI,819
|
|
13
|
-
stream_chat/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
stream_chat/types/rate_limit.py,sha256=v3Z4Ur0yoEdFLiHa1CNABEej2nxPlHQ6Bpy2XxW-TYQ,165
|
|
15
|
-
stream_chat/types/stream_response.py,sha256=UdMAORACY6UwYskDx_vNnMTmJral24ocqYvCR8TQQHs,2247
|
|
16
|
-
stream_chat-4.11.0.dist-info/LICENSE,sha256=H66SBDuPWSRHzKPEdyjAk3C0THQRcGPfqqve4naQuu0,14424
|
|
17
|
-
stream_chat-4.11.0.dist-info/METADATA,sha256=rZR3go2YcoU8ZUKVK6TnX960-JRv7QRhv8cv7nQbsK0,7332
|
|
18
|
-
stream_chat-4.11.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
19
|
-
stream_chat-4.11.0.dist-info/top_level.txt,sha256=26uTfg4bWcEaFrVKlzGGgfONH1p5DDe21G07EKfYSvo,12
|
|
20
|
-
stream_chat-4.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|