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.
- {mousetools-4.1.1 → mousetools-4.2.0}/PKG-INFO +1 -1
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/api/menu.py +3 -3
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/calendar/__init__.py +3 -3
- mousetools-4.2.0/mousetools/channels/characters/__init__.py +167 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/enums.py +5 -5
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/facilities/__init__.py +86 -40
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/facilitystatus/__init__.py +10 -18
- mousetools-4.2.0/mousetools/channels/forecastedwaittimes/__init__.py +117 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/decorators.py +6 -3
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/PKG-INFO +1 -1
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/SOURCES.txt +2 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/pyproject.toml +2 -1
- {mousetools-4.1.1 → mousetools-4.2.0}/LICENSE +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/README.md +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/__init__.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/api/__init__.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/api/search.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/auth.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/__init__.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/channels/facilities/enums.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/enums.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/mixins/__init__.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/mixins/couchbase.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/mixins/disney.py +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools/resources/environments.bin +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/dependency_links.txt +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/requires.txt +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/mousetools.egg-info/top_level.txt +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/setup.cfg +0 -0
- {mousetools-4.1.1 → mousetools-4.2.0}/tests/test_mixins.py +0 -0
|
@@ -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:
|
|
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) ->
|
|
400
|
+
def last_refresh(self) -> datetime:
|
|
401
401
|
"""
|
|
402
402
|
The last time the menus were refreshed.
|
|
403
403
|
|
|
404
404
|
Returns:
|
|
405
|
-
(
|
|
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:
|
|
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) ->
|
|
67
|
+
def last_update(self) -> datetime:
|
|
68
68
|
"""The last time the data was updated.
|
|
69
69
|
|
|
70
70
|
Returns:
|
|
71
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
DestinationShort.WALT_DISNEY_WORLD, CouchbaseChannels.
|
|
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
|
-
|
|
56
|
-
DestinationShort.DISNEYLAND_RESORT, CouchbaseChannels.
|
|
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:
|
|
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) ->
|
|
396
|
+
def last_update(self) -> datetime:
|
|
396
397
|
"""
|
|
397
398
|
The last time the data was updated.
|
|
398
399
|
|
|
399
400
|
Returns:
|
|
400
|
-
(
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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:
|
|
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) ->
|
|
73
|
+
def last_update(self) -> datetime:
|
|
76
74
|
"""The last time the data was updated.
|
|
77
75
|
|
|
78
76
|
Returns:
|
|
79
|
-
(
|
|
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 =
|
|
143
|
-
EntertainmentFacilityStatusChild =
|
|
144
|
-
EntertainmentVenueFacilityStatusChild =
|
|
145
|
-
LandFacilityStatusChild =
|
|
146
|
-
RestaurantFacilityStatusChild =
|
|
147
|
-
ThemeParkFacilityStatusChild =
|
|
148
|
-
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(
|
|
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
|
-
|
|
23
|
+
wrapper.__annotations__ = func.__annotations__
|
|
24
|
+
wrapper.__doc__ = func.__doc__
|
|
25
|
+
return property(wrapper)
|
|
23
26
|
|
|
24
27
|
return decorator
|
|
25
28
|
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|