bdo-toolkit 1.0.3__tar.gz → 1.0.4__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 (130) hide show
  1. {bdo_toolkit-1.0.3/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.4}/PKG-INFO +2 -1
  2. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/README.md +1 -0
  3. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_async_sessions.py +81 -23
  4. bdo_toolkit-1.0.4/src/bdo_toolkit/_async_utils.py +25 -0
  5. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/__init__.py +4 -0
  6. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_constants.py +42 -0
  7. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_formatting.py +65 -0
  8. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_records.py +711 -0
  9. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/analysis.py +610 -0
  10. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/capture.py +691 -0
  11. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/companions.py +413 -0
  12. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/live.py +252 -0
  13. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/models.py +419 -0
  14. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/observations.py +122 -0
  15. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/persistence.py +326 -0
  16. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/progress.py +113 -0
  17. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/validation.py +94 -0
  18. bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/workflow.py +140 -0
  19. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_backend.py +15 -0
  20. bdo_toolkit-1.0.4/src/bdo_toolkit/_deposit_origin.py +16 -0
  21. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_framing.py +54 -45
  22. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/__init__.py +8 -0
  23. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/_constants.py +28 -0
  24. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/_records.py +211 -0
  25. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/assembly.py +496 -0
  26. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/formatting.py +241 -0
  27. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/inventory.py +609 -0
  28. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/models.py +669 -0
  29. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/session.py +405 -0
  30. bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/storage.py +980 -0
  31. bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/__init__.py +4 -0
  32. bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/discovery.py +78 -0
  33. bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/manual.py +587 -0
  34. bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/models.py +202 -0
  35. bdo_toolkit-1.0.3/src/bdo_toolkit/_deposit_origin.py → bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/tracker.py +39 -783
  36. bdo_toolkit-1.0.4/src/bdo_toolkit/_profile_io.py +43 -0
  37. bdo_toolkit-1.0.4/src/bdo_toolkit/_record_geometry.py +39 -0
  38. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_version.py +1 -1
  39. bdo_toolkit-1.0.4/src/bdo_toolkit/calibration.py +98 -0
  40. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/capture.py +119 -38
  41. bdo_toolkit-1.0.4/src/bdo_toolkit/character_state.py +56 -0
  42. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/item_state.py +3 -3
  43. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/origin_learning.py +2 -43
  44. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/remote_profiles.py +1 -13
  45. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/async_session.py +4 -16
  46. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/session.py +2 -6
  47. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4/src/bdo_toolkit.egg-info}/PKG-INFO +2 -1
  48. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/SOURCES.txt +38 -1
  49. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_async_sessions.py +3 -13
  50. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration.py +408 -339
  51. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration_conveniences.py +5 -4
  52. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration_lifecycle.py +50 -20
  53. bdo_toolkit-1.0.4/tests/test_calibration_persistence.py +492 -0
  54. bdo_toolkit-1.0.4/tests/test_calibration_progress.py +758 -0
  55. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration_retention.py +8 -6
  56. bdo_toolkit-1.0.4/tests/test_capture_replay.py +131 -0
  57. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_capture_runtime.py +2 -13
  58. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_character_state.py +56 -49
  59. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_decoder_diagnostics.py +2 -0
  60. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_deposit_origin.py +202 -18
  61. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_direction.py +20 -20
  62. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_dynamic_storage_calibration_matrix.py +32 -15
  63. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_examples_and_writers.py +2 -0
  64. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_explicit_profile.py +3 -4
  65. bdo_toolkit-1.0.4/tests/test_fixture_catalog.py +92 -0
  66. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_fixture_regressions.py +2 -2
  67. bdo_toolkit-1.0.4/tests/test_framing.py +671 -0
  68. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_initial_load_storage.py +3 -3
  69. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_inventory_snapshots.py +13 -9
  70. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_item_state.py +16 -0
  71. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_live_capture_session.py +59 -16
  72. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_origin_learning.py +2 -2
  73. bdo_toolkit-1.0.4/tests/test_package_layout.py +17 -0
  74. bdo_toolkit-1.0.4/tests/test_profile_validation.py +319 -0
  75. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_reassembly.py +38 -6
  76. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_learning.py +15 -31
  77. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_real_captures.py +28 -29
  78. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_destination_validation.py +5 -7
  79. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_unknown_destination_calibration.py +2 -2
  80. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_toolkit_api.py +43 -14
  81. bdo_toolkit-1.0.3/src/bdo_toolkit/calibration.py +0 -3269
  82. bdo_toolkit-1.0.3/src/bdo_toolkit/character_state.py +0 -3520
  83. bdo_toolkit-1.0.3/tests/test_hardening.py +0 -1567
  84. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/LICENSE +0 -0
  85. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/pyproject.toml +0 -0
  86. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/setup.cfg +0 -0
  87. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/__init__.py +0 -0
  88. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_options.py +0 -0
  89. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_runtime.py +0 -0
  90. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_engine.py +0 -0
  91. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_profile_runtime.py +0 -0
  92. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_protocol.py +0 -0
  93. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_reassembly.py +0 -0
  94. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_specs.py +0 -0
  95. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
  96. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_hydration.py +0 -0
  97. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/cli.py +0 -0
  98. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/diagnostics.py +0 -0
  99. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/events.py +0 -0
  100. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/filters.py +0 -0
  101. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/profiles.py +0 -0
  102. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/py.typed +0 -0
  103. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/__init__.py +0 -0
  104. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_constants.py +0 -0
  105. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
  106. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_details.py +0 -0
  107. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_discovery.py +0 -0
  108. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
  109. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
  110. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_result.py +0 -0
  111. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_scanner.py +0 -0
  112. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_validation.py +0 -0
  113. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/models.py +0 -0
  114. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/replay.py +0 -0
  115. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/writers.py +0 -0
  116. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
  117. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
  118. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/requires.txt +0 -0
  119. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
  120. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_capture_backend.py +0 -0
  121. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_cli.py +0 -0
  122. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_engine.py +0 -0
  123. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_remote_profiles.py +0 -0
  124. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_api.py +0 -0
  125. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_details.py +0 -0
  126. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_learning_adversarial.py +0 -0
  127. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_replay.py +0 -0
  128. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_session.py +0 -0
  129. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_events.py +0 -0
  130. {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_hydration_tracker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bdo-toolkit
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Passive, read-only BDO packet parsing toolkit for app developers.
5
5
  Author: bdo-toolkit contributors
6
6
  License-Expression: MIT
@@ -104,6 +104,7 @@ installed with the Python wheel.
104
104
  | Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
105
105
  | Capture inventory and town storage on character load | [`examples/live_character_load_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_character_load_snapshot.py) |
106
106
  | Rebuild an item profile after a patch | [`examples/live_calibrate_profile.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibrate_profile.py) |
107
+ | Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
107
108
  | Capture an Arena of Solare leaderboard load | [`examples/solare_live_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/solare_live_snapshot.py) |
108
109
 
109
110
  See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
@@ -77,6 +77,7 @@ installed with the Python wheel.
77
77
  | Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
78
78
  | Capture inventory and town storage on character load | [`examples/live_character_load_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_character_load_snapshot.py) |
79
79
  | Rebuild an item profile after a patch | [`examples/live_calibrate_profile.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibrate_profile.py) |
80
+ | Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
80
81
  | Capture an Arena of Solare leaderboard load | [`examples/solare_live_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/solare_live_snapshot.py) |
81
82
 
82
83
  See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
@@ -9,6 +9,7 @@ thread and make cancellation deterministic.
9
9
  from __future__ import annotations
10
10
 
11
11
  import asyncio
12
+ import math
12
13
  from collections import deque
13
14
  from concurrent.futures import ThreadPoolExecutor
14
15
  from functools import partial
@@ -16,12 +17,14 @@ from pathlib import Path
16
17
  from types import TracebackType
17
18
  from typing import AsyncIterator, Callable, Optional
18
19
 
20
+ from ._async_utils import _await_preserving_future, _wait_ignoring_cancellation
19
21
  from ._capture_runtime import CaptureEndpoint, _attach_cleanup_owner
20
22
  from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
21
23
  from .calibration import (
22
24
  DEFAULT_CALIBRATION_MAX_RETAINED_BYTES,
23
25
  DEFAULT_CALIBRATION_MAX_RETAINED_FRAMES,
24
26
  CalibrationResult,
27
+ CalibrationProgress,
25
28
  CalibrationRetention,
26
29
  CalibrationSession,
27
30
  )
@@ -33,28 +36,6 @@ from .origin_learning import CompanionObservation
33
36
  from .profiles import OpcodeProfile
34
37
 
35
38
 
36
- async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
37
- """Wait for an already-started operation, even after caller cancellation."""
38
-
39
- while not future.done():
40
- try:
41
- # asyncio.wait() never propagates cancellation into ``future`` and
42
- # does not create a cancelled shield wrapper that may later log an
43
- # otherwise-retrieved worker exception on Python 3.14.
44
- await asyncio.wait((future,))
45
- except asyncio.CancelledError:
46
- # Cleanup must settle before the original cancellation escapes.
47
- continue
48
- return future.result()
49
-
50
-
51
- async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
52
- """Await without cancelling or wrapping the submitted worker future."""
53
-
54
- await asyncio.wait((future,))
55
- return future.result()
56
-
57
-
58
39
  def _thread_task[T](function: Callable[[], T]) -> asyncio.Task[T]:
59
40
  return asyncio.create_task(asyncio.to_thread(function))
60
41
 
@@ -390,6 +371,13 @@ class AsyncCalibrationSession:
390
371
  session. Exiting the async context before ``stop()`` calls ``abort()`` and
391
372
  discards the unfinished calibration, matching the synchronous context
392
373
  manager's safety behavior.
374
+
375
+ ``stop_on_complete=True`` enables automatic finalization. ``await wait()``
376
+ observes completion without stopping on timeout or cancellation. ``progress``,
377
+ ``result``, ``stopped``, and ``stop_reason`` expose the synchronous owner's
378
+ state. ``on_update`` is synchronous on that owner's worker or finalizing
379
+ thread; use ``loop.call_soon_threadsafe`` for async/UI state.
380
+ ``request_stop()`` is synchronous and callback-safe.
393
381
  """
394
382
 
395
383
  def __init__(
@@ -403,6 +391,8 @@ class AsyncCalibrationSession:
403
391
  min_confidence: float = 0.80,
404
392
  max_retained_frames: int = DEFAULT_CALIBRATION_MAX_RETAINED_FRAMES,
405
393
  max_retained_bytes: int = DEFAULT_CALIBRATION_MAX_RETAINED_BYTES,
394
+ stop_on_complete: bool = False,
395
+ on_update: Callable[[CalibrationProgress], object] | None = None,
406
396
  ) -> None:
407
397
  self._session = CalibrationSession(
408
398
  item_id=item_id,
@@ -413,6 +403,8 @@ class AsyncCalibrationSession:
413
403
  min_confidence=min_confidence,
414
404
  max_retained_frames=max_retained_frames,
415
405
  max_retained_bytes=max_retained_bytes,
406
+ stop_on_complete=stop_on_complete,
407
+ on_update=on_update,
416
408
  )
417
409
  self._active = False
418
410
  self._terminal_action: str | None = None
@@ -470,7 +462,69 @@ class AsyncCalibrationSession:
470
462
  def result(self) -> CalibrationResult | None:
471
463
  """Completed result, including one preserved across cancellation."""
472
464
 
473
- return self._result
465
+ if self._result is not None:
466
+ return self._result
467
+ if self._active and getattr(self._session, "stopped", False):
468
+ return self._session.result
469
+ return None
470
+
471
+ @property
472
+ def progress(self) -> CalibrationProgress | None:
473
+ return self._session.progress
474
+
475
+ @property
476
+ def stopped(self) -> bool:
477
+ return self._session.stopped
478
+
479
+ @property
480
+ def stop_reason(self) -> str | None:
481
+ return self._session.stop_reason
482
+
483
+ def request_stop(self) -> None:
484
+ """Callback-safe request; callbacks run on the synchronous worker."""
485
+ self._session.request_stop()
486
+
487
+ async def wait(self, timeout: float | None = None) -> CalibrationResult | None:
488
+ """Await completion; timeout/cancellation leaves capture running.
489
+
490
+ Cancellation settles the one bounded pending wait before escaping.
491
+ An enclosing async context still performs its usual cleanup on exit.
492
+ """
493
+ if timeout is not None and (
494
+ isinstance(timeout, bool) or not isinstance(timeout, (int, float))
495
+ or not math.isfinite(timeout) or timeout < 0
496
+ ):
497
+ raise ValueError("timeout must be finite and non-negative")
498
+ loop = asyncio.get_running_loop()
499
+ deadline = None if timeout is None else loop.time() + timeout
500
+ terminal_poll = False
501
+ while True:
502
+ remaining = None if deadline is None else max(0.0, deadline - loop.time())
503
+ wait_seconds = 0.2 if remaining is None else min(0.2, remaining)
504
+ if terminal_poll:
505
+ wait_seconds = 0.0
506
+ task = _thread_task(partial(self._session.wait, wait_seconds))
507
+ try:
508
+ result = await _await_preserving_future(task)
509
+ except asyncio.CancelledError:
510
+ try:
511
+ await _wait_ignoring_cancellation(task)
512
+ except BaseException:
513
+ pass
514
+ raise
515
+ if result is not None:
516
+ self._result = result
517
+ return result
518
+ if terminal_poll:
519
+ return None # Terminal discard, without a result or error.
520
+ if self.stopped:
521
+ # Completion may have raced the empty poll's delivery. Re-read
522
+ # through wait() to retrieve the final result or raise its error,
523
+ # using the same cancellation-safe worker path as ordinary polls.
524
+ terminal_poll = True
525
+ continue
526
+ if deadline is not None and loop.time() >= deadline:
527
+ return None
474
528
 
475
529
  async def start(self) -> None:
476
530
  """Begin calibration capture without blocking the event loop."""
@@ -581,6 +635,10 @@ class AsyncCalibrationSession:
581
635
  self._active = self._session.cleanup_incomplete
582
636
  raise
583
637
  else:
638
+ # Automatic finalization may have won the race with context exit.
639
+ # Preserve that completed result even when nobody awaited wait().
640
+ if getattr(self._session, "stopped", False):
641
+ self._result = self._session.result
584
642
  self._active = False
585
643
 
586
644
  async def abort(self) -> None:
@@ -0,0 +1,25 @@
1
+ """Cancellation-safe waits shared by asynchronous session facades."""
2
+
3
+ import asyncio
4
+
5
+
6
+ async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
7
+ """Wait for an already-started operation, even after caller cancellation."""
8
+
9
+ while not future.done():
10
+ try:
11
+ # asyncio.wait() never propagates cancellation into ``future`` and
12
+ # does not create a cancelled shield wrapper that may later log an
13
+ # otherwise-retrieved worker exception on Python 3.14.
14
+ await asyncio.wait((future,))
15
+ except asyncio.CancelledError:
16
+ # Cleanup must settle before the original cancellation escapes.
17
+ continue
18
+ return future.result()
19
+
20
+
21
+ async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
22
+ """Await without cancelling or wrapping the submitted worker future."""
23
+
24
+ await asyncio.wait((future,))
25
+ return future.result()
@@ -0,0 +1,4 @@
1
+ """Private calibration inference, passive capture, and profile persistence.
2
+
3
+ Internal modules do not import the public ``bdo_toolkit.calibration`` facade.
4
+ """
@@ -0,0 +1,42 @@
1
+ """Private calibration constants implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .._protocol import (
6
+ CHARACTER_LOAD_CONTEXT,
7
+ SOURCE_CONTEXT_LABELS,
8
+ STORAGE_DELTA_CONTEXTS,
9
+ )
10
+
11
+
12
+ CALIBRATION_ACTIONS = (
13
+ "loot-preview",
14
+ "storage-to-inventory",
15
+ "inventory-to-storage",
16
+ )
17
+ DEFAULT_CALIBRATION_MAX_RETAINED_FRAMES = 50_000
18
+ DEFAULT_CALIBRATION_MAX_RETAINED_BYTES = 64 * 1024 * 1024
19
+ _CALIBRATION_MAX_ACTIVE_FLOWS = 64
20
+ OPCODE_PROFILE_EVENTS = (
21
+ "LOOT_PREVIEW",
22
+ "INVENTORY_TRANSFER",
23
+ "SOURCE_CONTAINER_DECREMENT",
24
+ "SOURCE_STACK_DECREMENT",
25
+ "SOURCE_ITEM_REFERENCE",
26
+ "STORAGE_ITEM_DELTA",
27
+ )
28
+ _FAMILY_LABELS = {
29
+ "into_inventory": "storage->inventory",
30
+ "into_storage": "inventory->storage",
31
+ }
32
+ REFERENCE_FRAME_MAX_LENGTH = 128
33
+ SOURCE_DECREMENT_FRAME_MAX_LENGTH = 512
34
+ _HIGH_ENTROPY_CONTEXTS = tuple(
35
+ value
36
+ for value in SOURCE_CONTEXT_LABELS
37
+ if value != CHARACTER_LOAD_CONTEXT and value not in STORAGE_DELTA_CONTEXTS
38
+ )
39
+ _EXPECTED_FAMILY = {
40
+ "storage-to-inventory": "into_inventory",
41
+ "inventory-to-storage": "into_storage",
42
+ }
@@ -0,0 +1,65 @@
1
+ """Private calibration formatting implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ from typing import Iterable
7
+ from ._constants import OPCODE_PROFILE_EVENTS
8
+ from ._records import _Options
9
+ from .models import MessageSpec
10
+
11
+
12
+ def _dedupe_message_specs(specs: Iterable[MessageSpec]) -> list[MessageSpec]:
13
+ output: list[MessageSpec] = []
14
+ seen: set[tuple[object, ...]] = set()
15
+ for spec in specs:
16
+ key = spec.dedupe_key()
17
+ if key in seen:
18
+ continue
19
+ seen.add(key)
20
+ output.append(spec)
21
+ return output
22
+
23
+
24
+ def _confidence_label(score: float) -> str:
25
+ level = "high" if score >= 0.90 else "medium"
26
+ return f"calibrated-{level}"
27
+
28
+
29
+ def _calibration_source(options: _Options, action: str) -> str:
30
+ parts = [f"calibrate {action}", f"item_id={options.item_id}"]
31
+ if options.quantity is not None:
32
+ parts.append(f"qty={options.quantity}")
33
+ return " ".join(parts)
34
+
35
+
36
+ def _iso_timestamp(timestamp: float) -> str:
37
+ return (
38
+ dt.datetime.fromtimestamp(timestamp, tz=dt.UTC)
39
+ .isoformat(timespec="seconds")
40
+ .replace("+00:00", "Z")
41
+ )
42
+
43
+
44
+ def _utc_now_text() -> str:
45
+ return (
46
+ dt.datetime.now(tz=dt.UTC)
47
+ .isoformat(timespec="seconds")
48
+ .replace("+00:00", "Z")
49
+ )
50
+
51
+
52
+ def _events_for_action(action: str) -> tuple[str, ...]:
53
+ if action == "loot-preview":
54
+ return ("LOOT_PREVIEW",)
55
+ if action == "storage-to-inventory":
56
+ return ("INVENTORY_TRANSFER", "SOURCE_CONTAINER_DECREMENT")
57
+ if action == "inventory-to-storage":
58
+ return (
59
+ "SOURCE_STACK_DECREMENT",
60
+ "SOURCE_ITEM_REFERENCE",
61
+ "STORAGE_ITEM_DELTA",
62
+ )
63
+ # ``auto`` observes both transfer directions but never owns the separate
64
+ # loot-preview workflow.
65
+ return tuple(event for event in OPCODE_PROFILE_EVENTS if event != "LOOT_PREVIEW")