MouseTools 4.1.1__tar.gz → 4.2.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.2.0}/PKG-INFO +1 -1
  2. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/api/menu.py +3 -3
  3. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/calendar/__init__.py +3 -3
  4. mousetools-4.2.0/mousetools/channels/characters/__init__.py +167 -0
  5. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/enums.py +5 -5
  6. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/facilities/__init__.py +86 -40
  7. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/facilitystatus/__init__.py +10 -18
  8. mousetools-4.2.0/mousetools/channels/forecastedwaittimes/__init__.py +117 -0
  9. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/decorators.py +6 -3
  10. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/PKG-INFO +1 -1
  11. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/SOURCES.txt +2 -0
  12. {mousetools-4.1.1 → mousetools-4.2.0}/pyproject.toml +2 -1
  13. {mousetools-4.1.1 → mousetools-4.2.0}/LICENSE +0 -0
  14. {mousetools-4.1.1 → mousetools-4.2.0}/README.md +0 -0
  15. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/__init__.py +0 -0
  16. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/api/__init__.py +0 -0
  17. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/api/search.py +0 -0
  18. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/auth.py +0 -0
  19. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/__init__.py +0 -0
  20. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/facilities/enums.py +0 -0
  21. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/enums.py +0 -0
  22. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/mixins/__init__.py +0 -0
  23. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/mixins/couchbase.py +0 -0
  24. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/mixins/disney.py +0 -0
  25. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/resources/environments.bin +0 -0
  26. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/dependency_links.txt +0 -0
  27. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/requires.txt +0 -0
  28. {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/top_level.txt +0 -0
  29. {mousetools-4.1.1 → mousetools-4.2.0}/setup.cfg +0 -0
  30. {mousetools-4.1.1 → mousetools-4.2.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.2.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)
@@ -41,7 +41,7 @@ class CalendarChildChannel(CouchbaseMixin):
41
41
  self._refresh_interval: timedelta = timedelta(minutes=30)
42
42
 
43
43
  self._cb_data: typing.Optional[dict] = None
44
- self._cb_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz.value)
44
+ self._cb_data_pull_time: datetime = datetime.now(tz=self._tz.value)
45
45
  if not lazy_load:
46
46
  self.refresh()
47
47
 
@@ -64,11 +64,11 @@ class CalendarChildChannel(CouchbaseMixin):
64
64
  return self._cb_data["id"]
65
65
 
66
66
  @property
67
- def last_update(self) -> typing.Optional[datetime]:
67
+ def last_update(self) -> datetime:
68
68
  """The last time the data was updated.
69
69
 
70
70
  Returns:
71
- (typing.Optional[datetime]): The last time the entity's data was updated, or None if no such data exists.
71
+ (datetime): The last time the entity's data was updated, or None if no such data exists.
72
72
  """
73
73
  if self._cb_data is None:
74
74
  self._cb_data = self.get_channel_data(self.channel_id)
@@ -0,0 +1,167 @@
1
+ import logging
2
+ from datetime import datetime, timedelta
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+ from dateutil.parser import isoparse
7
+
8
+ from mousetools.channels import CouchbaseChannel
9
+ from mousetools.channels.enums import CouchbaseChannels, DestinationTimezones
10
+ from mousetools.decorators import disney_property
11
+ from mousetools.enums import DestinationShort
12
+ from mousetools.mixins.couchbase import CouchbaseMixin
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CharactersChildChannel(CouchbaseMixin):
18
+ def __init__(
19
+ self,
20
+ channel_id: str,
21
+ lazy_load: bool = True,
22
+ ):
23
+ """
24
+ Args:
25
+ channel_id (str): Forecasted Wait Times Channel ID
26
+ lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
27
+ """
28
+ if isinstance(channel_id, Enum):
29
+ channel_id = channel_id.value
30
+
31
+ self.channel_id: str = channel_id
32
+ self.entity_id: str = channel_id.rsplit(".", 1)[-1]
33
+
34
+ self._destination_short: DestinationShort = self.channel_id.split(".")[0]
35
+ self._tz: DestinationTimezones = (
36
+ DestinationTimezones.WALT_DISNEY_WORLD
37
+ if self._destination_short == DestinationShort.WALT_DISNEY_WORLD
38
+ else DestinationTimezones.DISNEYLAND_RESORT
39
+ )
40
+ self._refresh_interval: timedelta = timedelta(minutes=30)
41
+
42
+ self._cb_data: Optional[dict] = None
43
+ self._cb_data_pull_time: Optional[datetime] = datetime.now(tz=self._tz.value)
44
+ if not lazy_load:
45
+ self.refresh()
46
+
47
+ def __repr__(self):
48
+ return f"{self.__class__.__name__}(channel_id='{self.channel_id}')"
49
+
50
+ def refresh(self) -> None:
51
+ """Pulls initial data if none exists or if it is older than the refresh interval"""
52
+ if self._cb_data is None or datetime.now(tz=self._tz.value) - self.last_update > self._refresh_interval:
53
+ self._cb_data = self.get_channel_data(self.channel_id)
54
+ self._cb_data_pull_time = datetime.now(tz=self._tz.value)
55
+
56
+ @property
57
+ def last_update(self) -> datetime:
58
+ """The last time the data was updated.
59
+
60
+ Returns:
61
+ (datetime): The last time the entity's data was updated, or None if no such data exists.
62
+ """
63
+ if self._cb_data is None:
64
+ self._cb_data = self.get_channel_data(self.channel_id)
65
+
66
+ try:
67
+ dt = isoparse(self._cb_data["lastUpdate"])
68
+ dt = dt.replace(tzinfo=self._tz.value)
69
+ return dt
70
+ except (KeyError, TypeError, ValueError):
71
+ logger.debug("No last updated found for %s", self.channel_id)
72
+ return self._cb_data_pull_time
73
+
74
+ @disney_property()
75
+ def name(self) -> Optional[str]:
76
+ """The name of the entity.
77
+
78
+ Returns:
79
+ (Optional[str]): The name of the entity, or None if it was not found.
80
+ """
81
+ return self._cb_data["name"]
82
+
83
+ @disney_property()
84
+ def character_id(self) -> Optional[str]:
85
+ """The character ID of the entity. Strips the region from the channel ID.
86
+
87
+ Returns:
88
+ (Optional[str]): The character ID of the entity, or None if it was not found.
89
+ """
90
+ return self._cb_data["characterId"]
91
+
92
+ @disney_property()
93
+ def description(self) -> Optional[str]:
94
+ """The description of the entity.
95
+
96
+ Returns:
97
+ (Optional[str]): The description of the entity, or None if it was not found.
98
+ """
99
+ return self._cb_data["description"]
100
+
101
+ @disney_property()
102
+ def thumbnail_url(self) -> Optional[str]:
103
+ """The thumbnail URL of the entity.
104
+
105
+ Returns:
106
+ (Optional[str]): The thumbnail URL of the entity, or None if it was not found.
107
+ """
108
+ return self._cb_data["thumbnailUrl"]
109
+
110
+ @disney_property()
111
+ def banner_url(self) -> Optional[str]:
112
+ """The banner URL of the entity.
113
+
114
+ Returns:
115
+ (Optional[str]): The banner URL of the entity, or None if it was not found.
116
+ """
117
+ return self._cb_data["bannerUrl"]
118
+
119
+ def get_appearances(self) -> list[dict]:
120
+ """The current list of appearances of the character is scheduled for.
121
+
122
+ Returns:
123
+ (list[dict]): The list of appearances of the entity.
124
+ """
125
+ self.refresh()
126
+ try:
127
+ if "appearances" not in self._cb_data:
128
+ return []
129
+
130
+ _appearances = []
131
+ for i in self._cb_data["appearances"]:
132
+ tmp = {}
133
+ tmp["start_datetime"] = isoparse(i["startDateTime"]).astimezone(self._tz.value)
134
+ tmp["end_datetime"] = isoparse(i["endDateTime"]).astimezone(self._tz.value)
135
+ tmp["start_time_str"] = i["startTime"]
136
+ tmp["end_time_str"] = i["endTime"]
137
+ tmp["coordinates"] = {"latitude": i["latitude"], "longitude": i["longitude"]}
138
+ tmp["facility_id"] = i["facilityId"]
139
+ tmp["location_name"] = i["locationName"]
140
+ tmp["location_id"] = i["locationId"]
141
+ tmp["ancestor_land_id"] = i["ancestorLandId"]
142
+ tmp["ancestor_land_name"] = i["ancestorLandName"]
143
+ _appearances.append(tmp)
144
+
145
+ return sorted(_appearances, key=lambda i: i["start_datetime"])
146
+
147
+ except (KeyError, TypeError, ValueError):
148
+ logger.debug("No forecast found for %s", self.channel_id)
149
+ return []
150
+
151
+
152
+ class CharactersChannel(CouchbaseChannel):
153
+ def __init__(self, *args, **kwargs):
154
+ super().__init__(*args, **kwargs)
155
+
156
+ def get_children_channels(self) -> list[CharactersChildChannel]:
157
+ """Gets a list of children channels for the channel.
158
+
159
+ Returns:
160
+ (list[CharactersChildChannel]): A list of CharactersChildChannel
161
+ """
162
+ self.refresh()
163
+ channels = []
164
+ for i in self._cb_data["results"]:
165
+ if CouchbaseChannels.CHARACTERS in i["id"]:
166
+ channels.append(CharactersChildChannel(i["id"]))
167
+ 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,9 +1,11 @@
1
1
  import logging
2
+ import os
2
3
  import typing
3
4
  from datetime import datetime, time, timedelta
4
5
  from enum import Enum
5
6
  from zoneinfo import ZoneInfo
6
7
 
8
+ import httpx
7
9
  from dateutil.parser import isoparse
8
10
 
9
11
  from mousetools.api.menu import Menus
@@ -16,6 +18,7 @@ from mousetools.channels.enums import (
16
18
  WDWCouchbaseChannels,
17
19
  )
18
20
  from mousetools.channels.facilitystatus import FacilityStatusChildChannel
21
+ from mousetools.channels.forecastedwaittimes import ForecastedWaitTimesChildChannel
19
22
  from mousetools.decorators import disney_property
20
23
  from mousetools.enums import DestinationShort, EntityType
21
24
  from mousetools.mixins.couchbase import CouchbaseMixin
@@ -38,8 +41,6 @@ class FacilityChildChannel(CouchbaseMixin):
38
41
  {'latitude': 28.4160036778, 'longitude': -81.5811902834}
39
42
  """
40
43
 
41
- _subclasses: typing.ClassVar[dict[str, type["FacilityChildChannel"]]] = {}
42
-
43
44
  def __init__(
44
45
  self,
45
46
  channel_id: str,
@@ -67,7 +68,7 @@ class FacilityChildChannel(CouchbaseMixin):
67
68
  ) # properties are rarely updated, no need to spam the API
68
69
 
69
70
  self._cb_data: typing.Optional[dict] = None
70
- self._cb_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz.value)
71
+ self._cb_data_pull_time: datetime = datetime.now(tz=self._tz.value)
71
72
  if not lazy_load:
72
73
  self.refresh()
73
74
 
@@ -392,12 +393,12 @@ class FacilityChildChannel(CouchbaseMixin):
392
393
  return FacilityStatusChildChannel(facility_status_channel_id)
393
394
 
394
395
  @property
395
- def last_update(self) -> typing.Optional[datetime]:
396
+ def last_update(self) -> datetime:
396
397
  """
397
398
  The last time the data was updated.
398
399
 
399
400
  Returns:
400
- (typing.Optional[datetime]): The last time the entity's data was updated, or None if no such data exists.
401
+ (datetime): The last time the entity's data was updated, or None if no such data exists.
401
402
  """
402
403
  if self._cb_data is None:
403
404
  self._cb_data = self.get_channel_data(self.channel_id)
@@ -434,16 +435,32 @@ class FacilityChildChannel(CouchbaseMixin):
434
435
  """
435
436
  return self._cb_data["mealPeriods"]
436
437
 
437
- @property
438
- def menu(self) -> Menus:
438
+ def get_menu(self) -> Menus:
439
439
  """
440
440
  The menu of the restaurant.
441
441
 
442
442
  Returns:
443
- (typing.Optional[Menus]): The menu of the restaurant.
443
+ (Menus): The menu of the restaurant.
444
444
  """
445
445
  return Menus(self.channel_id)
446
446
 
447
+ def get_forecasted_wait_times_channel(self) -> ForecastedWaitTimesChildChannel:
448
+ """Get the forecasted wait times channel for this entity.
449
+
450
+ Returns:
451
+ (ForecastedWaitTimesChildChannel): The forecasted wait times channel
452
+
453
+ Example:
454
+ ```python
455
+ >>> f = FacilityChildChannel("wdw.facilities.1_0.en_us.attraction.80010177;entityType=Attraction", lazy_load=False)
456
+ >>> fwt = f.get_forecasted_wait_times_channel()
457
+ >>> fwt.get_forecast()
458
+ [{'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'))}, ...]
459
+ ```
460
+ """
461
+ 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}"
462
+ return ForecastedWaitTimesChildChannel(channel_id)
463
+
447
464
  @disney_property()
448
465
  def detail_image_url(self) -> typing.Optional[str]:
449
466
  """The detail image url of the entity.
@@ -503,6 +520,41 @@ class FacilityChildChannel(CouchbaseMixin):
503
520
  closed = self._today_calendar.get_closed(self.entity_id)
504
521
  return bool(refurb or closed)
505
522
 
523
+ def get_current_weather(self, api_key: typing.Optional[str] = None) -> dict:
524
+ """Get the current weather for the facility using the OpenWeatherMap API. https://openweathermap.org
525
+
526
+ Args:
527
+ 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.
528
+
529
+ Returns:
530
+ (dict): The current weather for the facility.
531
+ """
532
+
533
+ weather_key = api_key or os.environ.get("MT_OPEN_WEATHER_API_KEY")
534
+
535
+ if weather_key is None:
536
+ logger.warning("No weather API key found")
537
+ return {}
538
+
539
+ coordinates = self.coordinates
540
+ if coordinates is None:
541
+ return {}
542
+
543
+ weather_url = "https://api.openweathermap.org/data/2.5/weather"
544
+ params = {
545
+ "lat": coordinates["latitude"],
546
+ "lon": coordinates["longitude"],
547
+ "appid": weather_key,
548
+ }
549
+ try:
550
+ response = httpx.get(weather_url, params=params)
551
+ response.raise_for_status()
552
+ data = response.json()
553
+ return data
554
+ except httpx.HTTPError as err:
555
+ logger.error("Request failed: %s", err)
556
+ return {}
557
+
506
558
  def __str__(self) -> str:
507
559
  return f"{self.entity_type}: {self.name} ({self.channel_id})"
508
560
 
@@ -514,38 +566,32 @@ class FacilityChildChannel(CouchbaseMixin):
514
566
  return self.channel_id == other.channel_id
515
567
  return False
516
568
 
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")
569
+
570
+ AttractionFacilityChild = type("AttractionFacilityChild", (FacilityChildChannel,), {})
571
+ AudioTourFacilityChild = type("AudioTourFacilityChild", (FacilityChildChannel,), {})
572
+ BuildingFacilityChild = type("BuildingFacilityChild", (FacilityChildChannel,), {})
573
+ BusStopFacilityChild = type("BusStopFacilityChild", (FacilityChildChannel,), {})
574
+ DestinationFacilityChild = type("DestinationFacilityChild", (FacilityChildChannel,), {})
575
+ DiningEventFacilityChild = type("DiningEventFacilityChild", (FacilityChildChannel,), {})
576
+ DinnerShowFacilityChild = type("DinnerShowFacilityChild", (FacilityChildChannel,), {})
577
+ EntertainmentFacilityChild = type("EntertainmentFacilityChild", (FacilityChildChannel,), {})
578
+ EntertainmentVenueFacilityChild = type("EntertainmentVenueFacilityChild", (FacilityChildChannel,), {})
579
+ WaterParkFacilityChild = type("WaterParkFacilityChild", (FacilityChildChannel,), {})
580
+ GuestServiceFacilityChild = type("GuestServiceFacilityChild", (FacilityChildChannel,), {})
581
+ LandFacilityChild = type("LandFacilityChild", (FacilityChildChannel,), {})
582
+ MerchandiseFacilityChild = type("MerchandiseFacilityChild", (FacilityChildChannel,), {})
583
+ PhotopassFacilityChild = type("PhotopassFacilityChild", (FacilityChildChannel,), {})
584
+ PointOfInterestFacilityChild = type("PointOfInterestFacilityChild", (FacilityChildChannel,), {})
585
+ RecreationFacilityChild = type("RecreationFacilityChild", (FacilityChildChannel,), {})
586
+ RecreationActivityFacilityChild = type("RecreationActivityFacilityChild", (FacilityChildChannel,), {})
587
+ ResortFacilityChild = type("ResortFacilityChild", (FacilityChildChannel,), {})
588
+ ResortAreaFacilityChild = type("ResortAreaFacilityChild", (FacilityChildChannel,), {})
589
+ SpaFacilityChild = type("SpaFacilityChild", (FacilityChildChannel,), {})
590
+ ThemeParkFacilityChild = type("ThemeParkFacilityChild", (FacilityChildChannel,), {})
591
+ TourFacilityChild = type("TourFacilityChild", (FacilityChildChannel,), {})
592
+ TransportationFacilityChild = type("TransportationFacilityChild", (FacilityChildChannel,), {})
593
+ WaterParkFacilityChild = type("WaterParkFacilityChild", (FacilityChildChannel,), {})
594
+ RestaurantFacilityChild = type("RestaurantFacilityChild", (FacilityChildChannel,), {})
549
595
 
550
596
 
551
597
  class FacilityChannel(CouchbaseChannel):
@@ -14,8 +14,6 @@ logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
16
  class FacilityStatusChildChannel(CouchbaseMixin):
17
- _subclasses: typing.ClassVar[dict[str, type["FacilityStatusChildChannel"]]] = {}
18
-
19
17
  def __init__(self, channel_id: str, lazy_load: bool = True):
20
18
  """
21
19
  Args:
@@ -32,7 +30,7 @@ class FacilityStatusChildChannel(CouchbaseMixin):
32
30
  )
33
31
  self._refresh_interval: timedelta = timedelta(minutes=10)
34
32
  self._cb_data: typing.Optional[dict] = None
35
- self._cb_data_pull_time: typing.Optional[datetime] = datetime.now(tz=self._tz.value)
33
+ self._cb_data_pull_time: datetime = datetime.now(tz=self._tz.value)
36
34
 
37
35
  if not lazy_load:
38
36
  self.refresh()
@@ -72,11 +70,11 @@ class FacilityStatusChildChannel(CouchbaseMixin):
72
70
  return None
73
71
 
74
72
  @property
75
- def last_update(self) -> typing.Optional[datetime]:
73
+ def last_update(self) -> datetime:
76
74
  """The last time the data was updated.
77
75
 
78
76
  Returns:
79
- (typing.Optional[datetime]): The last time the entity's data was updated, or None if no such data exists.
77
+ (datetime): The last time the entity's data was updated, or None if no such data exists.
80
78
  """
81
79
  if self._cb_data is None:
82
80
  self._cb_data = self.get_channel_data(self.channel_id)
@@ -132,20 +130,14 @@ class FacilityStatusChildChannel(CouchbaseMixin):
132
130
  return self.channel_id == other.channel_id
133
131
  return False
134
132
 
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
-
141
133
 
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")
134
+ AttractionFacilityStatusChild = type("AttractionFacilityStatusChild", (FacilityStatusChildChannel,), {})
135
+ EntertainmentFacilityStatusChild = type("EntertainmentFacilityStatusChild", (FacilityStatusChildChannel,), {})
136
+ EntertainmentVenueFacilityStatusChild = type("EntertainmentVenueFacilityStatusChild", (FacilityStatusChildChannel,), {})
137
+ LandFacilityStatusChild = type("LandFacilityStatusChild", (FacilityStatusChildChannel,), {})
138
+ RestaurantFacilityStatusChild = type("RestaurantFacilityStatusChild", (FacilityStatusChildChannel,), {})
139
+ ThemeParkFacilityStatusChild = type("ThemeParkFacilityStatusChild", (FacilityStatusChildChannel,), {})
140
+ WaterParkFacilityStatusChild = type("WaterParkFacilityStatusChild", (FacilityStatusChildChannel,), {})
149
141
 
150
142
 
151
143
  class FacilityStatusChannel(CouchbaseChannel):
@@ -0,0 +1,117 @@
1
+ import logging
2
+ import typing
3
+ from datetime import datetime, timedelta
4
+ from enum import Enum
5
+
6
+ from dateutil.parser import isoparse
7
+
8
+ from mousetools.channels import CouchbaseChannel
9
+ from mousetools.channels.enums import CouchbaseChannels, DestinationTimezones
10
+ from mousetools.enums import DestinationShort
11
+ from mousetools.mixins.couchbase import CouchbaseMixin
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ForecastedWaitTimesChildChannel(CouchbaseMixin):
17
+ def __init__(
18
+ self,
19
+ channel_id: str,
20
+ lazy_load: bool = True,
21
+ ):
22
+ """
23
+ Args:
24
+ channel_id (str): Forecasted Wait Times Channel ID
25
+ lazy_load (bool, optional): If True, will not pull data until a method or property is called. Defaults to True.
26
+ """
27
+ if isinstance(channel_id, Enum):
28
+ channel_id = channel_id.value
29
+
30
+ self.channel_id: str = channel_id
31
+ self.entity_id: str = channel_id.rsplit(".", 1)[-1]
32
+
33
+ self._destination_short: DestinationShort = self.channel_id.split(".")[0]
34
+ self._tz: DestinationTimezones = (
35
+ DestinationTimezones.WALT_DISNEY_WORLD
36
+ if self._destination_short == DestinationShort.WALT_DISNEY_WORLD
37
+ else DestinationTimezones.DISNEYLAND_RESORT
38
+ )
39
+ self._refresh_interval: timedelta = timedelta(minutes=30)
40
+
41
+ self._cb_data: typing.Optional[dict] = None
42
+ self._cb_data_pull_time: datetime = datetime.now(tz=self._tz.value)
43
+ if not lazy_load:
44
+ self.refresh()
45
+
46
+ def __repr__(self):
47
+ return f"{self.__class__.__name__}(channel_id='{self.channel_id}')"
48
+
49
+ def refresh(self) -> None:
50
+ """Pulls initial data if none exists or if it is older than the refresh interval"""
51
+ if self._cb_data is None or datetime.now(tz=self._tz.value) - self.last_update > self._refresh_interval:
52
+ self._cb_data = self.get_channel_data(self.channel_id)
53
+ self._cb_data_pull_time = datetime.now(tz=self._tz.value)
54
+
55
+ @property
56
+ def last_update(self) -> datetime:
57
+ """The last time the data was updated.
58
+
59
+ Returns:
60
+ (datetime): The last time the entity's data was updated, or None if no such data exists.
61
+ """
62
+ if self._cb_data is None:
63
+ self._cb_data = self.get_channel_data(self.channel_id)
64
+
65
+ try:
66
+ dt = isoparse(self._cb_data["lastUpdate"])
67
+ dt = dt.replace(tzinfo=self._tz.value)
68
+ return dt
69
+ except (KeyError, TypeError, ValueError):
70
+ logger.debug("No last updated found for %s", self.channel_id)
71
+ return self._cb_data_pull_time
72
+
73
+ def get_forecast(self) -> list[dict]:
74
+ """Returns a list of forecasted wait times sorted by timestamp
75
+
76
+ Returns:
77
+ (list[dict]): A list of dictionaries. Each dictionary contains forecasted wait minutes, bar graph percentage, accessibility label, and timestamp
78
+ """
79
+ self.refresh()
80
+ try:
81
+ if "forecasts" not in self._cb_data:
82
+ return []
83
+
84
+ forecasts = []
85
+ for forecast in self._cb_data["forecasts"]:
86
+ tmp = {}
87
+ dt = isoparse(forecast["timestamp"])
88
+ dt = dt.astimezone(self._tz.value)
89
+ tmp["forecasted_wait_minutes"] = forecast["forecastedWaitMinutes"]
90
+ tmp["bar_graph_percentage"] = forecast["percentage"]
91
+ tmp["accessibility_label"] = forecast["accessibility12h"].split(" ", 1)[-1]
92
+ tmp["timestamp"] = dt
93
+ forecasts.append(tmp)
94
+
95
+ return sorted(forecasts, key=lambda i: i["timestamp"])
96
+
97
+ except (KeyError, TypeError, ValueError):
98
+ logger.debug("No forecast found for %s", self.channel_id)
99
+ return []
100
+
101
+
102
+ class ForecastedWaitTimesChannel(CouchbaseChannel):
103
+ def __init__(self, *args, **kwargs):
104
+ super().__init__(*args, **kwargs)
105
+
106
+ def get_children_channels(self) -> list[ForecastedWaitTimesChildChannel]:
107
+ """Gets a list of children channels for the channel.
108
+
109
+ Returns:
110
+ (list[ForecastedWaitTimesChildChannel]): A list of ForecastedWaitTimesChildChannel
111
+ """
112
+ self.refresh()
113
+ channels = []
114
+ for i in self._cb_data["results"]:
115
+ if CouchbaseChannels.FORECASTED_WAIT_TIMES in i["id"]:
116
+ channels.append(ForecastedWaitTimesChildChannel(i["id"]))
117
+ 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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mousetools
3
- Version: 4.1.1
3
+ Version: 4.2.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.2.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