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.
- {bdo_toolkit-1.0.3/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.4}/PKG-INFO +2 -1
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/README.md +1 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_async_sessions.py +81 -23
- bdo_toolkit-1.0.4/src/bdo_toolkit/_async_utils.py +25 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/__init__.py +4 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_constants.py +42 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_formatting.py +65 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_records.py +711 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/analysis.py +610 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/capture.py +691 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/companions.py +413 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/live.py +252 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/models.py +419 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/observations.py +122 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/persistence.py +326 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/progress.py +113 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/validation.py +94 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/workflow.py +140 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_backend.py +15 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_deposit_origin.py +16 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_framing.py +54 -45
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/__init__.py +8 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/_constants.py +28 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/_records.py +211 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/assembly.py +496 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/formatting.py +241 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/inventory.py +609 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/models.py +669 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/session.py +405 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/storage.py +980 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/__init__.py +4 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/discovery.py +78 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/manual.py +587 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/models.py +202 -0
- bdo_toolkit-1.0.3/src/bdo_toolkit/_deposit_origin.py → bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/tracker.py +39 -783
- bdo_toolkit-1.0.4/src/bdo_toolkit/_profile_io.py +43 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_record_geometry.py +39 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_version.py +1 -1
- bdo_toolkit-1.0.4/src/bdo_toolkit/calibration.py +98 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/capture.py +119 -38
- bdo_toolkit-1.0.4/src/bdo_toolkit/character_state.py +56 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/item_state.py +3 -3
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/origin_learning.py +2 -43
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/remote_profiles.py +1 -13
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/async_session.py +4 -16
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/session.py +2 -6
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4/src/bdo_toolkit.egg-info}/PKG-INFO +2 -1
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/SOURCES.txt +38 -1
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_async_sessions.py +3 -13
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration.py +408 -339
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration_conveniences.py +5 -4
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration_lifecycle.py +50 -20
- bdo_toolkit-1.0.4/tests/test_calibration_persistence.py +492 -0
- bdo_toolkit-1.0.4/tests/test_calibration_progress.py +758 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_calibration_retention.py +8 -6
- bdo_toolkit-1.0.4/tests/test_capture_replay.py +131 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_capture_runtime.py +2 -13
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_character_state.py +56 -49
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_decoder_diagnostics.py +2 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_deposit_origin.py +202 -18
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_direction.py +20 -20
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_dynamic_storage_calibration_matrix.py +32 -15
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_examples_and_writers.py +2 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_explicit_profile.py +3 -4
- bdo_toolkit-1.0.4/tests/test_fixture_catalog.py +92 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_fixture_regressions.py +2 -2
- bdo_toolkit-1.0.4/tests/test_framing.py +671 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_initial_load_storage.py +3 -3
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_inventory_snapshots.py +13 -9
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_item_state.py +16 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_live_capture_session.py +59 -16
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_origin_learning.py +2 -2
- bdo_toolkit-1.0.4/tests/test_package_layout.py +17 -0
- bdo_toolkit-1.0.4/tests/test_profile_validation.py +319 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_reassembly.py +38 -6
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_learning.py +15 -31
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_real_captures.py +28 -29
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_destination_validation.py +5 -7
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_unknown_destination_calibration.py +2 -2
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_toolkit_api.py +43 -14
- bdo_toolkit-1.0.3/src/bdo_toolkit/calibration.py +0 -3269
- bdo_toolkit-1.0.3/src/bdo_toolkit/character_state.py +0 -3520
- bdo_toolkit-1.0.3/tests/test_hardening.py +0 -1567
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/LICENSE +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/pyproject.toml +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/setup.cfg +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/__init__.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_engine.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_protocol.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_reassembly.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_hydration.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/cli.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/events.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/filters.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_discovery.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/models.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_capture_backend.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_replay.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_solare_session.py +0 -0
- {bdo_toolkit-1.0.3 → bdo_toolkit-1.0.4}/tests/test_storage_events.py +0 -0
- {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
|
+
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
|
-
|
|
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,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")
|