rootpy-client 1.31.1__py3-none-any.whl
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.
- rootpy/__init__.py +393 -0
- rootpy/_registry_loader.py +122 -0
- rootpy/accounts.py +554 -0
- rootpy/attach.py +243 -0
- rootpy/auth.py +310 -0
- rootpy/cache.py +190 -0
- rootpy/client.py +1702 -0
- rootpy/commands.py +444 -0
- rootpy/data/enums.json +1 -0
- rootpy/data/message_schemas.json +1 -0
- rootpy/data/messages.json +1 -0
- rootpy/data/rpc_services.json +1 -0
- rootpy/data/services.json +1 -0
- rootpy/discovery.py +578 -0
- rootpy/dm_member.py +305 -0
- rootpy/domain_managers.py +677 -0
- rootpy/emoji.py +129 -0
- rootpy/enums.py +315 -0
- rootpy/events.py +153 -0
- rootpy/exceptions.py +687 -0
- rootpy/features.py +949 -0
- rootpy/gateway.py +1477 -0
- rootpy/generated_rpc_registry.py +18 -0
- rootpy/highlevel.py +1472 -0
- rootpy/host.py +594 -0
- rootpy/identifiers.py +119 -0
- rootpy/media.py +43 -0
- rootpy/media_bootstrap.py +111 -0
- rootpy/models.py +944 -0
- rootpy/object_api.py +606 -0
- rootpy/packet_schemas.py +181 -0
- rootpy/packets.py +176 -0
- rootpy/pagination.py +31 -0
- rootpy/permissions.py +237 -0
- rootpy/presence.py +173 -0
- rootpy/protocol.py +164 -0
- rootpy/py.typed +0 -0
- rootpy/raw_api.py +244 -0
- rootpy/responses.py +110 -0
- rootpy/root_exception.py +306 -0
- rootpy/service_facade.py +32 -0
- rootpy/services/__init__.py +27 -0
- rootpy/services/assets.py +503 -0
- rootpy/services/calls.py +1411 -0
- rootpy/services/communities.py +735 -0
- rootpy/services/community_admin.py +1036 -0
- rootpy/services/direct_messages.py +524 -0
- rootpy/services/discovery.py +204 -0
- rootpy/services/messages.py +811 -0
- rootpy/services/users.py +440 -0
- rootpy/stats.py +208 -0
- rootpy/structured_api.py +956 -0
- rootpy/structured_registry.py +61 -0
- rootpy/transport.py +403 -0
- rootpy/typed_events.py +496 -0
- rootpy/unread.py +546 -0
- rootpy/users.py +187 -0
- rootpy/validation.py +145 -0
- rootpy_client-1.31.1.dist-info/METADATA +157 -0
- rootpy_client-1.31.1.dist-info/RECORD +63 -0
- rootpy_client-1.31.1.dist-info/WHEEL +5 -0
- rootpy_client-1.31.1.dist-info/licenses/LICENSE +21 -0
- rootpy_client-1.31.1.dist-info/top_level.txt +1 -0
rootpy/__init__.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = _pkg_version("rootpy-client")
|
|
5
|
+
except PackageNotFoundError: # running from a source tree, not installed
|
|
6
|
+
__version__ = "0.0.0.dev0"
|
|
7
|
+
|
|
8
|
+
from .client import RootClient
|
|
9
|
+
from .commands import (
|
|
10
|
+
Command,
|
|
11
|
+
CommandArgumentError,
|
|
12
|
+
CommandCheckFailure,
|
|
13
|
+
CommandError,
|
|
14
|
+
CommandNotFound,
|
|
15
|
+
Context,
|
|
16
|
+
parse_channel_id,
|
|
17
|
+
parse_channel_mention,
|
|
18
|
+
parse_user_id,
|
|
19
|
+
parse_user_mention,
|
|
20
|
+
)
|
|
21
|
+
from .emoji import normalize_reaction, parse_emoji_mention
|
|
22
|
+
from .events import (
|
|
23
|
+
MessageAction,
|
|
24
|
+
ChannelAction,
|
|
25
|
+
CommunityLeaveReason,
|
|
26
|
+
CallDetachedEvent,
|
|
27
|
+
ChannelDeletedEvent,
|
|
28
|
+
ChannelEvent,
|
|
29
|
+
CommandErrorEvent,
|
|
30
|
+
CommandEvent,
|
|
31
|
+
CommunityDeletedEvent,
|
|
32
|
+
CommunityEvent,
|
|
33
|
+
CommunityLeaveEvent,
|
|
34
|
+
MessageEvent,
|
|
35
|
+
NotificationEvent,
|
|
36
|
+
ReadyEvent,
|
|
37
|
+
)
|
|
38
|
+
from .exceptions import (
|
|
39
|
+
ErrorInfo,
|
|
40
|
+
get_error_info,
|
|
41
|
+
format_root_error,
|
|
42
|
+
UsernameLookupAuthenticationRequired,
|
|
43
|
+
GrpcStatus,
|
|
44
|
+
GrpcCancelled,
|
|
45
|
+
GrpcUnknown,
|
|
46
|
+
GrpcInvalidArgument,
|
|
47
|
+
GrpcDeadlineExceeded,
|
|
48
|
+
GrpcNotFound,
|
|
49
|
+
GrpcAlreadyExists,
|
|
50
|
+
GrpcPermissionDenied,
|
|
51
|
+
GrpcResourceExhausted,
|
|
52
|
+
GrpcFailedPrecondition,
|
|
53
|
+
GrpcAborted,
|
|
54
|
+
GrpcOutOfRange,
|
|
55
|
+
GrpcUnimplemented,
|
|
56
|
+
GrpcInternal,
|
|
57
|
+
GrpcUnavailable,
|
|
58
|
+
GrpcDataLoss,
|
|
59
|
+
GrpcUnauthenticated,
|
|
60
|
+
AccountAlreadyExists,
|
|
61
|
+
AuthenticationError,
|
|
62
|
+
EmailAlreadyExists,
|
|
63
|
+
GrpcWebError,
|
|
64
|
+
RootError,
|
|
65
|
+
SignUpError,
|
|
66
|
+
TurnstileRequired,
|
|
67
|
+
UsernameAlreadyExists,
|
|
68
|
+
)
|
|
69
|
+
from .identifiers import (
|
|
70
|
+
normalize_root_guid,
|
|
71
|
+
root_guid_datetime,
|
|
72
|
+
root_guid_type,
|
|
73
|
+
)
|
|
74
|
+
from .media import (
|
|
75
|
+
MediaBackendInfo,
|
|
76
|
+
media_backend_info,
|
|
77
|
+
)
|
|
78
|
+
from .media_bootstrap import (
|
|
79
|
+
ensure_ffmpeg,
|
|
80
|
+
ensure_media_dependencies,
|
|
81
|
+
)
|
|
82
|
+
from .models import (
|
|
83
|
+
AuthenticationSession,
|
|
84
|
+
CallSession,
|
|
85
|
+
Channel,
|
|
86
|
+
ChannelGroup,
|
|
87
|
+
Community,
|
|
88
|
+
CommunityExtended,
|
|
89
|
+
CurrentUser,
|
|
90
|
+
Message,
|
|
91
|
+
MessageAttachment,
|
|
92
|
+
MessageSendResult,
|
|
93
|
+
)
|
|
94
|
+
from .packets import (
|
|
95
|
+
PacketType,
|
|
96
|
+
SocketPacket,
|
|
97
|
+
)
|
|
98
|
+
from .permissions import (
|
|
99
|
+
PermissionName,
|
|
100
|
+
AccessRule,
|
|
101
|
+
ChannelOverlay,
|
|
102
|
+
ChannelPermission,
|
|
103
|
+
CommunityPermission,
|
|
104
|
+
ActionNotApplicable,
|
|
105
|
+
CallActionError,
|
|
106
|
+
ChannelPermissions,
|
|
107
|
+
MissingPermissions,
|
|
108
|
+
PermissionStateUnavailable,
|
|
109
|
+
)
|
|
110
|
+
from .services.assets import AssetService
|
|
111
|
+
from .services.direct_messages import (
|
|
112
|
+
DirectMessage,
|
|
113
|
+
DirectMessageService,
|
|
114
|
+
)
|
|
115
|
+
from .dm_member import DMMemberService
|
|
116
|
+
from .users import User
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
"__version__",
|
|
121
|
+
|
|
122
|
+
# Client
|
|
123
|
+
"RootClient",
|
|
124
|
+
"ErrorInfo",
|
|
125
|
+
"get_error_info",
|
|
126
|
+
|
|
127
|
+
# Multi-account hosting
|
|
128
|
+
"MultiClientHost",
|
|
129
|
+
"HostedAccount",
|
|
130
|
+
"Outcome",
|
|
131
|
+
"format_root_error",
|
|
132
|
+
|
|
133
|
+
# Reading unread
|
|
134
|
+
"UnreadReader",
|
|
135
|
+
"UnreadChannel",
|
|
136
|
+
"is_unread",
|
|
137
|
+
|
|
138
|
+
# Presence and attach lifetime
|
|
139
|
+
"AttachHold",
|
|
140
|
+
"PresenceWatch",
|
|
141
|
+
"effective_presence",
|
|
142
|
+
|
|
143
|
+
# Commands
|
|
144
|
+
"Command",
|
|
145
|
+
"CommandArgumentError",
|
|
146
|
+
"CommandCheckFailure",
|
|
147
|
+
"CommandError",
|
|
148
|
+
"CommandNotFound",
|
|
149
|
+
"Context",
|
|
150
|
+
"parse_channel_id",
|
|
151
|
+
"parse_channel_mention",
|
|
152
|
+
"parse_user_id",
|
|
153
|
+
"parse_user_mention",
|
|
154
|
+
|
|
155
|
+
"normalize_reaction",
|
|
156
|
+
"parse_emoji_mention",
|
|
157
|
+
|
|
158
|
+
# Events
|
|
159
|
+
"CallDetachedEvent",
|
|
160
|
+
"ChannelDeletedEvent",
|
|
161
|
+
"ChannelEvent",
|
|
162
|
+
"CommandErrorEvent",
|
|
163
|
+
"CommandEvent",
|
|
164
|
+
"CommunityDeletedEvent",
|
|
165
|
+
"CommunityEvent",
|
|
166
|
+
"CommunityLeaveEvent",
|
|
167
|
+
"MessageEvent",
|
|
168
|
+
"NotificationEvent",
|
|
169
|
+
"ReadyEvent",
|
|
170
|
+
|
|
171
|
+
# Exceptions
|
|
172
|
+
"RootError",
|
|
173
|
+
"AuthenticationError",
|
|
174
|
+
"GrpcWebError",
|
|
175
|
+
"TurnstileRequired",
|
|
176
|
+
"SignUpError",
|
|
177
|
+
"AccountAlreadyExists",
|
|
178
|
+
"UsernameAlreadyExists",
|
|
179
|
+
"EmailAlreadyExists",
|
|
180
|
+
|
|
181
|
+
"normalize_root_guid",
|
|
182
|
+
"root_guid_datetime",
|
|
183
|
+
"root_guid_type",
|
|
184
|
+
|
|
185
|
+
# Models
|
|
186
|
+
"AuthenticationSession",
|
|
187
|
+
"CallSession",
|
|
188
|
+
"Channel",
|
|
189
|
+
"ChannelGroup",
|
|
190
|
+
"Community",
|
|
191
|
+
"CommunityExtended",
|
|
192
|
+
"CurrentUser",
|
|
193
|
+
"Message",
|
|
194
|
+
"MessageAttachment",
|
|
195
|
+
"MessageSendResult",
|
|
196
|
+
"User",
|
|
197
|
+
|
|
198
|
+
# Packets
|
|
199
|
+
"PacketType",
|
|
200
|
+
"SocketPacket",
|
|
201
|
+
|
|
202
|
+
# Permissions
|
|
203
|
+
"ActionNotApplicable",
|
|
204
|
+
# The base the other three share. Exported so `except CallActionError`
|
|
205
|
+
# catches a refused mute/kick in one clause -- the subclasses were
|
|
206
|
+
# reachable before, their base was not.
|
|
207
|
+
"CallActionError",
|
|
208
|
+
"ChannelPermissions",
|
|
209
|
+
"MissingPermissions",
|
|
210
|
+
"PermissionStateUnavailable",
|
|
211
|
+
|
|
212
|
+
# Direct messages
|
|
213
|
+
"DirectMessage",
|
|
214
|
+
"DirectMessageService",
|
|
215
|
+
"DMMemberService",
|
|
216
|
+
"DirectMessageError",
|
|
217
|
+
|
|
218
|
+
"AssetService",
|
|
219
|
+
|
|
220
|
+
# Voice / media
|
|
221
|
+
"AudioPlayback",
|
|
222
|
+
"IceInfo",
|
|
223
|
+
"MediaBackendInfo",
|
|
224
|
+
"media_backend_info",
|
|
225
|
+
"ensure_ffmpeg",
|
|
226
|
+
"ensure_media_dependencies",
|
|
227
|
+
"RawAPI",
|
|
228
|
+
"RawService",
|
|
229
|
+
"RawMethod",
|
|
230
|
+
"RawRpcResult",
|
|
231
|
+
"AccessRule",
|
|
232
|
+
"ChannelOverlay",
|
|
233
|
+
"ChannelPermission",
|
|
234
|
+
"CommunityPermission",
|
|
235
|
+
"CommunityAdminService",
|
|
236
|
+
"GrpcStatus",
|
|
237
|
+
"GrpcCancelled",
|
|
238
|
+
"GrpcUnknown",
|
|
239
|
+
"GrpcInvalidArgument",
|
|
240
|
+
"GrpcDeadlineExceeded",
|
|
241
|
+
"GrpcNotFound",
|
|
242
|
+
"GrpcAlreadyExists",
|
|
243
|
+
"GrpcPermissionDenied",
|
|
244
|
+
"GrpcResourceExhausted",
|
|
245
|
+
"GrpcFailedPrecondition",
|
|
246
|
+
"GrpcAborted",
|
|
247
|
+
"GrpcOutOfRange",
|
|
248
|
+
"GrpcUnimplemented",
|
|
249
|
+
"GrpcInternal",
|
|
250
|
+
"GrpcUnavailable",
|
|
251
|
+
"GrpcDataLoss",
|
|
252
|
+
"GrpcUnauthenticated",
|
|
253
|
+
"MessageAction",
|
|
254
|
+
"ChannelAction",
|
|
255
|
+
"CommunityLeaveReason",
|
|
256
|
+
"PermissionName",
|
|
257
|
+
"AttrDict",
|
|
258
|
+
"EnumValue",
|
|
259
|
+
"StructuredAPI",
|
|
260
|
+
"StructuredMethod",
|
|
261
|
+
"StructuredResult",
|
|
262
|
+
"StructuredService",
|
|
263
|
+
"UsernameLookupAuthenticationRequired",
|
|
264
|
+
"RootServiceFacade",
|
|
265
|
+
"RoleManager",
|
|
266
|
+
"MemberManager",
|
|
267
|
+
"CommunityFileManager",
|
|
268
|
+
"LogManager",
|
|
269
|
+
"CommunityAppManager",
|
|
270
|
+
"VoiceAdminManager",
|
|
271
|
+
"FriendshipGroupManager",
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
from .services.community_admin import CommunityAdminService
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
from .service_facade import RootServiceFacade
|
|
279
|
+
from .domain_managers import (
|
|
280
|
+
RoleManager,
|
|
281
|
+
MemberManager,
|
|
282
|
+
CommunityFileManager,
|
|
283
|
+
LogManager,
|
|
284
|
+
CommunityAppManager,
|
|
285
|
+
VoiceAdminManager,
|
|
286
|
+
FriendshipGroupManager,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
from .object_api import CommunityManager, PermissionManager
|
|
290
|
+
|
|
291
|
+
from .pagination import AsyncPager, Page
|
|
292
|
+
|
|
293
|
+
from .events import EventErrorEvent
|
|
294
|
+
|
|
295
|
+
from .exceptions import HttpStatusError, BadRequest, Unauthorized, Forbidden, HttpNotFound, Conflict, PayloadTooLarge, RateLimited, ServerError, BadGateway, ServiceUnavailable, GatewayTimeout, RetryExhausted, DirectMessageError
|
|
296
|
+
|
|
297
|
+
from .root_exception import (
|
|
298
|
+
RootExceptionInfo,
|
|
299
|
+
ValidationError,
|
|
300
|
+
decode_root_exception,
|
|
301
|
+
decode_root_exception_header,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
from .enums import (
|
|
305
|
+
ErrorCodeType,
|
|
306
|
+
PacketErrorCode,
|
|
307
|
+
ContentFlagReason,
|
|
308
|
+
NotificationType,
|
|
309
|
+
MessageType,
|
|
310
|
+
ChannelType,
|
|
311
|
+
UserOnlineStatus,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
from .packet_schemas import PACKET_SCHEMAS, decode_packet
|
|
315
|
+
|
|
316
|
+
from .events import ChannelActivity
|
|
317
|
+
|
|
318
|
+
from .typed_events import (
|
|
319
|
+
BlockEvent,
|
|
320
|
+
ChannelEvent as TypedChannelEvent,
|
|
321
|
+
CommunityEvent as TypedCommunityEvent,
|
|
322
|
+
FriendEvent,
|
|
323
|
+
MemberEvent,
|
|
324
|
+
MemberRoleEvent,
|
|
325
|
+
ReactionEvent,
|
|
326
|
+
RoleEvent,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
from .cache import LRUCache, StateCache
|
|
330
|
+
|
|
331
|
+
from .validation import (
|
|
332
|
+
DEFAULT_PICTURE_HEX,
|
|
333
|
+
USERNAME_MAX_LENGTH,
|
|
334
|
+
USERNAME_MIN_LENGTH,
|
|
335
|
+
USERNAME_RULE,
|
|
336
|
+
normalize_hex_colour,
|
|
337
|
+
validate_nickname,
|
|
338
|
+
validate_username,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# The generated RPC registries are big and slow to import (~770 ms cold, about
|
|
342
|
+
# half of total import time), and most programs never touch them. They load on
|
|
343
|
+
# first use instead -- `from rootpy import StructuredAPI` still works, it just
|
|
344
|
+
# pays the cost at that moment rather than on every import of the package.
|
|
345
|
+
_LAZY_EXPORTS = {
|
|
346
|
+
"RawAPI": "raw_api",
|
|
347
|
+
"RawMethod": "raw_api",
|
|
348
|
+
"RawRpcResult": "raw_api",
|
|
349
|
+
"RawService": "raw_api",
|
|
350
|
+
"AttrDict": "structured_api",
|
|
351
|
+
"EnumValue": "structured_api",
|
|
352
|
+
"StructuredAPI": "structured_api",
|
|
353
|
+
"StructuredMethod": "structured_api",
|
|
354
|
+
"StructuredResult": "structured_api",
|
|
355
|
+
"StructuredService": "structured_api",
|
|
356
|
+
# Voice: rootpy.services.calls pulls in the media support modules, so it
|
|
357
|
+
# is kept off the import path too.
|
|
358
|
+
"AudioPlayback": "services.calls",
|
|
359
|
+
"IceInfo": "services.calls",
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def __getattr__(name):
|
|
364
|
+
module_name = _LAZY_EXPORTS.get(name)
|
|
365
|
+
if module_name is None:
|
|
366
|
+
raise AttributeError(f"module 'rootpy' has no attribute {name!r}")
|
|
367
|
+
import importlib
|
|
368
|
+
|
|
369
|
+
module = importlib.import_module(f".{module_name}", __name__)
|
|
370
|
+
value = getattr(module, name)
|
|
371
|
+
globals()[name] = value # cache it, so this happens once
|
|
372
|
+
return value
|
|
373
|
+
|
|
374
|
+
from .host import HostedAccount, MultiClientHost, Outcome
|
|
375
|
+
|
|
376
|
+
from .models import DetailedMember, UserProfile
|
|
377
|
+
|
|
378
|
+
from .stats import EndpointStats, TransportStats
|
|
379
|
+
|
|
380
|
+
from .unread import UnreadChannel, UnreadReader, is_unread
|
|
381
|
+
|
|
382
|
+
from .attach import AttachHold
|
|
383
|
+
|
|
384
|
+
from .presence import PresenceWatch, effective_presence
|
|
385
|
+
|
|
386
|
+
from .services.assets import Asset, AssetLink
|
|
387
|
+
|
|
388
|
+
from .accounts import (
|
|
389
|
+
AccountFactory,
|
|
390
|
+
AlreadyCreatedError,
|
|
391
|
+
CreatedAccount,
|
|
392
|
+
TurnstileChallenge,
|
|
393
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Lazy loading for the generated protocol registries.
|
|
2
|
+
|
|
3
|
+
The registries are large -- 842 message definitions, 842 raw schemas (the same
|
|
4
|
+
key set, one field table per message), 31 services -- and most programs touch
|
|
5
|
+
only a handful of them. Storing them as
|
|
6
|
+
Python literals meant every field of every message was parsed and built into
|
|
7
|
+
dicts before the first request could be sent.
|
|
8
|
+
|
|
9
|
+
They now live as JSON under ``rootpy/data/``. :class:`LazyRegistry` is a
|
|
10
|
+
read-only mapping that behaves exactly like the dict it replaced but defers
|
|
11
|
+
reading and decoding the file until something actually looks a key up. Import
|
|
12
|
+
stays cheap; the cost is paid once, on first use, by whoever needs it.
|
|
13
|
+
|
|
14
|
+
``DerivedRegistry`` is the same idea for indexes that can be computed from
|
|
15
|
+
another registry rather than stored (see ``SIMPLE_MESSAGES``).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Callable, Dict, Iterator, Mapping
|
|
23
|
+
|
|
24
|
+
_DATA_DIR = Path(__file__).resolve().parent / "data"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LazyRegistry(Mapping):
|
|
28
|
+
"""A read-only mapping backed by a JSON file, loaded on first access.
|
|
29
|
+
|
|
30
|
+
Implements the full :class:`~collections.abc.Mapping` protocol, so it is a
|
|
31
|
+
drop-in for the plain dicts these registries used to be: subscripting,
|
|
32
|
+
``in``, ``.get()``, ``.items()``, ``len()`` and iteration all work.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__slots__ = ("_filename", "_data")
|
|
36
|
+
|
|
37
|
+
def __init__(self, filename: str) -> None:
|
|
38
|
+
self._filename = filename
|
|
39
|
+
self._data: Dict[str, Any] | None = None
|
|
40
|
+
|
|
41
|
+
def _load(self) -> Dict[str, Any]:
|
|
42
|
+
data = self._data
|
|
43
|
+
if data is None:
|
|
44
|
+
path = _DATA_DIR / self._filename
|
|
45
|
+
try:
|
|
46
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
47
|
+
data = json.load(handle)
|
|
48
|
+
except FileNotFoundError as exc: # pragma: no cover - packaging bug
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
f"rootpy data file {self._filename!r} is missing. The "
|
|
51
|
+
"package was probably installed without its data files; "
|
|
52
|
+
"reinstall rootpy."
|
|
53
|
+
) from exc
|
|
54
|
+
self._data = data
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
# -- Mapping protocol ------------------------------------------------
|
|
58
|
+
def __getitem__(self, key: str) -> Any:
|
|
59
|
+
return self._load()[key]
|
|
60
|
+
|
|
61
|
+
def __iter__(self) -> Iterator[str]:
|
|
62
|
+
return iter(self._load())
|
|
63
|
+
|
|
64
|
+
def __len__(self) -> int:
|
|
65
|
+
return len(self._load())
|
|
66
|
+
|
|
67
|
+
def __contains__(self, key: object) -> bool:
|
|
68
|
+
return key in self._load()
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
state = "unloaded" if self._data is None else f"{len(self._data)} entries"
|
|
72
|
+
return f"<LazyRegistry {self._filename} ({state})>"
|
|
73
|
+
|
|
74
|
+
# -- conveniences ----------------------------------------------------
|
|
75
|
+
@property
|
|
76
|
+
def loaded(self) -> bool:
|
|
77
|
+
"""True once the backing file has been read."""
|
|
78
|
+
return self._data is not None
|
|
79
|
+
|
|
80
|
+
def preload(self) -> "LazyRegistry":
|
|
81
|
+
"""Force the load now, e.g. to keep it off a latency-sensitive path."""
|
|
82
|
+
self._load()
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DerivedRegistry(Mapping):
|
|
87
|
+
"""A mapping computed from another registry the first time it is used."""
|
|
88
|
+
|
|
89
|
+
__slots__ = ("_builder", "_data")
|
|
90
|
+
|
|
91
|
+
def __init__(self, builder: Callable[[], Dict[str, Any]]) -> None:
|
|
92
|
+
self._builder = builder
|
|
93
|
+
self._data: Dict[str, Any] | None = None
|
|
94
|
+
|
|
95
|
+
def _load(self) -> Dict[str, Any]:
|
|
96
|
+
if self._data is None:
|
|
97
|
+
self._data = self._builder()
|
|
98
|
+
return self._data
|
|
99
|
+
|
|
100
|
+
def __getitem__(self, key: str) -> Any:
|
|
101
|
+
return self._load()[key]
|
|
102
|
+
|
|
103
|
+
def __iter__(self) -> Iterator[str]:
|
|
104
|
+
return iter(self._load())
|
|
105
|
+
|
|
106
|
+
def __len__(self) -> int:
|
|
107
|
+
return len(self._load())
|
|
108
|
+
|
|
109
|
+
def __contains__(self, key: object) -> bool:
|
|
110
|
+
return key in self._load()
|
|
111
|
+
|
|
112
|
+
def __repr__(self) -> str:
|
|
113
|
+
state = "unloaded" if self._data is None else f"{len(self._data)} entries"
|
|
114
|
+
return f"<DerivedRegistry ({state})>"
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def loaded(self) -> bool:
|
|
118
|
+
return self._data is not None
|
|
119
|
+
|
|
120
|
+
def preload(self) -> "DerivedRegistry":
|
|
121
|
+
self._load()
|
|
122
|
+
return self
|