sltcodec 2.6.1__tar.gz → 2.7.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sltcodec
3
- Version: 2.6.1
3
+ Version: 2.7.0
4
4
  Summary: Decode and encode bytearrays according to struct layout definitions using sltcore.
5
5
  Project-URL: Homepage, https://github.com/fangface-hub/StructLayoutToolkitCodec
6
6
  Project-URL: Documentation, https://readthedocs.org
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "2.6.1"
3
+ version = "2.7.0"
4
4
  description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
5
5
  readme = "README_pypi.md"
6
6
  requires-python = ">=3.12"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  import re
6
+ import time
6
7
  from pathlib import Path
7
8
  from typing import Any, Callable
8
9
 
@@ -29,16 +30,34 @@ _DEFAULT_PADDING_ALIGNMENT_BITS = 32
29
30
 
30
31
  ProgressCallback = Callable[[float], None]
31
32
 
32
-
33
- def _notify_progress(progress_callback: ProgressCallback | None,
34
- offset: InfoSize, size: InfoSize, total_bits: int) -> None:
35
- """Notify caller of top-level encode/decode progress."""
36
- if progress_callback is None or total_bits <= 0:
37
- return
38
- progress = min((offset + size).bits / total_bits, 1.0)
39
- progress_callback(progress)
40
-
41
-
33
+ _PROGRESS_MIN_INTERVAL_SEC = 0.1
34
+ _PROGRESS_MIN_DELTA = 0.01
35
+
36
+
37
+ def _notify_progress(progress_callback: ProgressCallback | None,
38
+ progress_state: dict[str, float],
39
+ offset: InfoSize, size: InfoSize, total_bits: int) -> None:
40
+ """Notify caller of top-level encode/decode progress.
41
+
42
+ Calls are throttled: the callback fires on the first call, at most once
43
+ per 100 ms, or when progress changed by more than 0.01 since the last
44
+ notification. Completion (1.0) is always notified. ``progress_state`` is a
45
+ per-run dict holding the last notification time and progress.
46
+ """
47
+ if progress_callback is None or total_bits <= 0:
48
+ return
49
+ progress = min((offset + size).bits / total_bits, 1.0)
50
+ now = time.monotonic()
51
+ last_time = progress_state.get("time")
52
+ if (last_time is not None and progress < 1.0
53
+ and now - last_time < _PROGRESS_MIN_INTERVAL_SEC
54
+ and abs(progress - progress_state["progress"]) <=
55
+ _PROGRESS_MIN_DELTA):
56
+ return
57
+ progress_state["time"] = now
58
+ progress_state["progress"] = progress
59
+ progress_callback(progress)
60
+
42
61
  def _estimate_encode_total_bits(struct_instance: StructInstance,
43
62
  initial_size_bits: int,
44
63
  env: dict[str, Any] | None = None) -> int:
@@ -410,6 +429,7 @@ def encode(
410
429
  raise ValueError("StructLayout.type_dict is required for encode()")
411
430
 
412
431
  type_dict = struct_layout.type_dict
432
+ progress_state: dict[str, float] = {}
413
433
  env = env if env is not None else {}
414
434
  has_padding = False
415
435
  total_bits = _estimate_encode_total_bits(struct_instance, len(buf) * 8, env)
@@ -438,7 +458,7 @@ def encode(
438
458
  resolved_size = _resolve_info_size(field_def_repeat.size, env)
439
459
  encode_field(field_def_repeat, values[i], buf, env, type_dict,
440
460
  padding_alignment_bits)
441
- _notify_progress(progress_callback, current_offset,
461
+ _notify_progress(progress_callback, progress_state, current_offset,
442
462
  resolved_size, total_bits)
443
463
  env[field_def_repeat.name] = values[i]
444
464
  if isinstance(current_offset, InfoSize):
@@ -449,7 +469,7 @@ def encode(
449
469
  resolved_size = _resolve_info_size(field_def.size, env)
450
470
  encode_field(field_def, value, buf, env, type_dict,
451
471
  padding_alignment_bits)
452
- _notify_progress(progress_callback, resolved_offset, resolved_size,
472
+ _notify_progress(progress_callback, progress_state, resolved_offset, resolved_size,
453
473
  total_bits)
454
474
  env[field_def.name] = value
455
475
 
@@ -608,9 +628,10 @@ def decode(
608
628
  if struct_layout.type_dict is None:
609
629
  raise ValueError("StructLayout.type_dict is required for decode()")
610
630
 
631
+ progress_state: dict[str, float] = {}
611
632
  env = env if env is not None else {}
612
633
  type_dict = struct_layout.type_dict
613
- struct_def_obj = type_dict.struct_dict[struct_layout.struct_def_name]
634
+ struct_def_obj = type_dict.struct_dict[struct_layout.struct_def_name]
614
635
  result = StructInstance(struct_def=struct_def_obj)
615
636
  current_position = InfoSize(0, 0)
616
637
  padding_index = 0
@@ -682,7 +703,7 @@ def decode(
682
703
  if field_instance is not None:
683
704
  env[field_instance.field_def.name] = field_instance.value
684
705
  result.append_field_instance(field_instance)
685
- _notify_progress(progress_callback, resolved_offset,
706
+ _notify_progress(progress_callback, progress_state, resolved_offset,
686
707
  field_instance.field_def.size, data_bits)
687
708
  current_position = (resolved_offset +
688
709
  _resolve_info_size(field_def.size, env))
@@ -723,7 +744,7 @@ def decode(
723
744
  env[field_instance.field_def.name] = field_instance.value
724
745
  result.append_field_instance(field_instance)
725
746
  actual_size = field_instance.field_def.size
726
- _notify_progress(progress_callback, current_offset,
747
+ _notify_progress(progress_callback, progress_state, current_offset,
727
748
  actual_size, data_bits)
728
749
  else:
729
750
  actual_size = resolved_size
@@ -770,7 +791,7 @@ def decode(
770
791
  env[field_instance.field_def.name] = field_instance.value
771
792
  result.append_field_instance(field_instance)
772
793
  actual_size = field_instance.field_def.size
773
- _notify_progress(progress_callback, current_offset, actual_size,
794
+ _notify_progress(progress_callback, progress_state, current_offset, actual_size,
774
795
  data_bits)
775
796
  else:
776
797
  actual_size = _resolve_info_size(field_def_repeat.size, env)
@@ -7,7 +7,9 @@ from sltcore import InfoSize
7
7
  from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
8
8
  ProgressCallback, StructDef, StructInstance, StructLayout,
9
9
  TypeDict, decode, encode)
10
- from sltcodec.codec import _resolve_info_size, decode_field
10
+ from sltcodec import codec as codec_module
11
+ from sltcodec.codec import (_notify_progress, _resolve_info_size,
12
+ decode_field)
11
13
 
12
14
 
13
15
  def layout_for(struct_def):
@@ -968,3 +970,37 @@ def test_encode_without_padding_does_not_extend_buf_to_struct_size():
968
970
 
969
971
  assert encoded is buf
970
972
  assert encoded == bytearray(b"\x7f\x00\x00\x00")
973
+
974
+
975
+ def _run_notify(monkeypatch, steps):
976
+ """Feed (time, bits) steps through _notify_progress; return reported."""
977
+ clock = {"now": 0.0}
978
+ monkeypatch.setattr(codec_module.time, "monotonic", lambda: clock["now"])
979
+ reported = []
980
+ state = {}
981
+ for now, bits in steps:
982
+ clock["now"] = now
983
+ _notify_progress(reported.append, state, InfoSize(0, 0),
984
+ InfoSize(0, bits), 10000)
985
+ return reported
986
+
987
+
988
+ def test_notify_progress_throttles_by_progress_delta(monkeypatch):
989
+ """Test small changes within 100 ms are skipped, >0.01 changes pass."""
990
+ reported = _run_notify(monkeypatch, [(0.0, 100), (0.01, 150),
991
+ (0.02, 300), (0.03, 301),
992
+ (0.04, 500)])
993
+ assert reported == [0.01, 0.03, 0.05]
994
+
995
+
996
+ def test_notify_progress_throttles_by_time(monkeypatch):
997
+ """Test tiny changes are reported once 100 ms have elapsed."""
998
+ reported = _run_notify(monkeypatch, [(0.0, 100), (0.05, 101),
999
+ (0.1, 102), (0.15, 103)])
1000
+ assert reported == [0.01, 0.0102]
1001
+
1002
+
1003
+ def test_notify_progress_always_reports_completion(monkeypatch):
1004
+ """Test 1.0 is reported even if throttling conditions are not met."""
1005
+ reported = _run_notify(monkeypatch, [(0.0, 9995), (0.01, 10000)])
1006
+ assert reported == [0.9995, 1.0]
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "2.6.0"
77
+ version = "2.6.1"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "sltcalc" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes