bdo-toolkit 1.0.0__tar.gz → 1.0.1__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 (89) hide show
  1. {bdo_toolkit-1.0.0/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.1}/PKG-INFO +6 -5
  2. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/README.md +3 -3
  3. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/pyproject.toml +5 -1
  4. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_async_sessions.py +13 -18
  5. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_capture_runtime.py +15 -17
  6. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_framing.py +1 -1
  7. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_protocol.py +1 -1
  8. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_storage_hydration.py +1 -1
  9. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_version.py +1 -1
  10. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/calibration.py +11 -13
  11. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/capture.py +18 -22
  12. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/character_state.py +4 -5
  13. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/events.py +1 -1
  14. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/filters.py +2 -5
  15. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/origin_learning.py +3 -3
  16. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/remote_profiles.py +1 -1
  17. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_discovery.py +1 -1
  18. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/async_session.py +10 -18
  19. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/models.py +1 -1
  20. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/session.py +5 -6
  21. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1/src/bdo_toolkit.egg-info}/PKG-INFO +6 -5
  22. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_async_sessions.py +1 -4
  23. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_calibration_lifecycle.py +4 -5
  24. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_capture_runtime.py +4 -5
  25. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_session.py +13 -16
  26. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/LICENSE +0 -0
  27. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/setup.cfg +0 -0
  28. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/__init__.py +0 -0
  29. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_capture_backend.py +0 -0
  30. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_capture_options.py +0 -0
  31. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_deposit_origin.py +0 -0
  32. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_engine.py +0 -0
  33. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_profile_runtime.py +0 -0
  34. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_reassembly.py +0 -0
  35. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_specs.py +0 -0
  36. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
  37. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/cli.py +0 -0
  38. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/diagnostics.py +0 -0
  39. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/item_state.py +0 -0
  40. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/profiles.py +0 -0
  41. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/py.typed +0 -0
  42. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/__init__.py +0 -0
  43. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_constants.py +0 -0
  44. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
  45. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_details.py +0 -0
  46. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
  47. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
  48. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_result.py +0 -0
  49. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_scanner.py +0 -0
  50. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/_validation.py +0 -0
  51. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/solare/replay.py +0 -0
  52. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit/writers.py +0 -0
  53. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit.egg-info/SOURCES.txt +0 -0
  54. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
  55. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
  56. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit.egg-info/requires.txt +0 -0
  57. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
  58. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_calibration.py +0 -0
  59. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_calibration_conveniences.py +0 -0
  60. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_calibration_retention.py +0 -0
  61. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_character_state.py +0 -0
  62. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_cli.py +0 -0
  63. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_decoder_diagnostics.py +0 -0
  64. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_deposit_origin.py +0 -0
  65. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_direction.py +0 -0
  66. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
  67. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_engine.py +0 -0
  68. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_examples_and_writers.py +0 -0
  69. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_explicit_profile.py +0 -0
  70. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_fixture_regressions.py +0 -0
  71. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_hardening.py +0 -0
  72. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_initial_load_storage.py +0 -0
  73. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_inventory_snapshots.py +0 -0
  74. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_item_state.py +0 -0
  75. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_live_capture_session.py +0 -0
  76. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_origin_learning.py +0 -0
  77. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_reassembly.py +0 -0
  78. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_remote_profiles.py +0 -0
  79. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_api.py +0 -0
  80. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_details.py +0 -0
  81. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_learning.py +0 -0
  82. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_learning_adversarial.py +0 -0
  83. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_real_captures.py +0 -0
  84. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_solare_replay.py +0 -0
  85. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_storage_destination_validation.py +0 -0
  86. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_storage_events.py +0 -0
  87. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_storage_hydration_tracker.py +0 -0
  88. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_storage_unknown_destination_calibration.py +0 -0
  89. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.1}/tests/test_toolkit_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bdo-toolkit
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Passive, read-only BDO packet parsing toolkit for app developers.
5
5
  Author: bdo-toolkit contributors
6
6
  License-Expression: MIT
@@ -12,9 +12,10 @@ Keywords: bdo,packets,parse,pcap,passive
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
15
16
  Classifier: Programming Language :: Python :: 3 :: Only
16
17
  Classifier: Typing :: Typed
17
- Requires-Python: >=3.10
18
+ Requires-Python: >=3.14
18
19
  Description-Content-Type: text/markdown
19
20
  License-File: LICENSE
20
21
  Requires-Dist: scapy>=2.5
@@ -27,7 +28,7 @@ Dynamic: license-file
27
28
  # bdo-toolkit
28
29
 
29
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)
30
- [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
31
+ [![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)
31
32
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
32
33
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
33
34
  [![License: MIT](https://img.shields.io/badge/license-MIT-2f855a?style=flat-square)](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
@@ -62,7 +63,7 @@ services is unknown.
62
63
  python -m pip install bdo-toolkit
63
64
  ```
64
65
 
65
- Python 3.10 or newer is required. Before live capture, complete
66
+ Python 3.14 or newer is required. Before live capture, complete
66
67
  [Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
67
68
  including Npcap on Windows and permission to capture on the selected interface.
68
69
  Offline PCAP and PCAPNG replay does not require Npcap.
@@ -134,7 +135,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
134
135
  ```
135
136
 
136
137
  CI runs tests, type checking, wheel construction, and a CLI smoke test on
137
- Ubuntu and Windows with Python 3.10 and 3.14. Tests that require private
138
+ Ubuntu and Windows with Python 3.14. Tests that require private
138
139
  game-session captures skip when those local fixtures are absent.
139
140
 
140
141
  ## License
@@ -1,7 +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
- [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
4
+ [![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
5
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
6
6
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-2f855a?style=flat-square)](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
@@ -36,7 +36,7 @@ services is unknown.
36
36
  python -m pip install bdo-toolkit
37
37
  ```
38
38
 
39
- Python 3.10 or newer is required. Before live capture, complete
39
+ Python 3.14 or newer is required. Before live capture, complete
40
40
  [Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
41
41
  including Npcap on Windows and permission to capture on the selected interface.
42
42
  Offline PCAP and PCAPNG replay does not require Npcap.
@@ -108,7 +108,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
108
108
  ```
109
109
 
110
110
  CI runs tests, type checking, wheel construction, and a CLI smoke test on
111
- Ubuntu and Windows with Python 3.10 and 3.14. Tests that require private
111
+ Ubuntu and Windows with Python 3.14. Tests that require private
112
112
  game-session captures skip when those local fixtures are absent.
113
113
 
114
114
  ## License
@@ -7,7 +7,7 @@ name = "bdo-toolkit"
7
7
  dynamic = ["version"]
8
8
  description = "Passive, read-only BDO packet parsing toolkit for app developers."
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.14"
11
11
  dependencies = [
12
12
  "scapy>=2.5",
13
13
  ]
@@ -21,6 +21,7 @@ classifiers = [
21
21
  "Development Status :: 5 - Production/Stable",
22
22
  "Operating System :: OS Independent",
23
23
  "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.14",
24
25
  "Programming Language :: Python :: 3 :: Only",
25
26
  "Typing :: Typed",
26
27
  ]
@@ -47,6 +48,9 @@ where = ["src"]
47
48
  [tool.pytest.ini_options]
48
49
  testpaths = ["tests"]
49
50
 
51
+ [tool.mypy]
52
+ python_version = "3.14"
53
+
50
54
  [tool.setuptools.package-data]
51
55
  bdo_toolkit = ["py.typed"]
52
56
 
@@ -14,7 +14,7 @@ from concurrent.futures import ThreadPoolExecutor
14
14
  from functools import partial
15
15
  from pathlib import Path
16
16
  from types import TracebackType
17
- from typing import AsyncIterator, Callable, Optional, TypeVar
17
+ from typing import AsyncIterator, Callable, Optional
18
18
 
19
19
  from ._capture_runtime import CaptureEndpoint, _attach_cleanup_owner
20
20
  from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
@@ -33,10 +33,7 @@ from .origin_learning import CompanionObservation
33
33
  from .profiles import OpcodeProfile
34
34
 
35
35
 
36
- T = TypeVar("T")
37
-
38
-
39
- async def _wait_ignoring_cancellation(future: asyncio.Future[T]) -> T:
36
+ async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
40
37
  """Wait for an already-started operation, even after caller cancellation."""
41
38
 
42
39
  while not future.done():
@@ -51,14 +48,14 @@ async def _wait_ignoring_cancellation(future: asyncio.Future[T]) -> T:
51
48
  return future.result()
52
49
 
53
50
 
54
- async def _await_preserving_future(future: asyncio.Future[T]) -> T:
51
+ async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
55
52
  """Await without cancelling or wrapping the submitted worker future."""
56
53
 
57
54
  await asyncio.wait((future,))
58
55
  return future.result()
59
56
 
60
57
 
61
- def _thread_task(function: Callable[[], T]) -> asyncio.Task[T]:
58
+ def _thread_task[T](function: Callable[[], T]) -> asyncio.Task[T]:
62
59
  return asyncio.create_task(asyncio.to_thread(function))
63
60
 
64
61
 
@@ -143,7 +140,7 @@ class AsyncLiveCaptureSession:
143
140
 
144
141
  self._session.raise_if_failed()
145
142
 
146
- def _submit(self, function: Callable[[], T]) -> asyncio.Future[T]:
143
+ def _submit[T](self, function: Callable[[], T]) -> asyncio.Future[T]:
147
144
  if self._executor_closed:
148
145
  raise RuntimeError("async live capture session is already closed")
149
146
  loop = asyncio.get_running_loop()
@@ -380,11 +377,10 @@ class AsyncLiveCaptureSession:
380
377
  self,
381
378
  context="async live item capture context",
382
379
  )
383
- if hasattr(exc_value, "add_note"):
384
- exc_value.add_note(
385
- "async live item capture context cleanup also failed: "
386
- f"{cleanup_error!r}"
387
- )
380
+ exc_value.add_note(
381
+ "async live item capture context cleanup also failed: "
382
+ f"{cleanup_error!r}"
383
+ )
388
384
 
389
385
 
390
386
  class AsyncCalibrationSession:
@@ -644,8 +640,7 @@ class AsyncCalibrationSession:
644
640
  self,
645
641
  context="async live calibration context",
646
642
  )
647
- if hasattr(exc_value, "add_note"):
648
- exc_value.add_note(
649
- "async live calibration context cleanup also failed: "
650
- f"{cleanup_error!r}"
651
- )
643
+ exc_value.add_note(
644
+ "async live calibration context cleanup also failed: "
645
+ f"{cleanup_error!r}"
646
+ )
@@ -64,18 +64,17 @@ def _attach_cleanup_owner(
64
64
  # Built-in and ordinary application exceptions expose __dict__. Keep
65
65
  # the original failure identity even for an exotic immutable subtype.
66
66
  pass
67
- if hasattr(error, "add_note"):
68
- if attached:
69
- error.add_note(
70
- f"{context} cleanup is incomplete; retry stop() on "
71
- "exception.cleanup_owner"
72
- )
73
- else:
74
- error.add_note(
75
- f"{context} cleanup is incomplete; this exception subtype "
76
- "cannot expose cleanup_owner, so retain and stop the original "
77
- "session object"
78
- )
67
+ if attached:
68
+ error.add_note(
69
+ f"{context} cleanup is incomplete; retry stop() on "
70
+ "exception.cleanup_owner"
71
+ )
72
+ else:
73
+ error.add_note(
74
+ f"{context} cleanup is incomplete; this exception subtype "
75
+ "cannot expose cleanup_owner, so retain and stop the original "
76
+ "session object"
77
+ )
79
78
 
80
79
 
81
80
  @dataclass(frozen=True)
@@ -456,11 +455,10 @@ class LivePacketCapture:
456
455
  # Startup remains the primary failure, but incomplete
457
456
  # cleanup must be explicit in its traceback and remains
458
457
  # retryable through stop().
459
- if hasattr(exc, "add_note"):
460
- exc.add_note(
461
- "live capture startup cleanup also failed: "
462
- f"{cleanup_error!r}"
463
- )
458
+ exc.add_note(
459
+ "live capture startup cleanup also failed: "
460
+ f"{cleanup_error!r}"
461
+ )
464
462
  if not self._cleanup_incomplete:
465
463
  self._capture = None
466
464
  self._capture_socket = None
@@ -18,7 +18,7 @@ from ._protocol import (
18
18
  _TRANSFER_RECORD_MARKER = b"\x00" * 4 + b"\xff" * 8
19
19
  _TRANSFER_RECORD_MARKER_DELTA = 8
20
20
 
21
- MessageObserver = Callable[
21
+ type MessageObserver = Callable[
22
22
  [int, int, str, int, PacketContext, Optional[int]],
23
23
  object,
24
24
  ]
@@ -330,7 +330,7 @@ class LootEvent:
330
330
  storage_operation: Optional[str] = None
331
331
 
332
332
 
333
- EventCallback = Callable[[LootEvent, bytes], None]
333
+ type EventCallback = Callable[[LootEvent, bytes], None]
334
334
 
335
335
 
336
336
  @dataclass(frozen=True)
@@ -11,7 +11,7 @@ from typing import Callable, Optional
11
11
  from .events import BDOEvent, Flow
12
12
 
13
13
 
14
- _HydrationKey = tuple[Flow, int]
14
+ type _HydrationKey = tuple[Flow, int]
15
15
 
16
16
 
17
17
  def _hydration_key(event: BDOEvent) -> _HydrationKey:
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the BDO Toolkit release version."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.1"
@@ -1021,11 +1021,10 @@ class CalibrationSession:
1021
1021
  try:
1022
1022
  manager.finish()
1023
1023
  except BaseException as cleanup_error:
1024
- if hasattr(exc, "add_note"):
1025
- exc.add_note(
1026
- "calibration flow cleanup also failed: "
1027
- f"{cleanup_error!r}"
1028
- )
1024
+ exc.add_note(
1025
+ "calibration flow cleanup also failed: "
1026
+ f"{cleanup_error!r}"
1027
+ )
1029
1028
  self._manager = None
1030
1029
  self._capture = None
1031
1030
  raise
@@ -1187,11 +1186,10 @@ class CalibrationSession:
1187
1186
  self,
1188
1187
  context="live calibration context",
1189
1188
  )
1190
- if hasattr(exc_value, "add_note"):
1191
- exc_value.add_note(
1192
- "calibration context cleanup also failed: "
1193
- f"{cleanup_error!r}"
1194
- )
1189
+ exc_value.add_note(
1190
+ "calibration context cleanup also failed: "
1191
+ f"{cleanup_error!r}"
1192
+ )
1195
1193
 
1196
1194
 
1197
1195
  def calibrate_live(
@@ -3029,7 +3027,7 @@ def _calibration_source(options: _Options, action: str) -> str:
3029
3027
 
3030
3028
  def _iso_timestamp(timestamp: float) -> str:
3031
3029
  return (
3032
- dt.datetime.fromtimestamp(timestamp, tz=dt.timezone.utc)
3030
+ dt.datetime.fromtimestamp(timestamp, tz=dt.UTC)
3033
3031
  .isoformat(timespec="seconds")
3034
3032
  .replace("+00:00", "Z")
3035
3033
  )
@@ -3037,7 +3035,7 @@ def _iso_timestamp(timestamp: float) -> str:
3037
3035
 
3038
3036
  def _utc_now_text() -> str:
3039
3037
  return (
3040
- dt.datetime.now(tz=dt.timezone.utc)
3038
+ dt.datetime.now(tz=dt.UTC)
3041
3039
  .isoformat(timespec="seconds")
3042
3040
  .replace("+00:00", "Z")
3043
3041
  )
@@ -3173,7 +3171,7 @@ def _profile_dedupe_keys(data: dict[str, Any]) -> set[tuple[object, ...]]:
3173
3171
  def _backup_path(path: Path) -> Path:
3174
3172
  backup_dir = path.parent / "opcodes_backups"
3175
3173
  backup_dir.mkdir(parents=True, exist_ok=True)
3176
- stamp = dt.datetime.now(tz=dt.timezone.utc).strftime("%Y%m%d%H%M%S%f")
3174
+ stamp = dt.datetime.now(tz=dt.UTC).strftime("%Y%m%d%H%M%S%f")
3177
3175
  candidate = backup_dir / f"{path.name}.bak.{stamp}"
3178
3176
  suffix = 1
3179
3177
  while candidate.exists():
@@ -52,9 +52,9 @@ from .profiles import (
52
52
 
53
53
  _PACKET_WORKER_STOP = object()
54
54
  _TARGET_FRAME_HISTORY_LIMIT = 4096
55
- _TargetFrameKey = tuple[FlowKey, int, Optional[int], float, int, int]
55
+ type _TargetFrameKey = tuple[FlowKey, int, Optional[int], float, int, int]
56
56
  _INVENTORY_BOUNDARY_COHORT_LIMIT = 64
57
- _InventoryBoundaryKey = tuple[FlowKey, int, int]
57
+ type _InventoryBoundaryKey = tuple[FlowKey, int, int]
58
58
 
59
59
 
60
60
  @dataclass
@@ -1111,11 +1111,10 @@ class LiveCaptureSession:
1111
1111
  with self._state_lock:
1112
1112
  self._cleanup_incomplete = True
1113
1113
  for failure in cleanup_failures:
1114
- if hasattr(error, "add_note"):
1115
- error.add_note(
1116
- "live item capture startup cleanup also failed: "
1117
- f"{failure!r}"
1118
- )
1114
+ error.add_note(
1115
+ "live item capture startup cleanup also failed: "
1116
+ f"{failure!r}"
1117
+ )
1119
1118
  _attach_cleanup_owner(
1120
1119
  error,
1121
1120
  self,
@@ -1124,11 +1123,10 @@ class LiveCaptureSession:
1124
1123
  return
1125
1124
 
1126
1125
  for failure in cleanup_failures:
1127
- if hasattr(error, "add_note"):
1128
- error.add_note(
1129
- "live item capture startup cleanup reported a fully "
1130
- f"released error: {failure!r}"
1131
- )
1126
+ error.add_note(
1127
+ "live item capture startup cleanup reported a fully "
1128
+ f"released error: {failure!r}"
1129
+ )
1132
1130
  self._reset_after_failed_start()
1133
1131
 
1134
1132
  def _reset_after_failed_start(self) -> None:
@@ -1586,11 +1584,10 @@ class LiveCaptureSession:
1586
1584
  self,
1587
1585
  context="live item capture context",
1588
1586
  )
1589
- if hasattr(exc_value, "add_note"):
1590
- exc_value.add_note(
1591
- "live item capture context cleanup also failed: "
1592
- f"{cleanup_error!r}"
1593
- )
1587
+ exc_value.add_note(
1588
+ "live item capture context cleanup also failed: "
1589
+ f"{cleanup_error!r}"
1590
+ )
1594
1591
 
1595
1592
 
1596
1593
  def capture_live(
@@ -1651,11 +1648,10 @@ def capture_live(
1651
1648
  session,
1652
1649
  context="timed live item capture startup",
1653
1650
  )
1654
- if hasattr(exc, "add_note"):
1655
- exc.add_note(
1656
- "timed live item capture startup cleanup also "
1657
- f"failed: {cleanup_error!r}"
1658
- )
1651
+ exc.add_note(
1652
+ "timed live item capture startup cleanup also "
1653
+ f"failed: {cleanup_error!r}"
1654
+ )
1659
1655
  raise
1660
1656
  try:
1661
1657
  while True:
@@ -3244,11 +3244,10 @@ class CharacterLoadSession:
3244
3244
  self,
3245
3245
  context="character-load capture context",
3246
3246
  )
3247
- if hasattr(exc_value, "add_note"):
3248
- exc_value.add_note(
3249
- "character-load context cleanup also failed: "
3250
- f"{cleanup_error!r}"
3251
- )
3247
+ exc_value.add_note(
3248
+ "character-load context cleanup also failed: "
3249
+ f"{cleanup_error!r}"
3250
+ )
3252
3251
 
3253
3252
 
3254
3253
  def format_character_state(
@@ -121,7 +121,7 @@ class BDOEvent:
121
121
  def timestamp_iso(self) -> str:
122
122
  """Deterministic UTC ISO-8601 timestamp for serialization."""
123
123
  return (
124
- dt.datetime.fromtimestamp(self.timestamp, tz=dt.timezone.utc)
124
+ dt.datetime.fromtimestamp(self.timestamp, tz=dt.UTC)
125
125
  .isoformat(timespec="milliseconds")
126
126
  .replace("+00:00", "Z")
127
127
  )
@@ -3,14 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from typing import Iterable, Optional, TypeVar
6
+ from typing import Iterable, Optional
7
7
 
8
8
  from .events import BDOEvent
9
9
 
10
10
 
11
- T = TypeVar("T")
12
-
13
-
14
11
  _ACTIVITY_EVENT_TYPES = frozenset(
15
12
  {"loot_preview", "item_received", "storage_delta"}
16
13
  )
@@ -19,7 +16,7 @@ _SNAPSHOT_RECORD_EVENT_TYPES = frozenset(
19
16
  )
20
17
 
21
18
 
22
- def _freeze(values: Optional[Iterable[T]]) -> Optional[frozenset[T]]:
19
+ def _freeze[T](values: Optional[Iterable[T]]) -> Optional[frozenset[T]]:
23
20
  if values is None:
24
21
  return None
25
22
  if isinstance(values, (str, bytes)):
@@ -46,9 +46,9 @@ def _opcode_text(opcode: int) -> str:
46
46
 
47
47
  def _utc_text(timestamp: Optional[float] = None) -> str:
48
48
  value = (
49
- dt.datetime.fromtimestamp(timestamp, tz=dt.timezone.utc)
49
+ dt.datetime.fromtimestamp(timestamp, tz=dt.UTC)
50
50
  if timestamp is not None
51
- else dt.datetime.now(tz=dt.timezone.utc)
51
+ else dt.datetime.now(tz=dt.UTC)
52
52
  )
53
53
  return value.isoformat(timespec="seconds").replace("+00:00", "Z")
54
54
 
@@ -752,7 +752,7 @@ def promote_origin_candidates(
752
752
  def _unique_backup_path(path: Path) -> Path:
753
753
  backup_dir = path.parent / "opcodes_backups"
754
754
  backup_dir.mkdir(parents=True, exist_ok=True)
755
- stamp = dt.datetime.now(tz=dt.timezone.utc).strftime("%Y%m%d%H%M%S%f")
755
+ stamp = dt.datetime.now(tz=dt.UTC).strftime("%Y%m%d%H%M%S%f")
756
756
  return backup_dir / f"{path.name}.bak.{stamp}"
757
757
 
758
758
 
@@ -339,7 +339,7 @@ def _is_sha256(value: str) -> bool:
339
339
  def _next_backup_path(path: Path) -> Path:
340
340
  backup_dir = path.parent / "opcodes_backups"
341
341
  backup_dir.mkdir(parents=True, exist_ok=True)
342
- stamp = dt.datetime.now(tz=dt.timezone.utc).strftime("%Y%m%d%H%M%S%f")
342
+ stamp = dt.datetime.now(tz=dt.UTC).strftime("%Y%m%d%H%M%S%f")
343
343
  candidate = backup_dir / f"{path.name}.bak.{stamp}"
344
344
  suffix = 1
345
345
  while candidate.exists():
@@ -25,7 +25,7 @@ from ._constants import (
25
25
  )
26
26
 
27
27
 
28
- _FamilyKey = tuple[FlowKey, int, int, int]
28
+ type _FamilyKey = tuple[FlowKey, int, int, int]
29
29
 
30
30
 
31
31
  def _family_key(frame: BDOFrame) -> _FamilyKey:
@@ -8,7 +8,7 @@ from concurrent.futures import ThreadPoolExecutor
8
8
  from functools import partial
9
9
  from pathlib import Path
10
10
  from types import TracebackType
11
- from typing import AsyncIterator, Callable, Optional, TypeVar
11
+ from typing import AsyncIterator, Callable, Optional
12
12
 
13
13
  from bdo_toolkit._capture_options import PacketCaptureOptions
14
14
  from bdo_toolkit._capture_runtime import _attach_cleanup_owner
@@ -22,14 +22,7 @@ from .models import (
22
22
  from .session import LiveSolareSession, _validate_timeout
23
23
 
24
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:
25
+ async def _settle[T](future: asyncio.Future[T]) -> T:
33
26
  while not future.done():
34
27
  try:
35
28
  await asyncio.wait((future,))
@@ -38,7 +31,7 @@ async def _settle(future: asyncio.Future[T]) -> T:
38
31
  return future.result()
39
32
 
40
33
 
41
- async def _await_preserving_future(future: asyncio.Future[T]) -> T:
34
+ async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
42
35
  """Await a worker future without cancelling it or creating a shield."""
43
36
 
44
37
  await asyncio.wait((future,))
@@ -114,7 +107,7 @@ class AsyncLiveSolareSession:
114
107
  def raise_if_failed(self) -> None:
115
108
  self._session.raise_if_failed()
116
109
 
117
- def _submit(self, function: Callable[[], T]) -> asyncio.Future[T]:
110
+ def _submit[T](self, function: Callable[[], T]) -> asyncio.Future[T]:
118
111
  if self._closed:
119
112
  raise RuntimeError("async live Solare session is already closed")
120
113
  return asyncio.get_running_loop().run_in_executor(self._executor, function)
@@ -131,7 +124,7 @@ class AsyncLiveSolareSession:
131
124
 
132
125
  async def close_after_worker_and_calls_settle() -> None:
133
126
  while not self._session.stopped and not self._closed:
134
- await asyncio.sleep(_TERMINAL_SHUTDOWN_POLL_SECONDS)
127
+ await asyncio.sleep(0.01)
135
128
  while not self._closed and (
136
129
  self._start_active
137
130
  or self._poll_active
@@ -141,7 +134,7 @@ class AsyncLiveSolareSession:
141
134
  and not self._stop_future.done()
142
135
  )
143
136
  ):
144
- await asyncio.sleep(_TERMINAL_SHUTDOWN_POLL_SECONDS)
137
+ await asyncio.sleep(0.01)
145
138
  self._shutdown()
146
139
 
147
140
  self._terminal_shutdown_task = asyncio.create_task(
@@ -434,11 +427,10 @@ class AsyncLiveSolareSession:
434
427
  self,
435
428
  context="async live Solare context",
436
429
  )
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
- )
430
+ exc_value.add_note(
431
+ "async live Solare context cleanup also failed: "
432
+ f"{cleanup_error!r}"
433
+ )
442
434
  finally:
443
435
  if not self._session.cleanup_incomplete:
444
436
  self._shutdown()
@@ -445,7 +445,7 @@ class SolareLeaderboardSnapshot:
445
445
  @property
446
446
  def observed_at_iso(self) -> str:
447
447
  return (
448
- dt.datetime.fromtimestamp(self.observed_at, tz=dt.timezone.utc)
448
+ dt.datetime.fromtimestamp(self.observed_at, tz=dt.UTC)
449
449
  .isoformat(timespec="milliseconds")
450
450
  .replace("+00:00", "Z")
451
451
  )
@@ -980,11 +980,10 @@ class LiveSolareSession:
980
980
  self,
981
981
  context="live Solare context",
982
982
  )
983
- if hasattr(exc_value, "add_note"):
984
- exc_value.add_note(
985
- "live Solare context cleanup also failed: "
986
- f"{cleanup_error!r}"
987
- )
983
+ exc_value.add_note(
984
+ "live Solare context cleanup also failed: "
985
+ f"{cleanup_error!r}"
986
+ )
988
987
 
989
988
 
990
989
  def capture_solare_snapshot(
@@ -1043,7 +1042,7 @@ def capture_solare_snapshot(
1043
1042
  session,
1044
1043
  context="live Solare convenience wrapper",
1045
1044
  )
1046
- if cleanup_error is not exc and hasattr(exc, "add_note"):
1045
+ if cleanup_error is not exc:
1047
1046
  exc.add_note(
1048
1047
  "live Solare convenience cleanup also failed: "
1049
1048
  f"{cleanup_error!r}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bdo-toolkit
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Passive, read-only BDO packet parsing toolkit for app developers.
5
5
  Author: bdo-toolkit contributors
6
6
  License-Expression: MIT
@@ -12,9 +12,10 @@ Keywords: bdo,packets,parse,pcap,passive
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
15
16
  Classifier: Programming Language :: Python :: 3 :: Only
16
17
  Classifier: Typing :: Typed
17
- Requires-Python: >=3.10
18
+ Requires-Python: >=3.14
18
19
  Description-Content-Type: text/markdown
19
20
  License-File: LICENSE
20
21
  Requires-Dist: scapy>=2.5
@@ -27,7 +28,7 @@ Dynamic: license-file
27
28
  # bdo-toolkit
28
29
 
29
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)
30
- [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
31
+ [![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)
31
32
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
32
33
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
33
34
  [![License: MIT](https://img.shields.io/badge/license-MIT-2f855a?style=flat-square)](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
@@ -62,7 +63,7 @@ services is unknown.
62
63
  python -m pip install bdo-toolkit
63
64
  ```
64
65
 
65
- Python 3.10 or newer is required. Before live capture, complete
66
+ Python 3.14 or newer is required. Before live capture, complete
66
67
  [Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
67
68
  including Npcap on Windows and permission to capture on the selected interface.
68
69
  Offline PCAP and PCAPNG replay does not require Npcap.
@@ -134,7 +135,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
134
135
  ```
135
136
 
136
137
  CI runs tests, type checking, wheel construction, and a CLI smoke test on
137
- Ubuntu and Windows with Python 3.10 and 3.14. Tests that require private
138
+ Ubuntu and Windows with Python 3.14. Tests that require private
138
139
  game-session captures skip when those local fixtures are absent.
139
140
 
140
141
  ## License
@@ -1015,10 +1015,7 @@ def test_async_calibration_cancelled_reuse_exposes_current_run_for_retry(
1015
1015
  with pytest.raises(asyncio.CancelledError) as cancelled:
1016
1016
  await starting
1017
1017
 
1018
- # CPython 3.10 reconstructs CancelledError when it crosses the Task
1019
- # boundary, which necessarily drops custom exception attributes.
1020
- if hasattr(asyncio.CancelledError, "add_note"):
1021
- assert cancelled.value.cleanup_owner is session
1018
+ assert cancelled.value.cleanup_owner is session
1022
1019
  assert session.cleanup_incomplete
1023
1020
  assert session.running
1024
1021
  assert session.result is None
@@ -521,11 +521,10 @@ def test_context_cleanup_failure_is_retained_without_masking_block_error(
521
521
 
522
522
  assert caught.value is block_failure
523
523
  assert session.error is _RaisingStopSniffer.failure
524
- if hasattr(BaseException, "add_note"):
525
- assert any(
526
- "context cleanup also failed" in note
527
- for note in caught.value.__notes__
528
- )
524
+ assert any(
525
+ "context cleanup also failed" in note
526
+ for note in caught.value.__notes__
527
+ )
529
528
  assert not session.running
530
529
 
531
530
 
@@ -513,11 +513,10 @@ def test_startup_failure_retains_an_unstopped_backend_for_cleanup_retry(
513
513
  assert capture.running
514
514
  assert capture._capture is sniffer
515
515
  assert thread.join_calls == [runtime._CAPTURE_JOIN_TIMEOUT_SECONDS]
516
- if hasattr(BaseException, "add_note"):
517
- assert any(
518
- "startup cleanup also failed" in note
519
- for note in startup.value.__notes__
520
- )
516
+ assert any(
517
+ "startup cleanup also failed" in note
518
+ for note in startup.value.__notes__
519
+ )
521
520
 
522
521
  thread.alive = False
523
522
  sniffer.running = False
@@ -426,11 +426,7 @@ def test_capture_convenience_exposes_hidden_owner_on_incomplete_cleanup(
426
426
 
427
427
  assert failed.value is primary_failure
428
428
  assert primary_failure.cleanup_owner is IncompleteSession.instances[-1]
429
- if hasattr(BaseException, "add_note"):
430
- assert any(
431
- "cleanup also failed" in note
432
- for note in primary_failure.__notes__
433
- )
429
+ assert any("cleanup also failed" in note for note in primary_failure.__notes__)
434
430
 
435
431
 
436
432
  def test_capture_convenience_allows_explicit_infinite_wait(
@@ -1677,10 +1673,10 @@ def test_async_finished_poll_closes_executor_after_callback_returns(
1677
1673
  assert not session._closed
1678
1674
  assert not session.stopped
1679
1675
  release_callback.set()
1680
- assert await asyncio.to_thread(session._session._stopped.wait, 2.0)
1681
- terminal_shutdown = session._terminal_shutdown_task
1682
- assert terminal_shutdown is not None
1683
- await asyncio.wait_for(asyncio.shield(terminal_shutdown), timeout=2.0)
1676
+ for _ in range(200):
1677
+ if session.stopped and session._closed:
1678
+ break
1679
+ await asyncio.sleep(0.01)
1684
1680
  assert session.stopped
1685
1681
  assert session._closed
1686
1682
 
@@ -2061,15 +2057,16 @@ def test_async_cancellation_returns_after_bounded_incomplete_cleanup(
2061
2057
  assert preserved.kind is SolareUpdateKind.FINISHED
2062
2058
 
2063
2059
  release_callback.set()
2064
- assert await asyncio.to_thread(session._session._stopped.wait, 2.0)
2060
+ for _ in range(200):
2061
+ if session.stopped:
2062
+ break
2063
+ await asyncio.sleep(0.01)
2065
2064
  assert session.stopped
2066
2065
  if operation == "poll":
2067
- terminal_shutdown = session._terminal_shutdown_task
2068
- assert terminal_shutdown is not None
2069
- await asyncio.wait_for(
2070
- asyncio.shield(terminal_shutdown),
2071
- timeout=2.0,
2072
- )
2066
+ for _ in range(200):
2067
+ if session._closed:
2068
+ break
2069
+ await asyncio.sleep(0.01)
2073
2070
  assert session._closed
2074
2071
  with pytest.raises(RuntimeError, match="worker cleanup is incomplete"):
2075
2072
  await session.stop()
File without changes
File without changes