Skip to content

Commit 218b487

Browse files
committed
update the lib to use a proper iso parser. Properly type the User class
1 parent 8dff0a7 commit 218b487

File tree

8 files changed

+70
-57
lines changed

8 files changed

+70
-57
lines changed

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
aiohttp>=3.6.0,<3.8.0
1+
aiohttp>=3.6.0,<3.8.0
2+
iso8601

twitchio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@
3939
from .message import Message
4040
from .models import *
4141
from .rewards import *
42+
from .utils import *

twitchio/ext/commands/core.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,16 @@ def get_user(self, name: str) -> Optional[Union[PartialUser, User]]:
459459

460460
return None
461461

462+
C = TypeVar("C", bound="Command")
463+
G = TypeVar("G", bound="Group")
462464

463465
def command(
464-
*, name: str = None, aliases: Union[list, tuple] = None, cls=Command, no_global_checks=False
465-
) -> Callable[[Callable], Command]:
466+
*, name: str = None, aliases: Union[list, tuple] = None, cls: C = Command, no_global_checks=False
467+
) -> Callable[[Callable], C]:
466468
if cls and not inspect.isclass(cls):
467469
raise TypeError(f"cls must be of type <class> not <{type(cls)}>")
468470

469-
def decorator(func: Callable) -> cls:
471+
def decorator(func: Callable) -> C:
470472
fname = name or func.__name__
471473
return cls(
472474
name=fname,
@@ -482,14 +484,14 @@ def group(
482484
*,
483485
name: str = None,
484486
aliases: Union[list, tuple] = None,
485-
cls=Group,
487+
cls: G = Group,
486488
no_global_checks=False,
487489
invoke_with_subcommand=False,
488-
) -> Callable[[Callable], Group]:
490+
) -> Callable[[Callable], G]:
489491
if cls and not inspect.isclass(cls):
490492
raise TypeError(f"cls must be of type <class> not <{type(cls)}>")
491493

492-
def decorator(func: Callable) -> cls:
494+
def decorator(func: Callable) -> G:
493495
fname = name or func.__name__
494496
return cls(
495497
name=fname,

twitchio/ext/eventsub/models.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from aiohttp import web
99

10-
from twitchio import CustomReward, PartialUser
10+
from twitchio import CustomReward, PartialUser, parse_timestamp as _parse_datetime
1111

1212
if TYPE_CHECKING:
1313
from .server import EventSubClient
@@ -29,11 +29,6 @@ def _loads(s: str) -> dict:
2929
logger = logging.getLogger("twitchio.ext.eventsub")
3030

3131

32-
def _parse_datetime(time: str) -> datetime.datetime:
33-
# Exemple time: 2021-06-19T04:12:39.407371633Z
34-
return datetime.datetime.strptime(time[:26], "%Y-%m-%dT%H:%M:%S.%f")
35-
36-
3732
class EmptyObject:
3833
def __init__(self, **kwargs):
3934
self.__dict__.update(kwargs)

twitchio/ext/pubsub/models.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import datetime
2626
from typing import List, Optional
2727

28-
from twitchio import PartialUser, Client, Channel, CustomReward
28+
from twitchio import PartialUser, Client, Channel, CustomReward, parse_timestamp
2929

3030

3131
__all__ = (
@@ -185,7 +185,7 @@ def __init__(self, client: Client, topic: str, data: dict):
185185
)
186186
self.badge_tier: int = data["badge_tier"]
187187
self.message: str = data["chat_message"]
188-
self.timestamp = datetime.datetime.strptime(data["time"][0:25] + data["time"][28:], "%Y-%m-%dT%H:%M:%S.%fZ")
188+
self.timestamp = parse_timestamp(data["time"])
189189

190190

191191
class PubSubChannelPointsMessage(PubSubMessage):
@@ -217,9 +217,7 @@ def __init__(self, client: Client, topic: str, data: dict):
217217

218218
redemption = data["message"]["data"]["redemption"]
219219

220-
self.timestamp = datetime.datetime.strptime(
221-
redemption["redeemed_at"][0:25] + redemption["redeemed_at"][28:], "%Y-%m-%dT%H:%M:%S.%fZ"
222-
)
220+
self.timestamp = parse_timestamp(redemption["redeemed_at"])
223221
self.channel_id: int = int(redemption["channel_id"])
224222
self.id: str = redemption["id"]
225223
self.user = PartialUser(client._http, redemption["user"]["id"], redemption["user"]["display_name"])
@@ -336,16 +334,10 @@ def __init__(self, client: Client, topic: str, data: dict):
336334

337335
self.expires_at = self.updated_at = None
338336
if data["message"]["data"]["expires_at"]:
339-
self.expires_at = datetime.datetime.strptime(
340-
data["message"]["data"]["expires_at"][0:25] + data["message"]["data"]["expires_at"][28:],
341-
"%Y-%m-%dT%H:%M:%S.%fZ",
342-
)
337+
self.expires_at = parse_timestamp(data["message"]["data"]["expires_at"])
343338

344339
if data["message"]["data"]["updated_at"]:
345-
self.updated_at = datetime.datetime.strptime(
346-
data["message"]["data"]["updated_at"][0:25] + data["message"]["data"]["updated_at"][28:],
347-
"%Y-%m-%dT%H:%M:%S.%fZ",
348-
)
340+
self.updated_at = parse_timestamp(data["message"]["data"]["expires_at"])
349341

350342

351343
class PubSubModerationActionModeratorAdd(PubSubMessage):

twitchio/models.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from typing import Optional, Union, TYPE_CHECKING, List, Dict
2727

2828
from . import enums
29+
from .utils import parse_timestamp
2930
from .user import BitLeaderboardUser, PartialUser, User
3031

3132
if TYPE_CHECKING:
@@ -117,7 +118,7 @@ def __init__(self, http: "TwitchHTTP", data: dict):
117118
self.tiers = [CheerEmoteTier(x) for x in data["tiers"]]
118119
self.type: str = data["type"]
119120
self.order: str = data["order"]
120-
self.last_updated = datetime.datetime.strptime(data["last_updated"], "%Y-%m-%dT%H:%M:%SZ")
121+
self.last_updated = parse_timestamp(data["last_updated"])
121122
self.charitable: bool = data["is_charitable"]
122123

123124
def __repr__(self):
@@ -152,7 +153,7 @@ def __init__(self, http: "TwitchHTTP", data: dict):
152153
self.language = data["language"]
153154
self.title = data["title"]
154155
self.views = data["view_count"]
155-
self.created_at = datetime.datetime.strptime(data["created_at"], "%Y-%m-%dT%H:%M:%SZ")
156+
self.created_at = parse_timestamp(data["created_at"])
156157
self.thumbnail_url = data["thumbnail_url"]
157158

158159
def __repr__(self):
@@ -197,12 +198,10 @@ def __init__(self, http: "TwitchHTTP", data: dict):
197198
self.type: str = data["event_type"]
198199
self.version: str = data["version"]
199200
self.broadcaster = PartialUser(http, id=data["event_data"]["broadcaster_id"], name=None)
200-
self.timestamp = datetime.datetime.strptime(data["event_timestamp"], "%Y-%m-%dT%H:%M:%SZ")
201-
self.cooldown_end_time = datetime.datetime.strptime(
202-
data["event_data"]["cooldown_end_time"], "%Y-%m-%dT%H:%M:%SZ"
203-
)
204-
self.expiry = datetime.datetime.strptime(data["expires_at"], "%Y-%m-%dT%H:%M:%SZ")
205-
self.started_at = datetime.datetime.strptime(data["event_data"]["started_at"], "%Y-%m-%dT%H:%M:%SZ")
201+
self.timestamp = parse_timestamp(data["event_timestamp"])
202+
self.cooldown_end_time = parse_timestamp(data["event_data"]["cooldown_end_time"])
203+
self.expiry = parse_timestamp(data["expires_at"])
204+
self.started_at = parse_timestamp(data["event_data"]["started_at"])
206205
self.last_contribution = HypeTrainContribution(http, data["event_data"]["last_contribution"])
207206
self.level: int = data["event_data"]["level"]
208207
self.top_contributions = [HypeTrainContribution(http, x) for x in data["event_data"]["top_contributions"]]
@@ -219,14 +218,14 @@ class BanEvent:
219218
def __init__(self, http: "TwitchHTTP", data: dict, broadcaster: Optional[Union[PartialUser, User]]):
220219
self.id: str = data["id"]
221220
self.type: str = data["event_type"]
222-
self.timestamp = datetime.datetime.strptime(data["event_timestamp"], "%Y-%m-%dT%H:%M:%SZ")
221+
self.timestamp = parse_timestamp(data["event_timestamp"])
223222
self.version: float = float(data["version"])
224223
self.broadcaster = broadcaster or PartialUser(
225224
http, data["event_data"]["broadcaster_id"], data["event_data"]["broadcaster_name"]
226225
)
227226
self.user = PartialUser(http, data["event_data"]["user_id"], data["event_data"]["user_name"])
228227
self.expires_at = (
229-
datetime.datetime.strptime(data["event_data"]["expires_at"], "%Y-%m-%dT%H:%M:%SZ")
228+
parse_timestamp(data["event_data"]["expires_at"])
230229
if data["event_data"]["expires_at"]
231230
else None
232231
)
@@ -248,7 +247,7 @@ def __init__(
248247
):
249248
self.from_user = from_ or PartialUser(http, data["from_id"], data["from_name"])
250249
self.to_user = to or PartialUser(http, data["to_id"], data["to_name"])
251-
self.followed_at = datetime.datetime.strptime(data["followed_at"], "%Y-%m-%dT%H:%M:%SZ")
250+
self.followed_at = parse_timestamp(data["followed_at"])
252251

253252
def __repr__(self):
254253
return f"<FollowEvent from_user={self.from_user} to_user={self.to_user} followed_at={self.followed_at}>"
@@ -284,7 +283,7 @@ class Marker:
284283

285284
def __init__(self, data: dict):
286285
self.id: int = data["id"]
287-
self.created_at = datetime.datetime.strptime(data["created_at"], "%Y-%m-%dT%H:%M:%SZ")
286+
self.created_at = parse_timestamp(data["created_at"])
288287
self.description: str = data["description"]
289288
self.position: int = data["position_seconds"]
290289
self.url: Optional[str] = data.get("URL")
@@ -342,7 +341,7 @@ class ModEvent:
342341
def __init__(self, http: "TwitchHTTP", data: dict, broadcaster: Union[PartialUser, User]):
343342
self.id: int = data["id"]
344343
self.type = enums.ModEventEnum(value=data["event_type"])
345-
self.timestamp = datetime.datetime.strptime(data["event_timestamp"], "%Y-%m-%dT%H:%M:%SZ")
344+
self.timestamp = parse_timestamp(data["event_timestamp"])
346345
self.version: str = data["version"]
347346
self.broadcaster = broadcaster
348347
self.user = PartialUser(http, data["event_data"]["user_id"], data["event_data"]["user_name"])
@@ -487,8 +486,8 @@ def __init__(self, http: "TwitchHTTP", data: dict, user: Union[PartialUser, User
487486
self.user = user or PartialUser(http, data["user_id"], data["user_name"])
488487
self.title: str = data["title"]
489488
self.description: str = data["description"]
490-
self.created_at = datetime.datetime.strptime(data["created_at"], "%Y-%m-%dT%H:%M:%SZ")
491-
self.published_at = datetime.datetime.strptime(data["published_at"], "%Y-%m-%dT%H:%M:%SZ")
489+
self.created_at = parse_timestamp(data["created_at"])
490+
self.published_at = parse_timestamp(data["published_at"])
492491
self.url: str = data["url"]
493492
self.thumbnail_url: str = data["thumbnail_url"]
494493
self.viewable: str = data["viewable"]
@@ -533,7 +532,7 @@ class WebhookSubscription:
533532

534533
def __init__(self, data: dict):
535534
self.callback: str = data["callback"]
536-
self.expires_at = datetime.datetime.strptime(data["expires_at"], "%Y-%m-%dT%H:%M:%SZ")
535+
self.expires_at = parse_timestamp(data["expired_at"])
537536
self.topic: str = data["topic"]
538537

539538
def __repr__(self):
@@ -565,7 +564,7 @@ def __init__(self, http: "TwitchHTTP", data: dict):
565564
self.type: str = data["type"]
566565
self.title: str = data["title"]
567566
self.viewer_count: int = data["viewer_count"]
568-
self.started_at = datetime.datetime.strptime(data["started_at"], "%Y-%m-%dT%H:%M:%SZ")
567+
self.started_at = parse_timestamp(data["started_at"])
569568
self.language: str = data["language"]
570569
self.thumbnail_url: str = data["thumbnail_url"]
571570
self.tag_ids: List[str] = data["tag_ids"]
@@ -682,11 +681,11 @@ class ScheduleSegment:
682681

683682
def __init__(self, data: dict):
684683
self.id: str = data["id"]
685-
self.start_time = datetime.datetime.strptime(data["start_time"], "%Y-%m-%dT%H:%M:%SZ")
686-
self.end_time = datetime.datetime.strptime(data["end_time"], "%Y-%m-%dT%H:%M:%SZ")
684+
self.start_time = parse_timestamp(data["start_time"])
685+
self.end_time = parse_timestamp(data["end_time"])
687686
self.title: str = data["title"]
688687
self.canceled_until = (
689-
datetime.datetime.strptime(data["canceled_until"], "%Y-%m-%dT%H:%M:%SZ") if data["canceled_until"] else None
688+
parse_timestamp(data["canceled_until"]) if data["canceled_until"] else None
690689
)
691690
self.category = ScheduleCategory(data["category"]) if data["category"] else None
692691
self.is_recurring: bool = data["is_recurring"]
@@ -712,8 +711,8 @@ class ScheduleVacation:
712711
__slots__ = ("start_time", "end_time")
713712

714713
def __init__(self, data: dict):
715-
self.start_time = datetime.datetime.strptime(data["start_time"], "%Y-%m-%dT%H:%M:%SZ")
716-
self.end_time = datetime.datetime.strptime(data["end_time"], "%Y-%m-%dT%H:%M:%SZ")
714+
self.start_time = parse_timestamp(data["start_time"])
715+
self.end_time = parse_timestamp(data["end_time"])
717716

718717
def __repr__(self):
719718
return f"<ScheduleVacation start_time={self.start_time} end_time={self.end_time}>"

twitchio/user.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424

2525
import datetime
2626
import time
27-
from typing import TYPE_CHECKING, List, Optional, Union
27+
from typing import TYPE_CHECKING, List, Optional, Union, Tuple
2828

2929
from .enums import BroadcasterTypeEnum, UserTypeEnum
3030
from .errors import HTTPException, Unauthorized
3131
from .rewards import CustomReward
32+
from .utils import parse_timestamp
3233

3334

3435
if TYPE_CHECKING:
@@ -844,16 +845,16 @@ class User(PartialUser):
844845
def __init__(self, http: "TwitchHTTP", data: dict):
845846
self._http = http
846847
self.id = int(data["id"])
847-
self.name = data["login"]
848-
self.display_name = data["display_name"]
848+
self.name: str = data["login"]
849+
self.display_name: str = data["display_name"]
849850
self.type = UserTypeEnum(data["type"])
850851
self.broadcaster_type = BroadcasterTypeEnum(data["broadcaster_type"])
851-
self.description = data["description"]
852-
self.profile_image = data["profile_image_url"]
853-
self.offline_image = data["offline_image_url"]
854-
self.view_count = (data["view_count"],)
855-
self.created_at = data["created_at"]
856-
self.email = data.get("email")
852+
self.description: str = data["description"]
853+
self.profile_image: str = data["profile_image_url"]
854+
self.offline_image: str = data["offline_image_url"]
855+
self.view_count: Tuple[int] = (data["view_count"],) # this isn't supposed to be a tuple but too late to fix it!
856+
self.created_at = parse_timestamp(data["created_at"])
857+
self.email: Optional[str] = data.get("email")
857858
self._cached_rewards = None
858859

859860
def __repr__(self):

twitchio/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import datetime
2+
import iso8601
3+
4+
__all__ = (
5+
"parse_timestamp",
6+
)
7+
8+
def parse_timestamp(timestamp: str) -> datetime.datetime:
9+
"""
10+
11+
Parameters
12+
----------
13+
timestamp: :class:`str`
14+
The timestamp to be parsed, in an iso8601 format.
15+
16+
Returns
17+
-------
18+
:class:`datetime.datetime`
19+
The parsed timestamp.
20+
21+
"""
22+
return iso8601.parse_date(timestamp, datetime.timezone.utc)

0 commit comments

Comments
 (0)