bdo-toolkit 1.0.2__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.2/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.4}/PKG-INFO +17 -39
  2. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/README.md +16 -38
  3. {bdo_toolkit-1.0.2 → 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.2 → 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.2 → 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.2/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.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_reassembly.py +12 -7
  38. bdo_toolkit-1.0.4/src/bdo_toolkit/_record_geometry.py +39 -0
  39. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_version.py +1 -1
  40. bdo_toolkit-1.0.4/src/bdo_toolkit/calibration.py +98 -0
  41. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/capture.py +119 -38
  42. bdo_toolkit-1.0.4/src/bdo_toolkit/character_state.py +56 -0
  43. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/item_state.py +3 -3
  44. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/origin_learning.py +2 -43
  45. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/remote_profiles.py +1 -13
  46. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_live_tracker.py +3 -3
  47. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_replay_capture.py +2 -0
  48. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/async_session.py +4 -16
  49. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/session.py +9 -20
  50. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4/src/bdo_toolkit.egg-info}/PKG-INFO +17 -39
  51. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/SOURCES.txt +38 -1
  52. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_async_sessions.py +3 -13
  53. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration.py +436 -348
  54. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration_conveniences.py +5 -4
  55. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration_lifecycle.py +50 -20
  56. bdo_toolkit-1.0.4/tests/test_calibration_persistence.py +492 -0
  57. bdo_toolkit-1.0.4/tests/test_calibration_progress.py +758 -0
  58. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration_retention.py +8 -6
  59. bdo_toolkit-1.0.4/tests/test_capture_replay.py +131 -0
  60. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_capture_runtime.py +2 -13
  61. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_character_state.py +59 -52
  62. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_decoder_diagnostics.py +2 -0
  63. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_deposit_origin.py +202 -18
  64. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_direction.py +20 -20
  65. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_dynamic_storage_calibration_matrix.py +32 -15
  66. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_examples_and_writers.py +2 -0
  67. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_explicit_profile.py +3 -4
  68. bdo_toolkit-1.0.4/tests/test_fixture_catalog.py +92 -0
  69. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_fixture_regressions.py +2 -2
  70. bdo_toolkit-1.0.4/tests/test_framing.py +671 -0
  71. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_initial_load_storage.py +3 -3
  72. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_inventory_snapshots.py +13 -9
  73. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_item_state.py +16 -0
  74. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_live_capture_session.py +59 -16
  75. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_origin_learning.py +2 -2
  76. bdo_toolkit-1.0.4/tests/test_package_layout.py +17 -0
  77. bdo_toolkit-1.0.4/tests/test_profile_validation.py +319 -0
  78. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_reassembly.py +101 -6
  79. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_learning.py +15 -31
  80. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_real_captures.py +28 -29
  81. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_replay.py +4 -1
  82. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_session.py +11 -23
  83. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_destination_validation.py +5 -7
  84. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_unknown_destination_calibration.py +2 -2
  85. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_toolkit_api.py +43 -14
  86. bdo_toolkit-1.0.2/src/bdo_toolkit/calibration.py +0 -3221
  87. bdo_toolkit-1.0.2/src/bdo_toolkit/character_state.py +0 -3520
  88. bdo_toolkit-1.0.2/tests/test_hardening.py +0 -1567
  89. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/LICENSE +0 -0
  90. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/pyproject.toml +0 -0
  91. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/setup.cfg +0 -0
  92. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/__init__.py +0 -0
  93. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_options.py +0 -0
  94. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_runtime.py +0 -0
  95. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_engine.py +0 -0
  96. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_profile_runtime.py +0 -0
  97. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_protocol.py +0 -0
  98. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_specs.py +0 -0
  99. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
  100. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_hydration.py +0 -0
  101. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/cli.py +0 -0
  102. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/diagnostics.py +0 -0
  103. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/events.py +0 -0
  104. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/filters.py +0 -0
  105. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/profiles.py +0 -0
  106. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/py.typed +0 -0
  107. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/__init__.py +0 -0
  108. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_constants.py +0 -0
  109. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
  110. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_details.py +0 -0
  111. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_discovery.py +0 -0
  112. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_result.py +0 -0
  113. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_scanner.py +0 -0
  114. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_validation.py +0 -0
  115. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/models.py +0 -0
  116. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/replay.py +0 -0
  117. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/writers.py +0 -0
  118. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
  119. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
  120. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/requires.txt +0 -0
  121. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
  122. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_capture_backend.py +0 -0
  123. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_cli.py +0 -0
  124. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_engine.py +0 -0
  125. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_remote_profiles.py +0 -0
  126. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_api.py +0 -0
  127. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_details.py +0 -0
  128. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_learning_adversarial.py +0 -0
  129. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_events.py +0 -0
  130. {bdo_toolkit-1.0.2 → 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.2
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
@@ -28,6 +28,7 @@ Dynamic: license-file
28
28
  # bdo-toolkit
29
29
 
30
30
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
31
+ [![PyPI version](https://img.shields.io/pypi/v/bdo-toolkit?style=flat-square&label=version)](https://pypi.org/project/bdo-toolkit/)
31
32
  [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
32
33
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
33
34
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
@@ -48,11 +49,21 @@ traffic into structured, application-ready data.
48
49
 
49
50
  ## Capabilities
50
51
 
51
- | In-game task | Python result | Status | Guide |
52
+ bdo-toolkit exposes three passive workflows. Each can observe live traffic or
53
+ replay a saved PCAP or PCAPNG file.
54
+
55
+ | Capability | What it provides | Guide | Status |
52
56
  | --- | --- | --- | --- |
53
- | Watch supported item changes such as loot, gathering, and storage activity | A continuing stream of typed `BDOEvent` objects | Stable | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
54
- | Log in or switch characters | One observational `ItemStateSnapshot` of inventory, known balances, and town storage | Beta | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
55
- | Load the Arena of Solare Leaderboard | One `SolareCaptureResult`; a complete result contains a leaderboard snapshot | Beta | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
57
+ | Item activity | A continuing stream of typed `BDOEvent` objects for supported loot, gathering, inventory, and storage changes | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) | Stable |
58
+ | Inventory and town storage | A finite `ItemStateSnapshot` assembled from character-load traffic, with inventory, known balances, and observed town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) | Beta |
59
+ | Arena of Solare leaderboards | A finite `SolareCaptureResult` containing overall rankings, class tables, and player statistics when the capture is complete | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) | Beta |
60
+
61
+ These workflows include synchronous and
62
+ [asyncio](https://ychwu.github.io/bdo-toolkit/#asyncio) sessions, capture and
63
+ decoder health diagnostics, console and JSONL event writers, and a
64
+ [command-line interface](https://ychwu.github.io/bdo-toolkit/#cli). Exact
65
+ signatures, fields, lifecycle behavior, and failure contracts are in the
66
+ [API index](https://ychwu.github.io/bdo-toolkit/#api-index).
56
67
 
57
68
  Testing and validation cover **NA/EU only**. Compatibility with other regional
58
69
  services is unknown.
@@ -93,51 +104,18 @@ installed with the Python wheel.
93
104
  | Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
94
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) |
95
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) |
96
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) |
97
109
 
98
110
  See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
99
111
  for every script, its prerequisites, and the guide that explains it.
100
112
 
101
- ## Documentation
102
-
103
- The documentation separates task-focused guides from the symbol-first API
104
- reference. Exact signatures, fields, lifecycle behavior, and failure contracts
105
- live there rather than in this README.
106
-
107
- | Goal | Start here |
108
- | --- | --- |
109
- | Understand the package | [Overview](https://ychwu.github.io/bdo-toolkit/#overview) |
110
- | Build with live item events | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
111
- | Query character inventory and town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
112
- | Rebuild item decoding after a patch | [Calibration](https://ychwu.github.io/bdo-toolkit/#calibration-workflow) |
113
- | Capture and query an Arena of Solare leaderboard | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
114
- | Integrate with an asyncio application | [Asyncio integration](https://ychwu.github.io/bdo-toolkit/#asyncio) |
115
- | Look up a class, function, or model | [API index](https://ychwu.github.io/bdo-toolkit/#api-index) |
116
- | Use the terminal interface | [Command line](https://ychwu.github.io/bdo-toolkit/#cli) |
117
- | Diagnose a problem | [Troubleshooting](https://ychwu.github.io/bdo-toolkit/#errors) |
118
- | Review data handling and project boundaries | [Safety & privacy](https://ychwu.github.io/bdo-toolkit/#stability) |
119
-
120
113
  ## Support
121
114
 
122
115
  For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
123
116
 
124
117
  For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
125
118
 
126
- ## Development
127
-
128
- ```powershell
129
- git clone https://github.com/ychwu/bdo-toolkit.git
130
- cd bdo-toolkit
131
- python -m pip install -e ".[dev]"
132
- python -m pytest -q -W error
133
- python -m mypy src/bdo_toolkit
134
- python -m pip wheel . --no-deps --wheel-dir dist
135
- ```
136
-
137
- CI runs tests, type checking, wheel construction, and a CLI smoke test on
138
- Ubuntu and Windows with Python 3.14. Tests that require private
139
- game-session captures skip when those local fixtures are absent.
140
-
141
119
  ## License
142
120
 
143
121
  bdo-toolkit is available under the
@@ -1,6 +1,7 @@
1
1
  # bdo-toolkit
2
2
 
3
3
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/bdo-toolkit?style=flat-square&label=version)](https://pypi.org/project/bdo-toolkit/)
4
5
  [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
5
6
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
6
7
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
@@ -21,11 +22,21 @@ traffic into structured, application-ready data.
21
22
 
22
23
  ## Capabilities
23
24
 
24
- | In-game task | Python result | Status | Guide |
25
+ bdo-toolkit exposes three passive workflows. Each can observe live traffic or
26
+ replay a saved PCAP or PCAPNG file.
27
+
28
+ | Capability | What it provides | Guide | Status |
25
29
  | --- | --- | --- | --- |
26
- | Watch supported item changes such as loot, gathering, and storage activity | A continuing stream of typed `BDOEvent` objects | Stable | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
27
- | Log in or switch characters | One observational `ItemStateSnapshot` of inventory, known balances, and town storage | Beta | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
28
- | Load the Arena of Solare Leaderboard | One `SolareCaptureResult`; a complete result contains a leaderboard snapshot | Beta | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
30
+ | Item activity | A continuing stream of typed `BDOEvent` objects for supported loot, gathering, inventory, and storage changes | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) | Stable |
31
+ | Inventory and town storage | A finite `ItemStateSnapshot` assembled from character-load traffic, with inventory, known balances, and observed town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) | Beta |
32
+ | Arena of Solare leaderboards | A finite `SolareCaptureResult` containing overall rankings, class tables, and player statistics when the capture is complete | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) | Beta |
33
+
34
+ These workflows include synchronous and
35
+ [asyncio](https://ychwu.github.io/bdo-toolkit/#asyncio) sessions, capture and
36
+ decoder health diagnostics, console and JSONL event writers, and a
37
+ [command-line interface](https://ychwu.github.io/bdo-toolkit/#cli). Exact
38
+ signatures, fields, lifecycle behavior, and failure contracts are in the
39
+ [API index](https://ychwu.github.io/bdo-toolkit/#api-index).
29
40
 
30
41
  Testing and validation cover **NA/EU only**. Compatibility with other regional
31
42
  services is unknown.
@@ -66,51 +77,18 @@ installed with the Python wheel.
66
77
  | Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
67
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) |
68
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) |
69
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) |
70
82
 
71
83
  See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
72
84
  for every script, its prerequisites, and the guide that explains it.
73
85
 
74
- ## Documentation
75
-
76
- The documentation separates task-focused guides from the symbol-first API
77
- reference. Exact signatures, fields, lifecycle behavior, and failure contracts
78
- live there rather than in this README.
79
-
80
- | Goal | Start here |
81
- | --- | --- |
82
- | Understand the package | [Overview](https://ychwu.github.io/bdo-toolkit/#overview) |
83
- | Build with live item events | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
84
- | Query character inventory and town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
85
- | Rebuild item decoding after a patch | [Calibration](https://ychwu.github.io/bdo-toolkit/#calibration-workflow) |
86
- | Capture and query an Arena of Solare leaderboard | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
87
- | Integrate with an asyncio application | [Asyncio integration](https://ychwu.github.io/bdo-toolkit/#asyncio) |
88
- | Look up a class, function, or model | [API index](https://ychwu.github.io/bdo-toolkit/#api-index) |
89
- | Use the terminal interface | [Command line](https://ychwu.github.io/bdo-toolkit/#cli) |
90
- | Diagnose a problem | [Troubleshooting](https://ychwu.github.io/bdo-toolkit/#errors) |
91
- | Review data handling and project boundaries | [Safety & privacy](https://ychwu.github.io/bdo-toolkit/#stability) |
92
-
93
86
  ## Support
94
87
 
95
88
  For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
96
89
 
97
90
  For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
98
91
 
99
- ## Development
100
-
101
- ```powershell
102
- git clone https://github.com/ychwu/bdo-toolkit.git
103
- cd bdo-toolkit
104
- python -m pip install -e ".[dev]"
105
- python -m pytest -q -W error
106
- python -m mypy src/bdo_toolkit
107
- python -m pip wheel . --no-deps --wheel-dir dist
108
- ```
109
-
110
- CI runs tests, type checking, wheel construction, and a CLI smoke test on
111
- Ubuntu and Windows with Python 3.14. Tests that require private
112
- game-session captures skip when those local fixtures are absent.
113
-
114
92
  ## License
115
93
 
116
94
  bdo-toolkit is available under the
@@ -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")