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.
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/PKG-INFO +6 -2
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/__init__.py +38 -2
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/public.py +7 -1
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/config.py +1 -1
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/loaders.py +16 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/couchbase.py +17 -4
- mousetools-6.2.0/mousetools/_internal/services/ids.py +413 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/codec.py +8 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/connection.py +33 -6
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/protocol.py +37 -6
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/transport.py +11 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/client.py +152 -48
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/__init__.py +11 -0
- mousetools-6.2.0/mousetools/entities/_parse.py +177 -0
- mousetools-6.2.0/mousetools/entities/arrivals.py +143 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/base.py +121 -34
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/character.py +3 -6
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/destination.py +46 -6
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/entertainment.py +0 -1
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/facility.py +3 -7
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/mixins.py +2 -1
- mousetools-6.2.0/mousetools/entities/names.py +155 -0
- mousetools-6.2.0/mousetools/entities/offline.py +204 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/protocols.py +28 -5
- mousetools-6.2.0/mousetools/entities/related.py +151 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/restaurant.py +2 -1
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/types.py +3 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/values.py +56 -184
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/enums.py +7 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/errors.py +49 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/ext/passes.py +1 -1
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/ext/weather.py +11 -1
- mousetools-6.2.0/mousetools/stream/__init__.py +33 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/stream/events.py +74 -39
- mousetools-6.2.0/mousetools/stream/filters.py +308 -0
- mousetools-6.2.0/mousetools/stream/subscription.py +248 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/PKG-INFO +6 -2
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/SOURCES.txt +6 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/requires.txt +4 -1
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/pyproject.toml +63 -17
- mousetools-6.0.0rc2/mousetools/_internal/services/ids.py +0 -182
- mousetools-6.0.0rc2/mousetools/stream/__init__.py +0 -5
- mousetools-6.0.0rc2/mousetools/stream/subscription.py +0 -165
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/LICENSE +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/README.md +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/__init__.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/__init__.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/base.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/auth/syncgateway.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/client_registry.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/__init__.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/http.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/menu.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/services/search.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/_internal/stream/__init__.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/attraction.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/basic.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/container.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/menu.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/poi.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/search.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/entities/themepark.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/ext/__init__.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/py.typed +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/resources/environments.bin +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools/timezones.py +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/dependency_links.txt +0 -0
- {mousetools-6.0.0rc2 → mousetools-6.2.0}/mousetools.egg-info/top_level.txt +0 -0
- {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
|
|
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:
|
|
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
|
|
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(
|
|
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() +
|
|
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
|
|
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(
|
|
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
|
-
"
|
|
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
|
|