wandb 0.19.6rc4__py3-none-any.whl → 0.19.8__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.
- wandb/__init__.py +1 -1
- wandb/__init__.pyi +56 -6
- wandb/apis/public/_generated/__init__.py +21 -0
- wandb/apis/public/_generated/base.py +128 -0
- wandb/apis/public/_generated/enums.py +4 -0
- wandb/apis/public/_generated/input_types.py +4 -0
- wandb/apis/public/_generated/operations.py +15 -0
- wandb/apis/public/_generated/server_features_query.py +27 -0
- wandb/apis/public/_generated/typing_compat.py +14 -0
- wandb/apis/public/api.py +192 -6
- wandb/apis/public/artifacts.py +13 -45
- wandb/apis/public/registries.py +573 -0
- wandb/apis/public/utils.py +36 -0
- wandb/bin/gpu_stats +0 -0
- wandb/cli/cli.py +11 -20
- wandb/data_types.py +1 -1
- wandb/env.py +10 -0
- wandb/filesync/dir_watcher.py +2 -1
- wandb/proto/v3/wandb_internal_pb2.py +243 -222
- wandb/proto/v3/wandb_server_pb2.py +4 -4
- wandb/proto/v3/wandb_settings_pb2.py +2 -2
- wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v4/wandb_internal_pb2.py +226 -222
- wandb/proto/v4/wandb_server_pb2.py +4 -4
- wandb/proto/v4/wandb_settings_pb2.py +2 -2
- wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v5/wandb_internal_pb2.py +226 -222
- wandb/proto/v5/wandb_server_pb2.py +4 -4
- wandb/proto/v5/wandb_settings_pb2.py +2 -2
- wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
- wandb/sdk/artifacts/_graphql_fragments.py +126 -0
- wandb/sdk/artifacts/artifact.py +51 -95
- wandb/sdk/backend/backend.py +17 -6
- wandb/sdk/data_types/helper_types/bounding_boxes_2d.py +14 -6
- wandb/sdk/data_types/helper_types/image_mask.py +12 -6
- wandb/sdk/data_types/saved_model.py +35 -46
- wandb/sdk/data_types/video.py +7 -16
- wandb/sdk/interface/interface.py +87 -49
- wandb/sdk/interface/interface_queue.py +5 -15
- wandb/sdk/interface/interface_relay.py +7 -22
- wandb/sdk/interface/interface_shared.py +65 -136
- wandb/sdk/interface/interface_sock.py +3 -21
- wandb/sdk/interface/router.py +42 -68
- wandb/sdk/interface/router_queue.py +13 -11
- wandb/sdk/interface/router_relay.py +26 -13
- wandb/sdk/interface/router_sock.py +12 -16
- wandb/sdk/internal/handler.py +4 -3
- wandb/sdk/internal/internal_api.py +12 -1
- wandb/sdk/internal/sender.py +3 -19
- wandb/sdk/lib/apikey.py +87 -26
- wandb/sdk/lib/asyncio_compat.py +210 -0
- wandb/sdk/lib/console_capture.py +172 -0
- wandb/sdk/lib/progress.py +78 -16
- wandb/sdk/lib/redirect.py +102 -76
- wandb/sdk/lib/service_connection.py +37 -17
- wandb/sdk/lib/sock_client.py +6 -56
- wandb/sdk/mailbox/__init__.py +23 -0
- wandb/sdk/mailbox/mailbox.py +135 -0
- wandb/sdk/mailbox/mailbox_handle.py +127 -0
- wandb/sdk/mailbox/response_handle.py +167 -0
- wandb/sdk/mailbox/wait_with_progress.py +135 -0
- wandb/sdk/service/server_sock.py +9 -3
- wandb/sdk/service/streams.py +75 -78
- wandb/sdk/verify/verify.py +54 -2
- wandb/sdk/wandb_init.py +72 -75
- wandb/sdk/wandb_login.py +7 -4
- wandb/sdk/wandb_metadata.py +65 -34
- wandb/sdk/wandb_require.py +14 -8
- wandb/sdk/wandb_run.py +90 -97
- wandb/sdk/wandb_settings.py +10 -4
- wandb/sdk/wandb_setup.py +19 -8
- wandb/sdk/wandb_sync.py +2 -10
- wandb/util.py +3 -1
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/METADATA +2 -2
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/RECORD +78 -65
- wandb/sdk/interface/message_future.py +0 -27
- wandb/sdk/interface/message_future_poll.py +0 -50
- wandb/sdk/lib/mailbox.py +0 -442
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/WHEEL +0 -0
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/entry_points.txt +0 -0
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/licenses/LICENSE +0 -0
wandb/sdk/lib/mailbox.py
DELETED
@@ -1,442 +0,0 @@
|
|
1
|
-
"""mailbox."""
|
2
|
-
|
3
|
-
import secrets
|
4
|
-
import string
|
5
|
-
import threading
|
6
|
-
import time
|
7
|
-
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
|
8
|
-
|
9
|
-
from wandb.errors import Error
|
10
|
-
from wandb.proto import wandb_internal_pb2 as pb
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
from wandb.sdk.interface.interface_shared import InterfaceShared
|
14
|
-
|
15
|
-
|
16
|
-
def _generate_address(length: int = 12) -> str:
|
17
|
-
address = "".join(
|
18
|
-
secrets.choice(string.ascii_lowercase + string.digits) for i in range(length)
|
19
|
-
)
|
20
|
-
return address
|
21
|
-
|
22
|
-
|
23
|
-
class MailboxError(Error):
|
24
|
-
"""Generic Mailbox Exception."""
|
25
|
-
|
26
|
-
|
27
|
-
class _MailboxWaitAll:
|
28
|
-
_event: threading.Event
|
29
|
-
_lock: threading.Lock
|
30
|
-
_handles: List["MailboxHandle"]
|
31
|
-
_failed_handles: int
|
32
|
-
|
33
|
-
def __init__(self) -> None:
|
34
|
-
self._event = threading.Event()
|
35
|
-
self._lock = threading.Lock()
|
36
|
-
self._handles = []
|
37
|
-
self._failed_handles = 0
|
38
|
-
|
39
|
-
def notify(self) -> None:
|
40
|
-
with self._lock:
|
41
|
-
self._event.set()
|
42
|
-
|
43
|
-
def _add_handle(self, handle: "MailboxHandle") -> None:
|
44
|
-
handle._slot._set_wait_all(self)
|
45
|
-
self._handles.append(handle)
|
46
|
-
|
47
|
-
# set wait_all event if an event has already been set before added to wait_all
|
48
|
-
if handle._slot._event.is_set():
|
49
|
-
self._event.set()
|
50
|
-
|
51
|
-
@property
|
52
|
-
def active_handles(self) -> List["MailboxHandle"]:
|
53
|
-
return [h for h in self._handles if not h._is_failed]
|
54
|
-
|
55
|
-
@property
|
56
|
-
def active_handles_count(self) -> int:
|
57
|
-
return len(self.active_handles)
|
58
|
-
|
59
|
-
@property
|
60
|
-
def failed_handles_count(self) -> int:
|
61
|
-
return self._failed_handles
|
62
|
-
|
63
|
-
def _mark_handle_failed(self, handle: "MailboxHandle") -> None:
|
64
|
-
handle._mark_failed()
|
65
|
-
self._failed_handles += 1
|
66
|
-
|
67
|
-
def clear_handles(self) -> None:
|
68
|
-
for handle in self._handles:
|
69
|
-
handle._slot._clear_wait_all()
|
70
|
-
self._handles = []
|
71
|
-
|
72
|
-
def _wait(self, timeout: float) -> bool:
|
73
|
-
return self._event.wait(timeout=timeout)
|
74
|
-
|
75
|
-
def _get_and_clear(self, timeout: float) -> List["MailboxHandle"]:
|
76
|
-
found: List[MailboxHandle] = []
|
77
|
-
if self._wait(timeout=timeout):
|
78
|
-
with self._lock:
|
79
|
-
remove_handles = []
|
80
|
-
|
81
|
-
# Look through handles for triggered events
|
82
|
-
for handle in self._handles:
|
83
|
-
if handle._slot._event.is_set():
|
84
|
-
found.append(handle)
|
85
|
-
remove_handles.append(handle)
|
86
|
-
|
87
|
-
for handle in remove_handles:
|
88
|
-
self._handles.remove(handle)
|
89
|
-
|
90
|
-
self._event.clear()
|
91
|
-
return found
|
92
|
-
|
93
|
-
|
94
|
-
class _MailboxSlot:
|
95
|
-
_result: Optional[pb.Result]
|
96
|
-
_event: threading.Event
|
97
|
-
_lock: threading.Lock
|
98
|
-
_wait_all: Optional[_MailboxWaitAll]
|
99
|
-
_address: str
|
100
|
-
_abandoned: bool
|
101
|
-
|
102
|
-
def __init__(self, address: str) -> None:
|
103
|
-
self._result = None
|
104
|
-
self._event = threading.Event()
|
105
|
-
self._lock = threading.Lock()
|
106
|
-
self._address = address
|
107
|
-
self._wait_all = None
|
108
|
-
self._abandoned = False
|
109
|
-
|
110
|
-
def _set_wait_all(self, wait_all: _MailboxWaitAll) -> None:
|
111
|
-
assert not self._wait_all, "Only one caller can wait_all for a slot at a time"
|
112
|
-
self._wait_all = wait_all
|
113
|
-
|
114
|
-
def _clear_wait_all(self) -> None:
|
115
|
-
self._wait_all = None
|
116
|
-
|
117
|
-
def _wait(self, timeout: float) -> bool:
|
118
|
-
return self._event.wait(timeout=timeout)
|
119
|
-
|
120
|
-
def _get_and_clear(self, timeout: float) -> Tuple[Optional[pb.Result], bool]:
|
121
|
-
found = None
|
122
|
-
if self._wait(timeout=timeout):
|
123
|
-
with self._lock:
|
124
|
-
found = self._result
|
125
|
-
self._event.clear()
|
126
|
-
abandoned = self._abandoned
|
127
|
-
return found, abandoned
|
128
|
-
|
129
|
-
def _deliver(self, result: pb.Result) -> None:
|
130
|
-
with self._lock:
|
131
|
-
self._result = result
|
132
|
-
self._event.set()
|
133
|
-
|
134
|
-
if self._wait_all:
|
135
|
-
self._wait_all.notify()
|
136
|
-
|
137
|
-
def _notify_abandon(self) -> None:
|
138
|
-
self._abandoned = True
|
139
|
-
with self._lock:
|
140
|
-
self._event.set()
|
141
|
-
|
142
|
-
if self._wait_all:
|
143
|
-
self._wait_all.notify()
|
144
|
-
|
145
|
-
|
146
|
-
class MailboxProbe:
|
147
|
-
_result: Optional[pb.Result]
|
148
|
-
_handle: Optional["MailboxHandle"]
|
149
|
-
|
150
|
-
def __init__(self) -> None:
|
151
|
-
self._handle = None
|
152
|
-
self._result = None
|
153
|
-
|
154
|
-
def set_probe_result(self, result: pb.Result) -> None:
|
155
|
-
self._result = result
|
156
|
-
|
157
|
-
def get_probe_result(self) -> Optional[pb.Result]:
|
158
|
-
return self._result
|
159
|
-
|
160
|
-
def get_mailbox_handle(self) -> Optional["MailboxHandle"]:
|
161
|
-
return self._handle
|
162
|
-
|
163
|
-
def set_mailbox_handle(self, handle: "MailboxHandle") -> None:
|
164
|
-
self._handle = handle
|
165
|
-
|
166
|
-
|
167
|
-
class MailboxProgress:
|
168
|
-
_percent_done: float
|
169
|
-
_handle: "MailboxHandle"
|
170
|
-
_probe_handles: List[MailboxProbe]
|
171
|
-
_stopped: bool
|
172
|
-
|
173
|
-
def __init__(self, _handle: "MailboxHandle") -> None:
|
174
|
-
self._handle = _handle
|
175
|
-
self._percent_done = 0.0
|
176
|
-
self._probe_handles = []
|
177
|
-
self._stopped = False
|
178
|
-
|
179
|
-
@property
|
180
|
-
def percent_done(self) -> float:
|
181
|
-
return self._percent_done
|
182
|
-
|
183
|
-
def set_percent_done(self, percent_done: float) -> None:
|
184
|
-
self._percent_done = percent_done
|
185
|
-
|
186
|
-
def add_probe_handle(self, probe_handle: MailboxProbe) -> None:
|
187
|
-
self._probe_handles.append(probe_handle)
|
188
|
-
|
189
|
-
def get_probe_handles(self) -> List[MailboxProbe]:
|
190
|
-
return self._probe_handles
|
191
|
-
|
192
|
-
def wait_stop(self) -> None:
|
193
|
-
self._stopped = True
|
194
|
-
|
195
|
-
@property
|
196
|
-
def _is_stopped(self) -> bool:
|
197
|
-
return self._stopped
|
198
|
-
|
199
|
-
|
200
|
-
class MailboxProgressAll:
|
201
|
-
_progress_handles: List[MailboxProgress]
|
202
|
-
|
203
|
-
def __init__(self) -> None:
|
204
|
-
self._progress_handles = []
|
205
|
-
|
206
|
-
def add_progress_handle(self, progress_handle: MailboxProgress) -> None:
|
207
|
-
self._progress_handles.append(progress_handle)
|
208
|
-
|
209
|
-
def get_progress_handles(self) -> List[MailboxProgress]:
|
210
|
-
# only return progress handles for not failed handles
|
211
|
-
return [ph for ph in self._progress_handles if not ph._handle._is_failed]
|
212
|
-
|
213
|
-
|
214
|
-
class MailboxHandle:
|
215
|
-
_mailbox: "Mailbox"
|
216
|
-
_slot: _MailboxSlot
|
217
|
-
_on_probe: Optional[Callable[[MailboxProbe], None]]
|
218
|
-
_on_progress: Optional[Callable[[MailboxProgress], None]]
|
219
|
-
_interface: Optional["InterfaceShared"]
|
220
|
-
_keepalive: bool
|
221
|
-
_failed: bool
|
222
|
-
|
223
|
-
def __init__(self, mailbox: "Mailbox", slot: _MailboxSlot) -> None:
|
224
|
-
self._mailbox = mailbox
|
225
|
-
self._slot = slot
|
226
|
-
self._on_probe = None
|
227
|
-
self._on_progress = None
|
228
|
-
self._interface = None
|
229
|
-
self._keepalive = False
|
230
|
-
self._failed = False
|
231
|
-
|
232
|
-
def add_probe(self, on_probe: Callable[[MailboxProbe], None]) -> None:
|
233
|
-
self._on_probe = on_probe
|
234
|
-
|
235
|
-
def add_progress(self, on_progress: Callable[[MailboxProgress], None]) -> None:
|
236
|
-
self._on_progress = on_progress
|
237
|
-
|
238
|
-
def _time(self) -> float:
|
239
|
-
return time.monotonic()
|
240
|
-
|
241
|
-
def wait( # noqa: C901
|
242
|
-
self,
|
243
|
-
*,
|
244
|
-
timeout: float,
|
245
|
-
on_probe: Optional[Callable[[MailboxProbe], None]] = None,
|
246
|
-
on_progress: Optional[Callable[[MailboxProgress], None]] = None,
|
247
|
-
release: bool = True,
|
248
|
-
cancel: bool = False,
|
249
|
-
) -> Optional[pb.Result]:
|
250
|
-
probe_handle: Optional[MailboxProbe] = None
|
251
|
-
progress_handle: Optional[MailboxProgress] = None
|
252
|
-
found: Optional[pb.Result] = None
|
253
|
-
start_time = self._time()
|
254
|
-
percent_done = 0.0
|
255
|
-
progress_sent = False
|
256
|
-
wait_timeout = 1.0
|
257
|
-
if timeout >= 0:
|
258
|
-
wait_timeout = min(timeout, wait_timeout)
|
259
|
-
|
260
|
-
on_progress = on_progress or self._on_progress
|
261
|
-
if on_progress:
|
262
|
-
progress_handle = MailboxProgress(_handle=self)
|
263
|
-
|
264
|
-
on_probe = on_probe or self._on_probe
|
265
|
-
if on_probe:
|
266
|
-
probe_handle = MailboxProbe()
|
267
|
-
if progress_handle:
|
268
|
-
progress_handle.add_probe_handle(probe_handle)
|
269
|
-
|
270
|
-
while True:
|
271
|
-
if self._keepalive and self._interface:
|
272
|
-
if self._interface._transport_keepalive_failed():
|
273
|
-
raise MailboxError("transport failed")
|
274
|
-
|
275
|
-
found, abandoned = self._slot._get_and_clear(timeout=wait_timeout)
|
276
|
-
if found:
|
277
|
-
# Always update progress to 100% when done
|
278
|
-
if on_progress and progress_handle and progress_sent:
|
279
|
-
progress_handle.set_percent_done(100)
|
280
|
-
on_progress(progress_handle)
|
281
|
-
break
|
282
|
-
if abandoned:
|
283
|
-
break
|
284
|
-
now = self._time()
|
285
|
-
if timeout >= 0:
|
286
|
-
if now >= start_time + timeout:
|
287
|
-
# todo: communicate that we timed out
|
288
|
-
break
|
289
|
-
if on_probe and probe_handle:
|
290
|
-
on_probe(probe_handle)
|
291
|
-
if on_progress and progress_handle:
|
292
|
-
if timeout > 0:
|
293
|
-
percent_done = min((now - start_time) / timeout, 1.0)
|
294
|
-
progress_handle.set_percent_done(percent_done)
|
295
|
-
on_progress(progress_handle)
|
296
|
-
if progress_handle._is_stopped:
|
297
|
-
break
|
298
|
-
progress_sent = True
|
299
|
-
if not found and cancel:
|
300
|
-
self._cancel()
|
301
|
-
if release:
|
302
|
-
self._release()
|
303
|
-
return found
|
304
|
-
|
305
|
-
def _cancel(self) -> None:
|
306
|
-
mailbox_slot = self.address
|
307
|
-
if self._interface:
|
308
|
-
self._interface.publish_cancel(mailbox_slot)
|
309
|
-
|
310
|
-
def _release(self) -> None:
|
311
|
-
self._mailbox._release_slot(self.address)
|
312
|
-
|
313
|
-
def abandon(self) -> None:
|
314
|
-
self._slot._notify_abandon()
|
315
|
-
self._release()
|
316
|
-
|
317
|
-
@property
|
318
|
-
def _is_failed(self) -> bool:
|
319
|
-
return self._failed
|
320
|
-
|
321
|
-
def _mark_failed(self) -> None:
|
322
|
-
self._failed = True
|
323
|
-
|
324
|
-
@property
|
325
|
-
def address(self) -> str:
|
326
|
-
return self._slot._address
|
327
|
-
|
328
|
-
|
329
|
-
class Mailbox:
|
330
|
-
_slots: Dict[str, _MailboxSlot]
|
331
|
-
_keepalive: bool
|
332
|
-
|
333
|
-
def __init__(self) -> None:
|
334
|
-
self._slots = {}
|
335
|
-
self._keepalive = False
|
336
|
-
|
337
|
-
def enable_keepalive(self) -> None:
|
338
|
-
self._keepalive = True
|
339
|
-
|
340
|
-
def _time(self) -> float:
|
341
|
-
return time.monotonic()
|
342
|
-
|
343
|
-
def wait_all(
|
344
|
-
self,
|
345
|
-
handles: List[MailboxHandle],
|
346
|
-
*,
|
347
|
-
timeout: float,
|
348
|
-
on_progress_all: Optional[Callable[[MailboxProgressAll], None]] = None,
|
349
|
-
) -> bool:
|
350
|
-
progress_all_handle: Optional[MailboxProgressAll] = None
|
351
|
-
|
352
|
-
if on_progress_all:
|
353
|
-
progress_all_handle = MailboxProgressAll()
|
354
|
-
|
355
|
-
wait_all = _MailboxWaitAll()
|
356
|
-
for handle in handles:
|
357
|
-
wait_all._add_handle(handle)
|
358
|
-
if progress_all_handle and handle._on_progress:
|
359
|
-
progress_handle = MailboxProgress(_handle=handle)
|
360
|
-
if handle._on_probe:
|
361
|
-
probe_handle = MailboxProbe()
|
362
|
-
progress_handle.add_probe_handle(probe_handle)
|
363
|
-
progress_all_handle.add_progress_handle(progress_handle)
|
364
|
-
|
365
|
-
start_time = self._time()
|
366
|
-
|
367
|
-
while wait_all.active_handles_count > 0:
|
368
|
-
# Make sure underlying interfaces are still up
|
369
|
-
if self._keepalive:
|
370
|
-
for handle in wait_all.active_handles:
|
371
|
-
if not handle._interface:
|
372
|
-
continue
|
373
|
-
if handle._interface._transport_keepalive_failed():
|
374
|
-
wait_all._mark_handle_failed(handle)
|
375
|
-
|
376
|
-
# if there are no valid handles left, either break or raise exception
|
377
|
-
if not wait_all.active_handles_count:
|
378
|
-
if wait_all.failed_handles_count:
|
379
|
-
wait_all.clear_handles()
|
380
|
-
raise MailboxError("transport failed")
|
381
|
-
break
|
382
|
-
|
383
|
-
# wait for next event
|
384
|
-
wait_all._get_and_clear(timeout=1)
|
385
|
-
|
386
|
-
# TODO: we can do more careful timekeeping and not run probes and progress
|
387
|
-
# indications until a full second elapses in the case where we found a wait_all
|
388
|
-
# event. Extra probes should be ok for now.
|
389
|
-
|
390
|
-
if progress_all_handle and on_progress_all:
|
391
|
-
# Run all probe handles
|
392
|
-
for progress_handle in progress_all_handle.get_progress_handles():
|
393
|
-
for probe_handle in progress_handle.get_probe_handles():
|
394
|
-
if (
|
395
|
-
progress_handle._handle
|
396
|
-
and progress_handle._handle._on_probe
|
397
|
-
):
|
398
|
-
progress_handle._handle._on_probe(probe_handle)
|
399
|
-
|
400
|
-
on_progress_all(progress_all_handle)
|
401
|
-
|
402
|
-
now = self._time()
|
403
|
-
if timeout >= 0 and now >= start_time + timeout:
|
404
|
-
break
|
405
|
-
|
406
|
-
return wait_all.active_handles_count == 0
|
407
|
-
|
408
|
-
def deliver(self, result: pb.Result) -> None:
|
409
|
-
mailbox = result.control.mailbox_slot
|
410
|
-
slot = self._slots.get(mailbox)
|
411
|
-
if not slot:
|
412
|
-
return
|
413
|
-
slot._deliver(result)
|
414
|
-
|
415
|
-
def _allocate_slot(self) -> _MailboxSlot:
|
416
|
-
address = _generate_address()
|
417
|
-
slot = _MailboxSlot(address=address)
|
418
|
-
self._slots[address] = slot
|
419
|
-
return slot
|
420
|
-
|
421
|
-
def _release_slot(self, address: str) -> None:
|
422
|
-
self._slots.pop(address, None)
|
423
|
-
|
424
|
-
def get_handle(self) -> MailboxHandle:
|
425
|
-
slot = self._allocate_slot()
|
426
|
-
handle = MailboxHandle(mailbox=self, slot=slot)
|
427
|
-
return handle
|
428
|
-
|
429
|
-
def _deliver_record(
|
430
|
-
self, record: pb.Record, interface: "InterfaceShared"
|
431
|
-
) -> MailboxHandle:
|
432
|
-
handle = self.get_handle()
|
433
|
-
handle._interface = interface
|
434
|
-
handle._keepalive = self._keepalive
|
435
|
-
record.control.mailbox_slot = handle.address
|
436
|
-
try:
|
437
|
-
interface._publish(record)
|
438
|
-
except Exception:
|
439
|
-
interface._transport_mark_failed()
|
440
|
-
raise
|
441
|
-
interface._transport_mark_success()
|
442
|
-
return handle
|
File without changes
|
File without changes
|
File without changes
|