Skip to content

Commit 6539f1a

Browse files
committed
Add Poll endpoints and tidied schedule
Added poll endpoints with documentation Moved validation checks for schedule into user rather than http.
1 parent e0dc80a commit 6539f1a

File tree

5 files changed

+299
-22
lines changed

5 files changed

+299
-22
lines changed

docs/changelog.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Massive documentation updates
88
- Removed unexpected loop termination from `WSConnection._close()`
99
- Fix bug where # prefixed channel names in initial_channels would not trigger :func:`twitchio.Client.event_ready`
1010
- :func:`User.create_clip` has been fixed by converting bool to string in http request
11+
- Poll endpoints added :func:`User.fetch_polls` :func:`User.create_poll` and :func:`User.end_poll`
1112

1213
- ext.commands
1314
- :func:`Bot.handle_commands` now also invokes on threads / replies
@@ -30,7 +31,7 @@ Massive documentation updates
3031
- Fix :func:`twitchio.Client.wait_for_ready`
3132
- Remove loop= parameter inside :func:`twitchio.Client.wait_for` for 3.10 compatibility
3233
- Add ``is_broadcaster`` check to :class:`twitchio.PartialChatter`. This is accessible as ``Context.author.is_broadcaster``
33-
- :func:`twitchio.User.fetch_follow` will now return ``None`` if the FollowEvent does not exists
34+
- :func:`User.fetch_follow` will now return ``None`` if the FollowEvent does not exists
3435
- TwitchIO will now correctly handle error raised when only the prefix is typed in chat
3536
- Fix paginate logic in :func:`TwitchHTTP.request`
3637

docs/reference.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,20 @@ ModEvent
207207
:members:
208208
:inherited-members:
209209

210+
Poll
211+
-------------
212+
.. attributetable:: Poll
213+
214+
.. autoclass:: Poll
215+
:members:
216+
:inherited-members:
217+
218+
.. attributetable:: PollChoice
219+
220+
.. autoclass:: PollChoice
221+
:members:
222+
:inherited-members:
223+
210224
Predictions
211225
-------------
212226
.. attributetable:: Prediction

twitchio/http.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ async def _request(self, route, path, headers, utilize_bucket=True):
220220

221221
if 500 <= resp.status <= 504:
222222
reason = resp.reason
223-
await asyncio.sleep(2**attempt + 1)
223+
await asyncio.sleep(2 ** attempt + 1)
224224
continue
225225

226226
if utilize_bucket:
@@ -251,7 +251,7 @@ async def _request(self, route, path, headers, utilize_bucket=True):
251251
reason = "Ratelimit Reached"
252252

253253
if not utilize_bucket: # non Helix APIs don't have ratelimit headers
254-
await asyncio.sleep(3**attempt + 1)
254+
await asyncio.sleep(3 ** attempt + 1)
255255
continue
256256

257257
raise errors.HTTPException(
@@ -584,7 +584,9 @@ async def post_prediction(
584584

585585
async def post_create_clip(self, token: str, broadcaster_id: int, has_delay=False):
586586
return await self.request(
587-
Route("POST", "clips", query=[("broadcaster_id", broadcaster_id), ("has_delay", str(has_delay))], token=token),
587+
Route(
588+
"POST", "clips", query=[("broadcaster_id", broadcaster_id), ("has_delay", str(has_delay))], token=token
589+
),
588590
paginate=False,
589591
)
590592

@@ -759,23 +761,11 @@ async def patch_channel(
759761
async def get_channel_schedule(
760762
self,
761763
broadcaster_id: str,
762-
segment_ids: List[str] = None,
763-
start_time: datetime.datetime = None,
764-
utc_offset: int = None,
764+
segment_ids: Optional[List[str]] = None,
765+
start_time: Optional[datetime.datetime] = None,
766+
utc_offset: Optional[int] = None,
765767
first: int = 20,
766768
):
767-
768-
if first is not None and (first > 25 or first < 1):
769-
raise ValueError("The parameter 'first' was malformed: the value must be less than or equal to 25")
770-
if segment_ids is not None and len(segment_ids) > 100:
771-
raise ValueError("segment_id can only have 100 entries")
772-
773-
if start_time:
774-
start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
775-
776-
if utc_offset:
777-
utc_offset = str(utc_offset)
778-
779769
q = [
780770
x
781771
for x in [
@@ -927,3 +917,46 @@ async def get_teams(self, team_name: str = None, team_id: str = None):
927917
async def get_channel_teams(self, broadcaster_id: str):
928918
q = [("broadcaster_id", broadcaster_id)]
929919
return await self.request(Route("GET", "teams/channel", query=q))
920+
921+
async def get_polls(
922+
self,
923+
broadcaster_id: str,
924+
token: str,
925+
poll_ids: Optional[List[str]] = None,
926+
first: Optional[int] = 20,
927+
):
928+
q = [("broadcaster_id", broadcaster_id), ("first", first)]
929+
if poll_ids:
930+
q.extend(("id", poll_id) for poll_id in poll_ids)
931+
return await self.request(Route("GET", "polls", query=q, token=token), paginate=False, full_body=True)
932+
933+
async def post_poll(
934+
self,
935+
broadcaster_id: str,
936+
token: str,
937+
title: str,
938+
choices,
939+
duration: int,
940+
bits_voting_enabled: Optional[bool] = False,
941+
bits_per_vote: Optional[int] = None,
942+
channel_points_voting_enabled: Optional[bool] = False,
943+
channel_points_per_vote: Optional[int] = None,
944+
):
945+
body = {
946+
"broadcaster_id": broadcaster_id,
947+
"title": title,
948+
"choices": [{"title": choice} for choice in choices],
949+
"duration": duration,
950+
"bits_voting_enabled": str(bits_voting_enabled),
951+
"channel_points_voting_enabled": str(channel_points_voting_enabled),
952+
}
953+
if bits_voting_enabled and bits_per_vote:
954+
body["bits_per_vote"] = bits_per_vote
955+
if channel_points_voting_enabled and channel_points_per_vote:
956+
body["channel_points_per_vote"] = channel_points_per_vote
957+
958+
return await self.request(Route("POST", "polls", body=body, token=token))
959+
960+
async def patch_poll(self, broadcaster_id: str, token: str, id: str, status: str):
961+
body = {"broadcaster_id": broadcaster_id, "id": id, "status": status}
962+
return await self.request(Route("PATCH", "polls", body=body, token=token))

twitchio/models.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
"Team",
6969
"ChannelTeams",
7070
"ChannelInfo",
71+
"Poll",
72+
"PollChoice",
7173
)
7274

7375

@@ -1344,3 +1346,98 @@ def __init__(self, http: "TwitchHTTP", data: dict):
13441346

13451347
def __repr__(self):
13461348
return f"<ChannelTeams user={self.broadcaster} team_name={self.team_name} team_display_name={self.team_display_name} id={self.id} created_at={self.created_at}>"
1349+
1350+
1351+
class Poll:
1352+
"""
1353+
Represents a list of Polls for a broadcaster / channel
1354+
1355+
Attributes
1356+
-----------
1357+
id: :class:`str`
1358+
ID of a poll.
1359+
broadcaster: :class:`~twitchio.PartialUser`
1360+
User of the broadcaster.
1361+
title: :class:`str`
1362+
Question displayed for the poll.
1363+
choices: List[:class:`~twitchio.PollChoice`]
1364+
The poll choices.
1365+
bits_voting_enabled: :class:`bool`
1366+
Indicates if Bits can be used for voting.
1367+
bits_per_vote: :class:`int`
1368+
Number of Bits required to vote once with Bits.
1369+
channel_points_voting_enabled: :class:`bool`
1370+
Indicates if Channel Points can be used for voting.
1371+
channel_points_per_vote: :class:`int`
1372+
Number of Channel Points required to vote once with Channel Points.
1373+
status: :class:`str`
1374+
Poll status. Valid values: ACTIVE, COMPLETED, TERMINATED, ARCHIVED, MODERATED, INVALID
1375+
duration: :class:`int`
1376+
Total duration for the poll (in seconds).
1377+
started_at: :class:`datetime.datetime`
1378+
Date and time the the poll was started.
1379+
ended_at: :class:`datetime.datetime`
1380+
Date and time the the poll was ended.
1381+
"""
1382+
1383+
__slots__ = (
1384+
"id",
1385+
"broadcaster",
1386+
"title",
1387+
"choices",
1388+
"channel_points_voting_enabled",
1389+
"channel_points_per_vote",
1390+
"status",
1391+
"duration",
1392+
"started_at",
1393+
"ended_at",
1394+
)
1395+
1396+
def __init__(self, http: "TwitchHTTP", data: dict):
1397+
self.id: str = data["id"]
1398+
self.broadcaster = PartialUser(http, data["broadcaster_id"], data["broadcaster_login"])
1399+
self.title: str = data["title"]
1400+
self.choices: List[PollChoice] = [PollChoice(d) for d in data["choices"]] if data["choices"] else []
1401+
self.channel_points_voting_enabled: bool = data["channel_points_voting_enabled"]
1402+
self.channel_points_per_vote: int = data["channel_points_per_vote"]
1403+
self.status: str = data["status"]
1404+
self.duration: int = data["duration"]
1405+
self.started_at: datetime.datetime = parse_timestamp(data["started_at"])
1406+
try:
1407+
self.ended_at: Optional[datetime.datetime] = parse_timestamp(data["ended_at"])
1408+
except KeyError:
1409+
self.ended_at = None
1410+
1411+
def __repr__(self):
1412+
return f"<Polls id={self.id} broadcaster={self.broadcaster} title={self.title} status={self.status} duration={self.duration} started_at={self.started_at} ended_at={self.ended_at}>"
1413+
1414+
1415+
class PollChoice:
1416+
"""
1417+
Represents a polls choices
1418+
1419+
Attributes
1420+
-----------
1421+
id: :class:`str`
1422+
ID for the choice.
1423+
title: :class:`str`
1424+
Text displayed for the choice.
1425+
votes: :class:`int`
1426+
Total number of votes received for the choice across all methods of voting.
1427+
channel_points_votes: :class:`int`
1428+
Number of votes received via Channel Points.
1429+
bits_votes: :class:`int`
1430+
Number of votes received via Bits.
1431+
"""
1432+
1433+
__slots__ = ("id", "title", "votes", "channel_points_votes", "bits_votes")
1434+
1435+
def __init__(self, data: dict):
1436+
self.id: str = data["id"]
1437+
self.title: str = data["title"]
1438+
self.votes: int = data["votes"]
1439+
self.channel_points_votes: int = data["channel_points_votes"]
1440+
self.bits_votes: int = data["bits_votes"]
1441+
1442+
def __repr__(self):
1443+
return f"<PollChoice id={self.id} title={self.title} votes={self.votes} channel_points_votes={self.channel_points_votes} bits_votes={self.bits_votes}>"

0 commit comments

Comments
 (0)