zs-yaml 0.9.1.dev2__tar.gz → 0.9.2.dev10__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 (43) hide show
  1. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/.github/workflows/ci.yml +12 -0
  2. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/CHANGELOG.md +15 -0
  3. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/PKG-INFO +1 -1
  4. zs_yaml-0.9.2.dev10/examples/perf/.gitignore +4 -0
  5. zs_yaml-0.9.2.dev10/examples/perf/benchmark.py +138 -0
  6. zs_yaml-0.9.2.dev10/examples/perf/generate_perf_yaml.py +51 -0
  7. zs_yaml-0.9.2.dev10/examples/perf/perf.zs +20 -0
  8. zs_yaml-0.9.2.dev10/examples/perf/perf_reference.bin +0 -0
  9. zs_yaml-0.9.2.dev10/examples/perf/perf_reference_dict.sha256 +1 -0
  10. zs_yaml-0.9.2.dev10/examples/perf/test_fast_path.py +83 -0
  11. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml/_version.py +3 -3
  12. zs_yaml-0.9.2.dev10/zs_yaml/convert.py +616 -0
  13. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml.egg-info/PKG-INFO +1 -1
  14. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml.egg-info/SOURCES.txt +7 -0
  15. zs_yaml-0.9.1.dev2/zs_yaml/convert.py +0 -342
  16. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/.gitignore +0 -0
  17. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/LICENSE +0 -0
  18. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/README.md +0 -0
  19. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/api_docs_review.md +0 -0
  20. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/doc/zs-yaml.png +0 -0
  21. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/doc/zs-yaml.svg +0 -0
  22. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/extern_compression/extern_compression.zs +0 -0
  23. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/extern_compression/payload.yaml +0 -0
  24. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/extern_compression/test_compression.py +0 -0
  25. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/addresses.yaml +0 -0
  26. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/custom_transformations.py +0 -0
  27. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/hobbies.yaml +0 -0
  28. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/skills.yaml +0 -0
  29. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/team.zs +0 -0
  30. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/team1.yaml +0 -0
  31. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/test_all_conversions.py +0 -0
  32. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/test_pyobj_to_yaml.py +0 -0
  33. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/examples/team/work_experience.yaml +0 -0
  34. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/pyproject.toml +0 -0
  35. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/setup.cfg +0 -0
  36. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml/__init__.py +0 -0
  37. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml/built_in_transformations.py +0 -0
  38. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml/main.py +0 -0
  39. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml/yaml_transformer.py +0 -0
  40. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml.egg-info/dependency_links.txt +0 -0
  41. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml.egg-info/entry_points.txt +0 -0
  42. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml.egg-info/requires.txt +0 -0
  43. {zs_yaml-0.9.1.dev2 → zs_yaml-0.9.2.dev10}/zs_yaml.egg-info/top_level.txt +0 -0
@@ -77,6 +77,18 @@ jobs:
77
77
  echo "Compression tests failed"
78
78
  exit 1
79
79
  fi
80
+ - name: Generate schema API for perf example
81
+ run: |
82
+ cd examples/perf
83
+ zserio perf.zs -withTypeInfoCode -python zs_gen_api
84
+ - name: Perf regression - byte-identical reference
85
+ run: |
86
+ cd examples/perf
87
+ python benchmark.py
88
+ - name: Perf regression - fast-path structural assertion
89
+ run: |
90
+ cd examples/perf
91
+ python test_fast_path.py
80
92
  - name: Build package
81
93
  run: |
82
94
  python -m build
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.10.0] - 2026-05-07
11
+
12
+ ### Changed
13
+ - Conversion is significantly faster: `yaml_to_bin` ~10x and `bin_to_dict` ~3x on the perf benchmark (5,000 records). Drop-in upgrade — output is byte-identical to 0.9.x. Fixes #21
14
+
15
+ ### Added
16
+ - `examples/perf` benchmark plus CI guards to catch future performance regressions
17
+
18
+ ## [0.9.1] - 2026-04-27
19
+
20
+ ### Changed
21
+ - Improved API documentation: clearer top-level entry points and a landing-page overview
22
+
8
23
  ## [0.9.0] - 2026-04-23
9
24
 
10
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zs-yaml
3
- Version: 0.9.1.dev2
3
+ Version: 0.9.2.dev10
4
4
  Summary: YAML support for zserio
5
5
  Author-email: "Navigation Data Standard e.V." <support@nds-association.org>
6
6
  License-Expression: MIT
@@ -0,0 +1,4 @@
1
+ perf.yaml
2
+ perf.bin
3
+ zs_gen_api/
4
+ __pycache__/
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """Benchmark yaml_to_bin and bin_to_dict on the perf example.
3
+
4
+ Generates the perf YAML on demand, runs yaml_to_bin N times, then runs
5
+ bin_to_dict N times. Reports median wall time for each direction and,
6
+ if reference artifacts are present, verifies byte-identical output.
7
+
8
+ Usage:
9
+ python benchmark.py [--runs 3] [--records 5000] [--points-per-record 5]
10
+ """
11
+ import argparse
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import statistics
16
+ import sys
17
+ import time
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ sys.path.insert(0, os.path.abspath(os.path.join(HERE, "..", "..")))
21
+ sys.path.insert(0, os.path.join(HERE, "zs_gen_api"))
22
+
23
+ from zs_yaml.convert import yaml_to_bin, bin_to_dict # noqa: E402
24
+
25
+ from generate_perf_yaml import generate # noqa: E402
26
+
27
+ YAML_PATH = os.path.join(HERE, "perf.yaml")
28
+ BIN_PATH = os.path.join(HERE, "perf.bin")
29
+ REFERENCE_BIN = os.path.join(HERE, "perf_reference.bin")
30
+ REFERENCE_DICT_HASH = os.path.join(HERE, "perf_reference_dict.sha256")
31
+
32
+ SCHEMA_MODULE = "perf.api"
33
+ SCHEMA_TYPE = "Dataset"
34
+
35
+
36
+ def ensure_yaml(records: int, points_per_record: int) -> None:
37
+ if not os.path.exists(YAML_PATH):
38
+ print(f"Generating {YAML_PATH}...")
39
+ generate(records, points_per_record, YAML_PATH)
40
+
41
+
42
+ def run_yaml_to_bin() -> float:
43
+ if os.path.exists(BIN_PATH):
44
+ os.remove(BIN_PATH)
45
+ t0 = time.perf_counter()
46
+ yaml_to_bin(YAML_PATH, BIN_PATH)
47
+ return time.perf_counter() - t0
48
+
49
+
50
+ def run_bin_to_dict():
51
+ t0 = time.perf_counter()
52
+ data, _ = bin_to_dict(BIN_PATH, SCHEMA_MODULE, SCHEMA_TYPE)
53
+ return time.perf_counter() - t0, data
54
+
55
+
56
+ def _report(label, timings):
57
+ median = statistics.median(timings)
58
+ print(f"{label} median: {median:.3f}s "
59
+ f"(min {min(timings):.3f}s, max {max(timings):.3f}s, runs={len(timings)})")
60
+ return median
61
+
62
+
63
+ def main() -> int:
64
+ parser = argparse.ArgumentParser(description=__doc__)
65
+ parser.add_argument("--runs", type=int, default=3)
66
+ parser.add_argument("--records", type=int, default=5000)
67
+ parser.add_argument("--points-per-record", type=int, default=5)
68
+ parser.add_argument("--write-reference", action="store_true",
69
+ help="Overwrite reference artifacts with freshly produced output")
70
+ args = parser.parse_args()
71
+
72
+ ensure_yaml(args.records, args.points_per_record)
73
+
74
+ yaml_size = os.path.getsize(YAML_PATH)
75
+ print(f"Input: {YAML_PATH} ({yaml_size / (1024 * 1024):.2f} MiB)")
76
+
77
+ # Forward: yaml -> bin
78
+ print("\n[yaml_to_bin]")
79
+ yaml_timings = []
80
+ for i in range(args.runs):
81
+ elapsed = run_yaml_to_bin()
82
+ yaml_timings.append(elapsed)
83
+ print(f" run {i + 1}: {elapsed:.3f}s")
84
+ _report("yaml_to_bin", yaml_timings)
85
+ bin_size = os.path.getsize(BIN_PATH)
86
+ print(f"Output bin: {bin_size / (1024 * 1024):.2f} MiB")
87
+
88
+ # Reverse: bin -> dict
89
+ print("\n[bin_to_dict]")
90
+ bin_timings = []
91
+ last_dict = None
92
+ for i in range(args.runs):
93
+ elapsed, last_dict = run_bin_to_dict()
94
+ bin_timings.append(elapsed)
95
+ print(f" run {i + 1}: {elapsed:.3f}s")
96
+ _report("bin_to_dict", bin_timings)
97
+
98
+ dict_hash = hashlib.sha256(
99
+ json.dumps(last_dict, sort_keys=True, separators=(",", ":")).encode()
100
+ ).hexdigest()
101
+
102
+ if args.write_reference:
103
+ import shutil
104
+ shutil.copyfile(BIN_PATH, REFERENCE_BIN)
105
+ print(f"\nWrote reference bin -> {REFERENCE_BIN}")
106
+ with open(REFERENCE_DICT_HASH, "w") as f:
107
+ f.write(dict_hash + "\n")
108
+ print(f"Wrote reference dict hash -> {REFERENCE_DICT_HASH}")
109
+ return 0
110
+
111
+ status = 0
112
+ print()
113
+ if os.path.exists(REFERENCE_BIN):
114
+ with open(BIN_PATH, "rb") as a, open(REFERENCE_BIN, "rb") as b:
115
+ if a.read() == b.read():
116
+ print("bin matches perf_reference.bin byte-for-byte.")
117
+ else:
118
+ print("MISMATCH: bin differs from perf_reference.bin")
119
+ status = 1
120
+ else:
121
+ print("No perf_reference.bin yet; run with --write-reference to pin one.")
122
+
123
+ if os.path.exists(REFERENCE_DICT_HASH):
124
+ with open(REFERENCE_DICT_HASH) as f:
125
+ expected_hash = f.read().strip()
126
+ if dict_hash == expected_hash:
127
+ print("dict matches perf_reference_dict.sha256.")
128
+ else:
129
+ print(f"MISMATCH: dict hash {dict_hash} != expected {expected_hash}")
130
+ status = 1
131
+ else:
132
+ print("No perf_reference_dict.sha256 yet; run with --write-reference to pin one.")
133
+
134
+ return status
135
+
136
+
137
+ if __name__ == "__main__":
138
+ sys.exit(main())
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministically generate a sizeable perf YAML for benchmarking.
3
+
4
+ Produces examples/perf/perf.yaml with a Dataset containing a parameterized
5
+ number of records, each with a short nested array of Points. Matches the
6
+ "large array of small records with nested small arrays" pattern from
7
+ issue #21 - the shape where the JSON tokenizer cost dominates.
8
+
9
+ Usage:
10
+ python generate_perf_yaml.py [--records N] [--points-per-record M] [--out PATH]
11
+ """
12
+ import argparse
13
+ import os
14
+
15
+ DEFAULT_RECORDS = 5000
16
+ DEFAULT_POINTS_PER_RECORD = 5
17
+ DEFAULT_OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "perf.yaml")
18
+
19
+
20
+ def generate(records: int, points_per_record: int, out_path: str) -> None:
21
+ with open(out_path, "w") as f:
22
+ f.write("_meta:\n")
23
+ f.write(" schema_module: perf.api\n")
24
+ f.write(" schema_type: Dataset\n")
25
+ f.write("\n")
26
+ f.write('name: "perf-dataset"\n')
27
+ f.write("records:\n")
28
+ for i in range(records):
29
+ f.write(f" - id: {i}\n")
30
+ f.write(f' label: "record-{i:08d}"\n')
31
+ f.write(" points:\n")
32
+ for j in range(points_per_record):
33
+ x = (i * 31 + j) & 0x7FFFFFFF
34
+ y = (i * 17 - j) & 0x7FFFFFFF
35
+ f.write(f" - x: {x}\n")
36
+ f.write(f" y: {y}\n")
37
+
38
+
39
+ def main() -> None:
40
+ parser = argparse.ArgumentParser(description=__doc__)
41
+ parser.add_argument("--records", type=int, default=DEFAULT_RECORDS)
42
+ parser.add_argument("--points-per-record", type=int, default=DEFAULT_POINTS_PER_RECORD)
43
+ parser.add_argument("--out", default=DEFAULT_OUT)
44
+ args = parser.parse_args()
45
+ generate(args.records, args.points_per_record, args.out)
46
+ size = os.path.getsize(args.out)
47
+ print(f"Wrote {args.out} ({size / (1024 * 1024):.2f} MiB, {args.records} records x {args.points_per_record} points)")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,20 @@
1
+ package perf;
2
+
3
+ struct Point
4
+ {
5
+ int32 x;
6
+ int32 y;
7
+ };
8
+
9
+ struct Record
10
+ {
11
+ uint32 id;
12
+ string label;
13
+ Point points[];
14
+ };
15
+
16
+ struct Dataset
17
+ {
18
+ string name;
19
+ Record records[];
20
+ };
@@ -0,0 +1 @@
1
+ fd4029aa09b63ec6edbf4af9d823566ff665438f2099113025fb0fa5bb8160b4
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Structural perf-regression guard.
3
+
4
+ The 0.10 perf rewrite replaced `zserio.creator.ZserioTreeCreator` and
5
+ `zserio.walker.Walker` with a per-compound typeinfo descriptor cache.
6
+ Those two classes must never be instantiated on the `yaml_to_bin` /
7
+ `bin_to_dict` hot paths. This test asserts that invariant directly so a
8
+ future change that re-introduces the slow path fails CI - even on noisy
9
+ GitHub-hosted runners where wall-clock measurements are unreliable.
10
+ """
11
+ import os
12
+ import sys
13
+ import tempfile
14
+ from unittest.mock import patch
15
+
16
+ import zserio.creator
17
+ import zserio.walker
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ sys.path.insert(0, os.path.abspath(os.path.join(HERE, "..", "..")))
21
+ sys.path.insert(0, os.path.join(HERE, "zs_gen_api"))
22
+
23
+ from zs_yaml.convert import yaml_to_bin, bin_to_dict # noqa: E402
24
+
25
+ from generate_perf_yaml import generate # noqa: E402
26
+
27
+ YAML_PATH = os.path.join(HERE, "perf.yaml")
28
+ SCHEMA_MODULE = "perf.api"
29
+ SCHEMA_TYPE = "Dataset"
30
+
31
+
32
+ def _ensure_yaml():
33
+ if not os.path.exists(YAML_PATH):
34
+ generate(records=200, points_per_record=3, out_path=YAML_PATH)
35
+
36
+
37
+ def _run_with_patches(fn):
38
+ with patch.object(zserio.creator, "ZserioTreeCreator") as creator, \
39
+ patch.object(zserio.walker, "Walker") as walker:
40
+ fn()
41
+ return creator.call_count, walker.call_count
42
+
43
+
44
+ def test_yaml_to_bin_skips_slow_path():
45
+ _ensure_yaml()
46
+ with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp:
47
+ bin_path = tmp.name
48
+ try:
49
+ creator_calls, walker_calls = _run_with_patches(
50
+ lambda: yaml_to_bin(YAML_PATH, bin_path)
51
+ )
52
+ finally:
53
+ os.unlink(bin_path)
54
+ assert creator_calls == 0, f"ZserioTreeCreator instantiated {creator_calls}x - slow path re-introduced"
55
+ assert walker_calls == 0, f"Walker instantiated {walker_calls}x - slow path re-introduced"
56
+ print("yaml_to_bin: no ZserioTreeCreator, no Walker - fast path confirmed")
57
+
58
+
59
+ def test_bin_to_dict_skips_slow_path():
60
+ _ensure_yaml()
61
+ with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp:
62
+ bin_path = tmp.name
63
+ try:
64
+ yaml_to_bin(YAML_PATH, bin_path)
65
+ creator_calls, walker_calls = _run_with_patches(
66
+ lambda: bin_to_dict(bin_path, SCHEMA_MODULE, SCHEMA_TYPE)
67
+ )
68
+ finally:
69
+ os.unlink(bin_path)
70
+ assert creator_calls == 0, f"ZserioTreeCreator instantiated {creator_calls}x - slow path re-introduced"
71
+ assert walker_calls == 0, f"Walker instantiated {walker_calls}x - slow path re-introduced"
72
+ print("bin_to_dict: no ZserioTreeCreator, no Walker - fast path confirmed")
73
+
74
+
75
+ def main():
76
+ test_yaml_to_bin_skips_slow_path()
77
+ test_bin_to_dict_skips_slow_path()
78
+ print("\nAll fast-path assertions passed.")
79
+ return 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ sys.exit(main())
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.9.1.dev2'
22
- __version_tuple__ = version_tuple = (0, 9, 1, 'dev2')
21
+ __version__ = version = '0.9.2.dev10'
22
+ __version_tuple__ = version_tuple = (0, 9, 2, 'dev10')
23
23
 
24
- __commit_id__ = commit_id = 'g71a5fc59c'
24
+ __commit_id__ = commit_id = 'g9846aef5d'