MouseTools 4.1.1__tar.gz → 4.3.0__tar.gz

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.
Files changed (30) hide show
  1. {mousetools-4.1.1 → mousetools-4.3.0}/PKG-INFO +1 -1
  2. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/api/menu.py +3 -3
  3. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/channels/__init__.py +51 -0
  4. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/channels/calendar/__init__.py +4 -49
  5. mousetools-4.3.0/mousetools/channels/characters/__init__.py +122 -0
  6. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/channels/enums.py +5 -5
  7. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/channels/facilities/__init__.py +86 -83
  8. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/channels/facilitystatus/__init__.py +32 -59
  9. mousetools-4.3.0/mousetools/channels/forecastedwaittimes/__init__.py +71 -0
  10. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/decorators.py +6 -3
  11. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/mixins/couchbase.py +1 -1
  12. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools.egg-info/PKG-INFO +1 -1
  13. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools.egg-info/SOURCES.txt +2 -0
  14. {mousetools-4.1.1 → mousetools-4.3.0}/pyproject.toml +2 -1
  15. {mousetools-4.1.1 → mousetools-4.3.0}/LICENSE +0 -0
  16. {mousetools-4.1.1 → mousetools-4.3.0}/README.md +0 -0
  17. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/__init__.py +0 -0
  18. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/api/__init__.py +0 -0
  19. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/api/search.py +0 -0
  20. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/auth.py +0 -0
  21. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/channels/facilities/enums.py +0 -0
  22. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/enums.py +0 -0
  23. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/mixins/__init__.py +0 -0
  24. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/mixins/disney.py +0 -0
  25. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools/resources/environments.bin +0 -0
  26. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools.egg-info/dependency_links.txt +0 -0
  27. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools.egg-info/requires.txt +0 -0
  28. {mousetools-4.1.1 → mousetools-4.3.0}/mousetools.egg-info/top_level.txt +0 -0
  29. {mousetools-4.1.1 → mousetools-4.3.0}/setup.cfg +0 -0
  30. {mousetools-4.1.1 → mousetools-4.3.0}/tests/test_mixins.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mousetools
3
- Version: 4.1.1
3
+ Version: 4.3.0
4
4
  Summary: An unofficial Python wrapper for Disney's API
5
5
  Author: Scott Caratozzolo
6
6
  License-Expression: MIT
@@ -339,7 +339,7 @@ class Menus(DisneyAPIMixin):
339
339
  self._tz = ZoneInfo("UTC")
340
340
 
341
341
  self._disney_data: typing.Optional[dict] = None
342
- self._disney_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz)
342
+ self._disney_data_pull_time: datetime = datetime.now(tz=self._tz)
343
343
  if not lazy_load:
344
344
  self.refresh()
345
345
 
@@ -397,12 +397,12 @@ class Menus(DisneyAPIMixin):
397
397
  return self._disney_data["ancestorLocationLandArea"]
398
398
 
399
399
  @property
400
- def last_refresh(self) -> typing.Optional[datetime]:
400
+ def last_refresh(self) -> datetime:
401
401
  """
402
402
  The last time the menus were refreshed.
403
403
 
404
404
  Returns:
405
- (typing.Optional[datetime]): The last time the menus were refreshed.
405
+ (datetime): The last time the menus were refreshed.
406
406
  """
407
407
  if self._disney_data is None:
408
408
  self._disney_data = self.get_disney_data(self.entity_id)
@@ -1,11 +1,62 @@
1
+ import logging
1
2
  import typing
2
3
  from datetime import datetime, timedelta
3
4
  from enum import Enum
4
5
 
6
+ from dateutil.parser import isoparse
7
+
5
8
  from mousetools.channels.enums import DestinationTimezones, DLRCouchbaseChannels, WDWCouchbaseChannels
6
9
  from mousetools.enums import DestinationShort, EntityType
7
10
  from mousetools.mixins.couchbase import CouchbaseMixin
8
11
 
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CouchbaseChildChannel(CouchbaseMixin):
16
+ def __init__(self, channel_id: str, lazy_load: bool = True):
17
+ if isinstance(channel_id, Enum):
18
+ channel_id = channel_id.value
19
+
20
+ self.channel_id: str = channel_id
21
+ self.entity_id: str = channel_id.rsplit(".", 1)[-1]
22
+
23
+ self._destination_short: DestinationShort = self.channel_id.split(".")[0]
24
+ self._tz: DestinationTimezones = (
25
+ DestinationTimezones.WALT_DISNEY_WORLD
26
+ if self._destination_short == DestinationShort.WALT_DISNEY_WORLD
27
+ else DestinationTimezones.DISNEYLAND_RESORT
28
+ )
29
+ self._refresh_interval: timedelta = timedelta(minutes=30)
30
+ self._cb_data: typing.Optional[dict] = None
31
+ self._cb_data_pull_time: datetime = datetime.now(tz=self._tz.value)
32
+ if not lazy_load:
33
+ self.refresh()
34
+
35
+ def refresh(self) -> None:
36
+ """Pulls initial data if none exists or if it is older than the refresh interval"""
37
+ if self._cb_data is None or datetime.now(tz=self._tz.value) - self.last_update > self._refresh_interval:
38
+ self._cb_data = self.get_channel_data(self.channel_id)
39
+ self._cb_data_pull_time = datetime.now(tz=self._tz.value)
40
+
41
+ @property
42
+ def last_update(self) -> datetime:
43
+ """
44
+ The last time the data was updated.
45
+
46
+ Returns:
47
+ (datetime): The last time the entity's data was updated, or None if no such data exists.
48
+ """
49
+ if self._cb_data is None:
50
+ self._cb_data = self.get_channel_data(self.channel_id)
51
+
52
+ try:
53
+ dt = isoparse(self._cb_data["lastUpdate"])
54
+ dt = dt.replace(tzinfo=self._tz.value)
55
+ return dt
56
+ except (KeyError, TypeError, ValueError):
57
+ logger.debug("No last updated found for %s", self.channel_id)
58
+ return self._cb_data_pull_time
59
+
9
60
 
10
61
  class CouchbaseChannel(CouchbaseMixin):
11
62
  def __init__(self, channel_id: typing.Union[WDWCouchbaseChannels, DLRCouchbaseChannels], lazy_load: bool = True):
@@ -1,21 +1,17 @@
1
1
  import logging
2
2
  import typing
3
- from datetime import datetime, timedelta
4
- from enum import Enum
5
3
  from zoneinfo import ZoneInfo
6
4
 
7
5
  from dateutil.parser import isoparse
8
6
 
9
- from mousetools.channels import CouchbaseChannel
10
- from mousetools.channels.enums import CouchbaseChannels, DestinationTimezones
7
+ from mousetools.channels import CouchbaseChannel, CouchbaseChildChannel
8
+ from mousetools.channels.enums import CouchbaseChannels
11
9
  from mousetools.decorators import disney_property
12
- from mousetools.enums import DestinationShort
13
- from mousetools.mixins.couchbase import CouchbaseMixin
14
10
 
15
11
  logger = logging.getLogger(__name__)
16
12
 
17
13
 
18
- class CalendarChildChannel(CouchbaseMixin):
14
+ class CalendarChildChannel(CouchbaseChildChannel):
19
15
  def __init__(
20
16
  self,
21
17
  channel_id: str,
@@ -26,34 +22,11 @@ class CalendarChildChannel(CouchbaseMixin):
26
22
  channel_id (str): Calendar Channel ID
27
23
  lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
28
24
  """
29
- if isinstance(channel_id, Enum):
30
- channel_id = channel_id.value
31
-
32
- self.channel_id: str = channel_id
33
- self.entity_id: str = channel_id.rsplit(".", 1)[-1]
34
-
35
- self._destination_short: DestinationShort = self.channel_id.split(".")[0]
36
- self._tz: DestinationTimezones = (
37
- DestinationTimezones.WALT_DISNEY_WORLD
38
- if self._destination_short == DestinationShort.WALT_DISNEY_WORLD
39
- else DestinationTimezones.DISNEYLAND_RESORT
40
- )
41
- self._refresh_interval: timedelta = timedelta(minutes=30)
42
-
43
- self._cb_data: typing.Optional[dict] = None
44
- self._cb_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz.value)
45
- if not lazy_load:
46
- self.refresh()
25
+ super().__init__(channel_id, lazy_load=lazy_load)
47
26
 
48
27
  def __repr__(self):
49
28
  return f"{self.__class__.__name__}(channel_id='{self.channel_id}')"
50
29
 
51
- def refresh(self) -> None:
52
- """Pulls initial data if none exists or if it is older than the refresh interval"""
53
- if self._cb_data is None or datetime.now(tz=self._tz.value) - self.last_update > self._refresh_interval:
54
- self._cb_data = self.get_channel_data(self.channel_id)
55
- self._cb_data_pull_time = datetime.now(tz=self._tz.value)
56
-
57
30
  @disney_property()
58
31
  def calendar_id(self) -> typing.Optional[str]:
59
32
  """The ID of the calendar
@@ -63,24 +36,6 @@ class CalendarChildChannel(CouchbaseMixin):
63
36
  """
64
37
  return self._cb_data["id"]
65
38
 
66
- @property
67
- def last_update(self) -> typing.Optional[datetime]:
68
- """The last time the data was updated.
69
-
70
- Returns:
71
- (typing.Optional[datetime]): The last time the entity's data was updated, or None if no such data exists.
72
- """
73
- if self._cb_data is None:
74
- self._cb_data = self.get_channel_data(self.channel_id)
75
-
76
- try:
77
- dt = isoparse(self._cb_data["lastUpdate"])
78
- dt = dt.replace(tzinfo=self._tz.value)
79
- return dt
80
- except (KeyError, ValueError, TypeError):
81
- logger.debug("No last updated found for %s", self.channel_id)
82
- return self._cb_data_pull_time
83
-
84
39
  @disney_property()
85
40
  def all_closed(self) -> typing.Optional[list[dict]]:
86
41
  """The list of closed facilities.
@@ -0,0 +1,122 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from dateutil.parser import isoparse
5
+
6
+ from mousetools.channels import CouchbaseChannel, CouchbaseChildChannel
7
+ from mousetools.channels.enums import CouchbaseChannels
8
+ from mousetools.decorators import disney_property
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CharactersChildChannel(CouchbaseChildChannel):
14
+ def __init__(
15
+ self,
16
+ channel_id: str,
17
+ lazy_load: bool = True,
18
+ ):
19
+ """
20
+ Args:
21
+ channel_id (str): Forecasted Wait Times Channel ID
22
+ lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
23
+ """
24
+ super().__init__(channel_id, lazy_load=lazy_load)
25
+
26
+ def __repr__(self):
27
+ return f"{self.__class__.__name__}(channel_id='{self.channel_id}')"
28
+
29
+ @disney_property()
30
+ def name(self) -> Optional[str]:
31
+ """The name of the entity.
32
+
33
+ Returns:
34
+ (Optional[str]): The name of the entity, or None if it was not found.
35
+ """
36
+ return self._cb_data["name"]
37
+
38
+ @disney_property()
39
+ def character_id(self) -> Optional[str]:
40
+ """The character ID of the entity. Strips the region from the channel ID.
41
+
42
+ Returns:
43
+ (Optional[str]): The character ID of the entity, or None if it was not found.
44
+ """
45
+ return self._cb_data["characterId"]
46
+
47
+ @disney_property()
48
+ def description(self) -> Optional[str]:
49
+ """The description of the entity.
50
+
51
+ Returns:
52
+ (Optional[str]): The description of the entity, or None if it was not found.
53
+ """
54
+ return self._cb_data["description"]
55
+
56
+ @disney_property()
57
+ def thumbnail_url(self) -> Optional[str]:
58
+ """The thumbnail URL of the entity.
59
+
60
+ Returns:
61
+ (Optional[str]): The thumbnail URL of the entity, or None if it was not found.
62
+ """
63
+ return self._cb_data["thumbnailUrl"]
64
+
65
+ @disney_property()
66
+ def banner_url(self) -> Optional[str]:
67
+ """The banner URL of the entity.
68
+
69
+ Returns:
70
+ (Optional[str]): The banner URL of the entity, or None if it was not found.
71
+ """
72
+ return self._cb_data["bannerUrl"]
73
+
74
+ def get_appearances(self) -> list[dict]:
75
+ """The current list of appearances of the character is scheduled for.
76
+
77
+ Returns:
78
+ (list[dict]): The list of appearances of the entity.
79
+ """
80
+ self.refresh()
81
+ try:
82
+ if "appearances" not in self._cb_data:
83
+ return []
84
+
85
+ _appearances = []
86
+ for i in self._cb_data["appearances"]:
87
+ tmp = {}
88
+ tmp["start_datetime"] = isoparse(i["startDateTime"]).astimezone(self._tz.value)
89
+ tmp["end_datetime"] = isoparse(i["endDateTime"]).astimezone(self._tz.value)
90
+ tmp["start_time_str"] = i["startTime"]
91
+ tmp["end_time_str"] = i["endTime"]
92
+ tmp["coordinates"] = {"latitude": i["latitude"], "longitude": i["longitude"]}
93
+ tmp["facility_id"] = i["facilityId"]
94
+ tmp["location_name"] = i["locationName"]
95
+ tmp["location_id"] = i["locationId"]
96
+ tmp["ancestor_land_id"] = i["ancestorLandId"]
97
+ tmp["ancestor_land_name"] = i["ancestorLandName"]
98
+ _appearances.append(tmp)
99
+
100
+ return sorted(_appearances, key=lambda i: i["start_datetime"])
101
+
102
+ except (KeyError, TypeError, ValueError):
103
+ logger.debug("No forecast found for %s", self.channel_id)
104
+ return []
105
+
106
+
107
+ class CharactersChannel(CouchbaseChannel):
108
+ def __init__(self, *args, **kwargs):
109
+ super().__init__(*args, **kwargs)
110
+
111
+ def get_children_channels(self) -> list[CharactersChildChannel]:
112
+ """Gets a list of children channels for the channel.
113
+
114
+ Returns:
115
+ (list[CharactersChildChannel]): A list of CharactersChildChannel
116
+ """
117
+ self.refresh()
118
+ channels = []
119
+ for i in self._cb_data["results"]:
120
+ if CouchbaseChannels.CHARACTERS in i["id"]:
121
+ channels.append(CharactersChildChannel(i["id"]))
122
+ return channels
@@ -8,7 +8,7 @@ class CouchbaseChannels(str, Enum):
8
8
  FACILITIES = "facilities"
9
9
  FACILITY_STATUS = "facilitystatus"
10
10
  CALENDAR = "calendar"
11
- FORECASTED_WAIT_TIME = "forecastedwaittimes"
11
+ FORECASTED_WAIT_TIMES = "forecastedwaittimes"
12
12
  TODAY = "today"
13
13
  CHARACTERS = "characters"
14
14
 
@@ -33,8 +33,8 @@ class WDWCouchbaseChannels(str, Enum):
33
33
  """Walt Disney World Facility Status Channel"""
34
34
  CALENDAR = get_complete_channel_id(DestinationShort.WALT_DISNEY_WORLD, CouchbaseChannels.CALENDAR, "1_0", False)
35
35
  """Walt Disney World Calendar Channel"""
36
- FORECASTED_WAIT_TIME = get_complete_channel_id(
37
- DestinationShort.WALT_DISNEY_WORLD, CouchbaseChannels.FORECASTED_WAIT_TIME, "1_0", True
36
+ FORECASTED_WAIT_TIMES = get_complete_channel_id(
37
+ DestinationShort.WALT_DISNEY_WORLD, CouchbaseChannels.FORECASTED_WAIT_TIMES, "1_0", True
38
38
  )
39
39
  """Walt Disney World Forecasted Wait Time Channel"""
40
40
  TODAY = get_complete_channel_id(DestinationShort.WALT_DISNEY_WORLD, CouchbaseChannels.TODAY, "1_0", False)
@@ -52,8 +52,8 @@ class DLRCouchbaseChannels(str, Enum):
52
52
  """Disneyland Resort Facility Status Channel"""
53
53
  CALENDAR = get_complete_channel_id(DestinationShort.DISNEYLAND_RESORT, CouchbaseChannels.CALENDAR, "1_0", False)
54
54
  """Disneyland Resort Calendar Channel"""
55
- FORECASTED_WAIT_TIME = get_complete_channel_id(
56
- DestinationShort.DISNEYLAND_RESORT, CouchbaseChannels.FORECASTED_WAIT_TIME, "1_0", True
55
+ FORECASTED_WAIT_TIMES = get_complete_channel_id(
56
+ DestinationShort.DISNEYLAND_RESORT, CouchbaseChannels.FORECASTED_WAIT_TIMES, "1_0", True
57
57
  )
58
58
  """Disneyland Resort Forecasted Wait Time Channel"""
59
59
  TODAY = get_complete_channel_id(DestinationShort.DISNEYLAND_RESORT, CouchbaseChannels.TODAY, "1_0", False)
@@ -1,29 +1,28 @@
1
1
  import logging
2
+ import os
2
3
  import typing
3
4
  from datetime import datetime, time, timedelta
4
- from enum import Enum
5
5
  from zoneinfo import ZoneInfo
6
6
 
7
- from dateutil.parser import isoparse
7
+ import httpx
8
8
 
9
9
  from mousetools.api.menu import Menus
10
- from mousetools.channels import CouchbaseChannel
10
+ from mousetools.channels import CouchbaseChannel, CouchbaseChildChannel
11
11
  from mousetools.channels.calendar import CalendarChildChannel
12
12
  from mousetools.channels.enums import (
13
13
  CouchbaseChannels,
14
- DestinationTimezones,
15
14
  DLRCouchbaseChannels,
16
15
  WDWCouchbaseChannels,
17
16
  )
18
17
  from mousetools.channels.facilitystatus import FacilityStatusChildChannel
18
+ from mousetools.channels.forecastedwaittimes import ForecastedWaitTimesChildChannel
19
19
  from mousetools.decorators import disney_property
20
20
  from mousetools.enums import DestinationShort, EntityType
21
- from mousetools.mixins.couchbase import CouchbaseMixin
22
21
 
23
22
  logger = logging.getLogger(__name__)
24
23
 
25
24
 
26
- class FacilityChildChannel(CouchbaseMixin):
25
+ class FacilityChildChannel(CouchbaseChildChannel):
27
26
  """Base class for all Facility Child Channels. Subclasses like Attraction, Restaurant, Entertainment, etc. extend this class.
28
27
 
29
28
  Example:
@@ -38,8 +37,6 @@ class FacilityChildChannel(CouchbaseMixin):
38
37
  {'latitude': 28.4160036778, 'longitude': -81.5811902834}
39
38
  """
40
39
 
41
- _subclasses: typing.ClassVar[dict[str, type["FacilityChildChannel"]]] = {}
42
-
43
40
  def __init__(
44
41
  self,
45
42
  channel_id: str,
@@ -50,27 +47,12 @@ class FacilityChildChannel(CouchbaseMixin):
50
47
  channel_id (str): Facility Channel ID
51
48
  lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
52
49
  """
53
- if isinstance(channel_id, Enum):
54
- channel_id = channel_id.value
55
-
56
- self.channel_id: str = channel_id
57
- self.entity_id: str = channel_id.rsplit(".", 1)[-1]
50
+ super().__init__(channel_id, lazy_load=lazy_load)
58
51
 
59
- self._destination_short: DestinationShort = self.channel_id.split(".")[0]
60
- self._tz: DestinationTimezones = (
61
- DestinationTimezones.WALT_DISNEY_WORLD
62
- if self._destination_short == DestinationShort.WALT_DISNEY_WORLD
63
- else DestinationTimezones.DISNEYLAND_RESORT
64
- )
65
52
  self._refresh_interval: timedelta = timedelta(
66
53
  hours=12
67
54
  ) # properties are rarely updated, no need to spam the API
68
55
 
69
- self._cb_data: typing.Optional[dict] = None
70
- self._cb_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz.value)
71
- if not lazy_load:
72
- self.refresh()
73
-
74
56
  self._today_calendar: typing.Optional[CalendarChildChannel] = None
75
57
 
76
58
  def refresh(self) -> None:
@@ -391,30 +373,6 @@ class FacilityChildChannel(CouchbaseMixin):
391
373
 
392
374
  return FacilityStatusChildChannel(facility_status_channel_id)
393
375
 
394
- @property
395
- def last_update(self) -> typing.Optional[datetime]:
396
- """
397
- The last time the data was updated.
398
-
399
- Returns:
400
- (typing.Optional[datetime]): The last time the entity's data was updated, or None if no such data exists.
401
- """
402
- if self._cb_data is None:
403
- self._cb_data = self.get_channel_data(self.channel_id)
404
-
405
- try:
406
- tz = (
407
- DestinationTimezones.WALT_DISNEY_WORLD
408
- if "wdw" in self.channel_id
409
- else DestinationTimezones.DISNEYLAND_RESORT
410
- )
411
- dt = isoparse(self._cb_data["lastUpdate"])
412
- dt = dt.replace(tzinfo=tz.value)
413
- return dt
414
- except (KeyError, TypeError, ValueError):
415
- logger.debug("No last updated found for %s", self.channel_id)
416
- return self._cb_data_pull_time
417
-
418
376
  @disney_property()
419
377
  def sponsor_name(self) -> typing.Optional[str]:
420
378
  """The sponsor name of the entity.
@@ -434,16 +392,32 @@ class FacilityChildChannel(CouchbaseMixin):
434
392
  """
435
393
  return self._cb_data["mealPeriods"]
436
394
 
437
- @property
438
- def menu(self) -> Menus:
395
+ def get_menu(self) -> Menus:
439
396
  """
440
397
  The menu of the restaurant.
441
398
 
442
399
  Returns:
443
- (typing.Optional[Menus]): The menu of the restaurant.
400
+ (Menus): The menu of the restaurant.
444
401
  """
445
402
  return Menus(self.channel_id)
446
403
 
404
+ def get_forecasted_wait_times_channel(self) -> ForecastedWaitTimesChildChannel:
405
+ """Get the forecasted wait times channel for this entity.
406
+
407
+ Returns:
408
+ (ForecastedWaitTimesChildChannel): The forecasted wait times channel
409
+
410
+ Example:
411
+ ```python
412
+ >>> f = FacilityChildChannel("wdw.facilities.1_0.en_us.attraction.80010177;entityType=Attraction", lazy_load=False)
413
+ >>> fwt = f.get_forecasted_wait_times_channel()
414
+ >>> fwt.get_forecast()
415
+ [{'forecasted_wait_minutes': 85, 'bar_graph_percentage': 70, 'accessibility_label': 'High', 'timestamp': datetime.datetime(2025, 5, 27, 9, 0, tzinfo=zoneinfo.ZoneInfo(key='America/New_York'))}, ...]
416
+ ```
417
+ """
418
+ channel_id = f"{WDWCouchbaseChannels.FORECASTED_WAIT_TIMES.value if self._destination_short == DestinationShort.WALT_DISNEY_WORLD else DLRCouchbaseChannels.FORECASTED_WAIT_TIMES.value}.{self.entity_id}"
419
+ return ForecastedWaitTimesChildChannel(channel_id)
420
+
447
421
  @disney_property()
448
422
  def detail_image_url(self) -> typing.Optional[str]:
449
423
  """The detail image url of the entity.
@@ -503,6 +477,41 @@ class FacilityChildChannel(CouchbaseMixin):
503
477
  closed = self._today_calendar.get_closed(self.entity_id)
504
478
  return bool(refurb or closed)
505
479
 
480
+ def get_current_weather(self, api_key: typing.Optional[str] = None) -> dict:
481
+ """Get the current weather for the facility using the OpenWeatherMap API. https://openweathermap.org
482
+
483
+ Args:
484
+ api_key (typing.Optional[str], optional): The API key to use. Defaults to None. If None, will use the MT_OPEN_WEATHER_API_KEY environment variable.
485
+
486
+ Returns:
487
+ (dict): The current weather for the facility.
488
+ """
489
+
490
+ weather_key = api_key or os.environ.get("MT_OPEN_WEATHER_API_KEY")
491
+
492
+ if weather_key is None:
493
+ logger.warning("No weather API key found")
494
+ return {}
495
+
496
+ coordinates = self.coordinates
497
+ if coordinates is None:
498
+ return {}
499
+
500
+ weather_url = "https://api.openweathermap.org/data/2.5/weather"
501
+ params = {
502
+ "lat": coordinates["latitude"],
503
+ "lon": coordinates["longitude"],
504
+ "appid": weather_key,
505
+ }
506
+ try:
507
+ response = httpx.get(weather_url, params=params)
508
+ response.raise_for_status()
509
+ data = response.json()
510
+ return data
511
+ except httpx.HTTPError as err:
512
+ logger.error("Request failed: %s", err)
513
+ return {}
514
+
506
515
  def __str__(self) -> str:
507
516
  return f"{self.entity_type}: {self.name} ({self.channel_id})"
508
517
 
@@ -514,38 +523,32 @@ class FacilityChildChannel(CouchbaseMixin):
514
523
  return self.channel_id == other.channel_id
515
524
  return False
516
525
 
517
- @classmethod
518
- def get_subclass(cls, type_name: str) -> type["FacilityChildChannel"]:
519
- if type_name not in cls._subclasses:
520
- cls._subclasses[type_name] = type(type_name, (cls,), {"__doc__": f"Class for {type_name} Entities."})
521
- return cls._subclasses[type_name]
522
-
523
-
524
- AttractionFacilityChild = FacilityChildChannel.get_subclass("AttractionFacilityChild")
525
- AudioTourFacilityChild = FacilityChildChannel.get_subclass("AudioTourFacilityChild")
526
- BuildingFacilityChild = FacilityChildChannel.get_subclass("BuildingFacilityChild")
527
- BusStopFacilityChild = FacilityChildChannel.get_subclass("BusStopFacilityChild")
528
- DestinationFacilityChild = FacilityChildChannel.get_subclass("DestinationFacilityChild")
529
- DiningEventFacilityChild = FacilityChildChannel.get_subclass("DiningEventFacilityChild")
530
- DinnerShowFacilityChild = FacilityChildChannel.get_subclass("DinnerShowFacilityChild")
531
- EntertainmentFacilityChild = FacilityChildChannel.get_subclass("EntertainmentFacilityChild")
532
- EntertainmentVenueFacilityChild = FacilityChildChannel.get_subclass("EntertainmentVenueFacilityChild")
533
- WaterParkFacilityChild = FacilityChildChannel.get_subclass("WaterParkFacilityChild")
534
- GuestServiceFacilityChild = FacilityChildChannel.get_subclass("GuestServiceFacilityChild")
535
- LandFacilityChild = FacilityChildChannel.get_subclass("LandFacilityChild")
536
- MerchandiseFacilityChild = FacilityChildChannel.get_subclass("MerchandiseFacilityChild")
537
- PhotopassFacilityChild = FacilityChildChannel.get_subclass("PhotopassFacilityChild")
538
- PointOfInterestFacilityChild = FacilityChildChannel.get_subclass("PointOfInterestFacilityChild")
539
- RecreationFacilityChild = FacilityChildChannel.get_subclass("RecreationFacilityChild")
540
- RecreationActivityFacilityChild = FacilityChildChannel.get_subclass("RecreationActivityFacilityChild")
541
- ResortFacilityChild = FacilityChildChannel.get_subclass("ResortFacilityChild")
542
- ResortAreaFacilityChild = FacilityChildChannel.get_subclass("ResortAreaFacilityChild")
543
- SpaFacilityChild = FacilityChildChannel.get_subclass("SpaFacilityChild")
544
- ThemeParkFacilityChild = FacilityChildChannel.get_subclass("ThemeParkFacilityChild")
545
- TourFacilityChild = FacilityChildChannel.get_subclass("TourFacilityChild")
546
- TransportationFacilityChild = FacilityChildChannel.get_subclass("TransportationFacilityChild")
547
- WaterParkFacilityChild = FacilityChildChannel.get_subclass("WaterParkFacilityChild")
548
- RestaurantFacilityChild = FacilityChildChannel.get_subclass("RestaurantFacilityChild")
526
+
527
+ AttractionFacilityChild = type("AttractionFacilityChild", (FacilityChildChannel,), {})
528
+ AudioTourFacilityChild = type("AudioTourFacilityChild", (FacilityChildChannel,), {})
529
+ BuildingFacilityChild = type("BuildingFacilityChild", (FacilityChildChannel,), {})
530
+ BusStopFacilityChild = type("BusStopFacilityChild", (FacilityChildChannel,), {})
531
+ DestinationFacilityChild = type("DestinationFacilityChild", (FacilityChildChannel,), {})
532
+ DiningEventFacilityChild = type("DiningEventFacilityChild", (FacilityChildChannel,), {})
533
+ DinnerShowFacilityChild = type("DinnerShowFacilityChild", (FacilityChildChannel,), {})
534
+ EntertainmentFacilityChild = type("EntertainmentFacilityChild", (FacilityChildChannel,), {})
535
+ EntertainmentVenueFacilityChild = type("EntertainmentVenueFacilityChild", (FacilityChildChannel,), {})
536
+ WaterParkFacilityChild = type("WaterParkFacilityChild", (FacilityChildChannel,), {})
537
+ GuestServiceFacilityChild = type("GuestServiceFacilityChild", (FacilityChildChannel,), {})
538
+ LandFacilityChild = type("LandFacilityChild", (FacilityChildChannel,), {})
539
+ MerchandiseFacilityChild = type("MerchandiseFacilityChild", (FacilityChildChannel,), {})
540
+ PhotopassFacilityChild = type("PhotopassFacilityChild", (FacilityChildChannel,), {})
541
+ PointOfInterestFacilityChild = type("PointOfInterestFacilityChild", (FacilityChildChannel,), {})
542
+ RecreationFacilityChild = type("RecreationFacilityChild", (FacilityChildChannel,), {})
543
+ RecreationActivityFacilityChild = type("RecreationActivityFacilityChild", (FacilityChildChannel,), {})
544
+ ResortFacilityChild = type("ResortFacilityChild", (FacilityChildChannel,), {})
545
+ ResortAreaFacilityChild = type("ResortAreaFacilityChild", (FacilityChildChannel,), {})
546
+ SpaFacilityChild = type("SpaFacilityChild", (FacilityChildChannel,), {})
547
+ ThemeParkFacilityChild = type("ThemeParkFacilityChild", (FacilityChildChannel,), {})
548
+ TourFacilityChild = type("TourFacilityChild", (FacilityChildChannel,), {})
549
+ TransportationFacilityChild = type("TransportationFacilityChild", (FacilityChildChannel,), {})
550
+ WaterParkFacilityChild = type("WaterParkFacilityChild", (FacilityChildChannel,), {})
551
+ RestaurantFacilityChild = type("RestaurantFacilityChild", (FacilityChildChannel,), {})
549
552
 
550
553
 
551
554
  class FacilityChannel(CouchbaseChannel):
@@ -1,47 +1,25 @@
1
1
  import logging
2
2
  import typing
3
- from datetime import datetime, timedelta
3
+ from datetime import datetime
4
4
 
5
5
  from dateutil.parser import isoparse
6
6
 
7
- from mousetools.channels import CouchbaseChannel
8
- from mousetools.channels.enums import CouchbaseChannels, DestinationTimezones
7
+ from mousetools.channels import CouchbaseChannel, CouchbaseChildChannel
8
+ from mousetools.channels.enums import CouchbaseChannels
9
9
  from mousetools.decorators import disney_property
10
- from mousetools.enums import DestinationShort, EntityType
11
- from mousetools.mixins.couchbase import CouchbaseMixin
10
+ from mousetools.enums import EntityType
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
14
 
16
- class FacilityStatusChildChannel(CouchbaseMixin):
17
- _subclasses: typing.ClassVar[dict[str, type["FacilityStatusChildChannel"]]] = {}
18
-
15
+ class FacilityStatusChildChannel(CouchbaseChildChannel):
19
16
  def __init__(self, channel_id: str, lazy_load: bool = True):
20
17
  """
21
18
  Args:
22
19
  channel_id (typing.Union[WDWCouchbaseChannels, DLRCouchbaseChannels]): Channel ID from the enum
23
20
  lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
24
21
  """
25
- self.channel_id = channel_id
26
-
27
- self._destination_short: DestinationShort = channel_id.split(".")[0]
28
- self._tz: DestinationTimezones = (
29
- DestinationTimezones.WALT_DISNEY_WORLD
30
- if self._destination_short == DestinationShort.WALT_DISNEY_WORLD
31
- else DestinationTimezones.DISNEYLAND_RESORT
32
- )
33
- self._refresh_interval: timedelta = timedelta(minutes=10)
34
- self._cb_data: typing.Optional[dict] = None
35
- self._cb_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz.value)
36
-
37
- if not lazy_load:
38
- self.refresh()
39
-
40
- def refresh(self) -> None:
41
- """Pulls initial data if none exists or if it is older than the refresh interval"""
42
- if self._cb_data is None or datetime.now(tz=self._tz.value) - self.last_update > self._refresh_interval:
43
- self._cb_data = self.get_channel_data(self.channel_id)
44
- self._cb_data_pull_time = datetime.now(tz=self._tz.value)
22
+ super().__init__(channel_id, lazy_load=lazy_load)
45
23
 
46
24
  def get_status(self) -> typing.Optional[str]:
47
25
  """Get the operating status of the facility
@@ -71,24 +49,6 @@ class FacilityStatusChildChannel(CouchbaseMixin):
71
49
  logger.debug("No wait time found for %s", self.channel_id)
72
50
  return None
73
51
 
74
- @property
75
- def last_update(self) -> typing.Optional[datetime]:
76
- """The last time the data was updated.
77
-
78
- Returns:
79
- (typing.Optional[datetime]): The last time the entity's data was updated, or None if no such data exists.
80
- """
81
- if self._cb_data is None:
82
- self._cb_data = self.get_channel_data(self.channel_id)
83
-
84
- try:
85
- dt = isoparse(self._cb_data["lastUpdate"])
86
- dt = dt.replace(tzinfo=self._tz.value)
87
- return dt
88
- except (KeyError, TypeError, ValueError):
89
- logger.debug("No last updated found for %s", self.channel_id)
90
- return self._cb_data_pull_time
91
-
92
52
  @disney_property()
93
53
  def fast_pass_available(self) -> typing.Optional[bool]:
94
54
  return self._cb_data["fastPassAvailable"]
@@ -132,20 +92,14 @@ class FacilityStatusChildChannel(CouchbaseMixin):
132
92
  return self.channel_id == other.channel_id
133
93
  return False
134
94
 
135
- @classmethod
136
- def get_subclass(cls, type_name: str) -> type["FacilityStatusChildChannel"]:
137
- if type_name not in cls._subclasses:
138
- cls._subclasses[type_name] = type(type_name, (cls,), {"__doc__": f"Class for {type_name} Entities."})
139
- return cls._subclasses[type_name]
140
95
 
141
-
142
- AttractionFacilityStatusChild = FacilityStatusChildChannel.get_subclass("AttractionFacilityStatusChild")
143
- EntertainmentFacilityStatusChild = FacilityStatusChildChannel.get_subclass("EntertainmentFacilityStatusChild")
144
- EntertainmentVenueFacilityStatusChild = FacilityStatusChildChannel.get_subclass("EntertainmentVenueFacilityStatusChild")
145
- LandFacilityStatusChild = FacilityStatusChildChannel.get_subclass("LandFacilityStatusChild")
146
- RestaurantFacilityStatusChild = FacilityStatusChildChannel.get_subclass("RestaurantFacilityStatusChild")
147
- ThemeParkFacilityStatusChild = FacilityStatusChildChannel.get_subclass("ThemeParkFacilityStatusChild")
148
- WaterParkFacilityStatusChild = FacilityStatusChildChannel.get_subclass("WaterParkFacilityStatusChild")
96
+ AttractionFacilityStatusChild = type("AttractionFacilityStatusChild", (FacilityStatusChildChannel,), {})
97
+ EntertainmentFacilityStatusChild = type("EntertainmentFacilityStatusChild", (FacilityStatusChildChannel,), {})
98
+ EntertainmentVenueFacilityStatusChild = type("EntertainmentVenueFacilityStatusChild", (FacilityStatusChildChannel,), {})
99
+ LandFacilityStatusChild = type("LandFacilityStatusChild", (FacilityStatusChildChannel,), {})
100
+ RestaurantFacilityStatusChild = type("RestaurantFacilityStatusChild", (FacilityStatusChildChannel,), {})
101
+ ThemeParkFacilityStatusChild = type("ThemeParkFacilityStatusChild", (FacilityStatusChildChannel,), {})
102
+ WaterParkFacilityStatusChild = type("WaterParkFacilityStatusChild", (FacilityStatusChildChannel,), {})
149
103
 
150
104
 
151
105
  class FacilityStatusChannel(CouchbaseChannel):
@@ -165,6 +119,25 @@ class FacilityStatusChannel(CouchbaseChannel):
165
119
  channels.append(FacilityStatusChildChannel(i["id"]))
166
120
  return channels
167
121
 
122
+ def get_all_statuses(self) -> dict[str, dict[str, typing.Any]]:
123
+ """Get all statuses for the channel
124
+
125
+ Returns:
126
+ (dict[str, dict[str, typing.Any]]): A dictionary of all statuses for the channel
127
+ """
128
+ logger.info("Getting all children statuses for %s", self.channel_id)
129
+ children = self.get_children_channels()
130
+ logger.debug("Got %d children for %s", len(children), self.channel_id)
131
+
132
+ statuses = {}
133
+ for child in children:
134
+ tmp = {}
135
+ tmp["status"] = child.get_status()
136
+ tmp["wait_time"] = child.get_wait_time()
137
+ statuses[child.entity_id] = tmp
138
+
139
+ return statuses
140
+
168
141
  def get_children_attractions(self) -> list[AttractionFacilityStatusChild]: # type: ignore
169
142
  """Get AttractionFacilityStatusChild Children of a Facility
170
143
 
@@ -0,0 +1,71 @@
1
+ import logging
2
+
3
+ from dateutil.parser import isoparse
4
+
5
+ from mousetools.channels import CouchbaseChannel, CouchbaseChildChannel
6
+ from mousetools.channels.enums import CouchbaseChannels
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ForecastedWaitTimesChildChannel(CouchbaseChildChannel):
12
+ def __init__(
13
+ self,
14
+ channel_id: str,
15
+ lazy_load: bool = True,
16
+ ):
17
+ """
18
+ Args:
19
+ channel_id (str): Forecasted Wait Times Channel ID
20
+ lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
21
+ """
22
+ super().__init__(channel_id, lazy_load=lazy_load)
23
+
24
+ def __repr__(self):
25
+ return f"{self.__class__.__name__}(channel_id='{self.channel_id}')"
26
+
27
+ def get_forecast(self) -> list[dict]:
28
+ """Returns a list of forecasted wait times sorted by timestamp
29
+
30
+ Returns:
31
+ (list[dict]): A list of dictionaries. Each dictionary contains forecasted wait minutes, bar graph percentage, accessibility label, and timestamp
32
+ """
33
+ self.refresh()
34
+ try:
35
+ if "forecasts" not in self._cb_data:
36
+ return []
37
+
38
+ forecasts = []
39
+ for forecast in self._cb_data["forecasts"]:
40
+ tmp = {}
41
+ dt = isoparse(forecast["timestamp"])
42
+ dt = dt.astimezone(self._tz.value)
43
+ tmp["forecasted_wait_minutes"] = forecast["forecastedWaitMinutes"]
44
+ tmp["bar_graph_percentage"] = forecast["percentage"]
45
+ tmp["accessibility_label"] = forecast["accessibility12h"].split(" ", 1)[-1]
46
+ tmp["timestamp"] = dt
47
+ forecasts.append(tmp)
48
+
49
+ return sorted(forecasts, key=lambda i: i["timestamp"])
50
+
51
+ except (KeyError, TypeError, ValueError):
52
+ logger.debug("No forecast found for %s", self.channel_id)
53
+ return []
54
+
55
+
56
+ class ForecastedWaitTimesChannel(CouchbaseChannel):
57
+ def __init__(self, *args, **kwargs):
58
+ super().__init__(*args, **kwargs)
59
+
60
+ def get_children_channels(self) -> list[ForecastedWaitTimesChildChannel]:
61
+ """Gets a list of children channels for the channel.
62
+
63
+ Returns:
64
+ (list[ForecastedWaitTimesChildChannel]): A list of ForecastedWaitTimesChildChannel
65
+ """
66
+ self.refresh()
67
+ channels = []
68
+ for i in self._cb_data["results"]:
69
+ if CouchbaseChannels.FORECASTED_WAIT_TIMES in i["id"]:
70
+ channels.append(ForecastedWaitTimesChildChannel(i["id"]))
71
+ return channels
@@ -7,9 +7,10 @@ logger = logging.getLogger(__name__)
7
7
  T = TypeVar("T")
8
8
 
9
9
 
10
- def disney_property(refresh: bool = True, default_value: Optional[T] = None) -> Callable:
10
+ def disney_property(
11
+ refresh: bool = True, default_value: Optional[T] = None
12
+ ) -> Callable[[Callable[[Any], Optional[T]]], property]:
11
13
  def decorator(func: Callable) -> property:
12
- @property
13
14
  @functools.wraps(func)
14
15
  def wrapper(self) -> Optional[T]:
15
16
  if refresh:
@@ -19,7 +20,9 @@ def disney_property(refresh: bool = True, default_value: Optional[T] = None) ->
19
20
  except (KeyError, TypeError, ValueError):
20
21
  return default_value
21
22
 
22
- return wrapper
23
+ wrapper.__annotations__ = func.__annotations__
24
+ wrapper.__doc__ = func.__doc__
25
+ return property(wrapper)
23
26
 
24
27
  return decorator
25
28
 
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
13
  class CouchbaseMixin:
14
- _couchbase_url = "https://" + auth_obj._environments["syncGatewayUrl"].split("//")[1]
14
+ _couchbase_url = "https://" + auth_obj._environments["syncGatewayUrl"].split("//")[1].rstrip("/")
15
15
  _headers: dict = auth_obj.get_couchbase_headers()
16
16
 
17
17
  def get_channel_data(self, channel_name: str) -> typing.Union[dict, None]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mousetools
3
- Version: 4.1.1
3
+ Version: 4.3.0
4
4
  Summary: An unofficial Python wrapper for Disney's API
5
5
  Author: Scott Caratozzolo
6
6
  License-Expression: MIT
@@ -16,9 +16,11 @@ mousetools/api/search.py
16
16
  mousetools/channels/__init__.py
17
17
  mousetools/channels/enums.py
18
18
  mousetools/channels/calendar/__init__.py
19
+ mousetools/channels/characters/__init__.py
19
20
  mousetools/channels/facilities/__init__.py
20
21
  mousetools/channels/facilities/enums.py
21
22
  mousetools/channels/facilitystatus/__init__.py
23
+ mousetools/channels/forecastedwaittimes/__init__.py
22
24
  mousetools/mixins/__init__.py
23
25
  mousetools/mixins/couchbase.py
24
26
  mousetools/mixins/disney.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mousetools"
3
- version = "4.1.1"
3
+ version = "4.3.0"
4
4
  authors = [{name = "Scott Caratozzolo"}]
5
5
  description = "An unofficial Python wrapper for Disney's API"
6
6
  readme = "README.md"
@@ -42,6 +42,7 @@ dev-dependencies = [
42
42
  "go-task-bin>=3.43.3",
43
43
  "icecream>=2.1.4",
44
44
  "pytest-mock>=3.14.0",
45
+ "pytest-httpx>=0.35.0",
45
46
  ]
46
47
 
47
48
  [build-system]
File without changes
File without changes
File without changes