bdo-toolkit 1.0.0__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.
Files changed (48) hide show
  1. bdo_toolkit/__init__.py +87 -0
  2. bdo_toolkit/_async_sessions.py +651 -0
  3. bdo_toolkit/_capture_backend.py +194 -0
  4. bdo_toolkit/_capture_options.py +68 -0
  5. bdo_toolkit/_capture_runtime.py +626 -0
  6. bdo_toolkit/_deposit_origin.py +1599 -0
  7. bdo_toolkit/_engine.py +327 -0
  8. bdo_toolkit/_framing.py +904 -0
  9. bdo_toolkit/_profile_runtime.py +157 -0
  10. bdo_toolkit/_protocol.py +386 -0
  11. bdo_toolkit/_reassembly.py +654 -0
  12. bdo_toolkit/_specs.py +285 -0
  13. bdo_toolkit/_storage_destination_validation.py +167 -0
  14. bdo_toolkit/_storage_hydration.py +241 -0
  15. bdo_toolkit/_version.py +3 -0
  16. bdo_toolkit/calibration.py +3223 -0
  17. bdo_toolkit/capture.py +1713 -0
  18. bdo_toolkit/character_state.py +3506 -0
  19. bdo_toolkit/cli.py +948 -0
  20. bdo_toolkit/diagnostics.py +51 -0
  21. bdo_toolkit/events.py +214 -0
  22. bdo_toolkit/filters.py +105 -0
  23. bdo_toolkit/item_state.py +48 -0
  24. bdo_toolkit/origin_learning.py +779 -0
  25. bdo_toolkit/profiles.py +370 -0
  26. bdo_toolkit/py.typed +1 -0
  27. bdo_toolkit/remote_profiles.py +358 -0
  28. bdo_toolkit/solare/__init__.py +50 -0
  29. bdo_toolkit/solare/_constants.py +94 -0
  30. bdo_toolkit/solare/_detail_learning.py +1437 -0
  31. bdo_toolkit/solare/_details.py +796 -0
  32. bdo_toolkit/solare/_discovery.py +1212 -0
  33. bdo_toolkit/solare/_live_tracker.py +472 -0
  34. bdo_toolkit/solare/_replay_capture.py +182 -0
  35. bdo_toolkit/solare/_result.py +441 -0
  36. bdo_toolkit/solare/_scanner.py +203 -0
  37. bdo_toolkit/solare/_validation.py +11 -0
  38. bdo_toolkit/solare/async_session.py +444 -0
  39. bdo_toolkit/solare/models.py +806 -0
  40. bdo_toolkit/solare/replay.py +62 -0
  41. bdo_toolkit/solare/session.py +1051 -0
  42. bdo_toolkit/writers.py +30 -0
  43. bdo_toolkit-1.0.0.dist-info/METADATA +143 -0
  44. bdo_toolkit-1.0.0.dist-info/RECORD +48 -0
  45. bdo_toolkit-1.0.0.dist-info/WHEEL +5 -0
  46. bdo_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  47. bdo_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  48. bdo_toolkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,444 @@
1
+ """Asyncio facade for the synchronous Solare live session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections import deque
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from functools import partial
9
+ from pathlib import Path
10
+ from types import TracebackType
11
+ from typing import AsyncIterator, Callable, Optional, TypeVar
12
+
13
+ from bdo_toolkit._capture_options import PacketCaptureOptions
14
+ from bdo_toolkit._capture_runtime import _attach_cleanup_owner
15
+ from ._constants import LIVE_CAPTURE_BUFFER_BYTES
16
+ from .models import (
17
+ SolareCaptureEndpoint,
18
+ SolareCaptureResult,
19
+ SolareUpdate,
20
+ SolareUpdateKind,
21
+ )
22
+ from .session import LiveSolareSession, _validate_timeout
23
+
24
+
25
+ T = TypeVar("T")
26
+
27
+ # Stay above the 15.625 ms monotonic-clock quantum used by Windows CPython
28
+ # 3.10 so terminal cleanup polling yields instead of busy-spinning.
29
+ _TERMINAL_SHUTDOWN_POLL_SECONDS = 0.05
30
+
31
+
32
+ async def _settle(future: asyncio.Future[T]) -> T:
33
+ while not future.done():
34
+ try:
35
+ await asyncio.wait((future,))
36
+ except asyncio.CancelledError:
37
+ continue
38
+ return future.result()
39
+
40
+
41
+ async def _await_preserving_future(future: asyncio.Future[T]) -> T:
42
+ """Await a worker future without cancelling it or creating a shield."""
43
+
44
+ await asyncio.wait((future,))
45
+ return future.result()
46
+
47
+
48
+ class AsyncLiveSolareSession:
49
+ """Awaitable single-consumer facade over :class:`LiveSolareSession`."""
50
+
51
+ def __init__(
52
+ self,
53
+ *,
54
+ capture_options: Optional[PacketCaptureOptions] = None,
55
+ save_pcap: str | Path | None = None,
56
+ stop_on_complete: bool = True,
57
+ retain_raw_extensions: bool = False,
58
+ on_update: Optional[Callable[[SolareUpdate], None]] = None,
59
+ capture_buffer_bytes: int = LIVE_CAPTURE_BUFFER_BYTES,
60
+ ) -> None:
61
+ self._session = LiveSolareSession(
62
+ capture_options=capture_options,
63
+ save_pcap=save_pcap,
64
+ stop_on_complete=stop_on_complete,
65
+ retain_raw_extensions=retain_raw_extensions,
66
+ on_update=on_update,
67
+ capture_buffer_bytes=capture_buffer_bytes,
68
+ )
69
+ self._executor = ThreadPoolExecutor(
70
+ # One progress poll and one completion wait may coexist. Keep a
71
+ # third worker reserved so stop/cancellation can always wake both.
72
+ max_workers=3,
73
+ thread_name_prefix="bdo-toolkit-solare-async",
74
+ )
75
+ self._closed = False
76
+ self._started = False
77
+ self._start_active = False
78
+ self._poll_active = False
79
+ self._wait_active = False
80
+ self._pending_updates: deque[SolareUpdate] = deque()
81
+ self._stop_future: Optional[asyncio.Future[SolareCaptureResult]] = None
82
+ self._terminal_shutdown_task: Optional[asyncio.Task[None]] = None
83
+
84
+ @property
85
+ def running(self) -> bool:
86
+ return self._session.running
87
+
88
+ @property
89
+ def stopped(self) -> bool:
90
+ return self._session.stopped
91
+
92
+ @property
93
+ def cleanup_incomplete(self) -> bool:
94
+ """Whether native capture cleanup remains owned for another attempt."""
95
+
96
+ return self._session.cleanup_incomplete
97
+
98
+ @property
99
+ def result(self) -> Optional[SolareCaptureResult]:
100
+ return self._session.result
101
+
102
+ @property
103
+ def error(self) -> Optional[BaseException]:
104
+ return self._session.error
105
+
106
+ @property
107
+ def stop_reason(self) -> Optional[str]:
108
+ return self._session.stop_reason
109
+
110
+ @property
111
+ def endpoint(self) -> Optional[SolareCaptureEndpoint]:
112
+ return self._session.endpoint
113
+
114
+ def raise_if_failed(self) -> None:
115
+ self._session.raise_if_failed()
116
+
117
+ def _submit(self, function: Callable[[], T]) -> asyncio.Future[T]:
118
+ if self._closed:
119
+ raise RuntimeError("async live Solare session is already closed")
120
+ return asyncio.get_running_loop().run_in_executor(self._executor, function)
121
+
122
+ def _shutdown(self) -> None:
123
+ if not self._closed:
124
+ self._executor.shutdown(wait=False, cancel_futures=False)
125
+ self._closed = True
126
+
127
+ def _schedule_terminal_shutdown(self) -> None:
128
+ task = self._terminal_shutdown_task
129
+ if task is not None and not task.done():
130
+ return
131
+
132
+ async def close_after_worker_and_calls_settle() -> None:
133
+ while not self._session.stopped and not self._closed:
134
+ await asyncio.sleep(_TERMINAL_SHUTDOWN_POLL_SECONDS)
135
+ while not self._closed and (
136
+ self._start_active
137
+ or self._poll_active
138
+ or self._wait_active
139
+ or (
140
+ self._stop_future is not None
141
+ and not self._stop_future.done()
142
+ )
143
+ ):
144
+ await asyncio.sleep(_TERMINAL_SHUTDOWN_POLL_SECONDS)
145
+ self._shutdown()
146
+
147
+ self._terminal_shutdown_task = asyncio.create_task(
148
+ close_after_worker_and_calls_settle()
149
+ )
150
+
151
+ def _observe_polled_update(
152
+ self,
153
+ update: Optional[SolareUpdate],
154
+ ) -> Optional[SolareUpdate]:
155
+ if update is not None and update.kind is SolareUpdateKind.FINISHED:
156
+ self._schedule_terminal_shutdown()
157
+ elif self._session.stopped:
158
+ self._shutdown()
159
+ return update
160
+
161
+ async def start(self) -> None:
162
+ """Start capture without blocking the event-loop thread."""
163
+
164
+ if self._started or self._start_active:
165
+ raise RuntimeError("live Solare session was already started")
166
+ self._start_active = True
167
+ try:
168
+ try:
169
+ future = self._submit(self._session.start)
170
+ await _await_preserving_future(future)
171
+ except asyncio.CancelledError as exc:
172
+ started = False
173
+ stop_future: asyncio.Future[SolareCaptureResult] | None = None
174
+ try:
175
+ await _settle(future)
176
+ started = True
177
+ except BaseException:
178
+ pass
179
+ if started or self._session.cleanup_incomplete:
180
+ self._started = True
181
+ try:
182
+ stop_future = self._ensure_stop_future()
183
+ await _settle(stop_future)
184
+ except BaseException:
185
+ pass
186
+ if self._session.cleanup_incomplete:
187
+ if (
188
+ stop_future is not None
189
+ and self._stop_future is stop_future
190
+ ):
191
+ self._stop_future = None
192
+ _attach_cleanup_owner(
193
+ exc,
194
+ self,
195
+ context="async live Solare startup",
196
+ )
197
+ else:
198
+ self._shutdown()
199
+ raise
200
+ except BaseException as exc:
201
+ if self._session.cleanup_incomplete:
202
+ # The synchronous session deliberately retains the native
203
+ # callback owners so a later stop() can finish cleanup.
204
+ self._started = True
205
+ _attach_cleanup_owner(
206
+ exc,
207
+ self,
208
+ context="async live Solare startup",
209
+ )
210
+ else:
211
+ self._shutdown()
212
+ raise
213
+ self._started = True
214
+ finally:
215
+ self._start_active = False
216
+
217
+ def _ensure_stop_future(self) -> asyncio.Future[SolareCaptureResult]:
218
+ if self._stop_future is None:
219
+ self._stop_future = self._submit(self._session.stop)
220
+ return self._stop_future
221
+
222
+ def _inside_update_callback(self) -> bool:
223
+ predicate = getattr(self._session, "_inside_update_callback", None)
224
+ return bool(predicate is not None and predicate())
225
+
226
+ async def stop(self) -> SolareCaptureResult:
227
+ """Stop capture and return the structurally classified result."""
228
+
229
+ if self._inside_update_callback():
230
+ raise RuntimeError(
231
+ "stop() cannot block inside on_update; use "
232
+ "request_stop() instead"
233
+ )
234
+ if not self._started:
235
+ raise RuntimeError("live Solare session was not started")
236
+ if self._session.stopped:
237
+ try:
238
+ self._session.raise_if_failed()
239
+ result = self._session.result
240
+ if result is None:
241
+ raise RuntimeError(
242
+ "live Solare session stopped without a result"
243
+ )
244
+ return result
245
+ finally:
246
+ self._shutdown()
247
+
248
+ future = self._ensure_stop_future()
249
+ try:
250
+ result = await _await_preserving_future(future)
251
+ except asyncio.CancelledError:
252
+ try:
253
+ await _settle(future)
254
+ except BaseException:
255
+ pass
256
+ if self._session.cleanup_incomplete:
257
+ if self._stop_future is future:
258
+ self._stop_future = None
259
+ else:
260
+ self._shutdown()
261
+ raise
262
+ except BaseException:
263
+ if self._session.cleanup_incomplete:
264
+ if self._stop_future is future:
265
+ self._stop_future = None
266
+ else:
267
+ self._shutdown()
268
+ raise
269
+ self._shutdown()
270
+ return result
271
+
272
+ def request_stop(self) -> None:
273
+ """Request orderly shutdown without blocking the event loop."""
274
+
275
+ ready_callback_active = self._start_active and self._session.running
276
+ if not (self._started or ready_callback_active):
277
+ raise RuntimeError("live Solare session was not started")
278
+ self._session.request_stop()
279
+
280
+ async def wait(
281
+ self, timeout: Optional[float] = None
282
+ ) -> Optional[SolareCaptureResult]:
283
+ """Await automatic completion or a timeout.
284
+
285
+ After completed stop, ``timeout`` is ignored and terminal state is
286
+ returned immediately.
287
+ """
288
+
289
+ if self._inside_update_callback():
290
+ raise RuntimeError(
291
+ "wait() cannot block inside on_update; use request_stop() "
292
+ "and wait after the callback returns"
293
+ )
294
+ if not self._started:
295
+ raise RuntimeError("live Solare session was not started")
296
+ if self._wait_active:
297
+ raise RuntimeError("async live Solare session supports one waiter")
298
+ self._wait_active = True
299
+ try:
300
+ if self._session.stopped:
301
+ self._shutdown()
302
+ return self._session.wait(timeout=0)
303
+ future = self._submit(partial(self._session.wait, timeout))
304
+ try:
305
+ result = await _await_preserving_future(future)
306
+ except asyncio.CancelledError:
307
+ stop_future: asyncio.Future[SolareCaptureResult] | None = None
308
+ try:
309
+ stop_future = self._ensure_stop_future()
310
+ await _settle(stop_future)
311
+ except BaseException:
312
+ pass
313
+ try:
314
+ await _settle(future)
315
+ except BaseException:
316
+ pass
317
+ if self._session.cleanup_incomplete:
318
+ if (
319
+ stop_future is not None
320
+ and self._stop_future is stop_future
321
+ ):
322
+ self._stop_future = None
323
+ else:
324
+ self._shutdown()
325
+ raise
326
+ except BaseException:
327
+ if (
328
+ not self._session.cleanup_incomplete
329
+ and (self._session.stopped or self._session.error is not None)
330
+ ):
331
+ self._shutdown()
332
+ raise
333
+ if result is not None or self._session.stopped:
334
+ self._shutdown()
335
+ return result
336
+ finally:
337
+ self._wait_active = False
338
+
339
+ async def poll(self, timeout: Optional[float] = None) -> Optional[SolareUpdate]:
340
+ """Await one structured progress update.
341
+
342
+ After completed stop, ``timeout`` is ignored while progress drains.
343
+ """
344
+
345
+ if self._inside_update_callback():
346
+ raise RuntimeError(
347
+ "poll() cannot consume updates inside on_update; consume "
348
+ "them outside the callback"
349
+ )
350
+ if not self._started:
351
+ raise RuntimeError("live Solare session was not started")
352
+ if self._poll_active:
353
+ raise RuntimeError("async live Solare session supports one consumer")
354
+ stopped_at_entry = self._session.stopped
355
+ if not stopped_at_entry:
356
+ _validate_timeout(timeout, name="timeout")
357
+ self._poll_active = True
358
+ try:
359
+ if self._pending_updates:
360
+ return self._observe_polled_update(
361
+ self._pending_updates.popleft()
362
+ )
363
+ if self._session.stopped:
364
+ self._shutdown()
365
+ return self._session.poll(timeout=0)
366
+ future = self._submit(partial(self._session.poll, timeout))
367
+ try:
368
+ update = await _await_preserving_future(future)
369
+ except asyncio.CancelledError:
370
+ stop_future: asyncio.Future[SolareCaptureResult] | None = None
371
+ try:
372
+ stop_future = self._ensure_stop_future()
373
+ await _settle(stop_future)
374
+ except BaseException:
375
+ pass
376
+ try:
377
+ settled_update = await _settle(future)
378
+ except BaseException:
379
+ pass
380
+ else:
381
+ if settled_update is not None:
382
+ self._pending_updates.append(settled_update)
383
+ if self._session.cleanup_incomplete:
384
+ if (
385
+ stop_future is not None
386
+ and self._stop_future is stop_future
387
+ ):
388
+ self._stop_future = None
389
+ else:
390
+ self._shutdown()
391
+ raise
392
+ except BaseException:
393
+ if (
394
+ not self._session.cleanup_incomplete
395
+ and (self._session.stopped or self._session.error is not None)
396
+ ):
397
+ self._shutdown()
398
+ raise
399
+ return self._observe_polled_update(update)
400
+ finally:
401
+ self._poll_active = False
402
+
403
+ async def updates(self) -> AsyncIterator[SolareUpdate]:
404
+ while True:
405
+ update = await self.poll()
406
+ if update is None:
407
+ return
408
+ yield update
409
+
410
+ def __aiter__(self) -> AsyncIterator[SolareUpdate]:
411
+ return self.updates()
412
+
413
+ async def __aenter__(self) -> "AsyncLiveSolareSession":
414
+ await self.start()
415
+ return self
416
+
417
+ async def __aexit__(
418
+ self,
419
+ exc_type: type[BaseException] | None,
420
+ exc_value: BaseException | None,
421
+ traceback: TracebackType | None,
422
+ ) -> None:
423
+ try:
424
+ if exc_value is None:
425
+ await self.stop()
426
+ else:
427
+ try:
428
+ await self.stop()
429
+ except BaseException as cleanup_error:
430
+ # Preserve the exception already escaping the async block.
431
+ if self.cleanup_incomplete:
432
+ _attach_cleanup_owner(
433
+ exc_value,
434
+ self,
435
+ context="async live Solare context",
436
+ )
437
+ if hasattr(exc_value, "add_note"):
438
+ exc_value.add_note(
439
+ "async live Solare context cleanup also failed: "
440
+ f"{cleanup_error!r}"
441
+ )
442
+ finally:
443
+ if not self._session.cleanup_incomplete:
444
+ self._shutdown()