MouseTools 6.0.0rc2__tar.gz → 6.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 (69) hide show
  1. {mousetools-6.0.0rc2 → mousetools-6.2.0}/PKG-INFO +6 -2
  2. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/__init__.py +38 -2
  3. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/public.py +7 -1
  4. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/config.py +1 -1
  5. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/loaders.py +16 -0
  6. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/couchbase.py +17 -4
  7. mousetools-6.2.0/mousetools/_internal/services/ids.py +413 -0
  8. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/codec.py +8 -0
  9. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/connection.py +33 -6
  10. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/protocol.py +37 -6
  11. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/transport.py +11 -0
  12. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/client.py +152 -48
  13. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/__init__.py +11 -0
  14. mousetools-6.2.0/mousetools/entities/_parse.py +177 -0
  15. mousetools-6.2.0/mousetools/entities/arrivals.py +143 -0
  16. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/base.py +121 -34
  17. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/character.py +3 -6
  18. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/destination.py +46 -6
  19. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/entertainment.py +0 -1
  20. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/facility.py +3 -7
  21. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/mixins.py +2 -1
  22. mousetools-6.2.0/mousetools/entities/names.py +155 -0
  23. mousetools-6.2.0/mousetools/entities/offline.py +204 -0
  24. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/protocols.py +28 -5
  25. mousetools-6.2.0/mousetools/entities/related.py +151 -0
  26. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/restaurant.py +2 -1
  27. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/types.py +3 -0
  28. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/values.py +56 -184
  29. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/enums.py +7 -0
  30. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/errors.py +49 -0
  31. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/ext/passes.py +1 -1
  32. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/ext/weather.py +11 -1
  33. mousetools-6.2.0/mousetools/stream/__init__.py +33 -0
  34. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/stream/events.py +74 -39
  35. mousetools-6.2.0/mousetools/stream/filters.py +308 -0
  36. mousetools-6.2.0/mousetools/stream/subscription.py +248 -0
  37. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/PKG-INFO +6 -2
  38. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/SOURCES.txt +6 -0
  39. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/requires.txt +4 -1
  40. {mousetools-6.0.0rc2 → mousetools-6.2.0}/pyproject.toml +63 -17
  41. mousetools-6.0.0rc2/mousetools/_internal/services/ids.py +0 -182
  42. mousetools-6.0.0rc2/mousetools/stream/__init__.py +0 -5
  43. mousetools-6.0.0rc2/mousetools/stream/subscription.py +0 -165
  44. {mousetools-6.0.0rc2 → mousetools-6.2.0}/LICENSE +0 -0
  45. {mousetools-6.0.0rc2 → mousetools-6.2.0}/README.md +0 -0
  46. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/__init__.py +0 -0
  47. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/__init__.py +0 -0
  48. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/base.py +0 -0
  49. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/syncgateway.py +0 -0
  50. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/client_registry.py +0 -0
  51. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/__init__.py +0 -0
  52. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/http.py +0 -0
  53. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/menu.py +0 -0
  54. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/search.py +0 -0
  55. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/__init__.py +0 -0
  56. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/attraction.py +0 -0
  57. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/basic.py +0 -0
  58. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/container.py +0 -0
  59. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/menu.py +0 -0
  60. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/poi.py +0 -0
  61. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/search.py +0 -0
  62. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/themepark.py +0 -0
  63. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/ext/__init__.py +0 -0
  64. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/py.typed +0 -0
  65. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/resources/environments.bin +0 -0
  66. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/timezones.py +0 -0
  67. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/dependency_links.txt +0 -0
  68. {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/top_level.txt +0 -0
  69. {mousetools-6.0.0rc2 → mousetools-6.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mousetools
3
- Version: 6.0.0rc2
3
+ Version: 6.2.0
4
4
  Summary: An unofficial Python wrapper for Disney's API
5
5
  Author: Scott Caratozzolo
6
6
  License-Expression: MIT
@@ -11,15 +11,19 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
14
16
  Classifier: Operating System :: OS Independent
17
+ Classifier: Typing :: Typed
15
18
  Requires-Python: <4.0,>=3.11
16
19
  Description-Content-Type: text/markdown
17
20
  License-File: LICENSE
18
21
  Requires-Dist: more-itertools>=10.7.0
19
- Requires-Dist: pycryptodome>=3.22.0
22
+ Requires-Dist: pycryptodomex>=3.22.0
20
23
  Requires-Dist: python-dateutil>=2.9.0.post0
21
24
  Requires-Dist: niquests[http3,utls]>=3.20.0
22
25
  Requires-Dist: tzdata>=2025.2
26
+ Requires-Dist: typing-extensions>=4.5; python_version < "3.13"
23
27
  Requires-Dist: websockets>=13.0
24
28
  Dynamic: license-file
25
29
 
@@ -8,6 +8,7 @@ from mousetools.entities import (
8
8
  DISNEYLAND_RESORT,
9
9
  WALT_DISNEY_WORLD,
10
10
  Appearance,
11
+ ArrivalPrediction,
11
12
  Attraction,
12
13
  AudioTour,
13
14
  Avatar,
@@ -18,6 +19,7 @@ from mousetools.entities import (
18
19
  Coordinates,
19
20
  DailySchedule,
20
21
  Destination,
22
+ DestinationArrival,
21
23
  DiningEvent,
22
24
  DinnerShow,
23
25
  Entertainment,
@@ -35,12 +37,16 @@ from mousetools.entities import (
35
37
  MenuItem,
36
38
  MenuPrice,
37
39
  MerchandiseFacility,
40
+ NameParts,
38
41
  PartySizeStatus,
39
42
  PhotoPass,
40
43
  PointOfInterest,
41
44
  PriceRange,
42
45
  Recreation,
43
46
  RecreationActivity,
47
+ RelatedAncestor,
48
+ RelatedCoordinate,
49
+ RelatedLocation,
44
50
  Resort,
45
51
  ResortArea,
46
52
  Restaurant,
@@ -51,7 +57,9 @@ from mousetools.entities import (
51
57
  Spa,
52
58
  ThemePark,
53
59
  Tour,
60
+ TransportArrivals,
54
61
  Transportation,
62
+ TransportService,
55
63
  WaitTime,
56
64
  WalkUpAvailability,
57
65
  WaterPark,
@@ -59,15 +67,31 @@ from mousetools.entities import (
59
67
  load_many,
60
68
  )
61
69
  from mousetools.enums import ChannelSource, DestinationShort, EntityType, ScheduleSource
62
- from mousetools.stream import CalendarEvent, ChangeEvent, FacilityStatusEvent, ForecastEvent, RawChange
70
+ from mousetools.stream import (
71
+ ArrivalsEvent,
72
+ ArrivalsFilter,
73
+ CalendarEvent,
74
+ ChangeEvent,
75
+ CharacterEvent,
76
+ DiningFacilityStatusFilter,
77
+ DocId,
78
+ FacilityStatusEvent,
79
+ ForecastEvent,
80
+ NonDiningFacilityStatusFilter,
81
+ RawChange,
82
+ SubscriptionFilter,
83
+ )
63
84
 
64
- logger = logging.getLogger(__name__)
85
+ logger = logging.getLogger("mousetools")
65
86
  logger.addHandler(logging.NullHandler())
66
87
 
67
88
  __all__ = [
68
89
  "DISNEYLAND_RESORT",
69
90
  "WALT_DISNEY_WORLD",
70
91
  "Appearance",
92
+ "ArrivalPrediction",
93
+ "ArrivalsEvent",
94
+ "ArrivalsFilter",
71
95
  "Attraction",
72
96
  "AudioTour",
73
97
  "Avatar",
@@ -77,14 +101,18 @@ __all__ = [
77
101
  "ChangeEvent",
78
102
  "ChannelSource",
79
103
  "Character",
104
+ "CharacterEvent",
80
105
  "Client",
81
106
  "ContainerEntity",
82
107
  "Coordinates",
83
108
  "DailySchedule",
84
109
  "Destination",
110
+ "DestinationArrival",
85
111
  "DestinationShort",
86
112
  "DiningEvent",
113
+ "DiningFacilityStatusFilter",
87
114
  "DinnerShow",
115
+ "DocId",
88
116
  "Entertainment",
89
117
  "EntertainmentVenue",
90
118
  "Entity",
@@ -103,6 +131,8 @@ __all__ = [
103
131
  "MenuItem",
104
132
  "MenuPrice",
105
133
  "MerchandiseFacility",
134
+ "NameParts",
135
+ "NonDiningFacilityStatusFilter",
106
136
  "PartySizeStatus",
107
137
  "PhotoPass",
108
138
  "PointOfInterest",
@@ -110,6 +140,9 @@ __all__ = [
110
140
  "RawChange",
111
141
  "Recreation",
112
142
  "RecreationActivity",
143
+ "RelatedAncestor",
144
+ "RelatedCoordinate",
145
+ "RelatedLocation",
113
146
  "Resort",
114
147
  "ResortArea",
115
148
  "Restaurant",
@@ -119,8 +152,11 @@ __all__ = [
119
152
  "SearchResponse",
120
153
  "SearchResult",
121
154
  "Spa",
155
+ "SubscriptionFilter",
122
156
  "ThemePark",
123
157
  "Tour",
158
+ "TransportArrivals",
159
+ "TransportService",
124
160
  "Transportation",
125
161
  "WaitTime",
126
162
  "WalkUpAvailability",
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import logging
2
3
  from collections.abc import Callable
3
4
 
4
5
  from mousetools._internal.auth.base import LEEWAY_SECONDS, epoch_now
@@ -6,6 +7,8 @@ from mousetools._internal.config import load_config
6
7
  from mousetools._internal.transport import Transport
7
8
  from mousetools.errors import AuthError
8
9
 
10
+ logger = logging.getLogger("mousetools.network.auth")
11
+
9
12
 
10
13
  class PublicAuth:
11
14
  """Zero-config guest-token auth (assertion/public grant). The Client binds the
@@ -62,6 +65,7 @@ class PublicAuth:
62
65
  if self._transport is None:
63
66
  raise AuthError("PublicAuth used before bind(transport)")
64
67
  if self._token is not None and self._now() < self._expires_at - LEEWAY_SECONDS:
68
+ logger.debug("reusing cached public token, %.0fs of life left", self._expires_at - self._now())
65
69
  return self._token
66
70
  async with self._grant_lock:
67
71
  # Re-check inside the lock: a concurrent caller may have granted while
@@ -77,8 +81,10 @@ class PublicAuth:
77
81
  if resp.status_code is None or resp.status_code >= 400:
78
82
  raise AuthError(f"public token grant failed: {resp.status_code} from {self._token_url}")
79
83
  body = resp.json()
84
+ expires_in = int(body["expires_in"])
80
85
  self._token = body["access_token"]
81
- self._expires_at = self._now() + int(body["expires_in"])
86
+ self._expires_at = self._now() + expires_in
87
+ logger.info("public token refreshed, expires in %.0fs", expires_in)
82
88
  return self._token
83
89
 
84
90
  async def headers(self) -> dict[str, str]:
@@ -17,7 +17,7 @@ from dataclasses import dataclass
17
17
  from functools import lru_cache
18
18
  from importlib.resources import files
19
19
 
20
- from Crypto.Cipher import AES
20
+ from Cryptodome.Cipher import AES
21
21
 
22
22
  # AES-GCM appends a 16-byte authentication tag to the ciphertext.
23
23
  _GCM_TAG_BYTES = 16
@@ -1,11 +1,14 @@
1
1
  """Generic DataLoader: coalesce per-tick key loads into batched fetches."""
2
2
 
3
3
  import asyncio
4
+ import logging
4
5
  from collections.abc import Awaitable, Callable
5
6
  from typing import Generic, TypeVar
6
7
 
7
8
  from mousetools.errors import NotFound
8
9
 
10
+ logger = logging.getLogger("mousetools.network.loaders")
11
+
9
12
  T = TypeVar("T")
10
13
 
11
14
 
@@ -27,6 +30,7 @@ class DataLoader(Generic[T]):
27
30
  batch_fetch: Callable[[list[str]], Awaitable[dict[str, T]]],
28
31
  *,
29
32
  max_batch: int = 100,
33
+ name: str | None = None,
30
34
  ) -> None:
31
35
  """Create a loader around a batch-fetch callable.
32
36
 
@@ -36,9 +40,13 @@ class DataLoader(Generic[T]):
36
40
  max_batch: The largest chunk of keys passed to `batch_fetch` in
37
41
  one call; a tick's coalesced keys are split into chunks of
38
42
  this size.
43
+ name: Identifies this loader in its log records. Without it,
44
+ several loaders batching concurrently emit indistinguishable
45
+ lines; optional so any construction site still works.
39
46
  """
40
47
  self._batch_fetch = batch_fetch
41
48
  self._max_batch = max_batch
49
+ self._name = name or "unnamed"
42
50
  self._pending: dict[str, asyncio.Future[T]] = {}
43
51
  self._flush_scheduled = False
44
52
  self._tasks: set[asyncio.Task[None]] = set()
@@ -83,6 +91,8 @@ class DataLoader(Generic[T]):
83
91
  """
84
92
  try:
85
93
  keys = list(batch)
94
+ chunks = (len(keys) + self._max_batch - 1) // self._max_batch
95
+ logger.debug("batch[%s]: %d key(s) in %d chunk(s)", self._name, len(keys), chunks)
86
96
  await asyncio.gather(
87
97
  *(
88
98
  self._resolve_chunk(keys[start : start + self._max_batch], batch)
@@ -112,6 +122,12 @@ class DataLoader(Generic[T]):
112
122
  # settled with the error (its awaiters re-raise it) -- narrowing would
113
123
  # leave futures hung forever on an unexpected type. `Exception` (not
114
124
  # BaseException) deliberately lets CancelledError propagate.
125
+ logger.debug(
126
+ "batch[%s]: chunk fetch failed (%s); settling %d future(s) with it",
127
+ self._name,
128
+ type(exc).__name__,
129
+ len(chunk),
130
+ )
115
131
  for k in chunk:
116
132
  if not batch[k].done():
117
133
  batch[k].set_exception(exc)
@@ -15,7 +15,7 @@ from mousetools._internal.services.http import looks_like_bot_block
15
15
  from mousetools._internal.transport import Transport
16
16
  from mousetools.errors import AuthError, BotChallengeError, DisneyServerError, HTTPStatusError, RateLimited
17
17
 
18
- logger = logging.getLogger(__name__)
18
+ logger = logging.getLogger("mousetools.network.couchbase")
19
19
 
20
20
  _JSON_OBJECT = re.compile(r"\{.*\}", re.DOTALL)
21
21
  # Pull the boundary token out of a `multipart/related; boundary=...` Content-Type,
@@ -43,7 +43,7 @@ def _raise_for_status_code(code: int | None, url: str) -> None:
43
43
  HTTPStatusError: Any other status code of 400 or higher.
44
44
  """
45
45
  if code is None:
46
- raise DisneyServerError(f"no response from {url}")
46
+ raise DisneyServerError(f"no response from {url}", url=url)
47
47
  if code < 400:
48
48
  return
49
49
  if code == 401:
@@ -54,7 +54,7 @@ def _raise_for_status_code(code: int | None, url: str) -> None:
54
54
  if code == 429:
55
55
  raise RateLimited(url)
56
56
  if code >= 500:
57
- raise DisneyServerError(f"{code} from {url}")
57
+ raise DisneyServerError(f"{code} from {url}", status=code, url=url)
58
58
  raise HTTPStatusError(f"{code} from {url}", status=code, url=url)
59
59
 
60
60
 
@@ -278,6 +278,9 @@ async def bulk_get(
278
278
  (response-level or per-doc), including a per-doc error other than
279
279
  `"not_found"` (e.g. `"forbidden"`/`"unauthorized"`).
280
280
  """
281
+ # `base_url` is constant for the client's life, so it cannot tell two concurrent
282
+ # bulk_gets apart under HTTP/2 multiplexing -- name the ids instead, bounded.
283
+ logger.debug("bulk_get: requesting %d id(s): %s", len(channel_ids), channel_ids[:5])
281
284
  resp = await transport.request(
282
285
  "POST",
283
286
  f"{base_url}/_bulk_get",
@@ -305,10 +308,20 @@ async def bulk_get(
305
308
  unexplained = [cid for cid in channel_ids if cid not in out and cid not in reported_missing]
306
309
  if unexplained:
307
310
  logger.debug(
308
- "_bulk_get: %d requested id(s) resolved to neither a doc nor a not_found: %s",
311
+ "bulk_get: %d requested id(s) resolved to neither a doc nor a not_found: %s",
309
312
  len(unexplained),
310
313
  unexplained[:5],
311
314
  )
315
+ absent = [cid for cid in channel_ids if cid not in out]
316
+ # Names the requested ids AND the absent ones: the former is what pairs this
317
+ # record with its opener under H2 multiplexing, the latter is the diagnosis.
318
+ logger.debug(
319
+ "bulk_get: %d of %d id(s) returned for %s; absent: %s",
320
+ len(out),
321
+ len(channel_ids),
322
+ channel_ids[:5],
323
+ absent[:5],
324
+ )
312
325
  return out
313
326
 
314
327